'use client'; /** * Browser collection layer — resolve the binary bytes for a compiled * {@link VideoTimeline}'s asset plan. * * The pure compiler already produced the layout: `ir.assets.entries` names every * bundled asset, its `kind`, its zip-relative `path`, and whether its bytes are * `present`. This layer just fills those paths with real `Blob`s — narration and * media come straight from the Dexie records the DI factory already loaded (no * second read), and slide base frames are rendered here via `slideToPng`. Because * the plan owns paths and dedup, this is a byte-fetch loop, not a second planner. * * The slide-snapshot + generated-media-resolution logic is adapted from the * frame-export pipeline in PR #849 (the objectURL lifecycle in particular): a * cloned slide has its generated-media placeholders swapped for objectURLs, is * snapshotted, and the URLs are revoked immediately so memory stays bounded on a * large classroom. * * App-side / impure: reaches into Dexie records, the renderer snapshot, and the * DOM — outside the `lib/video-export/**` purity boundary by design. */ import { slideToPng } from '@openmaic/renderer/snapshot'; import type { PPTElement, PPTVideoElement, Slide } from '@openmaic/dsl'; import type { VideoTimeline } from '@/lib/video-export'; import type { Scene, SlideContent } from '@/lib/types/stage'; import { isMediaPlaceholder } from '@/lib/store/media-generation'; import type { MediaFileRecord } from '@/lib/utils/database'; import type { VideoTimelineRecords } from './timeline-deps'; import { useMediaGenerationStore } from '@/lib/store/media-generation'; import { withAssetUrl, type AssetUrlLeaseState } from '@/lib/media/use-asset-url'; import { MISSING_ASSET_LEASE, isConcreteMediaAddress, renderableMediaUrl, resolveMediaRef, type MediaTaskState, } from '@/lib/media/resolve-media-ref'; import { slideMediaReferenceSlots } from '@/lib/media/slide-media-slots'; import { resolveVideoMediaForElement } from '@/lib/media/media-task-resolution'; export interface CollectOptions { /** Slide-snapshot render width in px (frame height follows the slide ratio). Default 1920. */ frameWidth?: number; /** Called after each asset is resolved, for progress UX. */ onProgress?: (done: number, total: number) => void; } export interface CollectResult { /** zip-relative path → bytes, for every present asset the plan named. */ blobs: Map; /** Plan entries whose bytes could not be produced (missing record / render failure). */ missing: string[]; } type SnapshotMediaElement = { type: string; src?: string; mediaRef?: string; poster?: string }; /** `frame:` → ``. */ function frameSceneId(assetId: string): string | null { return assetId.startsWith('frame:') ? assetId.slice('frame:'.length) : null; } function blobWithType(blob: Blob, mimeType: string): Blob { return blob.type ? blob : new Blob([blob], { type: mimeType }); } /** Per-decode timeout (ms) so a video whose metadata/frame never loads can't wedge export. */ const FIRST_FRAME_TIMEOUT_MS = 8000; /** * Decode a video blob's first frame to a PNG object URL, for use as a poster. * * Generated videos often carry no poster (a provider-optional field), so the * base-frame snapshot — which can't draw a `