feat(plugins): add source-aware evidence and recording timelines

This commit is contained in:
chenmingxuan committed 2026-10-16 10:20:11 +08:00
1 parent 72ff342605
commit ef7af9988e
61 files changed
+3847 -108

No files matched your search

+76 -4
View File
@@ -546,10 +546,17 @@ class UnifiedMobileController:
output_dir = session.local_video_path.parent
new_video_path = output_dir / f"recording_{session.android_segment_index:03d}.mkv"
process = await self._spawn_scrcpy(
build_scrcpy_record_command("scrcpy", session.device_id, new_video_path)
self._archive_command(session.device_id, new_video_path, session.recording_codec)
)
spawned_at = time.time()
first_frame_at = await await_scrcpy_first_frame(process, spawned_at)
try:
first_frame_at = await await_scrcpy_first_frame(
process, spawned_at, require_marker=True
)
except (OSError, TimeoutError) as exc:
await self._stop_scrcpy(process)
self._record_recording_failure(session, f"录屏未取得首帧:{exc}")
return False
if process.returncode is not None:
stderr = await process.stderr.read()
logger.error(f"Failed to start scrcpy segment: {stderr.decode(errors='replace')}")
@@ -599,6 +606,21 @@ class UnifiedMobileController:
except Exception as e:
logger.error(f"Error in recording supervisor for {device_id}: {e}")
def _archive_command(self, device_id, path, codec):
from artemis.plugins.recording import recording_config
config = recording_config(self.ctx)
bitrate = config.h265_kbps if codec == "h265" else config.h264_kbps
return build_scrcpy_record_command(
"scrcpy",
device_id,
path,
f"{bitrate}K",
max_long_edge=config.max_long_edge,
fps=config.fps,
codec=codec,
)
async def start_video_recording(
self,
output_dir: Path | None = None,
@@ -623,6 +645,7 @@ class UnifiedMobileController:
message=f"Recording already in progress for device {device_id}",
)
session = None
try:
if not output_dir:
output_dir = Path(
@@ -658,7 +681,16 @@ class UnifiedMobileController:
session.capture_width, session.capture_height = display_state[1:]
# Start scrcpy in background
cmd = build_scrcpy_record_command("scrcpy", device_id, local_video_path)
from artemis.plugins.recording import recording_config
archive_config = recording_config(self.ctx)
session.recording_codec = (
"h265"
if archive_config.codec == "h265"
or (archive_config.codec == "auto" and archive_config.hevc_playback)
else "h264"
)
cmd = self._archive_command(device_id, local_video_path, session.recording_codec)
process = await self._spawn_scrcpy(cmd)
spawned_at = time.time()
@@ -671,7 +703,26 @@ class UnifiedMobileController:
# The file's t=0 is the first captured frame, about a second after
# spawn. Anchor the recording timeline there: segment offsets and
# analyzer clip trimming all treat session.start_time as that t=0.
first_frame_at = await await_scrcpy_first_frame(process, spawned_at)
try:
first_frame_at = await await_scrcpy_first_frame(
process, spawned_at, require_marker=True
)
except (OSError, TimeoutError):
await self._stop_scrcpy(process)
if session.recording_codec != "h265":
raise
first_frame_at = spawned_at
if process.returncode is not None and session.recording_codec == "h265":
logger.warning("设备 H.265 录制不可用,回退到 H.264 留档")
session.recording_codec = "h264"
process = await self._spawn_scrcpy(
self._archive_command(device_id, local_video_path, "h264")
)
session.process = process
spawned_at = time.time()
first_frame_at = await await_scrcpy_first_frame(
process, spawned_at, require_marker=True
)
if process.returncode is not None:
stderr = await process.stderr.read()
err_msg = stderr.decode()
@@ -722,8 +773,23 @@ class UnifiedMobileController:
source_revision=f"{session.video_id}:{session.generation}:active",
)
except asyncio.CancelledError:
if session and session.process:
await self._stop_scrcpy(session.process)
remove_active_session(device_id)
raise
except Exception as e:
logger.error(f"Failed to start scrcpy recording: {e}")
if session and session.process:
await self._stop_scrcpy(session.process)
if self.ctx and self.ctx.data_engine:
self.ctx.data_engine.record_video_start(
video_id=session.video_id,
device_id=device_id,
local_video_path=session.local_video_path,
start_time=session.start_time,
)
self._record_recording_failure(session, f"录屏启动失败:{e}")
remove_active_session(device_id)
return VideoRecordingResult(
success=False,
@@ -835,6 +901,12 @@ class UnifiedMobileController:
output_dir, mp4_paths, self._segment_session_offsets(session, mp4_paths)
)
if not manifest_path:
message = "录像文件缺少有效时长或画面尺寸,已保留文件供排查"
self._record_recording_failure(session, message)
remove_active_session(device_id)
return VideoRecordingResult(success=False, message=message)
remove_active_session(device_id)
# Persist update to local database if Data Engine is active