diff --git a/.env.example b/.env.example index 27a9fc5..2c6b072 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,9 @@ OCR_API_KEY= ADB_HOST=127.0.0.1 ADB_PORT=5037 # ADB_DEVICE_SERIAL= +# Keep the display awake, falling back to a wake lock or safe heartbeat when needed. +# Set to false if Artemis is used with a personal device whose screen should time out. +ARTEMIS_KEEP_DEVICE_AWAKE=true # Artemis Execution Defaults (flash or pro) ARTEMIS_DEFAULT_PROFILE=flash diff --git a/apps/admin_console/routers/media.py b/apps/admin_console/routers/media.py index ae40caf..4603ed6 100644 --- a/apps/admin_console/routers/media.py +++ b/apps/admin_console/routers/media.py @@ -92,10 +92,12 @@ async def get_session_video(session_id: str): row = session_repo.get_session_by_id(session_id) row_dict = dict(row) if row else {"session_id": session_id} v_url = media_service.resolve_video_url(row_dict, video_rec_map, video_idx) + video_segments = media_service.resolve_video_segments(v_url) return { "session_id": session_id, "has_video": bool(v_url), "video_url": v_url, + "video_segments": video_segments, } diff --git a/apps/admin_console/services/media_service.py b/apps/admin_console/services/media_service.py index 2e566de..fda93d5 100644 --- a/apps/admin_console/services/media_service.py +++ b/apps/admin_console/services/media_service.py @@ -127,6 +127,36 @@ class MediaService: return v_url + @classmethod + def resolve_video_segments(cls, video_url: str | None) -> list[dict[str, Any]]: + """Resolve a recording sidecar manifest into browser-safe segment URLs.""" + if not video_url or not video_url.startswith("/videos/"): + return [] + relative = urllib.parse.unquote(video_url.removeprefix("/videos/")) + video_path = (WORKSPACE_ROOT / relative).resolve() + manifest_path = video_path.parent / "recording.json" + if not manifest_path.is_file(): + return [] + try: + payload = json.loads(manifest_path.read_text(encoding="utf-8")) + segments = [] + for item in payload.get("segments", []): + segment_path = (manifest_path.parent / str(item["file"])).resolve() + if segment_path.parent != manifest_path.parent.resolve() or not segment_path.is_file(): + continue + segments.append( + { + "url": cls.path_to_video_url(segment_path), + "start": float(item.get("start", 0)), + "duration": float(item.get("duration", 0)), + "width": int(item.get("width", 0)), + "height": int(item.get("height", 0)), + } + ) + return segments + except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError): + return [] + @staticmethod def unwrap_payload(obj: Any) -> Any: """Unwraps trace payloads, storing base64 images as disk files and diff --git a/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html b/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html index 2e883fc..4cadb08 100644 --- a/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +++ b/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html @@ -134,7 +134,7 @@