From 3b772a010a326441b3b3c66b250055ee08cd0aaf Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 17 Sep 2026 16:33:10 -0700 Subject: [PATCH] feat(trace-viewer): show film strip lanes for all videos, grouped by page --- packages/trace-viewer/src/ui/filmStrip.tsx | 19 ++++++++++++++++--- .../trace-viewer/src/ui/videoThumbnails.ts | 18 ++++++++++-------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/trace-viewer/src/ui/filmStrip.tsx b/packages/trace-viewer/src/ui/filmStrip.tsx index 37a704ad61c34..4529b80d69cc1 100644 --- a/packages/trace-viewer/src/ui/filmStrip.tsx +++ b/packages/trace-viewer/src/ui/filmStrip.tsx @@ -40,8 +40,8 @@ export const FilmStrip: React.FunctionComponent<{ const [measure, ref] = useMeasure(); const lanesRef = React.useRef(null); - const video = model?.videos?.[0]; - const videoThumbnails = useVideoThumbnails(video, video && model ? model.createRelativeUrl(`file/${video.file}`) : undefined); + const videos = (model?.videos ?? []).map(video => ({ video, url: model!.createRelativeUrl(`file/${video.file}`) })); + const videoThumbnails = useVideoThumbnails(videos); let laneIndex = 0; if (lanesRef.current && previewPoint) { @@ -50,7 +50,7 @@ export const FilmStrip: React.FunctionComponent<{ } const pageLanes = (model?.pages ?? []).filter(page => page.screencastFrames.length); - const videoLanes: VideoThumbnail[][] = videoThumbnails.length ? [videoThumbnails] : []; + const videoLanes = videoLanesByPage(videos.map(({ video }) => video.pageId), videoThumbnails); let previewFrames: { timestamp: number, width: number, height: number, url: string }[] | undefined; if (laneIndex < pageLanes.length) @@ -98,6 +98,19 @@ export const FilmStrip: React.FunctionComponent<{ ; }; +function videoLanesByPage(pageIds: string[], thumbnails: VideoThumbnail[][]): VideoThumbnail[][] { + const lanes = new Map(); + pageIds.forEach((pageId, index) => { + let lane = lanes.get(pageId); + if (!lane) { + lane = []; + lanes.set(pageId, lane); + } + lane.push(...thumbnails[index]); + }); + return [...lanes.values()].filter(lane => lane.length).map(lane => lane.sort((a, b) => a.timestamp - b.timestamp)); +} + const VideoFilmStripLane: React.FunctionComponent<{ boundaries: Boundaries, thumbnails: VideoThumbnail[], diff --git a/packages/trace-viewer/src/ui/videoThumbnails.ts b/packages/trace-viewer/src/ui/videoThumbnails.ts index 168ae1cf654e2..b9abf73d33b72 100644 --- a/packages/trace-viewer/src/ui/videoThumbnails.ts +++ b/packages/trace-viewer/src/ui/videoThumbnails.ts @@ -35,19 +35,21 @@ const cache = new Map(); const maxThumbnails = 120; const thumbnailsPerSecond = 2; -export function useVideoThumbnails(video: trace.VideoTraceEvent | undefined, videoUrl: string | undefined): VideoThumbnail[] { +export function useVideoThumbnails(videos: { video: trace.VideoTraceEvent, url: string }[]): VideoThumbnail[][] { const [, setVersion] = React.useState(0); - const entry = video && videoUrl ? ensureEntry(video, videoUrl) : undefined; + const entries = videos.map(({ video, url }) => ensureEntry(video, url)); + const urls = videos.map(({ url }) => url).join('\n'); React.useEffect(() => { - if (!entry) - return; const listener = () => setVersion(version => version + 1); - entry.listeners.add(listener); + const subscribed = urls ? urls.split('\n').map(url => cache.get(url)!) : []; + for (const entry of subscribed) + entry.listeners.add(listener); return () => { - entry.listeners.delete(listener); + for (const entry of subscribed) + entry.listeners.delete(listener); }; - }, [entry]); - return entry?.thumbnails ?? []; + }, [urls]); + return entries.map(entry => entry.thumbnails); } function ensureEntry(video: trace.VideoTraceEvent, videoUrl: string): CacheEntry {