@pneuma-craft/video 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4465530: Define `PlaybackEngine`'s behavior at the end of the timeline.
8
+
9
+ Previously, `play()` with the playhead parked at the end of the composition was a no-op with a state flicker: the clock resumed from `duration`, the very first rAF tick satisfied the end-of-timeline check, and the engine re-paused. Consumers saw `playing` → `paused` inside one frame and `currentTime` never moved, so every transport UI had to discover this and hand-roll its own "rewind before play" policy.
10
+
11
+ - `play()` at the end of the timeline now rewinds to `0` and restarts, matching the HTML media element algorithm (and every video transport users already know).
12
+ - `play()` on a zero-duration composition is a documented no-op that leaves `state` untouched — no one-frame `playing` → `paused` flicker.
13
+ - New `PlaybackEngine.ended` — `true` when a composition is loaded, no `loop` region is set, `duration > 0`, and the playhead has reached the end. Positional like the media element's `ended`, so transports can render a replay affordance without re-deriving `currentTime >= duration ± ε`.
14
+ - `@pneuma-craft/react` exposes the same flag as `usePlayback().ended` (and `PneumaCraftStore.ended`).
15
+
16
+ `loop` behavior is unchanged: a loop region still bypasses the end-of-timeline pause, and keeps `ended` false.
17
+
3
18
  ## 0.5.1
4
19
 
5
20
  ### Patch Changes
package/README.md CHANGED
@@ -15,6 +15,20 @@ bun add @pneuma-craft/video
15
15
  - **Exporter** — renders frame-by-frame via WebCodecs `VideoEncoder`, mixes audio via `OfflineAudioContext`, muxes via MediaBunny `Output`
16
16
  - **Pluggable subtitle renderer** — shared between preview and export
17
17
 
18
+ ## Playback transport semantics
19
+
20
+ `PlaybackEngine` follows the HTML media element for the end of the timeline, so
21
+ a Play button needs no special-casing:
22
+
23
+ - The rAF loop pauses at `currentTime === duration` (unless a `loop` region is
24
+ set), and `engine.ended` becomes `true`.
25
+ - `play()` while `ended` rewinds to `0` and restarts from the top — one clean
26
+ `paused` → `playing` transition, no one-frame flicker.
27
+ - `play()` on a zero-duration composition is a no-op and leaves `state` alone.
28
+ - Read `engine.ended` (mirrored as `usePlayback().ended` in
29
+ `@pneuma-craft/react`) to render a replay affordance instead of comparing
30
+ `currentTime` against `duration` yourself.
31
+
18
32
  ## Browser support
19
33
 
20
34
  Requires WebCodecs (`VideoDecoder`/`VideoEncoder`) and modern Canvas 2D. Tested on Chromium-based browsers and Safari Tech Preview.
package/dist/index.cjs CHANGED
@@ -926,6 +926,7 @@ function createAudioScheduler(options) {
926
926
 
927
927
  // src/playback-engine.ts
928
928
  var DEFAULT_DECODE_TIMEOUT_MS = 1e3;
929
+ var END_EPSILON = 1e-6;
929
930
  var DEBUG_LOOP = typeof globalThis !== "undefined" && globalThis.__PNEUMA_CRAFT_PLAYBACK_DEBUG__ === true;
930
931
  function createPlaybackEngine(options) {
931
932
  const compositorType = options?.compositorType ?? "canvas2d";
@@ -1060,6 +1061,13 @@ function createPlaybackEngine(options) {
1060
1061
  get currentTime() {
1061
1062
  return _clock?.currentTime ?? 0;
1062
1063
  },
1064
+ get ended() {
1065
+ if (!_clock || !_composition) return false;
1066
+ if (_clock.loop) return false;
1067
+ const duration = _composition.duration;
1068
+ if (duration <= 0) return false;
1069
+ return _clock.currentTime >= duration - END_EPSILON;
1070
+ },
1063
1071
  get playbackRate() {
1064
1072
  return _clock?.playbackRate ?? _playbackRate;
1065
1073
  },
@@ -1131,6 +1139,15 @@ function createPlaybackEngine(options) {
1131
1139
  throw new Error("Cannot play: no composition loaded. Call load() first.");
1132
1140
  }
1133
1141
  if (_state === "playing") return;
1142
+ if (_composition.duration <= 0) {
1143
+ console.warn("[PlaybackEngine] play() ignored: composition has zero duration.");
1144
+ return;
1145
+ }
1146
+ if (engine.ended) {
1147
+ _seekId++;
1148
+ _clock.seek(0);
1149
+ emitTimeUpdate(_clock.currentTime);
1150
+ }
1134
1151
  if (_audioContext && _audioContext.state === "suspended") {
1135
1152
  _audioContext.resume().catch((err) => {
1136
1153
  console.warn("[PlaybackEngine] AudioContext resume failed:", err);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/media-decoder.ts","../src/canvas2d-compositor.ts","../src/gpu-compositor.ts","../src/compositor.ts","../src/frame-renderer.ts","../src/master-clock.ts","../src/audio-scheduler.ts","../src/playback-engine.ts","../src/offline-audio-renderer.ts","../src/export-engine.ts"],"sourcesContent":["// ── Types ──────────────────────────────────────────────────────────────\nexport type {\n AssetResolver,\n MediaInfo,\n MediaDecoder,\n CompositeLayer,\n Compositor,\n RenderedFrame,\n FrameRenderer,\n SubtitleRenderer,\n SubtitleRenderParams,\n ClockState,\n MasterClock,\n AudioScheduler,\n PlaybackState,\n PlaybackEngine,\n ExportOptions,\n ExportEngine,\n OfflineAudioRenderer,\n} from './types.js';\n\n// ── Creation Functions ─────────────────────────────────────────────────\nexport { createPlaybackEngine } from './playback-engine.js';\nexport type { PlaybackEngineOptions } from './playback-engine.js';\nexport { createExportEngine } from './export-engine.js';\nexport type { ExportEngineOptions } from './export-engine.js';\nexport { createFrameRenderer } from './frame-renderer.js';\nexport { createMasterClock } from './master-clock.js';\nexport type { MasterClockOptions } from './master-clock.js';\nexport { createAudioScheduler } from './audio-scheduler.js';\nexport type { AudioSchedulerOptions } from './audio-scheduler.js';\nexport { createMediaDecoder } from './media-decoder.js';\n\n// ── Compositor ─────────────────────────────────────────────────────────\nexport { createCompositor } from './compositor.js';\nexport type { CompositorType } from './compositor.js';\nexport { createCanvas2DCompositor } from './canvas2d-compositor.js';\nexport { createGPUCompositor } from './gpu-compositor.js';\n\n// ── Offline Audio ──────────────────────────────────────────────────────\nexport { createOfflineAudioRenderer } from './offline-audio-renderer.js';\n","import type { MediaDecoder, AssetResolver, MediaInfo } from './types.js';\nimport { Input, BlobSource, CanvasSink, AudioBufferSink, ALL_FORMATS } from 'mediabunny';\n\ninterface CachedAsset {\n input: InstanceType<typeof Input>;\n blob: Blob;\n videoSink: InstanceType<typeof CanvasSink> | null;\n imageBitmap: ImageBitmap | null;\n audioBuffer: AudioBuffer | null;\n mediaInfo: MediaInfo | null;\n}\n\n/**\n * Decodes an image blob and rasterizes it onto a (width × height) OffscreenCanvas\n * with \"contain\" fit (aspect-preserving scale, transparent letterbox). This matches\n * the CanvasSink's `fit: 'contain'` + `alpha: true` behavior so image layers and\n * video layers share the exact same output dimensions — important because the GPU\n * compositor copies external images into a texture of that exact size and rejects\n * mismatches. The `{ alpha: true }` context option preserves the source image's\n * alpha channel (e.g. a PNG with transparency) and leaves letterbox regions fully\n * transparent so underlying layers show through.\n */\nasync function decodeImageFitted(blob: Blob, width: number, height: number): Promise<ImageBitmap> {\n const raw = await createImageBitmap(blob);\n try {\n const canvas = new OffscreenCanvas(width, height);\n const ctx = canvas.getContext('2d', { alpha: true });\n if (!ctx) throw new Error('Failed to get 2d context for image resize');\n const scale = Math.min(width / raw.width, height / raw.height);\n const drawW = raw.width * scale;\n const drawH = raw.height * scale;\n const dx = (width - drawW) / 2;\n const dy = (height - drawH) / 2;\n ctx.drawImage(raw, 0, 0, raw.width, raw.height, dx, dy, drawW, drawH);\n return await createImageBitmap(canvas);\n } finally {\n raw.close();\n }\n}\n\nexport function createMediaDecoder(\n resolver: AssetResolver,\n audioContext: BaseAudioContext,\n): MediaDecoder {\n const cache = new Map<string, CachedAsset>();\n const initPromises = new Map<string, Promise<CachedAsset>>();\n\n async function getOrCreateAsset(assetId: string): Promise<CachedAsset> {\n const existing = cache.get(assetId);\n if (existing) return existing;\n\n const pending = initPromises.get(assetId);\n if (pending) return pending;\n\n const promise = (async () => {\n const blob = await resolver.fetchBlob(assetId);\n const input = new Input({ source: new BlobSource(blob), formats: ALL_FORMATS });\n const asset: CachedAsset = { input, blob, videoSink: null, imageBitmap: null, audioBuffer: null, mediaInfo: null };\n cache.set(assetId, asset);\n initPromises.delete(assetId);\n return asset;\n })();\n\n // Remove from initPromises on failure so future attempts can retry\n promise.catch(() => {\n initPromises.delete(assetId);\n });\n\n initPromises.set(assetId, promise);\n return promise;\n }\n\n return {\n async decodeVideoFrame(assetId, time, width, height) {\n const asset = await getOrCreateAsset(assetId);\n\n // Image fast path — cached ImageBitmap (already fitted to the target\n // dimensions), timestamp ignored. An image clip behaves as a static\n // frame that occupies its Clip's full duration.\n if (asset.imageBitmap) return asset.imageBitmap;\n\n if (!asset.videoSink) {\n let videoTrack: Awaited<ReturnType<typeof asset.input.getPrimaryVideoTrack>> | null = null;\n try {\n videoTrack = await asset.input.getPrimaryVideoTrack();\n } catch {\n // MediaBunny couldn't parse this blob as a media container — likely\n // a standalone image (png/jpg/webp/gif). Fall through to ImageBitmap.\n }\n if (!videoTrack) {\n try {\n asset.imageBitmap = await decodeImageFitted(asset.blob, width, height);\n return asset.imageBitmap;\n } catch (err) {\n throw new Error(\n `Asset ${assetId} has no video track and is not a decodable image: ${(err as Error).message}`,\n );\n }\n }\n asset.videoSink = new CanvasSink(videoTrack, {\n width,\n height,\n fit: 'contain',\n poolSize: 5,\n // Preserve source alpha — required for transparent videos (e.g. WebM\n // with VP8/VP9 alpha, HEVC/AV1 with alpha). With `alpha: false`\n // (mediabunny default) CanvasSink flattens transparent pixels to a\n // black background, breaking compositing of alpha videos.\n alpha: true,\n });\n }\n const result = await asset.videoSink.getCanvas(time);\n if (!result) throw new Error(`Failed to decode frame at ${time}s for asset ${assetId}`);\n return result.canvas;\n },\n\n async decodeAudio(assetId) {\n const asset = await getOrCreateAsset(assetId);\n if (asset.audioBuffer) return asset.audioBuffer;\n\n // Fast path: try browser-native decodeAudioData on the whole blob.\n // This works for standalone audio files (mp3/wav/flac/ogg/m4a) and is\n // much simpler and faster than iterating through MediaBunny packets.\n try {\n const arrayBuffer = await asset.blob.arrayBuffer();\n // decodeAudioData detaches the buffer, so pass a copy (slice(0) is cheap).\n const buffer = await audioContext.decodeAudioData(arrayBuffer.slice(0));\n asset.audioBuffer = buffer;\n return buffer;\n } catch {\n // Fall through to MediaBunny path — typical for video containers whose\n // audio track the browser's decodeAudioData can't extract directly.\n }\n\n const audioTrack = await asset.input.getPrimaryAudioTrack();\n if (!audioTrack) throw new Error(`No audio track in asset ${assetId}`);\n\n // Iterate the full track via AudioBufferSink.buffers() and concatenate\n // into one AudioBuffer. getBuffer(0) alone only returns the first packet.\n const sink = new AudioBufferSink(audioTrack);\n const chunks: AudioBuffer[] = [];\n let totalFrames = 0;\n let sampleRate = 0;\n let numberOfChannels = 0;\n for await (const wrapped of sink.buffers()) {\n const buf = wrapped.buffer;\n chunks.push(buf);\n totalFrames += buf.length;\n if (!sampleRate) sampleRate = buf.sampleRate;\n if (!numberOfChannels) numberOfChannels = buf.numberOfChannels;\n }\n if (chunks.length === 0 || totalFrames === 0) {\n throw new Error(`Failed to decode audio for asset ${assetId}`);\n }\n\n const merged = audioContext.createBuffer(numberOfChannels, totalFrames, sampleRate);\n let offset = 0;\n for (const chunk of chunks) {\n for (let ch = 0; ch < numberOfChannels; ch++) {\n const src = chunk.getChannelData(Math.min(ch, chunk.numberOfChannels - 1));\n merged.copyToChannel(src, ch, offset);\n }\n offset += chunk.length;\n }\n asset.audioBuffer = merged;\n return merged;\n },\n\n async getMediaInfo(assetId) {\n const asset = await getOrCreateAsset(assetId);\n if (asset.mediaInfo) return asset.mediaInfo;\n const videoTrack = await asset.input.getPrimaryVideoTrack();\n const audioTrack = await asset.input.getPrimaryAudioTrack();\n const duration = await asset.input.computeDuration();\n let fps = 0;\n if (videoTrack) {\n const stats = await videoTrack.computePacketStats(100);\n fps = stats.averagePacketRate;\n }\n const info: MediaInfo = {\n duration,\n width: videoTrack?.displayWidth ?? 0,\n height: videoTrack?.displayHeight ?? 0,\n fps,\n hasVideo: videoTrack !== null,\n hasAudio: audioTrack !== null,\n videoCodec: videoTrack?.codec ?? null,\n audioCodec: audioTrack?.codec ?? null,\n sampleRate: audioTrack?.sampleRate ?? 0,\n channels: audioTrack?.numberOfChannels ?? 0,\n };\n asset.mediaInfo = info;\n return info;\n },\n\n destroy() {\n for (const asset of cache.values()) {\n asset.input.dispose();\n asset.imageBitmap?.close();\n }\n cache.clear();\n initPromises.clear();\n },\n };\n}\n","import type { Compositor, CompositeLayer } from './types.js';\n\nexport function createCanvas2DCompositor(width: number, height: number): Compositor {\n // `{ alpha: true }` keeps the backing buffer's alpha channel, so transparent\n // pixels from source layers (alpha videos, PNGs, letterboxed content) remain\n // transparent in the composed ImageBitmap instead of being flattened to black.\n let canvas = new OffscreenCanvas(width, height);\n let ctx = canvas.getContext('2d', { alpha: true })!;\n let currentWidth = width;\n let currentHeight = height;\n\n return {\n async composite(layers: CompositeLayer[]): Promise<ImageBitmap> {\n ctx.clearRect(0, 0, currentWidth, currentHeight);\n\n // Sort by zIndex ascending (low = bottom, high = top)\n const sorted = [...layers].sort((a, b) => a.zIndex - b.zIndex);\n\n for (const layer of sorted) {\n if (layer.opacity <= 0) continue;\n\n ctx.globalAlpha = layer.opacity;\n ctx.drawImage(layer.source, 0, 0, currentWidth, currentHeight);\n }\n\n ctx.globalAlpha = 1;\n return createImageBitmap(canvas);\n },\n\n resize(w: number, h: number): void {\n currentWidth = w;\n currentHeight = h;\n canvas = new OffscreenCanvas(w, h);\n ctx = canvas.getContext('2d', { alpha: true })!;\n },\n\n destroy(): void {\n // OffscreenCanvas has no explicit cleanup needed\n },\n };\n}\n","import type { Compositor, CompositeLayer } from './types.js';\n\nconst VERTEX_SHADER = /* wgsl */ `\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) texCoord: vec2f,\n};\n\n@vertex\nfn main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {\n // Full-screen quad: 6 vertices, 2 triangles\n var positions = array<vec2f, 6>(\n vec2f(-1.0, -1.0),\n vec2f( 1.0, -1.0),\n vec2f(-1.0, 1.0),\n vec2f(-1.0, 1.0),\n vec2f( 1.0, -1.0),\n vec2f( 1.0, 1.0),\n );\n\n var texCoords = array<vec2f, 6>(\n vec2f(0.0, 1.0),\n vec2f(1.0, 1.0),\n vec2f(0.0, 0.0),\n vec2f(0.0, 0.0),\n vec2f(1.0, 1.0),\n vec2f(1.0, 0.0),\n );\n\n var output: VertexOutput;\n output.position = vec4f(positions[vertexIndex], 0.0, 1.0);\n output.texCoord = texCoords[vertexIndex];\n return output;\n}\n`;\n\nconst FRAGMENT_SHADER = /* wgsl */ `\n@group(0) @binding(0) var texSampler: sampler;\n@group(0) @binding(1) var texSource: texture_2d<f32>;\n@group(0) @binding(2) var<uniform> opacity: f32;\n\n@fragment\nfn main(@location(0) texCoord: vec2f) -> @location(0) vec4f {\n var color = textureSample(texSource, texSampler, texCoord);\n // Multiply alpha by uniform opacity\n color.a = color.a * opacity;\n // Premultiply RGB by the new alpha\n color = vec4f(color.rgb * color.a, color.a);\n return color;\n}\n`;\n\nexport async function createGPUCompositor(width: number, height: number): Promise<Compositor> {\n if (!navigator.gpu) {\n throw new Error('WebGPU not available');\n }\n const adapter = await navigator.gpu.requestAdapter();\n if (!adapter) {\n throw new Error('Failed to get WebGPU adapter');\n }\n\n const device = await adapter.requestDevice();\n\n // Create sampler\n const sampler = device.createSampler({\n magFilter: 'linear',\n minFilter: 'linear',\n });\n\n // Create uniform buffer for opacity (f32 = 4 bytes, aligned to 16)\n const opacityBuffer = device.createBuffer({\n size: 16,\n usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,\n });\n\n // Create bind group layout\n const bindGroupLayout = device.createBindGroupLayout({\n entries: [\n { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: {} },\n { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: {} },\n { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },\n ],\n });\n\n const pipelineLayout = device.createPipelineLayout({\n bindGroupLayouts: [bindGroupLayout],\n });\n\n // Create shader modules\n const vertexModule = device.createShaderModule({ code: VERTEX_SHADER });\n const fragmentModule = device.createShaderModule({ code: FRAGMENT_SHADER });\n\n // Create render pipeline with premultiplied alpha blending\n const pipeline = device.createRenderPipeline({\n layout: pipelineLayout,\n vertex: {\n module: vertexModule,\n entryPoint: 'main',\n },\n fragment: {\n module: fragmentModule,\n entryPoint: 'main',\n targets: [{\n format: 'rgba8unorm',\n blend: {\n color: {\n srcFactor: 'one',\n dstFactor: 'one-minus-src-alpha',\n operation: 'add',\n },\n alpha: {\n srcFactor: 'one',\n dstFactor: 'one-minus-src-alpha',\n operation: 'add',\n },\n },\n }],\n },\n primitive: {\n topology: 'triangle-list',\n },\n });\n\n // Create output texture\n let currentWidth = width;\n let currentHeight = height;\n let outputTexture = createOutputTexture(device, width, height);\n\n function createOutputTexture(dev: GPUDevice, w: number, h: number): GPUTexture {\n return dev.createTexture({\n size: { width: w, height: h },\n format: 'rgba8unorm',\n usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.TEXTURE_BINDING,\n });\n }\n\n return {\n async composite(layers: CompositeLayer[]): Promise<ImageBitmap> {\n const sorted = [...layers]\n .filter((l) => l.opacity > 0)\n .sort((a, b) => a.zIndex - b.zIndex);\n\n const encoder = device.createCommandEncoder();\n const outputView = outputTexture.createView();\n\n const layerTextures: GPUTexture[] = [];\n\n if (sorted.length === 0) {\n // Clear pass only\n const pass = encoder.beginRenderPass({\n colorAttachments: [{\n view: outputView,\n loadOp: 'clear',\n storeOp: 'store',\n clearValue: { r: 0, g: 0, b: 0, a: 0 },\n }],\n });\n pass.end();\n } else {\n for (let i = 0; i < sorted.length; i++) {\n const layer = sorted[i];\n\n // Create texture from source\n const layerTexture = device.createTexture({\n size: { width: currentWidth, height: currentHeight },\n format: 'rgba8unorm',\n usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,\n });\n layerTextures.push(layerTexture);\n\n device.queue.copyExternalImageToTexture(\n { source: layer.source as ImageBitmap },\n { texture: layerTexture },\n { width: currentWidth, height: currentHeight },\n );\n\n // Write opacity uniform\n const opacityData = new Float32Array([layer.opacity]);\n device.queue.writeBuffer(opacityBuffer, 0, opacityData);\n\n // Create bind group for this layer\n const bindGroup = device.createBindGroup({\n layout: bindGroupLayout,\n entries: [\n { binding: 0, resource: sampler },\n { binding: 1, resource: layerTexture.createView() },\n { binding: 2, resource: { buffer: opacityBuffer } },\n ],\n });\n\n // Render pass: first pass clears, subsequent passes load\n const loadOp: 'load' | 'clear' = i === 0 ? 'clear' : 'load';\n const pass = encoder.beginRenderPass({\n colorAttachments: [{\n view: outputView,\n loadOp,\n storeOp: 'store',\n ...(loadOp === 'clear' ? { clearValue: { r: 0, g: 0, b: 0, a: 0 } } : {}),\n }],\n });\n\n pass.setPipeline(pipeline);\n pass.setBindGroup(0, bindGroup);\n pass.draw(6);\n pass.end();\n }\n }\n\n // Copy output texture to staging buffer for CPU readback\n const bytesPerRow = Math.ceil(currentWidth * 4 / 256) * 256;\n const bufferSize = bytesPerRow * currentHeight;\n const stagingBuffer = device.createBuffer({\n size: bufferSize,\n usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,\n });\n\n encoder.copyTextureToBuffer(\n { texture: outputTexture },\n { buffer: stagingBuffer, bytesPerRow },\n { width: currentWidth, height: currentHeight },\n );\n\n device.queue.submit([encoder.finish()]);\n\n // Destroy layer textures after command submission\n for (const tex of layerTextures) {\n tex.destroy();\n }\n\n // Read back pixels from staging buffer\n await stagingBuffer.mapAsync(GPUMapMode.READ);\n const rawData = stagingBuffer.getMappedRange();\n\n // Copy pixel data row by row (bytesPerRow may have padding)\n const pixelData = new Uint8ClampedArray(currentWidth * currentHeight * 4);\n const src = new Uint8Array(rawData);\n for (let row = 0; row < currentHeight; row++) {\n const srcOffset = row * bytesPerRow;\n const dstOffset = row * currentWidth * 4;\n pixelData.set(src.subarray(srcOffset, srcOffset + currentWidth * 4), dstOffset);\n }\n\n stagingBuffer.unmap();\n stagingBuffer.destroy();\n\n // The fragment shader outputs premultiplied RGB (see FRAGMENT_SHADER),\n // so `pixelData` is in premultiplied form. `putImageData` treats its\n // input as straight (unpremultiplied) alpha — feeding premultiplied\n // bytes in would cause the canvas to double-premultiply at semi-\n // transparent pixels, producing darkened/wrong colors on alpha-blended\n // edges. Unpremultiply so the straight-alpha ImageData contract holds.\n for (let i = 0; i < pixelData.length; i += 4) {\n const a = pixelData[i + 3];\n if (a > 0 && a < 255) {\n const inv = 255 / a;\n pixelData[i] = Math.min(255, Math.round(pixelData[i] * inv));\n pixelData[i + 1] = Math.min(255, Math.round(pixelData[i + 1] * inv));\n pixelData[i + 2] = Math.min(255, Math.round(pixelData[i + 2] * inv));\n }\n }\n\n // `{ alpha: true }` keeps the readback canvas's alpha channel so a fully\n // transparent output texture stays transparent (default `alpha: false`\n // would flatten it to opaque black).\n const readbackCanvas = new OffscreenCanvas(currentWidth, currentHeight);\n const ctx = readbackCanvas.getContext('2d', { alpha: true })!;\n const imageData = new ImageData(pixelData, currentWidth, currentHeight);\n ctx.putImageData(imageData, 0, 0);\n return createImageBitmap(readbackCanvas);\n },\n\n resize(w: number, h: number): void {\n outputTexture.destroy();\n currentWidth = w;\n currentHeight = h;\n outputTexture = createOutputTexture(device, w, h);\n },\n\n destroy(): void {\n outputTexture.destroy();\n opacityBuffer.destroy();\n device.destroy();\n },\n };\n}\n","import type { Compositor } from './types.js';\nimport { createCanvas2DCompositor } from './canvas2d-compositor.js';\nimport { createGPUCompositor } from './gpu-compositor.js';\n\nexport type CompositorType = 'gpu' | 'canvas2d' | 'auto';\n\nexport async function createCompositor(\n width: number,\n height: number,\n type: CompositorType = 'auto',\n): Promise<Compositor> {\n if (type === 'canvas2d') {\n return createCanvas2DCompositor(width, height);\n }\n\n if (type === 'gpu' || type === 'auto') {\n if (typeof navigator !== 'undefined' && (navigator as unknown as { gpu?: unknown }).gpu) {\n try {\n return await createGPUCompositor(width, height);\n } catch {\n if (type === 'gpu') {\n throw new Error('WebGPU compositor requested but initialization failed');\n }\n }\n } else if (type === 'gpu') {\n throw new Error('WebGPU not available');\n }\n }\n\n return createCanvas2DCompositor(width, height);\n}\n","import type {\n FrameRenderer,\n RenderedFrame,\n MediaDecoder,\n Compositor,\n CompositeLayer,\n SubtitleRenderer,\n} from './types.js';\nimport type { Composition, ResolvedClip, ResolvedPreviewFrame } from '@pneuma-craft/timeline';\nimport { resolveFrame } from '@pneuma-craft/timeline';\n\nexport interface CreateFrameRendererOptions {\n readonly subtitleRenderer?: SubtitleRenderer;\n /**\n * Render preview frames in tracks where no real clip covers the current\n * time. Defaults to false — preview frames are a planning-layer visual\n * that should NOT appear in final exports unless explicitly requested.\n * Engines wire their own defaults: PlaybackEngine passes true, ExportEngine\n * passes false.\n */\n readonly includePreviewFrames?: boolean;\n}\n\n// Accept either the legacy positional SubtitleRenderer (pre-preview-frame\n// callers) or the new options object. Internal callers (playback-engine,\n// export-engine) and tests have been migrated to the options form.\ntype LegacyArg = SubtitleRenderer | CreateFrameRendererOptions | undefined;\n\nfunction normalizeOptions(arg: LegacyArg): CreateFrameRendererOptions {\n if (arg === undefined) return {};\n if (typeof arg === 'function') return { subtitleRenderer: arg };\n return arg;\n}\n\nexport function createFrameRenderer(\n decoder: MediaDecoder,\n compositor: Compositor,\n width: number,\n height: number,\n optionsOrLegacySubtitleRenderer?: LegacyArg,\n): FrameRenderer {\n const options = normalizeOptions(optionsOrLegacySubtitleRenderer);\n const { subtitleRenderer, includePreviewFrames = false } = options;\n\n return {\n async renderFrame(composition: Composition, time: number): Promise<RenderedFrame> {\n const resolved = resolveFrame(composition, time);\n\n // Bucket per track for O(1) lookup. resolveFrame guarantees clips and\n // previewFrames are mutually exclusive per track, so we walk\n // composition.tracks once and pick at most one bucket per track.\n const clipsByTrack = new Map<string, ResolvedClip[]>();\n for (const rc of resolved.clips) {\n const arr = clipsByTrack.get(rc.track.id);\n if (arr) arr.push(rc); else clipsByTrack.set(rc.track.id, [rc]);\n }\n const previewByTrack = new Map<string, ResolvedPreviewFrame>();\n for (const rpf of resolved.previewFrames) previewByTrack.set(rpf.track.id, rpf);\n\n const layers: CompositeLayer[] = [];\n let zIndex = 0;\n\n // Video layers — iterate tracks in array order so z-stack matches\n // composition.tracks. Per-track decision: clip wins; preview is the\n // fallback only when allowed by the renderer's includePreviewFrames flag.\n for (const track of composition.tracks) {\n if (track.type !== 'video') continue;\n\n const trackClips = clipsByTrack.get(track.id);\n if (trackClips && trackClips.length > 0) {\n for (const rc of trackClips) {\n const source = await decoder.decodeVideoFrame(\n rc.clip.assetId, rc.localTime, width, height,\n );\n layers.push({ source, opacity: 1, zIndex: zIndex++ });\n }\n continue;\n }\n\n if (includePreviewFrames) {\n const rpf = previewByTrack.get(track.id);\n if (rpf) {\n // Image asset: decoder.decodeVideoFrame uses the cached-ImageBitmap\n // fast path and ignores the time argument.\n const source = await decoder.decodeVideoFrame(\n rpf.previewFrame.assetId, 0, width, height,\n );\n layers.push({ source, opacity: 1, zIndex: zIndex++ });\n }\n }\n }\n\n // Subtitles always composite on top of every video / preview layer,\n // regardless of track ordering in the composition. Track ordering\n // between multiple subtitle tracks is preserved.\n if (subtitleRenderer) {\n const subtitleClips = resolved.clips.filter(rc => rc.track.type === 'subtitle');\n for (const rc of subtitleClips) {\n const rendered = await subtitleRenderer({\n clip: rc.clip,\n localTime: rc.localTime,\n width,\n height,\n });\n if (rendered) {\n layers.push({ source: rendered, opacity: 1, zIndex: zIndex++ });\n }\n }\n }\n\n const image = await compositor.composite(layers);\n\n return { image, time, width, height };\n },\n\n destroy(): void {\n decoder.destroy();\n compositor.destroy();\n },\n };\n}\n","import type { MasterClock, ClockState } from './types.js';\n\nexport interface MasterClockOptions {\n audioContext: AudioContext;\n duration?: number;\n frameRate?: number;\n}\n\nexport function createMasterClock(options: MasterClockOptions): MasterClock {\n const audioContext = options.audioContext;\n\n let state: ClockState = 'stopped';\n let _duration = options.duration ?? 0;\n let _playbackRate = 1;\n let _loop: { start: number; end: number } | null = null;\n\n // Timing anchors\n let startAudioContextTime = 0;\n let startTimelineTime = 0;\n let pausedAt = 0;\n\n // Drift tracking\n let _driftMs = 0;\n\n // Subscribers\n const timeUpdateListeners = new Set<(time: number) => void>();\n const stateChangeListeners = new Set<(state: ClockState) => void>();\n\n function computeCurrentTime(): number {\n if (state !== 'playing') return pausedAt;\n\n const elapsed = (audioContext.currentTime - startAudioContextTime) * _playbackRate;\n let time = startTimelineTime + elapsed;\n\n if (_loop && _loop.end > _loop.start && time >= _loop.end) {\n const loopDuration = _loop.end - _loop.start;\n time = _loop.start + ((time - _loop.start) % loopDuration);\n }\n\n return Math.max(0, Math.min(time, _duration || Infinity));\n }\n\n function notifyTimeUpdate(time: number): void {\n for (const cb of timeUpdateListeners) {\n try { cb(time); } catch (e) { console.error('[MasterClock] listener error:', e); }\n }\n }\n\n function notifyStateChange(newState: ClockState): void {\n for (const cb of stateChangeListeners) {\n try { cb(newState); } catch (e) { console.error('[MasterClock] listener error:', e); }\n }\n }\n\n function setState(newState: ClockState): void {\n if (state === newState) return;\n state = newState;\n notifyStateChange(newState);\n }\n\n const clock: MasterClock = {\n get currentTime() { return computeCurrentTime(); },\n get state() { return state; },\n get driftMs() { return _driftMs; },\n\n get playbackRate() { return _playbackRate; },\n set playbackRate(rate: number) {\n const clamped = Math.max(0.1, Math.min(rate, 16));\n if (state === 'playing') {\n const current = computeCurrentTime();\n startTimelineTime = current;\n startAudioContextTime = audioContext.currentTime;\n }\n _playbackRate = clamped;\n },\n\n get duration() { return _duration; },\n set duration(d: number) { _duration = Math.max(0, d); },\n\n get loop() { return _loop; },\n set loop(l) { _loop = l; },\n\n play(): void {\n if (state === 'playing') return;\n startTimelineTime = pausedAt;\n startAudioContextTime = audioContext.currentTime;\n setState('playing');\n if (audioContext.state === 'suspended') {\n audioContext.resume();\n }\n },\n\n pause(): void {\n if (state !== 'playing') return;\n pausedAt = computeCurrentTime();\n setState('paused');\n },\n\n seek(time: number): void {\n const clamped = Math.max(0, Math.min(time, _duration));\n if (state === 'playing') {\n startTimelineTime = clamped;\n startAudioContextTime = audioContext.currentTime;\n } else {\n pausedAt = clamped;\n }\n notifyTimeUpdate(clamped);\n },\n\n reportVideoTime(videoTime: number): void {\n const audioTime = computeCurrentTime();\n _driftMs = (audioTime - videoTime) * 1000;\n },\n\n onTimeUpdate(cb) {\n timeUpdateListeners.add(cb);\n return () => { timeUpdateListeners.delete(cb); };\n },\n\n onStateChange(cb) {\n stateChangeListeners.add(cb);\n return () => { stateChangeListeners.delete(cb); };\n },\n\n destroy(): void {\n if (state === 'playing') {\n pausedAt = computeCurrentTime();\n }\n state = 'stopped';\n timeUpdateListeners.clear();\n stateChangeListeners.clear();\n _driftMs = 0;\n },\n };\n\n return clock;\n}\n","import type { AudioScheduler } from './types.js';\nimport type { Composition, Clip, Track } from '@pneuma-craft/timeline';\n\nexport interface AudioSchedulerOptions {\n audioContext: AudioContext;\n}\n\ninterface ActiveSource {\n source: AudioBufferSourceNode;\n clipGain: GainNode;\n}\n\nexport function createAudioScheduler(options: AudioSchedulerOptions): AudioScheduler {\n const { audioContext } = options;\n\n // Buffer storage: clipId → AudioBuffer\n const clipBuffers = new Map<string, AudioBuffer>();\n\n // Per-track gain nodes: trackId → GainNode\n const trackGains = new Map<string, GainNode>();\n\n // Per-track volume (separate from mute): trackId → volume\n const trackVolumes = new Map<string, number>();\n\n // Per-track muted state: trackId → muted\n const trackMutedStates = new Map<string, boolean>();\n\n // Currently active sources\n let activeSources: ActiveSource[] = [];\n\n // Scheduler tick state for future clip scheduling\n let schedulerIntervalId: ReturnType<typeof setInterval> | null = null;\n let scheduledClipIds = new Set<string>();\n let currentComposition: Composition | null = null;\n\n // Playback rate (applied to elapsed time and source nodes)\n let _playbackRate = 1;\n\n // Whether playback is currently active\n let _isPlaying = false;\n\n // Previous timeline time for loop-wrap detection\n let _prevTimelineTime = 0;\n\n // Callback to get current timeline time from the master clock (loop-aware)\n let _getCurrentTime: (() => number) | null = null;\n\n // Look-ahead window in seconds\n const LOOK_AHEAD = 0.2;\n\n // Master gain node connected to destination\n const masterGain = audioContext.createGain();\n masterGain.gain.value = 1;\n masterGain.connect(audioContext.destination);\n\n function getOrCreateTrackGain(track: Track): GainNode {\n let node = trackGains.get(track.id);\n if (!node) {\n node = audioContext.createGain();\n // Use runtime overrides if they exist, otherwise fall back to composition data\n const vol = trackVolumes.get(track.id) ?? track.volume ?? 1;\n const muted = trackMutedStates.get(track.id) ?? track.muted;\n node.gain.value = muted ? 0 : vol;\n trackVolumes.set(track.id, vol);\n node.connect(masterGain);\n trackGains.set(track.id, node);\n }\n return node;\n }\n\n function clearSchedulerTick(): void {\n if (schedulerIntervalId !== null) {\n clearInterval(schedulerIntervalId);\n schedulerIntervalId = null;\n }\n }\n\n function stopAllSources(): void {\n for (const { source } of activeSources) {\n try {\n source.stop();\n } catch {\n // Source may already be stopped\n }\n source.disconnect();\n }\n activeSources = [];\n }\n\n function isClipActiveAt(clip: Clip, fromTime: number): boolean {\n return clip.startTime <= fromTime && fromTime < clip.startTime + clip.duration;\n }\n\n function scheduleClip(clip: Clip, track: Track, fromTime: number): void {\n const buffer = clipBuffers.get(clip.id);\n if (!buffer) return;\n\n const trackGain = getOrCreateTrackGain(track);\n\n // Create per-clip gain node\n const clipGain = audioContext.createGain();\n const clipVolume = clip.volume ?? 1;\n clipGain.gain.value = clipVolume;\n\n // Create source node\n const source = audioContext.createBufferSource();\n source.buffer = buffer;\n\n // Apply playback rate to source node\n source.playbackRate.value = _playbackRate;\n\n // Connect: source → clipGain → trackGain → masterGain → destination\n source.connect(clipGain);\n clipGain.connect(trackGain);\n\n const now = audioContext.currentTime;\n\n // Determine if the clip starts in the future relative to current timeline time\n let contextStartTime: number;\n let sourceOffset: number;\n let remainingDuration: number;\n\n if (clip.startTime > fromTime) {\n // Future clip: schedule it to start at the correct AudioContext time\n // Divide by playbackRate: at 2x rate, 1 timeline second = 0.5 real seconds\n const timeUntilClipStart = (clip.startTime - fromTime) / _playbackRate;\n contextStartTime = now + timeUntilClipStart;\n sourceOffset = clip.inPoint;\n remainingDuration = clip.duration;\n } else {\n // Already active clip: start immediately with offset\n contextStartTime = 0;\n const elapsed = fromTime - clip.startTime;\n sourceOffset = clip.inPoint + elapsed;\n remainingDuration = clip.duration - elapsed;\n }\n\n // Apply fade automations\n // Fade durations must be divided by playbackRate since AudioContext runs in real time\n if (clip.fadeIn !== undefined && clip.fadeIn > 0) {\n const realFadeIn = clip.fadeIn / _playbackRate;\n if (clip.startTime > fromTime) {\n // Future clip: anchor fade to contextStartTime\n clipGain.gain.setValueAtTime(0, contextStartTime);\n clipGain.gain.linearRampToValueAtTime(clipVolume, contextStartTime + realFadeIn);\n } else {\n const fadeInEnd = clip.startTime + clip.fadeIn;\n if (fromTime < fadeInEnd) {\n // Still within fade-in region\n const fadeElapsed = fromTime - clip.startTime;\n const currentLevel = fadeElapsed > 0 ? (fadeElapsed / clip.fadeIn) * clipVolume : 0;\n const fadeRemaining = (clip.fadeIn - fadeElapsed) / _playbackRate;\n clipGain.gain.setValueAtTime(currentLevel, now);\n clipGain.gain.linearRampToValueAtTime(clipVolume, now + fadeRemaining);\n }\n }\n }\n\n if (clip.fadeOut !== undefined && clip.fadeOut > 0) {\n const realFadeOut = clip.fadeOut / _playbackRate;\n const realDuration = clip.duration / _playbackRate;\n if (clip.startTime > fromTime) {\n // Future clip: anchor fade to contextStartTime\n const fadeOutStart = contextStartTime + realDuration - realFadeOut;\n clipGain.gain.setValueAtTime(clipVolume, fadeOutStart);\n clipGain.gain.linearRampToValueAtTime(0, contextStartTime + realDuration);\n } else {\n const fadeOutStart = clip.startTime + clip.duration - clip.fadeOut;\n const fadeOutContextTime = now + Math.max(0, (fadeOutStart - fromTime) / _playbackRate);\n clipGain.gain.setValueAtTime(clipVolume, fadeOutContextTime);\n clipGain.gain.linearRampToValueAtTime(0, fadeOutContextTime + realFadeOut);\n }\n }\n\n source.start(contextStartTime, sourceOffset, remainingDuration);\n\n activeSources.push({ source, clipGain });\n }\n\n function isClipInLookAhead(clip: Clip, currentTime: number, lookAhead: number): boolean {\n const clipEnd = clip.startTime + clip.duration;\n // Clip starts within the look-ahead window and hasn't ended\n return clip.startTime > currentTime && clip.startTime <= currentTime + lookAhead && clipEnd > currentTime;\n }\n\n function scheduleComposition(fromTime: number, composition: Composition): void {\n for (const track of composition.tracks) {\n // Both audio tracks and video tracks contribute audio — a video clip's\n // embedded audio track gets scheduled alongside pure audio clips. Skip\n // anything that isn't a media track (e.g. subtitle) and anything that\n // the user has muted. Clips whose asset has no audio track never get a\n // buffer loaded (see loadClip in playback-engine.ts), so `scheduleClip`\n // naturally skips them.\n if (track.type !== 'audio' && track.type !== 'video') continue;\n if (track.muted) continue;\n\n for (const clip of track.clips) {\n if (!isClipActiveAt(clip, fromTime)) continue;\n if (scheduledClipIds.has(clip.id)) continue;\n scheduledClipIds.add(clip.id);\n scheduleClip(clip, track, fromTime);\n }\n }\n }\n\n function startSchedulerTick(fromTime: number, composition: Composition): void {\n clearSchedulerTick();\n currentComposition = composition;\n\n // Fallback values for computing timeline time when no getCurrentTime callback\n const tickStartContextTime = audioContext.currentTime;\n const tickStartFromTime = fromTime;\n\n schedulerIntervalId = setInterval(() => {\n if (!currentComposition) return;\n\n // Use clock's getCurrentTime if available (loop-aware), otherwise fallback\n let currentTimelineTime: number;\n if (_getCurrentTime) {\n currentTimelineTime = _getCurrentTime();\n } else {\n const elapsed = (audioContext.currentTime - tickStartContextTime) * _playbackRate;\n currentTimelineTime = tickStartFromTime + elapsed;\n }\n\n // Detect loop wrap: timeline time went backwards\n if (currentTimelineTime < _prevTimelineTime) {\n scheduledClipIds.clear();\n stopAllSources();\n }\n _prevTimelineTime = currentTimelineTime;\n\n for (const track of currentComposition.tracks) {\n if (track.type !== 'audio') continue;\n if (track.muted) continue;\n\n for (const clip of track.clips) {\n if (scheduledClipIds.has(clip.id)) continue;\n if (isClipInLookAhead(clip, currentTimelineTime, LOOK_AHEAD) || isClipActiveAt(clip, currentTimelineTime)) {\n scheduledClipIds.add(clip.id);\n scheduleClip(clip, track, currentTimelineTime);\n }\n }\n }\n }, 100);\n }\n\n const scheduler: AudioScheduler = {\n get audioContext() {\n return audioContext;\n },\n\n loadClip(clipId: string, audioBuffer: AudioBuffer): void {\n clipBuffers.set(clipId, audioBuffer);\n },\n\n play(fromTime: number, composition: Composition, getCurrentTime?: () => number): void {\n stopAllSources();\n clearSchedulerTick();\n scheduledClipIds = new Set();\n _prevTimelineTime = fromTime;\n _getCurrentTime = getCurrentTime ?? null;\n _isPlaying = true;\n currentComposition = composition;\n scheduleComposition(fromTime, composition);\n startSchedulerTick(fromTime, composition);\n },\n\n pause(): void {\n clearSchedulerTick();\n stopAllSources();\n scheduledClipIds = new Set();\n _isPlaying = false;\n },\n\n seek(time: number, composition: Composition): void {\n clearSchedulerTick();\n stopAllSources();\n scheduledClipIds = new Set();\n _prevTimelineTime = time;\n scheduleComposition(time, composition);\n startSchedulerTick(time, composition);\n },\n\n setPlaybackRate(rate: number): void {\n _playbackRate = rate;\n // If playing, reschedule everything with new rate so future clip\n // contextStartTime values are recalculated correctly.\n if (_isPlaying && currentComposition && _getCurrentTime) {\n stopAllSources();\n scheduledClipIds = new Set();\n const currentTime = _getCurrentTime();\n scheduleComposition(currentTime, currentComposition);\n clearSchedulerTick();\n startSchedulerTick(currentTime, currentComposition);\n } else {\n // Not playing or no getCurrentTime — just update active sources\n for (const scheduled of activeSources) {\n scheduled.source.playbackRate.value = rate;\n }\n }\n },\n\n setTrackVolume(trackId: string, volume: number): void {\n trackVolumes.set(trackId, volume);\n const node = trackGains.get(trackId);\n if (node) {\n // Only apply gain if the track is not muted\n const isMuted = trackMutedStates.get(trackId) ?? false;\n if (!isMuted) {\n node.gain.setValueAtTime(volume, audioContext.currentTime);\n }\n }\n },\n\n setTrackMute(trackId: string, muted: boolean): void {\n trackMutedStates.set(trackId, muted);\n const node = trackGains.get(trackId);\n if (node) {\n if (muted) {\n node.gain.value = 0;\n } else {\n const vol = trackVolumes.get(trackId) ?? 1;\n node.gain.value = vol;\n }\n }\n },\n\n destroy(): void {\n clearSchedulerTick();\n stopAllSources();\n scheduledClipIds = new Set();\n currentComposition = null;\n _isPlaying = false;\n for (const node of trackGains.values()) {\n node.disconnect();\n }\n trackGains.clear();\n trackVolumes.clear();\n trackMutedStates.clear();\n clipBuffers.clear();\n masterGain.disconnect();\n },\n };\n\n return scheduler;\n}\n","import type {\n PlaybackEngine,\n PlaybackState,\n RenderedFrame,\n AssetResolver,\n MediaDecoder,\n Compositor,\n FrameRenderer,\n MasterClock,\n AudioScheduler,\n SubtitleRenderer,\n} from './types.js';\nimport type { Composition } from '@pneuma-craft/timeline';\nimport { createMediaDecoder } from './media-decoder.js';\nimport { createCompositor, type CompositorType } from './compositor.js';\nimport { createFrameRenderer } from './frame-renderer.js';\nimport { createMasterClock } from './master-clock.js';\nimport { createAudioScheduler } from './audio-scheduler.js';\n\nexport interface PlaybackEngineOptions {\n compositorType?: CompositorType;\n /** Max time a single frame decode may block before the loop skips it. Default 1000ms. */\n decodeTimeoutMs?: number;\n /**\n * Optional rasterizer for subtitle-track clips. When provided, active\n * subtitle clips are composited on top of the video layers — the same code\n * path used for export, so preview and export stay in lockstep. When\n * omitted, subtitle tracks render nothing (legacy behavior).\n */\n subtitleRenderer?: SubtitleRenderer;\n /**\n * Render preview frames (planning-layer visuals attached to time points\n * on a track) when no real clip covers the current time. Defaults to\n * `true` for the playback engine — the user scrubbing the timeline\n * should see the agent's planned vibe before any real video clips land.\n */\n includePreviewFrames?: boolean;\n}\n\n/** Default per-frame decode timeout. Decodes exceeding this are logged and skipped. */\nconst DEFAULT_DECODE_TIMEOUT_MS = 1000;\n\n/** Enables verbose rAF-loop logs for debugging playback stalls. */\nconst DEBUG_LOOP = typeof globalThis !== 'undefined'\n && (globalThis as { __PNEUMA_CRAFT_PLAYBACK_DEBUG__?: boolean }).__PNEUMA_CRAFT_PLAYBACK_DEBUG__ === true;\n\nexport function createPlaybackEngine(options?: PlaybackEngineOptions): PlaybackEngine {\n const compositorType = options?.compositorType ?? 'canvas2d';\n const decodeTimeoutMs = options?.decodeTimeoutMs ?? DEFAULT_DECODE_TIMEOUT_MS;\n const subtitleRenderer = options?.subtitleRenderer;\n const includePreviewFrames = options?.includePreviewFrames ?? true;\n\n let _state: PlaybackState = 'idle';\n let _playbackRate = 1;\n let _loop: { start: number; end: number } | null = null;\n\n // Subsystems (created on load)\n let _decoder: MediaDecoder | null = null;\n let _compositor: Compositor | null = null;\n let _frameRenderer: FrameRenderer | null = null;\n let _clock: MasterClock | null = null;\n let _audioScheduler: AudioScheduler | null = null;\n let _audioContext: AudioContext | null = null;\n let _composition: Composition | null = null;\n\n // rAF handle\n let _rafId: number | null = null;\n\n // True while a frame decode is awaiting — prevents stacking concurrent renders\n // when one decode runs slower than a rAF tick.\n let _frameInFlight = false;\n\n // Seek ID counter for discarding stale paused-seek renders\n let _seekId = 0;\n\n // Subscribers\n const stateChangeListeners = new Set<(state: PlaybackState) => void>();\n const timeUpdateListeners = new Set<(time: number) => void>();\n const frameRenderedListeners = new Set<(frame: RenderedFrame) => void>();\n\n function setState(newState: PlaybackState): void {\n if (_state === newState) return;\n _state = newState;\n for (const cb of stateChangeListeners) {\n try { cb(newState); } catch (e) { console.error('[PlaybackEngine] listener error:', e); }\n }\n }\n\n function emitTimeUpdate(time: number): void {\n for (const cb of timeUpdateListeners) {\n try { cb(time); } catch (e) { console.error('[PlaybackEngine] listener error:', e); }\n }\n }\n\n function emitFrameRendered(frame: RenderedFrame): void {\n for (const cb of frameRenderedListeners) {\n try { cb(frame); } catch (e) { console.error('[PlaybackEngine] listener error:', e); }\n }\n }\n\n function destroySubsystems(): void {\n if (_rafId !== null) {\n cancelAnimationFrame(_rafId);\n _rafId = null;\n }\n _frameInFlight = false;\n _frameRenderer?.destroy();\n _clock?.destroy();\n _audioScheduler?.destroy();\n _audioContext?.close();\n _decoder = null;\n _compositor = null;\n _frameRenderer = null;\n _clock = null;\n _audioScheduler = null;\n _audioContext = null;\n _composition = null;\n }\n\n function startRafLoop(): void {\n if (_rafId !== null) return;\n\n // The loop is designed so that time updates and rAF scheduling are INDEPENDENT\n // of frame decoding. This means:\n // 1. If a decode hangs or is slow, the clock/slider still advances and pause/seek\n // remain responsive.\n // 2. At most one decode runs at a time (no concurrent pile-up).\n // 3. A decode exceeding `decodeTimeoutMs` is logged and skipped so the loop can\n // recover from a stuck codec packet pump.\n let lastLoggedState: 'render' | 'skip-inflight' | null = null;\n const logStateChange = (state: 'render' | 'skip-inflight', time: number, extra?: string): void => {\n if (!DEBUG_LOOP) return;\n if (lastLoggedState === state) return;\n lastLoggedState = state;\n console.log(`[engine] ${state} @ t=${time.toFixed(3)}${extra ? ' ' + extra : ''}`);\n };\n\n const loop = (): void => {\n _rafId = null;\n if (_state !== 'playing' || !_clock || !_frameRenderer || !_composition) return;\n\n const time = _clock.currentTime;\n\n // Emit time update up-front — consumers see the clock tick regardless of decode state.\n emitTimeUpdate(time);\n\n // End-of-timeline check.\n if (!_clock.loop && time >= _composition.duration) {\n engine.pause();\n return;\n }\n\n // Always schedule the next rAF synchronously so timing is decoupled from decode.\n _rafId = requestAnimationFrame(loop);\n\n // Skip this tick's render if the previous decode is still in flight — back-pressure\n // so slow decodes don't stack concurrent calls. The watchdog below recovers if a\n // decode never resolves; we never get stuck.\n //\n // NOTE: we intentionally do NOT skip based on _clock.driftMs. A prior implementation\n // did, which caused a deadlock: when the first decode ran long (e.g., cold-start\n // fetch of a 50 MB blob), reportVideoTime captured a large stale-drift value, then\n // every subsequent tick skipped on drift, so reportVideoTime never ran again, and\n // drift was frozen at the stale value forever. The clock is the source of truth;\n // each render just targets its current value, which naturally catches up.\n if (_frameInFlight) {\n logStateChange('skip-inflight', time);\n return;\n }\n\n logStateChange('render', time);\n\n // Kick off the decode. A watchdog timer clears _frameInFlight so the loop can\n // recover if renderFrame never resolves. We attach .then directly (no Promise.race)\n // to preserve the microtask profile — emitFrameRendered fires on the same tick\n // that renderFrame resolves.\n _frameInFlight = true;\n const clockRef = _clock;\n const compositionRef = _composition;\n let timedOut = false;\n const timeoutId = setTimeout(() => {\n timedOut = true;\n _frameInFlight = false;\n console.warn(\n `[PlaybackEngine] decode timeout at t=${time.toFixed(3)}s (>${decodeTimeoutMs}ms) — skipping frame`,\n );\n }, decodeTimeoutMs);\n\n _frameRenderer.renderFrame(compositionRef, time).then(frame => {\n clearTimeout(timeoutId);\n if (timedOut) return;\n _frameInFlight = false;\n if (_state !== 'playing' || _clock !== clockRef) return;\n clockRef.reportVideoTime(time);\n emitFrameRendered(frame);\n }).catch(err => {\n clearTimeout(timeoutId);\n if (timedOut) return;\n _frameInFlight = false;\n console.error('[PlaybackEngine] render error:', err);\n });\n };\n\n _rafId = requestAnimationFrame(loop);\n }\n\n function stopRafLoop(): void {\n if (_rafId !== null) {\n cancelAnimationFrame(_rafId);\n _rafId = null;\n }\n _frameInFlight = false;\n }\n\n const engine: PlaybackEngine = {\n get state() { return _state; },\n\n get currentTime() {\n return _clock?.currentTime ?? 0;\n },\n\n get playbackRate() {\n return _clock?.playbackRate ?? _playbackRate;\n },\n set playbackRate(rate: number) {\n _playbackRate = rate;\n if (_clock) {\n _clock.playbackRate = rate;\n }\n if (_audioScheduler) {\n _audioScheduler.setPlaybackRate(rate);\n }\n },\n\n get loop() {\n return _clock?.loop ?? _loop;\n },\n set loop(l: { start: number; end: number } | null) {\n _loop = l;\n if (_clock) {\n _clock.loop = l;\n }\n },\n\n async load(composition: Composition, resolver: AssetResolver): Promise<void> {\n // Invalidate any pending paused-seek renders from previous session\n _seekId++;\n\n // If previously loaded, destroy old subsystems\n if (_state !== 'idle') {\n destroySubsystems();\n }\n\n setState('loading');\n\n try {\n // Create AudioContext\n _audioContext = new AudioContext();\n\n // Create subsystems\n _decoder = createMediaDecoder(resolver, _audioContext);\n _compositor = await createCompositor(\n composition.settings.width,\n composition.settings.height,\n compositorType,\n );\n _frameRenderer = createFrameRenderer(\n _decoder,\n _compositor,\n composition.settings.width,\n composition.settings.height,\n { subtitleRenderer, includePreviewFrames },\n );\n _clock = createMasterClock({\n audioContext: _audioContext,\n duration: composition.duration,\n frameRate: composition.settings.fps,\n });\n _audioScheduler = createAudioScheduler({ audioContext: _audioContext });\n\n // Apply pending settings\n _clock.playbackRate = _playbackRate;\n _clock.loop = _loop;\n\n _composition = composition;\n\n // Pre-load audio buffers from every clip on every audio OR video track.\n // Video clips carry their own embedded audio (MediaDecoder.decodeAudio\n // extracts it from the container), so they participate in the audio\n // graph just like standalone audio clips. Clips with no audio stream\n // (images, silent videos) throw — we swallow and skip so they simply\n // have no buffer loaded, which makes scheduleClip a no-op for them.\n const mediaClips = composition.tracks\n .filter(track => track.type === 'audio' || track.type === 'video')\n .flatMap(track => track.clips);\n\n for (const clip of mediaClips) {\n try {\n const audioBuffer = await _decoder.decodeAudio(clip.assetId);\n _audioScheduler.loadClip(clip.id, audioBuffer);\n } catch {\n // Clip has no audio (image asset, silent video, or decode failure).\n // Leave the clip without a buffer; audio-scheduler tolerates this.\n }\n }\n\n setState('ready');\n } catch (err) {\n // Clean up any partially-created subsystems\n destroySubsystems();\n setState('idle');\n throw err;\n }\n },\n\n play(): void {\n if (!_clock || !_audioScheduler || !_composition) {\n throw new Error('Cannot play: no composition loaded. Call load() first.');\n }\n if (_state === 'playing') return;\n\n // AudioContext may start in 'suspended' state because it was created\n // inside engine.load() after awaits, losing the user-gesture chain.\n // resume() works as long as ANY user gesture has occurred on the page —\n // clicking the play button is sufficient. Fire-and-forget: audio will\n // unmute as soon as resume settles, which in practice is <1 frame.\n if (_audioContext && _audioContext.state === 'suspended') {\n _audioContext.resume().catch((err) => {\n console.warn('[PlaybackEngine] AudioContext resume failed:', err);\n });\n }\n\n _clock.play();\n _audioScheduler.setPlaybackRate(_playbackRate);\n const clockRef = _clock;\n _audioScheduler.play(_clock.currentTime, _composition, () => clockRef.currentTime);\n setState('playing');\n startRafLoop();\n },\n\n pause(): void {\n if (_state !== 'playing') return;\n\n stopRafLoop();\n _clock?.pause();\n _audioScheduler?.pause();\n setState('paused');\n },\n\n seek(time: number): void {\n if (!_clock || !_audioScheduler || !_composition) {\n throw new Error('Cannot seek: no composition loaded. Call load() first.');\n }\n\n _clock.seek(time);\n // Use the clamped time from clock so audio/video stay in sync\n const clampedTime = _clock.currentTime;\n\n // Only reschedule audio when playing — seeking while paused should not produce sound\n if (_state === 'playing') {\n _audioScheduler.setPlaybackRate(_playbackRate);\n _audioScheduler.seek(clampedTime, _composition);\n }\n\n // If not playing, render the frame at the seeked position\n if (_state !== 'playing' && _frameRenderer) {\n const thisSeekId = ++_seekId;\n _frameRenderer.renderFrame(_composition, clampedTime).then(frame => {\n if (thisSeekId !== _seekId) return; // Stale render, discard\n emitFrameRendered(frame);\n emitTimeUpdate(clampedTime);\n }).catch(err => {\n console.error('[PlaybackEngine] seek render error:', err);\n });\n }\n },\n\n onStateChange(cb: (state: PlaybackState) => void): () => void {\n stateChangeListeners.add(cb);\n return () => { stateChangeListeners.delete(cb); };\n },\n\n onTimeUpdate(cb: (time: number) => void): () => void {\n timeUpdateListeners.add(cb);\n return () => { timeUpdateListeners.delete(cb); };\n },\n\n onFrameRendered(cb: (frame: RenderedFrame) => void): () => void {\n frameRenderedListeners.add(cb);\n return () => { frameRenderedListeners.delete(cb); };\n },\n\n destroy(): void {\n stopRafLoop();\n destroySubsystems();\n stateChangeListeners.clear();\n timeUpdateListeners.clear();\n frameRenderedListeners.clear();\n _playbackRate = 1;\n _loop = null;\n _state = 'idle';\n },\n };\n\n return engine;\n}\n","import type { OfflineAudioRenderer } from './types.js';\nimport type { Clip, Track } from '@pneuma-craft/timeline';\n\nexport function createOfflineAudioRenderer(): OfflineAudioRenderer {\n return {\n async render(composition, _resolver, decodeAudio) {\n const sampleRate = composition.settings.sampleRate ?? 48000;\n const channels = 2;\n const duration = composition.duration;\n const length = Math.ceil(duration * sampleRate);\n\n const offlineCtx = new OfflineAudioContext(channels, length, sampleRate);\n\n // Collect clips from every unmuted media track. Video clips contribute\n // their embedded audio exactly like standalone audio clips — this is\n // what keeps the exported audio in lockstep with what the preview\n // plays. Subtitle/other non-media tracks are skipped.\n const audioClips: Array<{ clip: Clip; track: Track }> = [];\n for (const track of composition.tracks) {\n if (track.type !== 'audio' && track.type !== 'video') continue;\n if (track.muted) continue;\n for (const clip of track.clips) {\n audioClips.push({ clip, track });\n }\n }\n\n // Schedule each clip\n for (const { clip, track } of audioClips) {\n let buffer: AudioBuffer;\n try {\n buffer = await decodeAudio(clip.assetId);\n } catch {\n // Clip has no audio stream (image asset, silent video) or decode\n // failed. Skip silently — video-track clips routinely have no\n // audio, so a warning here would be noise. True decode failures\n // on audio assets surface via the preview path instead.\n continue;\n }\n\n const source = offlineCtx.createBufferSource();\n source.buffer = buffer;\n\n const clipGain = offlineCtx.createGain();\n const clipVolume = clip.volume ?? 1;\n clipGain.gain.value = clipVolume;\n\n const trackGain = offlineCtx.createGain();\n trackGain.gain.value = track.volume;\n\n source.connect(clipGain);\n clipGain.connect(trackGain);\n trackGain.connect(offlineCtx.destination);\n\n // Fade automation\n const fadeIn = clip.fadeIn;\n const fadeOut = clip.fadeOut;\n\n if (fadeIn && fadeIn > 0) {\n clipGain.gain.setValueAtTime(0, clip.startTime);\n clipGain.gain.linearRampToValueAtTime(clipVolume, clip.startTime + fadeIn);\n }\n\n if (fadeOut && fadeOut > 0) {\n const fadeOutStart = clip.startTime + clip.duration - fadeOut;\n clipGain.gain.setValueAtTime(clipVolume, fadeOutStart);\n clipGain.gain.linearRampToValueAtTime(0, clip.startTime + clip.duration);\n }\n\n source.start(clip.startTime, clip.inPoint, clip.duration);\n }\n\n return offlineCtx.startRendering();\n },\n };\n}\n","import type { ExportEngine, ExportOptions, AssetResolver, SubtitleRenderer } from './types.js';\nimport type { Composition } from '@pneuma-craft/timeline';\nimport { createMediaDecoder } from './media-decoder.js';\nimport { createCompositor } from './compositor.js';\nimport { createFrameRenderer } from './frame-renderer.js';\nimport { createOfflineAudioRenderer } from './offline-audio-renderer.js';\nimport {\n Output,\n CanvasSource,\n AudioBufferSource,\n Mp4OutputFormat,\n WebMOutputFormat,\n BufferTarget,\n} from 'mediabunny';\n\nexport interface ExportEngineOptions {\n /**\n * Optional rasterizer for subtitle-track clips. Pass the same renderer used\n * by `createPlaybackEngine` so the exported video is pixel-identical to the\n * preview. When omitted, subtitle tracks are skipped during export.\n */\n subtitleRenderer?: SubtitleRenderer;\n /**\n * Include preview frames (planning-layer visuals attached to time points\n * on a track) in the exported video. Defaults to `false` because an export\n * normally represents a finished cut. Set true for review-grade exports of\n * unfinished timelines — e.g., agent producing a \"草样片\" for the user to\n * scrub before committing to expensive real renders.\n */\n includePreviewFrames?: boolean;\n}\n\nexport function createExportEngine(options?: ExportEngineOptions): ExportEngine {\n const subtitleRenderer = options?.subtitleRenderer;\n const includePreviewFrames = options?.includePreviewFrames ?? false;\n const progressListeners = new Set<(progress: number) => void>();\n let abortController: AbortController | null = null;\n\n function notifyProgress(progress: number): void {\n for (const cb of progressListeners) {\n try { cb(progress); } catch (e) { console.error('[ExportEngine]', e); }\n }\n }\n\n return {\n async export(\n composition: Composition,\n options: ExportOptions,\n resolver: AssetResolver,\n ): Promise<Blob> {\n abortController = new AbortController();\n const signal = abortController.signal;\n\n const fps = options.fps ?? composition.settings.fps;\n const width = options.width ?? composition.settings.width;\n const height = options.height ?? composition.settings.height;\n const totalFrames = Math.ceil(composition.duration * fps);\n\n // Minimal OfflineAudioContext used only for decodeAudioData during asset\n // decoding (export has its own offline context for actual audio rendering).\n const decoderAudioCtx = new OfflineAudioContext(1, 1, composition.settings.sampleRate || 48000);\n const decoder = createMediaDecoder(resolver, decoderAudioCtx);\n const compositor = await createCompositor(width, height, 'canvas2d');\n const renderer = createFrameRenderer(decoder, compositor, width, height, {\n subtitleRenderer,\n includePreviewFrames,\n });\n\n try {\n const format = options.format === 'webm'\n ? new WebMOutputFormat()\n : new Mp4OutputFormat({ fastStart: 'in-memory' });\n\n const target = new BufferTarget();\n const output = new Output({ format, target });\n\n const renderCanvas = new OffscreenCanvas(width, height);\n const videoSource = new CanvasSource(renderCanvas, {\n codec: options.videoCodec,\n bitrate: options.videoBitrate,\n });\n\n const audioSource = new AudioBufferSource({\n codec: options.audioCodec,\n bitrate: options.audioBitrate,\n });\n\n output.addVideoTrack(videoSource);\n output.addAudioTrack(audioSource);\n await output.start();\n\n // Render video frames\n const renderCtx = renderCanvas.getContext('2d')!;\n\n for (let frame = 0; frame < totalFrames; frame++) {\n if (signal.aborted) throw new Error('Export aborted');\n\n const time = frame / fps;\n const rendered = await renderer.renderFrame(composition, time);\n\n renderCtx.clearRect(0, 0, width, height);\n renderCtx.drawImage(rendered.image, 0, 0, width, height);\n rendered.image.close();\n\n await videoSource.add(time, 1 / fps);\n\n if ((frame + 1) % 5 === 0 || frame === totalFrames - 1) {\n notifyProgress((frame + 1) / totalFrames);\n }\n\n // Yield to event loop periodically\n if ((frame + 1) % 5 === 0) {\n await new Promise(r => setTimeout(r, 0));\n }\n }\n\n videoSource.close();\n if (signal.aborted) throw new Error('Export aborted');\n\n // Render audio offline\n const offlineRenderer = createOfflineAudioRenderer();\n const audioBuffer = await offlineRenderer.render(\n composition,\n resolver,\n (assetId) => decoder.decodeAudio(assetId),\n );\n if (signal.aborted) throw new Error('Export aborted');\n\n await audioSource.add(audioBuffer);\n audioSource.close();\n if (signal.aborted) throw new Error('Export aborted');\n\n await output.finalize();\n\n const buffer = target.buffer;\n if (!buffer) throw new Error('Export produced no output');\n\n const mimeType = options.format === 'webm' ? 'video/webm' : 'video/mp4';\n return new Blob([buffer], { type: mimeType });\n } finally {\n renderer.destroy();\n abortController = null;\n }\n },\n\n onProgress(cb: (progress: number) => void): () => void {\n progressListeners.add(cb);\n return () => { progressListeners.delete(cb); };\n },\n\n abort(): void {\n abortController?.abort();\n },\n };\n}\n"],"mappings":"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","names":["import_mediabunny","options"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/media-decoder.ts","../src/canvas2d-compositor.ts","../src/gpu-compositor.ts","../src/compositor.ts","../src/frame-renderer.ts","../src/master-clock.ts","../src/audio-scheduler.ts","../src/playback-engine.ts","../src/offline-audio-renderer.ts","../src/export-engine.ts"],"sourcesContent":["// ── Types ──────────────────────────────────────────────────────────────\nexport type {\n AssetResolver,\n MediaInfo,\n MediaDecoder,\n CompositeLayer,\n Compositor,\n RenderedFrame,\n FrameRenderer,\n SubtitleRenderer,\n SubtitleRenderParams,\n ClockState,\n MasterClock,\n AudioScheduler,\n PlaybackState,\n PlaybackEngine,\n ExportOptions,\n ExportEngine,\n OfflineAudioRenderer,\n} from './types.js';\n\n// ── Creation Functions ─────────────────────────────────────────────────\nexport { createPlaybackEngine } from './playback-engine.js';\nexport type { PlaybackEngineOptions } from './playback-engine.js';\nexport { createExportEngine } from './export-engine.js';\nexport type { ExportEngineOptions } from './export-engine.js';\nexport { createFrameRenderer } from './frame-renderer.js';\nexport { createMasterClock } from './master-clock.js';\nexport type { MasterClockOptions } from './master-clock.js';\nexport { createAudioScheduler } from './audio-scheduler.js';\nexport type { AudioSchedulerOptions } from './audio-scheduler.js';\nexport { createMediaDecoder } from './media-decoder.js';\n\n// ── Compositor ─────────────────────────────────────────────────────────\nexport { createCompositor } from './compositor.js';\nexport type { CompositorType } from './compositor.js';\nexport { createCanvas2DCompositor } from './canvas2d-compositor.js';\nexport { createGPUCompositor } from './gpu-compositor.js';\n\n// ── Offline Audio ──────────────────────────────────────────────────────\nexport { createOfflineAudioRenderer } from './offline-audio-renderer.js';\n","import type { MediaDecoder, AssetResolver, MediaInfo } from './types.js';\nimport { Input, BlobSource, CanvasSink, AudioBufferSink, ALL_FORMATS } from 'mediabunny';\n\ninterface CachedAsset {\n input: InstanceType<typeof Input>;\n blob: Blob;\n videoSink: InstanceType<typeof CanvasSink> | null;\n imageBitmap: ImageBitmap | null;\n audioBuffer: AudioBuffer | null;\n mediaInfo: MediaInfo | null;\n}\n\n/**\n * Decodes an image blob and rasterizes it onto a (width × height) OffscreenCanvas\n * with \"contain\" fit (aspect-preserving scale, transparent letterbox). This matches\n * the CanvasSink's `fit: 'contain'` + `alpha: true` behavior so image layers and\n * video layers share the exact same output dimensions — important because the GPU\n * compositor copies external images into a texture of that exact size and rejects\n * mismatches. The `{ alpha: true }` context option preserves the source image's\n * alpha channel (e.g. a PNG with transparency) and leaves letterbox regions fully\n * transparent so underlying layers show through.\n */\nasync function decodeImageFitted(blob: Blob, width: number, height: number): Promise<ImageBitmap> {\n const raw = await createImageBitmap(blob);\n try {\n const canvas = new OffscreenCanvas(width, height);\n const ctx = canvas.getContext('2d', { alpha: true });\n if (!ctx) throw new Error('Failed to get 2d context for image resize');\n const scale = Math.min(width / raw.width, height / raw.height);\n const drawW = raw.width * scale;\n const drawH = raw.height * scale;\n const dx = (width - drawW) / 2;\n const dy = (height - drawH) / 2;\n ctx.drawImage(raw, 0, 0, raw.width, raw.height, dx, dy, drawW, drawH);\n return await createImageBitmap(canvas);\n } finally {\n raw.close();\n }\n}\n\nexport function createMediaDecoder(\n resolver: AssetResolver,\n audioContext: BaseAudioContext,\n): MediaDecoder {\n const cache = new Map<string, CachedAsset>();\n const initPromises = new Map<string, Promise<CachedAsset>>();\n\n async function getOrCreateAsset(assetId: string): Promise<CachedAsset> {\n const existing = cache.get(assetId);\n if (existing) return existing;\n\n const pending = initPromises.get(assetId);\n if (pending) return pending;\n\n const promise = (async () => {\n const blob = await resolver.fetchBlob(assetId);\n const input = new Input({ source: new BlobSource(blob), formats: ALL_FORMATS });\n const asset: CachedAsset = { input, blob, videoSink: null, imageBitmap: null, audioBuffer: null, mediaInfo: null };\n cache.set(assetId, asset);\n initPromises.delete(assetId);\n return asset;\n })();\n\n // Remove from initPromises on failure so future attempts can retry\n promise.catch(() => {\n initPromises.delete(assetId);\n });\n\n initPromises.set(assetId, promise);\n return promise;\n }\n\n return {\n async decodeVideoFrame(assetId, time, width, height) {\n const asset = await getOrCreateAsset(assetId);\n\n // Image fast path — cached ImageBitmap (already fitted to the target\n // dimensions), timestamp ignored. An image clip behaves as a static\n // frame that occupies its Clip's full duration.\n if (asset.imageBitmap) return asset.imageBitmap;\n\n if (!asset.videoSink) {\n let videoTrack: Awaited<ReturnType<typeof asset.input.getPrimaryVideoTrack>> | null = null;\n try {\n videoTrack = await asset.input.getPrimaryVideoTrack();\n } catch {\n // MediaBunny couldn't parse this blob as a media container — likely\n // a standalone image (png/jpg/webp/gif). Fall through to ImageBitmap.\n }\n if (!videoTrack) {\n try {\n asset.imageBitmap = await decodeImageFitted(asset.blob, width, height);\n return asset.imageBitmap;\n } catch (err) {\n throw new Error(\n `Asset ${assetId} has no video track and is not a decodable image: ${(err as Error).message}`,\n );\n }\n }\n asset.videoSink = new CanvasSink(videoTrack, {\n width,\n height,\n fit: 'contain',\n poolSize: 5,\n // Preserve source alpha — required for transparent videos (e.g. WebM\n // with VP8/VP9 alpha, HEVC/AV1 with alpha). With `alpha: false`\n // (mediabunny default) CanvasSink flattens transparent pixels to a\n // black background, breaking compositing of alpha videos.\n alpha: true,\n });\n }\n const result = await asset.videoSink.getCanvas(time);\n if (!result) throw new Error(`Failed to decode frame at ${time}s for asset ${assetId}`);\n return result.canvas;\n },\n\n async decodeAudio(assetId) {\n const asset = await getOrCreateAsset(assetId);\n if (asset.audioBuffer) return asset.audioBuffer;\n\n // Fast path: try browser-native decodeAudioData on the whole blob.\n // This works for standalone audio files (mp3/wav/flac/ogg/m4a) and is\n // much simpler and faster than iterating through MediaBunny packets.\n try {\n const arrayBuffer = await asset.blob.arrayBuffer();\n // decodeAudioData detaches the buffer, so pass a copy (slice(0) is cheap).\n const buffer = await audioContext.decodeAudioData(arrayBuffer.slice(0));\n asset.audioBuffer = buffer;\n return buffer;\n } catch {\n // Fall through to MediaBunny path — typical for video containers whose\n // audio track the browser's decodeAudioData can't extract directly.\n }\n\n const audioTrack = await asset.input.getPrimaryAudioTrack();\n if (!audioTrack) throw new Error(`No audio track in asset ${assetId}`);\n\n // Iterate the full track via AudioBufferSink.buffers() and concatenate\n // into one AudioBuffer. getBuffer(0) alone only returns the first packet.\n const sink = new AudioBufferSink(audioTrack);\n const chunks: AudioBuffer[] = [];\n let totalFrames = 0;\n let sampleRate = 0;\n let numberOfChannels = 0;\n for await (const wrapped of sink.buffers()) {\n const buf = wrapped.buffer;\n chunks.push(buf);\n totalFrames += buf.length;\n if (!sampleRate) sampleRate = buf.sampleRate;\n if (!numberOfChannels) numberOfChannels = buf.numberOfChannels;\n }\n if (chunks.length === 0 || totalFrames === 0) {\n throw new Error(`Failed to decode audio for asset ${assetId}`);\n }\n\n const merged = audioContext.createBuffer(numberOfChannels, totalFrames, sampleRate);\n let offset = 0;\n for (const chunk of chunks) {\n for (let ch = 0; ch < numberOfChannels; ch++) {\n const src = chunk.getChannelData(Math.min(ch, chunk.numberOfChannels - 1));\n merged.copyToChannel(src, ch, offset);\n }\n offset += chunk.length;\n }\n asset.audioBuffer = merged;\n return merged;\n },\n\n async getMediaInfo(assetId) {\n const asset = await getOrCreateAsset(assetId);\n if (asset.mediaInfo) return asset.mediaInfo;\n const videoTrack = await asset.input.getPrimaryVideoTrack();\n const audioTrack = await asset.input.getPrimaryAudioTrack();\n const duration = await asset.input.computeDuration();\n let fps = 0;\n if (videoTrack) {\n const stats = await videoTrack.computePacketStats(100);\n fps = stats.averagePacketRate;\n }\n const info: MediaInfo = {\n duration,\n width: videoTrack?.displayWidth ?? 0,\n height: videoTrack?.displayHeight ?? 0,\n fps,\n hasVideo: videoTrack !== null,\n hasAudio: audioTrack !== null,\n videoCodec: videoTrack?.codec ?? null,\n audioCodec: audioTrack?.codec ?? null,\n sampleRate: audioTrack?.sampleRate ?? 0,\n channels: audioTrack?.numberOfChannels ?? 0,\n };\n asset.mediaInfo = info;\n return info;\n },\n\n destroy() {\n for (const asset of cache.values()) {\n asset.input.dispose();\n asset.imageBitmap?.close();\n }\n cache.clear();\n initPromises.clear();\n },\n };\n}\n","import type { Compositor, CompositeLayer } from './types.js';\n\nexport function createCanvas2DCompositor(width: number, height: number): Compositor {\n // `{ alpha: true }` keeps the backing buffer's alpha channel, so transparent\n // pixels from source layers (alpha videos, PNGs, letterboxed content) remain\n // transparent in the composed ImageBitmap instead of being flattened to black.\n let canvas = new OffscreenCanvas(width, height);\n let ctx = canvas.getContext('2d', { alpha: true })!;\n let currentWidth = width;\n let currentHeight = height;\n\n return {\n async composite(layers: CompositeLayer[]): Promise<ImageBitmap> {\n ctx.clearRect(0, 0, currentWidth, currentHeight);\n\n // Sort by zIndex ascending (low = bottom, high = top)\n const sorted = [...layers].sort((a, b) => a.zIndex - b.zIndex);\n\n for (const layer of sorted) {\n if (layer.opacity <= 0) continue;\n\n ctx.globalAlpha = layer.opacity;\n ctx.drawImage(layer.source, 0, 0, currentWidth, currentHeight);\n }\n\n ctx.globalAlpha = 1;\n return createImageBitmap(canvas);\n },\n\n resize(w: number, h: number): void {\n currentWidth = w;\n currentHeight = h;\n canvas = new OffscreenCanvas(w, h);\n ctx = canvas.getContext('2d', { alpha: true })!;\n },\n\n destroy(): void {\n // OffscreenCanvas has no explicit cleanup needed\n },\n };\n}\n","import type { Compositor, CompositeLayer } from './types.js';\n\nconst VERTEX_SHADER = /* wgsl */ `\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) texCoord: vec2f,\n};\n\n@vertex\nfn main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {\n // Full-screen quad: 6 vertices, 2 triangles\n var positions = array<vec2f, 6>(\n vec2f(-1.0, -1.0),\n vec2f( 1.0, -1.0),\n vec2f(-1.0, 1.0),\n vec2f(-1.0, 1.0),\n vec2f( 1.0, -1.0),\n vec2f( 1.0, 1.0),\n );\n\n var texCoords = array<vec2f, 6>(\n vec2f(0.0, 1.0),\n vec2f(1.0, 1.0),\n vec2f(0.0, 0.0),\n vec2f(0.0, 0.0),\n vec2f(1.0, 1.0),\n vec2f(1.0, 0.0),\n );\n\n var output: VertexOutput;\n output.position = vec4f(positions[vertexIndex], 0.0, 1.0);\n output.texCoord = texCoords[vertexIndex];\n return output;\n}\n`;\n\nconst FRAGMENT_SHADER = /* wgsl */ `\n@group(0) @binding(0) var texSampler: sampler;\n@group(0) @binding(1) var texSource: texture_2d<f32>;\n@group(0) @binding(2) var<uniform> opacity: f32;\n\n@fragment\nfn main(@location(0) texCoord: vec2f) -> @location(0) vec4f {\n var color = textureSample(texSource, texSampler, texCoord);\n // Multiply alpha by uniform opacity\n color.a = color.a * opacity;\n // Premultiply RGB by the new alpha\n color = vec4f(color.rgb * color.a, color.a);\n return color;\n}\n`;\n\nexport async function createGPUCompositor(width: number, height: number): Promise<Compositor> {\n if (!navigator.gpu) {\n throw new Error('WebGPU not available');\n }\n const adapter = await navigator.gpu.requestAdapter();\n if (!adapter) {\n throw new Error('Failed to get WebGPU adapter');\n }\n\n const device = await adapter.requestDevice();\n\n // Create sampler\n const sampler = device.createSampler({\n magFilter: 'linear',\n minFilter: 'linear',\n });\n\n // Create uniform buffer for opacity (f32 = 4 bytes, aligned to 16)\n const opacityBuffer = device.createBuffer({\n size: 16,\n usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,\n });\n\n // Create bind group layout\n const bindGroupLayout = device.createBindGroupLayout({\n entries: [\n { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: {} },\n { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: {} },\n { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },\n ],\n });\n\n const pipelineLayout = device.createPipelineLayout({\n bindGroupLayouts: [bindGroupLayout],\n });\n\n // Create shader modules\n const vertexModule = device.createShaderModule({ code: VERTEX_SHADER });\n const fragmentModule = device.createShaderModule({ code: FRAGMENT_SHADER });\n\n // Create render pipeline with premultiplied alpha blending\n const pipeline = device.createRenderPipeline({\n layout: pipelineLayout,\n vertex: {\n module: vertexModule,\n entryPoint: 'main',\n },\n fragment: {\n module: fragmentModule,\n entryPoint: 'main',\n targets: [{\n format: 'rgba8unorm',\n blend: {\n color: {\n srcFactor: 'one',\n dstFactor: 'one-minus-src-alpha',\n operation: 'add',\n },\n alpha: {\n srcFactor: 'one',\n dstFactor: 'one-minus-src-alpha',\n operation: 'add',\n },\n },\n }],\n },\n primitive: {\n topology: 'triangle-list',\n },\n });\n\n // Create output texture\n let currentWidth = width;\n let currentHeight = height;\n let outputTexture = createOutputTexture(device, width, height);\n\n function createOutputTexture(dev: GPUDevice, w: number, h: number): GPUTexture {\n return dev.createTexture({\n size: { width: w, height: h },\n format: 'rgba8unorm',\n usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.TEXTURE_BINDING,\n });\n }\n\n return {\n async composite(layers: CompositeLayer[]): Promise<ImageBitmap> {\n const sorted = [...layers]\n .filter((l) => l.opacity > 0)\n .sort((a, b) => a.zIndex - b.zIndex);\n\n const encoder = device.createCommandEncoder();\n const outputView = outputTexture.createView();\n\n const layerTextures: GPUTexture[] = [];\n\n if (sorted.length === 0) {\n // Clear pass only\n const pass = encoder.beginRenderPass({\n colorAttachments: [{\n view: outputView,\n loadOp: 'clear',\n storeOp: 'store',\n clearValue: { r: 0, g: 0, b: 0, a: 0 },\n }],\n });\n pass.end();\n } else {\n for (let i = 0; i < sorted.length; i++) {\n const layer = sorted[i];\n\n // Create texture from source\n const layerTexture = device.createTexture({\n size: { width: currentWidth, height: currentHeight },\n format: 'rgba8unorm',\n usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,\n });\n layerTextures.push(layerTexture);\n\n device.queue.copyExternalImageToTexture(\n { source: layer.source as ImageBitmap },\n { texture: layerTexture },\n { width: currentWidth, height: currentHeight },\n );\n\n // Write opacity uniform\n const opacityData = new Float32Array([layer.opacity]);\n device.queue.writeBuffer(opacityBuffer, 0, opacityData);\n\n // Create bind group for this layer\n const bindGroup = device.createBindGroup({\n layout: bindGroupLayout,\n entries: [\n { binding: 0, resource: sampler },\n { binding: 1, resource: layerTexture.createView() },\n { binding: 2, resource: { buffer: opacityBuffer } },\n ],\n });\n\n // Render pass: first pass clears, subsequent passes load\n const loadOp: 'load' | 'clear' = i === 0 ? 'clear' : 'load';\n const pass = encoder.beginRenderPass({\n colorAttachments: [{\n view: outputView,\n loadOp,\n storeOp: 'store',\n ...(loadOp === 'clear' ? { clearValue: { r: 0, g: 0, b: 0, a: 0 } } : {}),\n }],\n });\n\n pass.setPipeline(pipeline);\n pass.setBindGroup(0, bindGroup);\n pass.draw(6);\n pass.end();\n }\n }\n\n // Copy output texture to staging buffer for CPU readback\n const bytesPerRow = Math.ceil(currentWidth * 4 / 256) * 256;\n const bufferSize = bytesPerRow * currentHeight;\n const stagingBuffer = device.createBuffer({\n size: bufferSize,\n usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,\n });\n\n encoder.copyTextureToBuffer(\n { texture: outputTexture },\n { buffer: stagingBuffer, bytesPerRow },\n { width: currentWidth, height: currentHeight },\n );\n\n device.queue.submit([encoder.finish()]);\n\n // Destroy layer textures after command submission\n for (const tex of layerTextures) {\n tex.destroy();\n }\n\n // Read back pixels from staging buffer\n await stagingBuffer.mapAsync(GPUMapMode.READ);\n const rawData = stagingBuffer.getMappedRange();\n\n // Copy pixel data row by row (bytesPerRow may have padding)\n const pixelData = new Uint8ClampedArray(currentWidth * currentHeight * 4);\n const src = new Uint8Array(rawData);\n for (let row = 0; row < currentHeight; row++) {\n const srcOffset = row * bytesPerRow;\n const dstOffset = row * currentWidth * 4;\n pixelData.set(src.subarray(srcOffset, srcOffset + currentWidth * 4), dstOffset);\n }\n\n stagingBuffer.unmap();\n stagingBuffer.destroy();\n\n // The fragment shader outputs premultiplied RGB (see FRAGMENT_SHADER),\n // so `pixelData` is in premultiplied form. `putImageData` treats its\n // input as straight (unpremultiplied) alpha — feeding premultiplied\n // bytes in would cause the canvas to double-premultiply at semi-\n // transparent pixels, producing darkened/wrong colors on alpha-blended\n // edges. Unpremultiply so the straight-alpha ImageData contract holds.\n for (let i = 0; i < pixelData.length; i += 4) {\n const a = pixelData[i + 3];\n if (a > 0 && a < 255) {\n const inv = 255 / a;\n pixelData[i] = Math.min(255, Math.round(pixelData[i] * inv));\n pixelData[i + 1] = Math.min(255, Math.round(pixelData[i + 1] * inv));\n pixelData[i + 2] = Math.min(255, Math.round(pixelData[i + 2] * inv));\n }\n }\n\n // `{ alpha: true }` keeps the readback canvas's alpha channel so a fully\n // transparent output texture stays transparent (default `alpha: false`\n // would flatten it to opaque black).\n const readbackCanvas = new OffscreenCanvas(currentWidth, currentHeight);\n const ctx = readbackCanvas.getContext('2d', { alpha: true })!;\n const imageData = new ImageData(pixelData, currentWidth, currentHeight);\n ctx.putImageData(imageData, 0, 0);\n return createImageBitmap(readbackCanvas);\n },\n\n resize(w: number, h: number): void {\n outputTexture.destroy();\n currentWidth = w;\n currentHeight = h;\n outputTexture = createOutputTexture(device, w, h);\n },\n\n destroy(): void {\n outputTexture.destroy();\n opacityBuffer.destroy();\n device.destroy();\n },\n };\n}\n","import type { Compositor } from './types.js';\nimport { createCanvas2DCompositor } from './canvas2d-compositor.js';\nimport { createGPUCompositor } from './gpu-compositor.js';\n\nexport type CompositorType = 'gpu' | 'canvas2d' | 'auto';\n\nexport async function createCompositor(\n width: number,\n height: number,\n type: CompositorType = 'auto',\n): Promise<Compositor> {\n if (type === 'canvas2d') {\n return createCanvas2DCompositor(width, height);\n }\n\n if (type === 'gpu' || type === 'auto') {\n if (typeof navigator !== 'undefined' && (navigator as unknown as { gpu?: unknown }).gpu) {\n try {\n return await createGPUCompositor(width, height);\n } catch {\n if (type === 'gpu') {\n throw new Error('WebGPU compositor requested but initialization failed');\n }\n }\n } else if (type === 'gpu') {\n throw new Error('WebGPU not available');\n }\n }\n\n return createCanvas2DCompositor(width, height);\n}\n","import type {\n FrameRenderer,\n RenderedFrame,\n MediaDecoder,\n Compositor,\n CompositeLayer,\n SubtitleRenderer,\n} from './types.js';\nimport type { Composition, ResolvedClip, ResolvedPreviewFrame } from '@pneuma-craft/timeline';\nimport { resolveFrame } from '@pneuma-craft/timeline';\n\nexport interface CreateFrameRendererOptions {\n readonly subtitleRenderer?: SubtitleRenderer;\n /**\n * Render preview frames in tracks where no real clip covers the current\n * time. Defaults to false — preview frames are a planning-layer visual\n * that should NOT appear in final exports unless explicitly requested.\n * Engines wire their own defaults: PlaybackEngine passes true, ExportEngine\n * passes false.\n */\n readonly includePreviewFrames?: boolean;\n}\n\n// Accept either the legacy positional SubtitleRenderer (pre-preview-frame\n// callers) or the new options object. Internal callers (playback-engine,\n// export-engine) and tests have been migrated to the options form.\ntype LegacyArg = SubtitleRenderer | CreateFrameRendererOptions | undefined;\n\nfunction normalizeOptions(arg: LegacyArg): CreateFrameRendererOptions {\n if (arg === undefined) return {};\n if (typeof arg === 'function') return { subtitleRenderer: arg };\n return arg;\n}\n\nexport function createFrameRenderer(\n decoder: MediaDecoder,\n compositor: Compositor,\n width: number,\n height: number,\n optionsOrLegacySubtitleRenderer?: LegacyArg,\n): FrameRenderer {\n const options = normalizeOptions(optionsOrLegacySubtitleRenderer);\n const { subtitleRenderer, includePreviewFrames = false } = options;\n\n return {\n async renderFrame(composition: Composition, time: number): Promise<RenderedFrame> {\n const resolved = resolveFrame(composition, time);\n\n // Bucket per track for O(1) lookup. resolveFrame guarantees clips and\n // previewFrames are mutually exclusive per track, so we walk\n // composition.tracks once and pick at most one bucket per track.\n const clipsByTrack = new Map<string, ResolvedClip[]>();\n for (const rc of resolved.clips) {\n const arr = clipsByTrack.get(rc.track.id);\n if (arr) arr.push(rc); else clipsByTrack.set(rc.track.id, [rc]);\n }\n const previewByTrack = new Map<string, ResolvedPreviewFrame>();\n for (const rpf of resolved.previewFrames) previewByTrack.set(rpf.track.id, rpf);\n\n const layers: CompositeLayer[] = [];\n let zIndex = 0;\n\n // Video layers — iterate tracks in array order so z-stack matches\n // composition.tracks. Per-track decision: clip wins; preview is the\n // fallback only when allowed by the renderer's includePreviewFrames flag.\n for (const track of composition.tracks) {\n if (track.type !== 'video') continue;\n\n const trackClips = clipsByTrack.get(track.id);\n if (trackClips && trackClips.length > 0) {\n for (const rc of trackClips) {\n const source = await decoder.decodeVideoFrame(\n rc.clip.assetId, rc.localTime, width, height,\n );\n layers.push({ source, opacity: 1, zIndex: zIndex++ });\n }\n continue;\n }\n\n if (includePreviewFrames) {\n const rpf = previewByTrack.get(track.id);\n if (rpf) {\n // Image asset: decoder.decodeVideoFrame uses the cached-ImageBitmap\n // fast path and ignores the time argument.\n const source = await decoder.decodeVideoFrame(\n rpf.previewFrame.assetId, 0, width, height,\n );\n layers.push({ source, opacity: 1, zIndex: zIndex++ });\n }\n }\n }\n\n // Subtitles always composite on top of every video / preview layer,\n // regardless of track ordering in the composition. Track ordering\n // between multiple subtitle tracks is preserved.\n if (subtitleRenderer) {\n const subtitleClips = resolved.clips.filter(rc => rc.track.type === 'subtitle');\n for (const rc of subtitleClips) {\n const rendered = await subtitleRenderer({\n clip: rc.clip,\n localTime: rc.localTime,\n width,\n height,\n });\n if (rendered) {\n layers.push({ source: rendered, opacity: 1, zIndex: zIndex++ });\n }\n }\n }\n\n const image = await compositor.composite(layers);\n\n return { image, time, width, height };\n },\n\n destroy(): void {\n decoder.destroy();\n compositor.destroy();\n },\n };\n}\n","import type { MasterClock, ClockState } from './types.js';\n\nexport interface MasterClockOptions {\n audioContext: AudioContext;\n duration?: number;\n frameRate?: number;\n}\n\nexport function createMasterClock(options: MasterClockOptions): MasterClock {\n const audioContext = options.audioContext;\n\n let state: ClockState = 'stopped';\n let _duration = options.duration ?? 0;\n let _playbackRate = 1;\n let _loop: { start: number; end: number } | null = null;\n\n // Timing anchors\n let startAudioContextTime = 0;\n let startTimelineTime = 0;\n let pausedAt = 0;\n\n // Drift tracking\n let _driftMs = 0;\n\n // Subscribers\n const timeUpdateListeners = new Set<(time: number) => void>();\n const stateChangeListeners = new Set<(state: ClockState) => void>();\n\n function computeCurrentTime(): number {\n if (state !== 'playing') return pausedAt;\n\n const elapsed = (audioContext.currentTime - startAudioContextTime) * _playbackRate;\n let time = startTimelineTime + elapsed;\n\n if (_loop && _loop.end > _loop.start && time >= _loop.end) {\n const loopDuration = _loop.end - _loop.start;\n time = _loop.start + ((time - _loop.start) % loopDuration);\n }\n\n return Math.max(0, Math.min(time, _duration || Infinity));\n }\n\n function notifyTimeUpdate(time: number): void {\n for (const cb of timeUpdateListeners) {\n try { cb(time); } catch (e) { console.error('[MasterClock] listener error:', e); }\n }\n }\n\n function notifyStateChange(newState: ClockState): void {\n for (const cb of stateChangeListeners) {\n try { cb(newState); } catch (e) { console.error('[MasterClock] listener error:', e); }\n }\n }\n\n function setState(newState: ClockState): void {\n if (state === newState) return;\n state = newState;\n notifyStateChange(newState);\n }\n\n const clock: MasterClock = {\n get currentTime() { return computeCurrentTime(); },\n get state() { return state; },\n get driftMs() { return _driftMs; },\n\n get playbackRate() { return _playbackRate; },\n set playbackRate(rate: number) {\n const clamped = Math.max(0.1, Math.min(rate, 16));\n if (state === 'playing') {\n const current = computeCurrentTime();\n startTimelineTime = current;\n startAudioContextTime = audioContext.currentTime;\n }\n _playbackRate = clamped;\n },\n\n get duration() { return _duration; },\n set duration(d: number) { _duration = Math.max(0, d); },\n\n get loop() { return _loop; },\n set loop(l) { _loop = l; },\n\n play(): void {\n if (state === 'playing') return;\n startTimelineTime = pausedAt;\n startAudioContextTime = audioContext.currentTime;\n setState('playing');\n if (audioContext.state === 'suspended') {\n audioContext.resume();\n }\n },\n\n pause(): void {\n if (state !== 'playing') return;\n pausedAt = computeCurrentTime();\n setState('paused');\n },\n\n seek(time: number): void {\n const clamped = Math.max(0, Math.min(time, _duration));\n if (state === 'playing') {\n startTimelineTime = clamped;\n startAudioContextTime = audioContext.currentTime;\n } else {\n pausedAt = clamped;\n }\n notifyTimeUpdate(clamped);\n },\n\n reportVideoTime(videoTime: number): void {\n const audioTime = computeCurrentTime();\n _driftMs = (audioTime - videoTime) * 1000;\n },\n\n onTimeUpdate(cb) {\n timeUpdateListeners.add(cb);\n return () => { timeUpdateListeners.delete(cb); };\n },\n\n onStateChange(cb) {\n stateChangeListeners.add(cb);\n return () => { stateChangeListeners.delete(cb); };\n },\n\n destroy(): void {\n if (state === 'playing') {\n pausedAt = computeCurrentTime();\n }\n state = 'stopped';\n timeUpdateListeners.clear();\n stateChangeListeners.clear();\n _driftMs = 0;\n },\n };\n\n return clock;\n}\n","import type { AudioScheduler } from './types.js';\nimport type { Composition, Clip, Track } from '@pneuma-craft/timeline';\n\nexport interface AudioSchedulerOptions {\n audioContext: AudioContext;\n}\n\ninterface ActiveSource {\n source: AudioBufferSourceNode;\n clipGain: GainNode;\n}\n\nexport function createAudioScheduler(options: AudioSchedulerOptions): AudioScheduler {\n const { audioContext } = options;\n\n // Buffer storage: clipId → AudioBuffer\n const clipBuffers = new Map<string, AudioBuffer>();\n\n // Per-track gain nodes: trackId → GainNode\n const trackGains = new Map<string, GainNode>();\n\n // Per-track volume (separate from mute): trackId → volume\n const trackVolumes = new Map<string, number>();\n\n // Per-track muted state: trackId → muted\n const trackMutedStates = new Map<string, boolean>();\n\n // Currently active sources\n let activeSources: ActiveSource[] = [];\n\n // Scheduler tick state for future clip scheduling\n let schedulerIntervalId: ReturnType<typeof setInterval> | null = null;\n let scheduledClipIds = new Set<string>();\n let currentComposition: Composition | null = null;\n\n // Playback rate (applied to elapsed time and source nodes)\n let _playbackRate = 1;\n\n // Whether playback is currently active\n let _isPlaying = false;\n\n // Previous timeline time for loop-wrap detection\n let _prevTimelineTime = 0;\n\n // Callback to get current timeline time from the master clock (loop-aware)\n let _getCurrentTime: (() => number) | null = null;\n\n // Look-ahead window in seconds\n const LOOK_AHEAD = 0.2;\n\n // Master gain node connected to destination\n const masterGain = audioContext.createGain();\n masterGain.gain.value = 1;\n masterGain.connect(audioContext.destination);\n\n function getOrCreateTrackGain(track: Track): GainNode {\n let node = trackGains.get(track.id);\n if (!node) {\n node = audioContext.createGain();\n // Use runtime overrides if they exist, otherwise fall back to composition data\n const vol = trackVolumes.get(track.id) ?? track.volume ?? 1;\n const muted = trackMutedStates.get(track.id) ?? track.muted;\n node.gain.value = muted ? 0 : vol;\n trackVolumes.set(track.id, vol);\n node.connect(masterGain);\n trackGains.set(track.id, node);\n }\n return node;\n }\n\n function clearSchedulerTick(): void {\n if (schedulerIntervalId !== null) {\n clearInterval(schedulerIntervalId);\n schedulerIntervalId = null;\n }\n }\n\n function stopAllSources(): void {\n for (const { source } of activeSources) {\n try {\n source.stop();\n } catch {\n // Source may already be stopped\n }\n source.disconnect();\n }\n activeSources = [];\n }\n\n function isClipActiveAt(clip: Clip, fromTime: number): boolean {\n return clip.startTime <= fromTime && fromTime < clip.startTime + clip.duration;\n }\n\n function scheduleClip(clip: Clip, track: Track, fromTime: number): void {\n const buffer = clipBuffers.get(clip.id);\n if (!buffer) return;\n\n const trackGain = getOrCreateTrackGain(track);\n\n // Create per-clip gain node\n const clipGain = audioContext.createGain();\n const clipVolume = clip.volume ?? 1;\n clipGain.gain.value = clipVolume;\n\n // Create source node\n const source = audioContext.createBufferSource();\n source.buffer = buffer;\n\n // Apply playback rate to source node\n source.playbackRate.value = _playbackRate;\n\n // Connect: source → clipGain → trackGain → masterGain → destination\n source.connect(clipGain);\n clipGain.connect(trackGain);\n\n const now = audioContext.currentTime;\n\n // Determine if the clip starts in the future relative to current timeline time\n let contextStartTime: number;\n let sourceOffset: number;\n let remainingDuration: number;\n\n if (clip.startTime > fromTime) {\n // Future clip: schedule it to start at the correct AudioContext time\n // Divide by playbackRate: at 2x rate, 1 timeline second = 0.5 real seconds\n const timeUntilClipStart = (clip.startTime - fromTime) / _playbackRate;\n contextStartTime = now + timeUntilClipStart;\n sourceOffset = clip.inPoint;\n remainingDuration = clip.duration;\n } else {\n // Already active clip: start immediately with offset\n contextStartTime = 0;\n const elapsed = fromTime - clip.startTime;\n sourceOffset = clip.inPoint + elapsed;\n remainingDuration = clip.duration - elapsed;\n }\n\n // Apply fade automations\n // Fade durations must be divided by playbackRate since AudioContext runs in real time\n if (clip.fadeIn !== undefined && clip.fadeIn > 0) {\n const realFadeIn = clip.fadeIn / _playbackRate;\n if (clip.startTime > fromTime) {\n // Future clip: anchor fade to contextStartTime\n clipGain.gain.setValueAtTime(0, contextStartTime);\n clipGain.gain.linearRampToValueAtTime(clipVolume, contextStartTime + realFadeIn);\n } else {\n const fadeInEnd = clip.startTime + clip.fadeIn;\n if (fromTime < fadeInEnd) {\n // Still within fade-in region\n const fadeElapsed = fromTime - clip.startTime;\n const currentLevel = fadeElapsed > 0 ? (fadeElapsed / clip.fadeIn) * clipVolume : 0;\n const fadeRemaining = (clip.fadeIn - fadeElapsed) / _playbackRate;\n clipGain.gain.setValueAtTime(currentLevel, now);\n clipGain.gain.linearRampToValueAtTime(clipVolume, now + fadeRemaining);\n }\n }\n }\n\n if (clip.fadeOut !== undefined && clip.fadeOut > 0) {\n const realFadeOut = clip.fadeOut / _playbackRate;\n const realDuration = clip.duration / _playbackRate;\n if (clip.startTime > fromTime) {\n // Future clip: anchor fade to contextStartTime\n const fadeOutStart = contextStartTime + realDuration - realFadeOut;\n clipGain.gain.setValueAtTime(clipVolume, fadeOutStart);\n clipGain.gain.linearRampToValueAtTime(0, contextStartTime + realDuration);\n } else {\n const fadeOutStart = clip.startTime + clip.duration - clip.fadeOut;\n const fadeOutContextTime = now + Math.max(0, (fadeOutStart - fromTime) / _playbackRate);\n clipGain.gain.setValueAtTime(clipVolume, fadeOutContextTime);\n clipGain.gain.linearRampToValueAtTime(0, fadeOutContextTime + realFadeOut);\n }\n }\n\n source.start(contextStartTime, sourceOffset, remainingDuration);\n\n activeSources.push({ source, clipGain });\n }\n\n function isClipInLookAhead(clip: Clip, currentTime: number, lookAhead: number): boolean {\n const clipEnd = clip.startTime + clip.duration;\n // Clip starts within the look-ahead window and hasn't ended\n return clip.startTime > currentTime && clip.startTime <= currentTime + lookAhead && clipEnd > currentTime;\n }\n\n function scheduleComposition(fromTime: number, composition: Composition): void {\n for (const track of composition.tracks) {\n // Both audio tracks and video tracks contribute audio — a video clip's\n // embedded audio track gets scheduled alongside pure audio clips. Skip\n // anything that isn't a media track (e.g. subtitle) and anything that\n // the user has muted. Clips whose asset has no audio track never get a\n // buffer loaded (see loadClip in playback-engine.ts), so `scheduleClip`\n // naturally skips them.\n if (track.type !== 'audio' && track.type !== 'video') continue;\n if (track.muted) continue;\n\n for (const clip of track.clips) {\n if (!isClipActiveAt(clip, fromTime)) continue;\n if (scheduledClipIds.has(clip.id)) continue;\n scheduledClipIds.add(clip.id);\n scheduleClip(clip, track, fromTime);\n }\n }\n }\n\n function startSchedulerTick(fromTime: number, composition: Composition): void {\n clearSchedulerTick();\n currentComposition = composition;\n\n // Fallback values for computing timeline time when no getCurrentTime callback\n const tickStartContextTime = audioContext.currentTime;\n const tickStartFromTime = fromTime;\n\n schedulerIntervalId = setInterval(() => {\n if (!currentComposition) return;\n\n // Use clock's getCurrentTime if available (loop-aware), otherwise fallback\n let currentTimelineTime: number;\n if (_getCurrentTime) {\n currentTimelineTime = _getCurrentTime();\n } else {\n const elapsed = (audioContext.currentTime - tickStartContextTime) * _playbackRate;\n currentTimelineTime = tickStartFromTime + elapsed;\n }\n\n // Detect loop wrap: timeline time went backwards\n if (currentTimelineTime < _prevTimelineTime) {\n scheduledClipIds.clear();\n stopAllSources();\n }\n _prevTimelineTime = currentTimelineTime;\n\n for (const track of currentComposition.tracks) {\n if (track.type !== 'audio') continue;\n if (track.muted) continue;\n\n for (const clip of track.clips) {\n if (scheduledClipIds.has(clip.id)) continue;\n if (isClipInLookAhead(clip, currentTimelineTime, LOOK_AHEAD) || isClipActiveAt(clip, currentTimelineTime)) {\n scheduledClipIds.add(clip.id);\n scheduleClip(clip, track, currentTimelineTime);\n }\n }\n }\n }, 100);\n }\n\n const scheduler: AudioScheduler = {\n get audioContext() {\n return audioContext;\n },\n\n loadClip(clipId: string, audioBuffer: AudioBuffer): void {\n clipBuffers.set(clipId, audioBuffer);\n },\n\n play(fromTime: number, composition: Composition, getCurrentTime?: () => number): void {\n stopAllSources();\n clearSchedulerTick();\n scheduledClipIds = new Set();\n _prevTimelineTime = fromTime;\n _getCurrentTime = getCurrentTime ?? null;\n _isPlaying = true;\n currentComposition = composition;\n scheduleComposition(fromTime, composition);\n startSchedulerTick(fromTime, composition);\n },\n\n pause(): void {\n clearSchedulerTick();\n stopAllSources();\n scheduledClipIds = new Set();\n _isPlaying = false;\n },\n\n seek(time: number, composition: Composition): void {\n clearSchedulerTick();\n stopAllSources();\n scheduledClipIds = new Set();\n _prevTimelineTime = time;\n scheduleComposition(time, composition);\n startSchedulerTick(time, composition);\n },\n\n setPlaybackRate(rate: number): void {\n _playbackRate = rate;\n // If playing, reschedule everything with new rate so future clip\n // contextStartTime values are recalculated correctly.\n if (_isPlaying && currentComposition && _getCurrentTime) {\n stopAllSources();\n scheduledClipIds = new Set();\n const currentTime = _getCurrentTime();\n scheduleComposition(currentTime, currentComposition);\n clearSchedulerTick();\n startSchedulerTick(currentTime, currentComposition);\n } else {\n // Not playing or no getCurrentTime — just update active sources\n for (const scheduled of activeSources) {\n scheduled.source.playbackRate.value = rate;\n }\n }\n },\n\n setTrackVolume(trackId: string, volume: number): void {\n trackVolumes.set(trackId, volume);\n const node = trackGains.get(trackId);\n if (node) {\n // Only apply gain if the track is not muted\n const isMuted = trackMutedStates.get(trackId) ?? false;\n if (!isMuted) {\n node.gain.setValueAtTime(volume, audioContext.currentTime);\n }\n }\n },\n\n setTrackMute(trackId: string, muted: boolean): void {\n trackMutedStates.set(trackId, muted);\n const node = trackGains.get(trackId);\n if (node) {\n if (muted) {\n node.gain.value = 0;\n } else {\n const vol = trackVolumes.get(trackId) ?? 1;\n node.gain.value = vol;\n }\n }\n },\n\n destroy(): void {\n clearSchedulerTick();\n stopAllSources();\n scheduledClipIds = new Set();\n currentComposition = null;\n _isPlaying = false;\n for (const node of trackGains.values()) {\n node.disconnect();\n }\n trackGains.clear();\n trackVolumes.clear();\n trackMutedStates.clear();\n clipBuffers.clear();\n masterGain.disconnect();\n },\n };\n\n return scheduler;\n}\n","import type {\n PlaybackEngine,\n PlaybackState,\n RenderedFrame,\n AssetResolver,\n MediaDecoder,\n Compositor,\n FrameRenderer,\n MasterClock,\n AudioScheduler,\n SubtitleRenderer,\n} from './types.js';\nimport type { Composition } from '@pneuma-craft/timeline';\nimport { createMediaDecoder } from './media-decoder.js';\nimport { createCompositor, type CompositorType } from './compositor.js';\nimport { createFrameRenderer } from './frame-renderer.js';\nimport { createMasterClock } from './master-clock.js';\nimport { createAudioScheduler } from './audio-scheduler.js';\n\nexport interface PlaybackEngineOptions {\n compositorType?: CompositorType;\n /** Max time a single frame decode may block before the loop skips it. Default 1000ms. */\n decodeTimeoutMs?: number;\n /**\n * Optional rasterizer for subtitle-track clips. When provided, active\n * subtitle clips are composited on top of the video layers — the same code\n * path used for export, so preview and export stay in lockstep. When\n * omitted, subtitle tracks render nothing (legacy behavior).\n */\n subtitleRenderer?: SubtitleRenderer;\n /**\n * Render preview frames (planning-layer visuals attached to time points\n * on a track) when no real clip covers the current time. Defaults to\n * `true` for the playback engine — the user scrubbing the timeline\n * should see the agent's planned vibe before any real video clips land.\n */\n includePreviewFrames?: boolean;\n}\n\n/** Default per-frame decode timeout. Decodes exceeding this are logged and skipped. */\nconst DEFAULT_DECODE_TIMEOUT_MS = 1000;\n\n/**\n * Float slack for the end-of-timeline comparison. The clock clamps\n * `currentTime` to `duration`, so reaching the end is an exact equality in\n * practice; this only absorbs rounding in a caller-supplied seek target.\n */\nconst END_EPSILON = 1e-6;\n\n/** Enables verbose rAF-loop logs for debugging playback stalls. */\nconst DEBUG_LOOP = typeof globalThis !== 'undefined'\n && (globalThis as { __PNEUMA_CRAFT_PLAYBACK_DEBUG__?: boolean }).__PNEUMA_CRAFT_PLAYBACK_DEBUG__ === true;\n\nexport function createPlaybackEngine(options?: PlaybackEngineOptions): PlaybackEngine {\n const compositorType = options?.compositorType ?? 'canvas2d';\n const decodeTimeoutMs = options?.decodeTimeoutMs ?? DEFAULT_DECODE_TIMEOUT_MS;\n const subtitleRenderer = options?.subtitleRenderer;\n const includePreviewFrames = options?.includePreviewFrames ?? true;\n\n let _state: PlaybackState = 'idle';\n let _playbackRate = 1;\n let _loop: { start: number; end: number } | null = null;\n\n // Subsystems (created on load)\n let _decoder: MediaDecoder | null = null;\n let _compositor: Compositor | null = null;\n let _frameRenderer: FrameRenderer | null = null;\n let _clock: MasterClock | null = null;\n let _audioScheduler: AudioScheduler | null = null;\n let _audioContext: AudioContext | null = null;\n let _composition: Composition | null = null;\n\n // rAF handle\n let _rafId: number | null = null;\n\n // True while a frame decode is awaiting — prevents stacking concurrent renders\n // when one decode runs slower than a rAF tick.\n let _frameInFlight = false;\n\n // Seek ID counter for discarding stale paused-seek renders\n let _seekId = 0;\n\n // Subscribers\n const stateChangeListeners = new Set<(state: PlaybackState) => void>();\n const timeUpdateListeners = new Set<(time: number) => void>();\n const frameRenderedListeners = new Set<(frame: RenderedFrame) => void>();\n\n function setState(newState: PlaybackState): void {\n if (_state === newState) return;\n _state = newState;\n for (const cb of stateChangeListeners) {\n try { cb(newState); } catch (e) { console.error('[PlaybackEngine] listener error:', e); }\n }\n }\n\n function emitTimeUpdate(time: number): void {\n for (const cb of timeUpdateListeners) {\n try { cb(time); } catch (e) { console.error('[PlaybackEngine] listener error:', e); }\n }\n }\n\n function emitFrameRendered(frame: RenderedFrame): void {\n for (const cb of frameRenderedListeners) {\n try { cb(frame); } catch (e) { console.error('[PlaybackEngine] listener error:', e); }\n }\n }\n\n function destroySubsystems(): void {\n if (_rafId !== null) {\n cancelAnimationFrame(_rafId);\n _rafId = null;\n }\n _frameInFlight = false;\n _frameRenderer?.destroy();\n _clock?.destroy();\n _audioScheduler?.destroy();\n _audioContext?.close();\n _decoder = null;\n _compositor = null;\n _frameRenderer = null;\n _clock = null;\n _audioScheduler = null;\n _audioContext = null;\n _composition = null;\n }\n\n function startRafLoop(): void {\n if (_rafId !== null) return;\n\n // The loop is designed so that time updates and rAF scheduling are INDEPENDENT\n // of frame decoding. This means:\n // 1. If a decode hangs or is slow, the clock/slider still advances and pause/seek\n // remain responsive.\n // 2. At most one decode runs at a time (no concurrent pile-up).\n // 3. A decode exceeding `decodeTimeoutMs` is logged and skipped so the loop can\n // recover from a stuck codec packet pump.\n let lastLoggedState: 'render' | 'skip-inflight' | null = null;\n const logStateChange = (state: 'render' | 'skip-inflight', time: number, extra?: string): void => {\n if (!DEBUG_LOOP) return;\n if (lastLoggedState === state) return;\n lastLoggedState = state;\n console.log(`[engine] ${state} @ t=${time.toFixed(3)}${extra ? ' ' + extra : ''}`);\n };\n\n const loop = (): void => {\n _rafId = null;\n if (_state !== 'playing' || !_clock || !_frameRenderer || !_composition) return;\n\n const time = _clock.currentTime;\n\n // Emit time update up-front — consumers see the clock tick regardless of decode state.\n emitTimeUpdate(time);\n\n // End-of-timeline check.\n if (!_clock.loop && time >= _composition.duration) {\n engine.pause();\n return;\n }\n\n // Always schedule the next rAF synchronously so timing is decoupled from decode.\n _rafId = requestAnimationFrame(loop);\n\n // Skip this tick's render if the previous decode is still in flight — back-pressure\n // so slow decodes don't stack concurrent calls. The watchdog below recovers if a\n // decode never resolves; we never get stuck.\n //\n // NOTE: we intentionally do NOT skip based on _clock.driftMs. A prior implementation\n // did, which caused a deadlock: when the first decode ran long (e.g., cold-start\n // fetch of a 50 MB blob), reportVideoTime captured a large stale-drift value, then\n // every subsequent tick skipped on drift, so reportVideoTime never ran again, and\n // drift was frozen at the stale value forever. The clock is the source of truth;\n // each render just targets its current value, which naturally catches up.\n if (_frameInFlight) {\n logStateChange('skip-inflight', time);\n return;\n }\n\n logStateChange('render', time);\n\n // Kick off the decode. A watchdog timer clears _frameInFlight so the loop can\n // recover if renderFrame never resolves. We attach .then directly (no Promise.race)\n // to preserve the microtask profile — emitFrameRendered fires on the same tick\n // that renderFrame resolves.\n _frameInFlight = true;\n const clockRef = _clock;\n const compositionRef = _composition;\n let timedOut = false;\n const timeoutId = setTimeout(() => {\n timedOut = true;\n _frameInFlight = false;\n console.warn(\n `[PlaybackEngine] decode timeout at t=${time.toFixed(3)}s (>${decodeTimeoutMs}ms) — skipping frame`,\n );\n }, decodeTimeoutMs);\n\n _frameRenderer.renderFrame(compositionRef, time).then(frame => {\n clearTimeout(timeoutId);\n if (timedOut) return;\n _frameInFlight = false;\n if (_state !== 'playing' || _clock !== clockRef) return;\n clockRef.reportVideoTime(time);\n emitFrameRendered(frame);\n }).catch(err => {\n clearTimeout(timeoutId);\n if (timedOut) return;\n _frameInFlight = false;\n console.error('[PlaybackEngine] render error:', err);\n });\n };\n\n _rafId = requestAnimationFrame(loop);\n }\n\n function stopRafLoop(): void {\n if (_rafId !== null) {\n cancelAnimationFrame(_rafId);\n _rafId = null;\n }\n _frameInFlight = false;\n }\n\n const engine: PlaybackEngine = {\n get state() { return _state; },\n\n get currentTime() {\n return _clock?.currentTime ?? 0;\n },\n\n get ended() {\n if (!_clock || !_composition) return false;\n if (_clock.loop) return false;\n const duration = _composition.duration;\n if (duration <= 0) return false;\n return _clock.currentTime >= duration - END_EPSILON;\n },\n\n get playbackRate() {\n return _clock?.playbackRate ?? _playbackRate;\n },\n set playbackRate(rate: number) {\n _playbackRate = rate;\n if (_clock) {\n _clock.playbackRate = rate;\n }\n if (_audioScheduler) {\n _audioScheduler.setPlaybackRate(rate);\n }\n },\n\n get loop() {\n return _clock?.loop ?? _loop;\n },\n set loop(l: { start: number; end: number } | null) {\n _loop = l;\n if (_clock) {\n _clock.loop = l;\n }\n },\n\n async load(composition: Composition, resolver: AssetResolver): Promise<void> {\n // Invalidate any pending paused-seek renders from previous session\n _seekId++;\n\n // If previously loaded, destroy old subsystems\n if (_state !== 'idle') {\n destroySubsystems();\n }\n\n setState('loading');\n\n try {\n // Create AudioContext\n _audioContext = new AudioContext();\n\n // Create subsystems\n _decoder = createMediaDecoder(resolver, _audioContext);\n _compositor = await createCompositor(\n composition.settings.width,\n composition.settings.height,\n compositorType,\n );\n _frameRenderer = createFrameRenderer(\n _decoder,\n _compositor,\n composition.settings.width,\n composition.settings.height,\n { subtitleRenderer, includePreviewFrames },\n );\n _clock = createMasterClock({\n audioContext: _audioContext,\n duration: composition.duration,\n frameRate: composition.settings.fps,\n });\n _audioScheduler = createAudioScheduler({ audioContext: _audioContext });\n\n // Apply pending settings\n _clock.playbackRate = _playbackRate;\n _clock.loop = _loop;\n\n _composition = composition;\n\n // Pre-load audio buffers from every clip on every audio OR video track.\n // Video clips carry their own embedded audio (MediaDecoder.decodeAudio\n // extracts it from the container), so they participate in the audio\n // graph just like standalone audio clips. Clips with no audio stream\n // (images, silent videos) throw — we swallow and skip so they simply\n // have no buffer loaded, which makes scheduleClip a no-op for them.\n const mediaClips = composition.tracks\n .filter(track => track.type === 'audio' || track.type === 'video')\n .flatMap(track => track.clips);\n\n for (const clip of mediaClips) {\n try {\n const audioBuffer = await _decoder.decodeAudio(clip.assetId);\n _audioScheduler.loadClip(clip.id, audioBuffer);\n } catch {\n // Clip has no audio (image asset, silent video, or decode failure).\n // Leave the clip without a buffer; audio-scheduler tolerates this.\n }\n }\n\n setState('ready');\n } catch (err) {\n // Clean up any partially-created subsystems\n destroySubsystems();\n setState('idle');\n throw err;\n }\n },\n\n play(): void {\n if (!_clock || !_audioScheduler || !_composition) {\n throw new Error('Cannot play: no composition loaded. Call load() first.');\n }\n if (_state === 'playing') return;\n\n // Nothing to play. Accepting the call here would start the rAF loop only\n // for it to hit the end-of-timeline check on the very first tick, which\n // shows up in consumers as a one-frame 'playing' → 'paused' flicker.\n if (_composition.duration <= 0) {\n console.warn('[PlaybackEngine] play() ignored: composition has zero duration.');\n return;\n }\n\n // Play at end-of-timeline restarts from the top, matching the HTML media\n // element algorithm (and every video transport users have ever used).\n // Without this the call would be self-absorbing: the clock resumes from\n // `duration`, the first tick satisfies the end check, and the engine\n // re-pauses having gone nowhere.\n if (engine.ended) {\n _seekId++; // discard any in-flight paused-seek render at the old position\n _clock.seek(0);\n emitTimeUpdate(_clock.currentTime);\n }\n\n // AudioContext may start in 'suspended' state because it was created\n // inside engine.load() after awaits, losing the user-gesture chain.\n // resume() works as long as ANY user gesture has occurred on the page —\n // clicking the play button is sufficient. Fire-and-forget: audio will\n // unmute as soon as resume settles, which in practice is <1 frame.\n if (_audioContext && _audioContext.state === 'suspended') {\n _audioContext.resume().catch((err) => {\n console.warn('[PlaybackEngine] AudioContext resume failed:', err);\n });\n }\n\n _clock.play();\n _audioScheduler.setPlaybackRate(_playbackRate);\n const clockRef = _clock;\n _audioScheduler.play(_clock.currentTime, _composition, () => clockRef.currentTime);\n setState('playing');\n startRafLoop();\n },\n\n pause(): void {\n if (_state !== 'playing') return;\n\n stopRafLoop();\n _clock?.pause();\n _audioScheduler?.pause();\n setState('paused');\n },\n\n seek(time: number): void {\n if (!_clock || !_audioScheduler || !_composition) {\n throw new Error('Cannot seek: no composition loaded. Call load() first.');\n }\n\n _clock.seek(time);\n // Use the clamped time from clock so audio/video stay in sync\n const clampedTime = _clock.currentTime;\n\n // Only reschedule audio when playing — seeking while paused should not produce sound\n if (_state === 'playing') {\n _audioScheduler.setPlaybackRate(_playbackRate);\n _audioScheduler.seek(clampedTime, _composition);\n }\n\n // If not playing, render the frame at the seeked position\n if (_state !== 'playing' && _frameRenderer) {\n const thisSeekId = ++_seekId;\n _frameRenderer.renderFrame(_composition, clampedTime).then(frame => {\n if (thisSeekId !== _seekId) return; // Stale render, discard\n emitFrameRendered(frame);\n emitTimeUpdate(clampedTime);\n }).catch(err => {\n console.error('[PlaybackEngine] seek render error:', err);\n });\n }\n },\n\n onStateChange(cb: (state: PlaybackState) => void): () => void {\n stateChangeListeners.add(cb);\n return () => { stateChangeListeners.delete(cb); };\n },\n\n onTimeUpdate(cb: (time: number) => void): () => void {\n timeUpdateListeners.add(cb);\n return () => { timeUpdateListeners.delete(cb); };\n },\n\n onFrameRendered(cb: (frame: RenderedFrame) => void): () => void {\n frameRenderedListeners.add(cb);\n return () => { frameRenderedListeners.delete(cb); };\n },\n\n destroy(): void {\n stopRafLoop();\n destroySubsystems();\n stateChangeListeners.clear();\n timeUpdateListeners.clear();\n frameRenderedListeners.clear();\n _playbackRate = 1;\n _loop = null;\n _state = 'idle';\n },\n };\n\n return engine;\n}\n","import type { OfflineAudioRenderer } from './types.js';\nimport type { Clip, Track } from '@pneuma-craft/timeline';\n\nexport function createOfflineAudioRenderer(): OfflineAudioRenderer {\n return {\n async render(composition, _resolver, decodeAudio) {\n const sampleRate = composition.settings.sampleRate ?? 48000;\n const channels = 2;\n const duration = composition.duration;\n const length = Math.ceil(duration * sampleRate);\n\n const offlineCtx = new OfflineAudioContext(channels, length, sampleRate);\n\n // Collect clips from every unmuted media track. Video clips contribute\n // their embedded audio exactly like standalone audio clips — this is\n // what keeps the exported audio in lockstep with what the preview\n // plays. Subtitle/other non-media tracks are skipped.\n const audioClips: Array<{ clip: Clip; track: Track }> = [];\n for (const track of composition.tracks) {\n if (track.type !== 'audio' && track.type !== 'video') continue;\n if (track.muted) continue;\n for (const clip of track.clips) {\n audioClips.push({ clip, track });\n }\n }\n\n // Schedule each clip\n for (const { clip, track } of audioClips) {\n let buffer: AudioBuffer;\n try {\n buffer = await decodeAudio(clip.assetId);\n } catch {\n // Clip has no audio stream (image asset, silent video) or decode\n // failed. Skip silently — video-track clips routinely have no\n // audio, so a warning here would be noise. True decode failures\n // on audio assets surface via the preview path instead.\n continue;\n }\n\n const source = offlineCtx.createBufferSource();\n source.buffer = buffer;\n\n const clipGain = offlineCtx.createGain();\n const clipVolume = clip.volume ?? 1;\n clipGain.gain.value = clipVolume;\n\n const trackGain = offlineCtx.createGain();\n trackGain.gain.value = track.volume;\n\n source.connect(clipGain);\n clipGain.connect(trackGain);\n trackGain.connect(offlineCtx.destination);\n\n // Fade automation\n const fadeIn = clip.fadeIn;\n const fadeOut = clip.fadeOut;\n\n if (fadeIn && fadeIn > 0) {\n clipGain.gain.setValueAtTime(0, clip.startTime);\n clipGain.gain.linearRampToValueAtTime(clipVolume, clip.startTime + fadeIn);\n }\n\n if (fadeOut && fadeOut > 0) {\n const fadeOutStart = clip.startTime + clip.duration - fadeOut;\n clipGain.gain.setValueAtTime(clipVolume, fadeOutStart);\n clipGain.gain.linearRampToValueAtTime(0, clip.startTime + clip.duration);\n }\n\n source.start(clip.startTime, clip.inPoint, clip.duration);\n }\n\n return offlineCtx.startRendering();\n },\n };\n}\n","import type { ExportEngine, ExportOptions, AssetResolver, SubtitleRenderer } from './types.js';\nimport type { Composition } from '@pneuma-craft/timeline';\nimport { createMediaDecoder } from './media-decoder.js';\nimport { createCompositor } from './compositor.js';\nimport { createFrameRenderer } from './frame-renderer.js';\nimport { createOfflineAudioRenderer } from './offline-audio-renderer.js';\nimport {\n Output,\n CanvasSource,\n AudioBufferSource,\n Mp4OutputFormat,\n WebMOutputFormat,\n BufferTarget,\n} from 'mediabunny';\n\nexport interface ExportEngineOptions {\n /**\n * Optional rasterizer for subtitle-track clips. Pass the same renderer used\n * by `createPlaybackEngine` so the exported video is pixel-identical to the\n * preview. When omitted, subtitle tracks are skipped during export.\n */\n subtitleRenderer?: SubtitleRenderer;\n /**\n * Include preview frames (planning-layer visuals attached to time points\n * on a track) in the exported video. Defaults to `false` because an export\n * normally represents a finished cut. Set true for review-grade exports of\n * unfinished timelines — e.g., agent producing a \"草样片\" for the user to\n * scrub before committing to expensive real renders.\n */\n includePreviewFrames?: boolean;\n}\n\nexport function createExportEngine(options?: ExportEngineOptions): ExportEngine {\n const subtitleRenderer = options?.subtitleRenderer;\n const includePreviewFrames = options?.includePreviewFrames ?? false;\n const progressListeners = new Set<(progress: number) => void>();\n let abortController: AbortController | null = null;\n\n function notifyProgress(progress: number): void {\n for (const cb of progressListeners) {\n try { cb(progress); } catch (e) { console.error('[ExportEngine]', e); }\n }\n }\n\n return {\n async export(\n composition: Composition,\n options: ExportOptions,\n resolver: AssetResolver,\n ): Promise<Blob> {\n abortController = new AbortController();\n const signal = abortController.signal;\n\n const fps = options.fps ?? composition.settings.fps;\n const width = options.width ?? composition.settings.width;\n const height = options.height ?? composition.settings.height;\n const totalFrames = Math.ceil(composition.duration * fps);\n\n // Minimal OfflineAudioContext used only for decodeAudioData during asset\n // decoding (export has its own offline context for actual audio rendering).\n const decoderAudioCtx = new OfflineAudioContext(1, 1, composition.settings.sampleRate || 48000);\n const decoder = createMediaDecoder(resolver, decoderAudioCtx);\n const compositor = await createCompositor(width, height, 'canvas2d');\n const renderer = createFrameRenderer(decoder, compositor, width, height, {\n subtitleRenderer,\n includePreviewFrames,\n });\n\n try {\n const format = options.format === 'webm'\n ? new WebMOutputFormat()\n : new Mp4OutputFormat({ fastStart: 'in-memory' });\n\n const target = new BufferTarget();\n const output = new Output({ format, target });\n\n const renderCanvas = new OffscreenCanvas(width, height);\n const videoSource = new CanvasSource(renderCanvas, {\n codec: options.videoCodec,\n bitrate: options.videoBitrate,\n });\n\n const audioSource = new AudioBufferSource({\n codec: options.audioCodec,\n bitrate: options.audioBitrate,\n });\n\n output.addVideoTrack(videoSource);\n output.addAudioTrack(audioSource);\n await output.start();\n\n // Render video frames\n const renderCtx = renderCanvas.getContext('2d')!;\n\n for (let frame = 0; frame < totalFrames; frame++) {\n if (signal.aborted) throw new Error('Export aborted');\n\n const time = frame / fps;\n const rendered = await renderer.renderFrame(composition, time);\n\n renderCtx.clearRect(0, 0, width, height);\n renderCtx.drawImage(rendered.image, 0, 0, width, height);\n rendered.image.close();\n\n await videoSource.add(time, 1 / fps);\n\n if ((frame + 1) % 5 === 0 || frame === totalFrames - 1) {\n notifyProgress((frame + 1) / totalFrames);\n }\n\n // Yield to event loop periodically\n if ((frame + 1) % 5 === 0) {\n await new Promise(r => setTimeout(r, 0));\n }\n }\n\n videoSource.close();\n if (signal.aborted) throw new Error('Export aborted');\n\n // Render audio offline\n const offlineRenderer = createOfflineAudioRenderer();\n const audioBuffer = await offlineRenderer.render(\n composition,\n resolver,\n (assetId) => decoder.decodeAudio(assetId),\n );\n if (signal.aborted) throw new Error('Export aborted');\n\n await audioSource.add(audioBuffer);\n audioSource.close();\n if (signal.aborted) throw new Error('Export aborted');\n\n await output.finalize();\n\n const buffer = target.buffer;\n if (!buffer) throw new Error('Export produced no output');\n\n const mimeType = options.format === 'webm' ? 'video/webm' : 'video/mp4';\n return new Blob([buffer], { type: mimeType });\n } finally {\n renderer.destroy();\n abortController = null;\n }\n },\n\n onProgress(cb: (progress: number) => void): () => void {\n progressListeners.add(cb);\n return () => { progressListeners.delete(cb); };\n },\n\n abort(): void {\n abortController?.abort();\n },\n };\n}\n"],"mappings":"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","names":["import_mediabunny","options"]}
package/dist/index.d.cts CHANGED
@@ -87,12 +87,36 @@ type PlaybackState = 'idle' | 'loading' | 'ready' | 'playing' | 'paused';
87
87
  interface PlaybackEngine {
88
88
  readonly state: PlaybackState;
89
89
  readonly currentTime: number;
90
+ /**
91
+ * True when the playhead sits at the end of the composition and playback
92
+ * cannot continue from there — i.e. a composition is loaded, no `loop`
93
+ * region is set, `duration > 0`, and `currentTime` has reached `duration`.
94
+ *
95
+ * Positional, like the HTML media element's `ended`: it becomes true both
96
+ * when playback runs to its natural end and when a consumer seeks to the
97
+ * end, and false again as soon as the playhead moves back. Transports can
98
+ * read it to render a "replay" affordance instead of re-deriving
99
+ * `currentTime >= duration ± ε` themselves.
100
+ */
101
+ readonly ended: boolean;
90
102
  playbackRate: number;
91
103
  loop: {
92
104
  start: number;
93
105
  end: number;
94
106
  } | null;
95
107
  load(composition: Composition, resolver: AssetResolver): Promise<void>;
108
+ /**
109
+ * Start playback.
110
+ *
111
+ * - When `ended` is true, the playhead first rewinds to 0 and playback
112
+ * restarts from the top (the HTML media element semantic, and what every
113
+ * video transport does on Play-at-end).
114
+ * - When the composition has zero duration there is nothing to play: the
115
+ * call is a documented no-op that leaves `state` untouched (no
116
+ * `playing` → `paused` flicker).
117
+ *
118
+ * Throws when no composition is loaded.
119
+ */
96
120
  play(): void;
97
121
  pause(): void;
98
122
  seek(time: number): void;
package/dist/index.d.ts CHANGED
@@ -87,12 +87,36 @@ type PlaybackState = 'idle' | 'loading' | 'ready' | 'playing' | 'paused';
87
87
  interface PlaybackEngine {
88
88
  readonly state: PlaybackState;
89
89
  readonly currentTime: number;
90
+ /**
91
+ * True when the playhead sits at the end of the composition and playback
92
+ * cannot continue from there — i.e. a composition is loaded, no `loop`
93
+ * region is set, `duration > 0`, and `currentTime` has reached `duration`.
94
+ *
95
+ * Positional, like the HTML media element's `ended`: it becomes true both
96
+ * when playback runs to its natural end and when a consumer seeks to the
97
+ * end, and false again as soon as the playhead moves back. Transports can
98
+ * read it to render a "replay" affordance instead of re-deriving
99
+ * `currentTime >= duration ± ε` themselves.
100
+ */
101
+ readonly ended: boolean;
90
102
  playbackRate: number;
91
103
  loop: {
92
104
  start: number;
93
105
  end: number;
94
106
  } | null;
95
107
  load(composition: Composition, resolver: AssetResolver): Promise<void>;
108
+ /**
109
+ * Start playback.
110
+ *
111
+ * - When `ended` is true, the playhead first rewinds to 0 and playback
112
+ * restarts from the top (the HTML media element semantic, and what every
113
+ * video transport does on Play-at-end).
114
+ * - When the composition has zero duration there is nothing to play: the
115
+ * call is a documented no-op that leaves `state` untouched (no
116
+ * `playing` → `paused` flicker).
117
+ *
118
+ * Throws when no composition is loaded.
119
+ */
96
120
  play(): void;
97
121
  pause(): void;
98
122
  seek(time: number): void;
package/dist/index.js CHANGED
@@ -891,6 +891,7 @@ function createAudioScheduler(options) {
891
891
 
892
892
  // src/playback-engine.ts
893
893
  var DEFAULT_DECODE_TIMEOUT_MS = 1e3;
894
+ var END_EPSILON = 1e-6;
894
895
  var DEBUG_LOOP = typeof globalThis !== "undefined" && globalThis.__PNEUMA_CRAFT_PLAYBACK_DEBUG__ === true;
895
896
  function createPlaybackEngine(options) {
896
897
  const compositorType = options?.compositorType ?? "canvas2d";
@@ -1025,6 +1026,13 @@ function createPlaybackEngine(options) {
1025
1026
  get currentTime() {
1026
1027
  return _clock?.currentTime ?? 0;
1027
1028
  },
1029
+ get ended() {
1030
+ if (!_clock || !_composition) return false;
1031
+ if (_clock.loop) return false;
1032
+ const duration = _composition.duration;
1033
+ if (duration <= 0) return false;
1034
+ return _clock.currentTime >= duration - END_EPSILON;
1035
+ },
1028
1036
  get playbackRate() {
1029
1037
  return _clock?.playbackRate ?? _playbackRate;
1030
1038
  },
@@ -1096,6 +1104,15 @@ function createPlaybackEngine(options) {
1096
1104
  throw new Error("Cannot play: no composition loaded. Call load() first.");
1097
1105
  }
1098
1106
  if (_state === "playing") return;
1107
+ if (_composition.duration <= 0) {
1108
+ console.warn("[PlaybackEngine] play() ignored: composition has zero duration.");
1109
+ return;
1110
+ }
1111
+ if (engine.ended) {
1112
+ _seekId++;
1113
+ _clock.seek(0);
1114
+ emitTimeUpdate(_clock.currentTime);
1115
+ }
1099
1116
  if (_audioContext && _audioContext.state === "suspended") {
1100
1117
  _audioContext.resume().catch((err) => {
1101
1118
  console.warn("[PlaybackEngine] AudioContext resume failed:", err);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/media-decoder.ts","../src/canvas2d-compositor.ts","../src/gpu-compositor.ts","../src/compositor.ts","../src/frame-renderer.ts","../src/master-clock.ts","../src/audio-scheduler.ts","../src/playback-engine.ts","../src/offline-audio-renderer.ts","../src/export-engine.ts"],"sourcesContent":["import type { MediaDecoder, AssetResolver, MediaInfo } from './types.js';\nimport { Input, BlobSource, CanvasSink, AudioBufferSink, ALL_FORMATS } from 'mediabunny';\n\ninterface CachedAsset {\n input: InstanceType<typeof Input>;\n blob: Blob;\n videoSink: InstanceType<typeof CanvasSink> | null;\n imageBitmap: ImageBitmap | null;\n audioBuffer: AudioBuffer | null;\n mediaInfo: MediaInfo | null;\n}\n\n/**\n * Decodes an image blob and rasterizes it onto a (width × height) OffscreenCanvas\n * with \"contain\" fit (aspect-preserving scale, transparent letterbox). This matches\n * the CanvasSink's `fit: 'contain'` + `alpha: true` behavior so image layers and\n * video layers share the exact same output dimensions — important because the GPU\n * compositor copies external images into a texture of that exact size and rejects\n * mismatches. The `{ alpha: true }` context option preserves the source image's\n * alpha channel (e.g. a PNG with transparency) and leaves letterbox regions fully\n * transparent so underlying layers show through.\n */\nasync function decodeImageFitted(blob: Blob, width: number, height: number): Promise<ImageBitmap> {\n const raw = await createImageBitmap(blob);\n try {\n const canvas = new OffscreenCanvas(width, height);\n const ctx = canvas.getContext('2d', { alpha: true });\n if (!ctx) throw new Error('Failed to get 2d context for image resize');\n const scale = Math.min(width / raw.width, height / raw.height);\n const drawW = raw.width * scale;\n const drawH = raw.height * scale;\n const dx = (width - drawW) / 2;\n const dy = (height - drawH) / 2;\n ctx.drawImage(raw, 0, 0, raw.width, raw.height, dx, dy, drawW, drawH);\n return await createImageBitmap(canvas);\n } finally {\n raw.close();\n }\n}\n\nexport function createMediaDecoder(\n resolver: AssetResolver,\n audioContext: BaseAudioContext,\n): MediaDecoder {\n const cache = new Map<string, CachedAsset>();\n const initPromises = new Map<string, Promise<CachedAsset>>();\n\n async function getOrCreateAsset(assetId: string): Promise<CachedAsset> {\n const existing = cache.get(assetId);\n if (existing) return existing;\n\n const pending = initPromises.get(assetId);\n if (pending) return pending;\n\n const promise = (async () => {\n const blob = await resolver.fetchBlob(assetId);\n const input = new Input({ source: new BlobSource(blob), formats: ALL_FORMATS });\n const asset: CachedAsset = { input, blob, videoSink: null, imageBitmap: null, audioBuffer: null, mediaInfo: null };\n cache.set(assetId, asset);\n initPromises.delete(assetId);\n return asset;\n })();\n\n // Remove from initPromises on failure so future attempts can retry\n promise.catch(() => {\n initPromises.delete(assetId);\n });\n\n initPromises.set(assetId, promise);\n return promise;\n }\n\n return {\n async decodeVideoFrame(assetId, time, width, height) {\n const asset = await getOrCreateAsset(assetId);\n\n // Image fast path — cached ImageBitmap (already fitted to the target\n // dimensions), timestamp ignored. An image clip behaves as a static\n // frame that occupies its Clip's full duration.\n if (asset.imageBitmap) return asset.imageBitmap;\n\n if (!asset.videoSink) {\n let videoTrack: Awaited<ReturnType<typeof asset.input.getPrimaryVideoTrack>> | null = null;\n try {\n videoTrack = await asset.input.getPrimaryVideoTrack();\n } catch {\n // MediaBunny couldn't parse this blob as a media container — likely\n // a standalone image (png/jpg/webp/gif). Fall through to ImageBitmap.\n }\n if (!videoTrack) {\n try {\n asset.imageBitmap = await decodeImageFitted(asset.blob, width, height);\n return asset.imageBitmap;\n } catch (err) {\n throw new Error(\n `Asset ${assetId} has no video track and is not a decodable image: ${(err as Error).message}`,\n );\n }\n }\n asset.videoSink = new CanvasSink(videoTrack, {\n width,\n height,\n fit: 'contain',\n poolSize: 5,\n // Preserve source alpha — required for transparent videos (e.g. WebM\n // with VP8/VP9 alpha, HEVC/AV1 with alpha). With `alpha: false`\n // (mediabunny default) CanvasSink flattens transparent pixels to a\n // black background, breaking compositing of alpha videos.\n alpha: true,\n });\n }\n const result = await asset.videoSink.getCanvas(time);\n if (!result) throw new Error(`Failed to decode frame at ${time}s for asset ${assetId}`);\n return result.canvas;\n },\n\n async decodeAudio(assetId) {\n const asset = await getOrCreateAsset(assetId);\n if (asset.audioBuffer) return asset.audioBuffer;\n\n // Fast path: try browser-native decodeAudioData on the whole blob.\n // This works for standalone audio files (mp3/wav/flac/ogg/m4a) and is\n // much simpler and faster than iterating through MediaBunny packets.\n try {\n const arrayBuffer = await asset.blob.arrayBuffer();\n // decodeAudioData detaches the buffer, so pass a copy (slice(0) is cheap).\n const buffer = await audioContext.decodeAudioData(arrayBuffer.slice(0));\n asset.audioBuffer = buffer;\n return buffer;\n } catch {\n // Fall through to MediaBunny path — typical for video containers whose\n // audio track the browser's decodeAudioData can't extract directly.\n }\n\n const audioTrack = await asset.input.getPrimaryAudioTrack();\n if (!audioTrack) throw new Error(`No audio track in asset ${assetId}`);\n\n // Iterate the full track via AudioBufferSink.buffers() and concatenate\n // into one AudioBuffer. getBuffer(0) alone only returns the first packet.\n const sink = new AudioBufferSink(audioTrack);\n const chunks: AudioBuffer[] = [];\n let totalFrames = 0;\n let sampleRate = 0;\n let numberOfChannels = 0;\n for await (const wrapped of sink.buffers()) {\n const buf = wrapped.buffer;\n chunks.push(buf);\n totalFrames += buf.length;\n if (!sampleRate) sampleRate = buf.sampleRate;\n if (!numberOfChannels) numberOfChannels = buf.numberOfChannels;\n }\n if (chunks.length === 0 || totalFrames === 0) {\n throw new Error(`Failed to decode audio for asset ${assetId}`);\n }\n\n const merged = audioContext.createBuffer(numberOfChannels, totalFrames, sampleRate);\n let offset = 0;\n for (const chunk of chunks) {\n for (let ch = 0; ch < numberOfChannels; ch++) {\n const src = chunk.getChannelData(Math.min(ch, chunk.numberOfChannels - 1));\n merged.copyToChannel(src, ch, offset);\n }\n offset += chunk.length;\n }\n asset.audioBuffer = merged;\n return merged;\n },\n\n async getMediaInfo(assetId) {\n const asset = await getOrCreateAsset(assetId);\n if (asset.mediaInfo) return asset.mediaInfo;\n const videoTrack = await asset.input.getPrimaryVideoTrack();\n const audioTrack = await asset.input.getPrimaryAudioTrack();\n const duration = await asset.input.computeDuration();\n let fps = 0;\n if (videoTrack) {\n const stats = await videoTrack.computePacketStats(100);\n fps = stats.averagePacketRate;\n }\n const info: MediaInfo = {\n duration,\n width: videoTrack?.displayWidth ?? 0,\n height: videoTrack?.displayHeight ?? 0,\n fps,\n hasVideo: videoTrack !== null,\n hasAudio: audioTrack !== null,\n videoCodec: videoTrack?.codec ?? null,\n audioCodec: audioTrack?.codec ?? null,\n sampleRate: audioTrack?.sampleRate ?? 0,\n channels: audioTrack?.numberOfChannels ?? 0,\n };\n asset.mediaInfo = info;\n return info;\n },\n\n destroy() {\n for (const asset of cache.values()) {\n asset.input.dispose();\n asset.imageBitmap?.close();\n }\n cache.clear();\n initPromises.clear();\n },\n };\n}\n","import type { Compositor, CompositeLayer } from './types.js';\n\nexport function createCanvas2DCompositor(width: number, height: number): Compositor {\n // `{ alpha: true }` keeps the backing buffer's alpha channel, so transparent\n // pixels from source layers (alpha videos, PNGs, letterboxed content) remain\n // transparent in the composed ImageBitmap instead of being flattened to black.\n let canvas = new OffscreenCanvas(width, height);\n let ctx = canvas.getContext('2d', { alpha: true })!;\n let currentWidth = width;\n let currentHeight = height;\n\n return {\n async composite(layers: CompositeLayer[]): Promise<ImageBitmap> {\n ctx.clearRect(0, 0, currentWidth, currentHeight);\n\n // Sort by zIndex ascending (low = bottom, high = top)\n const sorted = [...layers].sort((a, b) => a.zIndex - b.zIndex);\n\n for (const layer of sorted) {\n if (layer.opacity <= 0) continue;\n\n ctx.globalAlpha = layer.opacity;\n ctx.drawImage(layer.source, 0, 0, currentWidth, currentHeight);\n }\n\n ctx.globalAlpha = 1;\n return createImageBitmap(canvas);\n },\n\n resize(w: number, h: number): void {\n currentWidth = w;\n currentHeight = h;\n canvas = new OffscreenCanvas(w, h);\n ctx = canvas.getContext('2d', { alpha: true })!;\n },\n\n destroy(): void {\n // OffscreenCanvas has no explicit cleanup needed\n },\n };\n}\n","import type { Compositor, CompositeLayer } from './types.js';\n\nconst VERTEX_SHADER = /* wgsl */ `\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) texCoord: vec2f,\n};\n\n@vertex\nfn main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {\n // Full-screen quad: 6 vertices, 2 triangles\n var positions = array<vec2f, 6>(\n vec2f(-1.0, -1.0),\n vec2f( 1.0, -1.0),\n vec2f(-1.0, 1.0),\n vec2f(-1.0, 1.0),\n vec2f( 1.0, -1.0),\n vec2f( 1.0, 1.0),\n );\n\n var texCoords = array<vec2f, 6>(\n vec2f(0.0, 1.0),\n vec2f(1.0, 1.0),\n vec2f(0.0, 0.0),\n vec2f(0.0, 0.0),\n vec2f(1.0, 1.0),\n vec2f(1.0, 0.0),\n );\n\n var output: VertexOutput;\n output.position = vec4f(positions[vertexIndex], 0.0, 1.0);\n output.texCoord = texCoords[vertexIndex];\n return output;\n}\n`;\n\nconst FRAGMENT_SHADER = /* wgsl */ `\n@group(0) @binding(0) var texSampler: sampler;\n@group(0) @binding(1) var texSource: texture_2d<f32>;\n@group(0) @binding(2) var<uniform> opacity: f32;\n\n@fragment\nfn main(@location(0) texCoord: vec2f) -> @location(0) vec4f {\n var color = textureSample(texSource, texSampler, texCoord);\n // Multiply alpha by uniform opacity\n color.a = color.a * opacity;\n // Premultiply RGB by the new alpha\n color = vec4f(color.rgb * color.a, color.a);\n return color;\n}\n`;\n\nexport async function createGPUCompositor(width: number, height: number): Promise<Compositor> {\n if (!navigator.gpu) {\n throw new Error('WebGPU not available');\n }\n const adapter = await navigator.gpu.requestAdapter();\n if (!adapter) {\n throw new Error('Failed to get WebGPU adapter');\n }\n\n const device = await adapter.requestDevice();\n\n // Create sampler\n const sampler = device.createSampler({\n magFilter: 'linear',\n minFilter: 'linear',\n });\n\n // Create uniform buffer for opacity (f32 = 4 bytes, aligned to 16)\n const opacityBuffer = device.createBuffer({\n size: 16,\n usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,\n });\n\n // Create bind group layout\n const bindGroupLayout = device.createBindGroupLayout({\n entries: [\n { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: {} },\n { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: {} },\n { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },\n ],\n });\n\n const pipelineLayout = device.createPipelineLayout({\n bindGroupLayouts: [bindGroupLayout],\n });\n\n // Create shader modules\n const vertexModule = device.createShaderModule({ code: VERTEX_SHADER });\n const fragmentModule = device.createShaderModule({ code: FRAGMENT_SHADER });\n\n // Create render pipeline with premultiplied alpha blending\n const pipeline = device.createRenderPipeline({\n layout: pipelineLayout,\n vertex: {\n module: vertexModule,\n entryPoint: 'main',\n },\n fragment: {\n module: fragmentModule,\n entryPoint: 'main',\n targets: [{\n format: 'rgba8unorm',\n blend: {\n color: {\n srcFactor: 'one',\n dstFactor: 'one-minus-src-alpha',\n operation: 'add',\n },\n alpha: {\n srcFactor: 'one',\n dstFactor: 'one-minus-src-alpha',\n operation: 'add',\n },\n },\n }],\n },\n primitive: {\n topology: 'triangle-list',\n },\n });\n\n // Create output texture\n let currentWidth = width;\n let currentHeight = height;\n let outputTexture = createOutputTexture(device, width, height);\n\n function createOutputTexture(dev: GPUDevice, w: number, h: number): GPUTexture {\n return dev.createTexture({\n size: { width: w, height: h },\n format: 'rgba8unorm',\n usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.TEXTURE_BINDING,\n });\n }\n\n return {\n async composite(layers: CompositeLayer[]): Promise<ImageBitmap> {\n const sorted = [...layers]\n .filter((l) => l.opacity > 0)\n .sort((a, b) => a.zIndex - b.zIndex);\n\n const encoder = device.createCommandEncoder();\n const outputView = outputTexture.createView();\n\n const layerTextures: GPUTexture[] = [];\n\n if (sorted.length === 0) {\n // Clear pass only\n const pass = encoder.beginRenderPass({\n colorAttachments: [{\n view: outputView,\n loadOp: 'clear',\n storeOp: 'store',\n clearValue: { r: 0, g: 0, b: 0, a: 0 },\n }],\n });\n pass.end();\n } else {\n for (let i = 0; i < sorted.length; i++) {\n const layer = sorted[i];\n\n // Create texture from source\n const layerTexture = device.createTexture({\n size: { width: currentWidth, height: currentHeight },\n format: 'rgba8unorm',\n usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,\n });\n layerTextures.push(layerTexture);\n\n device.queue.copyExternalImageToTexture(\n { source: layer.source as ImageBitmap },\n { texture: layerTexture },\n { width: currentWidth, height: currentHeight },\n );\n\n // Write opacity uniform\n const opacityData = new Float32Array([layer.opacity]);\n device.queue.writeBuffer(opacityBuffer, 0, opacityData);\n\n // Create bind group for this layer\n const bindGroup = device.createBindGroup({\n layout: bindGroupLayout,\n entries: [\n { binding: 0, resource: sampler },\n { binding: 1, resource: layerTexture.createView() },\n { binding: 2, resource: { buffer: opacityBuffer } },\n ],\n });\n\n // Render pass: first pass clears, subsequent passes load\n const loadOp: 'load' | 'clear' = i === 0 ? 'clear' : 'load';\n const pass = encoder.beginRenderPass({\n colorAttachments: [{\n view: outputView,\n loadOp,\n storeOp: 'store',\n ...(loadOp === 'clear' ? { clearValue: { r: 0, g: 0, b: 0, a: 0 } } : {}),\n }],\n });\n\n pass.setPipeline(pipeline);\n pass.setBindGroup(0, bindGroup);\n pass.draw(6);\n pass.end();\n }\n }\n\n // Copy output texture to staging buffer for CPU readback\n const bytesPerRow = Math.ceil(currentWidth * 4 / 256) * 256;\n const bufferSize = bytesPerRow * currentHeight;\n const stagingBuffer = device.createBuffer({\n size: bufferSize,\n usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,\n });\n\n encoder.copyTextureToBuffer(\n { texture: outputTexture },\n { buffer: stagingBuffer, bytesPerRow },\n { width: currentWidth, height: currentHeight },\n );\n\n device.queue.submit([encoder.finish()]);\n\n // Destroy layer textures after command submission\n for (const tex of layerTextures) {\n tex.destroy();\n }\n\n // Read back pixels from staging buffer\n await stagingBuffer.mapAsync(GPUMapMode.READ);\n const rawData = stagingBuffer.getMappedRange();\n\n // Copy pixel data row by row (bytesPerRow may have padding)\n const pixelData = new Uint8ClampedArray(currentWidth * currentHeight * 4);\n const src = new Uint8Array(rawData);\n for (let row = 0; row < currentHeight; row++) {\n const srcOffset = row * bytesPerRow;\n const dstOffset = row * currentWidth * 4;\n pixelData.set(src.subarray(srcOffset, srcOffset + currentWidth * 4), dstOffset);\n }\n\n stagingBuffer.unmap();\n stagingBuffer.destroy();\n\n // The fragment shader outputs premultiplied RGB (see FRAGMENT_SHADER),\n // so `pixelData` is in premultiplied form. `putImageData` treats its\n // input as straight (unpremultiplied) alpha — feeding premultiplied\n // bytes in would cause the canvas to double-premultiply at semi-\n // transparent pixels, producing darkened/wrong colors on alpha-blended\n // edges. Unpremultiply so the straight-alpha ImageData contract holds.\n for (let i = 0; i < pixelData.length; i += 4) {\n const a = pixelData[i + 3];\n if (a > 0 && a < 255) {\n const inv = 255 / a;\n pixelData[i] = Math.min(255, Math.round(pixelData[i] * inv));\n pixelData[i + 1] = Math.min(255, Math.round(pixelData[i + 1] * inv));\n pixelData[i + 2] = Math.min(255, Math.round(pixelData[i + 2] * inv));\n }\n }\n\n // `{ alpha: true }` keeps the readback canvas's alpha channel so a fully\n // transparent output texture stays transparent (default `alpha: false`\n // would flatten it to opaque black).\n const readbackCanvas = new OffscreenCanvas(currentWidth, currentHeight);\n const ctx = readbackCanvas.getContext('2d', { alpha: true })!;\n const imageData = new ImageData(pixelData, currentWidth, currentHeight);\n ctx.putImageData(imageData, 0, 0);\n return createImageBitmap(readbackCanvas);\n },\n\n resize(w: number, h: number): void {\n outputTexture.destroy();\n currentWidth = w;\n currentHeight = h;\n outputTexture = createOutputTexture(device, w, h);\n },\n\n destroy(): void {\n outputTexture.destroy();\n opacityBuffer.destroy();\n device.destroy();\n },\n };\n}\n","import type { Compositor } from './types.js';\nimport { createCanvas2DCompositor } from './canvas2d-compositor.js';\nimport { createGPUCompositor } from './gpu-compositor.js';\n\nexport type CompositorType = 'gpu' | 'canvas2d' | 'auto';\n\nexport async function createCompositor(\n width: number,\n height: number,\n type: CompositorType = 'auto',\n): Promise<Compositor> {\n if (type === 'canvas2d') {\n return createCanvas2DCompositor(width, height);\n }\n\n if (type === 'gpu' || type === 'auto') {\n if (typeof navigator !== 'undefined' && (navigator as unknown as { gpu?: unknown }).gpu) {\n try {\n return await createGPUCompositor(width, height);\n } catch {\n if (type === 'gpu') {\n throw new Error('WebGPU compositor requested but initialization failed');\n }\n }\n } else if (type === 'gpu') {\n throw new Error('WebGPU not available');\n }\n }\n\n return createCanvas2DCompositor(width, height);\n}\n","import type {\n FrameRenderer,\n RenderedFrame,\n MediaDecoder,\n Compositor,\n CompositeLayer,\n SubtitleRenderer,\n} from './types.js';\nimport type { Composition, ResolvedClip, ResolvedPreviewFrame } from '@pneuma-craft/timeline';\nimport { resolveFrame } from '@pneuma-craft/timeline';\n\nexport interface CreateFrameRendererOptions {\n readonly subtitleRenderer?: SubtitleRenderer;\n /**\n * Render preview frames in tracks where no real clip covers the current\n * time. Defaults to false — preview frames are a planning-layer visual\n * that should NOT appear in final exports unless explicitly requested.\n * Engines wire their own defaults: PlaybackEngine passes true, ExportEngine\n * passes false.\n */\n readonly includePreviewFrames?: boolean;\n}\n\n// Accept either the legacy positional SubtitleRenderer (pre-preview-frame\n// callers) or the new options object. Internal callers (playback-engine,\n// export-engine) and tests have been migrated to the options form.\ntype LegacyArg = SubtitleRenderer | CreateFrameRendererOptions | undefined;\n\nfunction normalizeOptions(arg: LegacyArg): CreateFrameRendererOptions {\n if (arg === undefined) return {};\n if (typeof arg === 'function') return { subtitleRenderer: arg };\n return arg;\n}\n\nexport function createFrameRenderer(\n decoder: MediaDecoder,\n compositor: Compositor,\n width: number,\n height: number,\n optionsOrLegacySubtitleRenderer?: LegacyArg,\n): FrameRenderer {\n const options = normalizeOptions(optionsOrLegacySubtitleRenderer);\n const { subtitleRenderer, includePreviewFrames = false } = options;\n\n return {\n async renderFrame(composition: Composition, time: number): Promise<RenderedFrame> {\n const resolved = resolveFrame(composition, time);\n\n // Bucket per track for O(1) lookup. resolveFrame guarantees clips and\n // previewFrames are mutually exclusive per track, so we walk\n // composition.tracks once and pick at most one bucket per track.\n const clipsByTrack = new Map<string, ResolvedClip[]>();\n for (const rc of resolved.clips) {\n const arr = clipsByTrack.get(rc.track.id);\n if (arr) arr.push(rc); else clipsByTrack.set(rc.track.id, [rc]);\n }\n const previewByTrack = new Map<string, ResolvedPreviewFrame>();\n for (const rpf of resolved.previewFrames) previewByTrack.set(rpf.track.id, rpf);\n\n const layers: CompositeLayer[] = [];\n let zIndex = 0;\n\n // Video layers — iterate tracks in array order so z-stack matches\n // composition.tracks. Per-track decision: clip wins; preview is the\n // fallback only when allowed by the renderer's includePreviewFrames flag.\n for (const track of composition.tracks) {\n if (track.type !== 'video') continue;\n\n const trackClips = clipsByTrack.get(track.id);\n if (trackClips && trackClips.length > 0) {\n for (const rc of trackClips) {\n const source = await decoder.decodeVideoFrame(\n rc.clip.assetId, rc.localTime, width, height,\n );\n layers.push({ source, opacity: 1, zIndex: zIndex++ });\n }\n continue;\n }\n\n if (includePreviewFrames) {\n const rpf = previewByTrack.get(track.id);\n if (rpf) {\n // Image asset: decoder.decodeVideoFrame uses the cached-ImageBitmap\n // fast path and ignores the time argument.\n const source = await decoder.decodeVideoFrame(\n rpf.previewFrame.assetId, 0, width, height,\n );\n layers.push({ source, opacity: 1, zIndex: zIndex++ });\n }\n }\n }\n\n // Subtitles always composite on top of every video / preview layer,\n // regardless of track ordering in the composition. Track ordering\n // between multiple subtitle tracks is preserved.\n if (subtitleRenderer) {\n const subtitleClips = resolved.clips.filter(rc => rc.track.type === 'subtitle');\n for (const rc of subtitleClips) {\n const rendered = await subtitleRenderer({\n clip: rc.clip,\n localTime: rc.localTime,\n width,\n height,\n });\n if (rendered) {\n layers.push({ source: rendered, opacity: 1, zIndex: zIndex++ });\n }\n }\n }\n\n const image = await compositor.composite(layers);\n\n return { image, time, width, height };\n },\n\n destroy(): void {\n decoder.destroy();\n compositor.destroy();\n },\n };\n}\n","import type { MasterClock, ClockState } from './types.js';\n\nexport interface MasterClockOptions {\n audioContext: AudioContext;\n duration?: number;\n frameRate?: number;\n}\n\nexport function createMasterClock(options: MasterClockOptions): MasterClock {\n const audioContext = options.audioContext;\n\n let state: ClockState = 'stopped';\n let _duration = options.duration ?? 0;\n let _playbackRate = 1;\n let _loop: { start: number; end: number } | null = null;\n\n // Timing anchors\n let startAudioContextTime = 0;\n let startTimelineTime = 0;\n let pausedAt = 0;\n\n // Drift tracking\n let _driftMs = 0;\n\n // Subscribers\n const timeUpdateListeners = new Set<(time: number) => void>();\n const stateChangeListeners = new Set<(state: ClockState) => void>();\n\n function computeCurrentTime(): number {\n if (state !== 'playing') return pausedAt;\n\n const elapsed = (audioContext.currentTime - startAudioContextTime) * _playbackRate;\n let time = startTimelineTime + elapsed;\n\n if (_loop && _loop.end > _loop.start && time >= _loop.end) {\n const loopDuration = _loop.end - _loop.start;\n time = _loop.start + ((time - _loop.start) % loopDuration);\n }\n\n return Math.max(0, Math.min(time, _duration || Infinity));\n }\n\n function notifyTimeUpdate(time: number): void {\n for (const cb of timeUpdateListeners) {\n try { cb(time); } catch (e) { console.error('[MasterClock] listener error:', e); }\n }\n }\n\n function notifyStateChange(newState: ClockState): void {\n for (const cb of stateChangeListeners) {\n try { cb(newState); } catch (e) { console.error('[MasterClock] listener error:', e); }\n }\n }\n\n function setState(newState: ClockState): void {\n if (state === newState) return;\n state = newState;\n notifyStateChange(newState);\n }\n\n const clock: MasterClock = {\n get currentTime() { return computeCurrentTime(); },\n get state() { return state; },\n get driftMs() { return _driftMs; },\n\n get playbackRate() { return _playbackRate; },\n set playbackRate(rate: number) {\n const clamped = Math.max(0.1, Math.min(rate, 16));\n if (state === 'playing') {\n const current = computeCurrentTime();\n startTimelineTime = current;\n startAudioContextTime = audioContext.currentTime;\n }\n _playbackRate = clamped;\n },\n\n get duration() { return _duration; },\n set duration(d: number) { _duration = Math.max(0, d); },\n\n get loop() { return _loop; },\n set loop(l) { _loop = l; },\n\n play(): void {\n if (state === 'playing') return;\n startTimelineTime = pausedAt;\n startAudioContextTime = audioContext.currentTime;\n setState('playing');\n if (audioContext.state === 'suspended') {\n audioContext.resume();\n }\n },\n\n pause(): void {\n if (state !== 'playing') return;\n pausedAt = computeCurrentTime();\n setState('paused');\n },\n\n seek(time: number): void {\n const clamped = Math.max(0, Math.min(time, _duration));\n if (state === 'playing') {\n startTimelineTime = clamped;\n startAudioContextTime = audioContext.currentTime;\n } else {\n pausedAt = clamped;\n }\n notifyTimeUpdate(clamped);\n },\n\n reportVideoTime(videoTime: number): void {\n const audioTime = computeCurrentTime();\n _driftMs = (audioTime - videoTime) * 1000;\n },\n\n onTimeUpdate(cb) {\n timeUpdateListeners.add(cb);\n return () => { timeUpdateListeners.delete(cb); };\n },\n\n onStateChange(cb) {\n stateChangeListeners.add(cb);\n return () => { stateChangeListeners.delete(cb); };\n },\n\n destroy(): void {\n if (state === 'playing') {\n pausedAt = computeCurrentTime();\n }\n state = 'stopped';\n timeUpdateListeners.clear();\n stateChangeListeners.clear();\n _driftMs = 0;\n },\n };\n\n return clock;\n}\n","import type { AudioScheduler } from './types.js';\nimport type { Composition, Clip, Track } from '@pneuma-craft/timeline';\n\nexport interface AudioSchedulerOptions {\n audioContext: AudioContext;\n}\n\ninterface ActiveSource {\n source: AudioBufferSourceNode;\n clipGain: GainNode;\n}\n\nexport function createAudioScheduler(options: AudioSchedulerOptions): AudioScheduler {\n const { audioContext } = options;\n\n // Buffer storage: clipId → AudioBuffer\n const clipBuffers = new Map<string, AudioBuffer>();\n\n // Per-track gain nodes: trackId → GainNode\n const trackGains = new Map<string, GainNode>();\n\n // Per-track volume (separate from mute): trackId → volume\n const trackVolumes = new Map<string, number>();\n\n // Per-track muted state: trackId → muted\n const trackMutedStates = new Map<string, boolean>();\n\n // Currently active sources\n let activeSources: ActiveSource[] = [];\n\n // Scheduler tick state for future clip scheduling\n let schedulerIntervalId: ReturnType<typeof setInterval> | null = null;\n let scheduledClipIds = new Set<string>();\n let currentComposition: Composition | null = null;\n\n // Playback rate (applied to elapsed time and source nodes)\n let _playbackRate = 1;\n\n // Whether playback is currently active\n let _isPlaying = false;\n\n // Previous timeline time for loop-wrap detection\n let _prevTimelineTime = 0;\n\n // Callback to get current timeline time from the master clock (loop-aware)\n let _getCurrentTime: (() => number) | null = null;\n\n // Look-ahead window in seconds\n const LOOK_AHEAD = 0.2;\n\n // Master gain node connected to destination\n const masterGain = audioContext.createGain();\n masterGain.gain.value = 1;\n masterGain.connect(audioContext.destination);\n\n function getOrCreateTrackGain(track: Track): GainNode {\n let node = trackGains.get(track.id);\n if (!node) {\n node = audioContext.createGain();\n // Use runtime overrides if they exist, otherwise fall back to composition data\n const vol = trackVolumes.get(track.id) ?? track.volume ?? 1;\n const muted = trackMutedStates.get(track.id) ?? track.muted;\n node.gain.value = muted ? 0 : vol;\n trackVolumes.set(track.id, vol);\n node.connect(masterGain);\n trackGains.set(track.id, node);\n }\n return node;\n }\n\n function clearSchedulerTick(): void {\n if (schedulerIntervalId !== null) {\n clearInterval(schedulerIntervalId);\n schedulerIntervalId = null;\n }\n }\n\n function stopAllSources(): void {\n for (const { source } of activeSources) {\n try {\n source.stop();\n } catch {\n // Source may already be stopped\n }\n source.disconnect();\n }\n activeSources = [];\n }\n\n function isClipActiveAt(clip: Clip, fromTime: number): boolean {\n return clip.startTime <= fromTime && fromTime < clip.startTime + clip.duration;\n }\n\n function scheduleClip(clip: Clip, track: Track, fromTime: number): void {\n const buffer = clipBuffers.get(clip.id);\n if (!buffer) return;\n\n const trackGain = getOrCreateTrackGain(track);\n\n // Create per-clip gain node\n const clipGain = audioContext.createGain();\n const clipVolume = clip.volume ?? 1;\n clipGain.gain.value = clipVolume;\n\n // Create source node\n const source = audioContext.createBufferSource();\n source.buffer = buffer;\n\n // Apply playback rate to source node\n source.playbackRate.value = _playbackRate;\n\n // Connect: source → clipGain → trackGain → masterGain → destination\n source.connect(clipGain);\n clipGain.connect(trackGain);\n\n const now = audioContext.currentTime;\n\n // Determine if the clip starts in the future relative to current timeline time\n let contextStartTime: number;\n let sourceOffset: number;\n let remainingDuration: number;\n\n if (clip.startTime > fromTime) {\n // Future clip: schedule it to start at the correct AudioContext time\n // Divide by playbackRate: at 2x rate, 1 timeline second = 0.5 real seconds\n const timeUntilClipStart = (clip.startTime - fromTime) / _playbackRate;\n contextStartTime = now + timeUntilClipStart;\n sourceOffset = clip.inPoint;\n remainingDuration = clip.duration;\n } else {\n // Already active clip: start immediately with offset\n contextStartTime = 0;\n const elapsed = fromTime - clip.startTime;\n sourceOffset = clip.inPoint + elapsed;\n remainingDuration = clip.duration - elapsed;\n }\n\n // Apply fade automations\n // Fade durations must be divided by playbackRate since AudioContext runs in real time\n if (clip.fadeIn !== undefined && clip.fadeIn > 0) {\n const realFadeIn = clip.fadeIn / _playbackRate;\n if (clip.startTime > fromTime) {\n // Future clip: anchor fade to contextStartTime\n clipGain.gain.setValueAtTime(0, contextStartTime);\n clipGain.gain.linearRampToValueAtTime(clipVolume, contextStartTime + realFadeIn);\n } else {\n const fadeInEnd = clip.startTime + clip.fadeIn;\n if (fromTime < fadeInEnd) {\n // Still within fade-in region\n const fadeElapsed = fromTime - clip.startTime;\n const currentLevel = fadeElapsed > 0 ? (fadeElapsed / clip.fadeIn) * clipVolume : 0;\n const fadeRemaining = (clip.fadeIn - fadeElapsed) / _playbackRate;\n clipGain.gain.setValueAtTime(currentLevel, now);\n clipGain.gain.linearRampToValueAtTime(clipVolume, now + fadeRemaining);\n }\n }\n }\n\n if (clip.fadeOut !== undefined && clip.fadeOut > 0) {\n const realFadeOut = clip.fadeOut / _playbackRate;\n const realDuration = clip.duration / _playbackRate;\n if (clip.startTime > fromTime) {\n // Future clip: anchor fade to contextStartTime\n const fadeOutStart = contextStartTime + realDuration - realFadeOut;\n clipGain.gain.setValueAtTime(clipVolume, fadeOutStart);\n clipGain.gain.linearRampToValueAtTime(0, contextStartTime + realDuration);\n } else {\n const fadeOutStart = clip.startTime + clip.duration - clip.fadeOut;\n const fadeOutContextTime = now + Math.max(0, (fadeOutStart - fromTime) / _playbackRate);\n clipGain.gain.setValueAtTime(clipVolume, fadeOutContextTime);\n clipGain.gain.linearRampToValueAtTime(0, fadeOutContextTime + realFadeOut);\n }\n }\n\n source.start(contextStartTime, sourceOffset, remainingDuration);\n\n activeSources.push({ source, clipGain });\n }\n\n function isClipInLookAhead(clip: Clip, currentTime: number, lookAhead: number): boolean {\n const clipEnd = clip.startTime + clip.duration;\n // Clip starts within the look-ahead window and hasn't ended\n return clip.startTime > currentTime && clip.startTime <= currentTime + lookAhead && clipEnd > currentTime;\n }\n\n function scheduleComposition(fromTime: number, composition: Composition): void {\n for (const track of composition.tracks) {\n // Both audio tracks and video tracks contribute audio — a video clip's\n // embedded audio track gets scheduled alongside pure audio clips. Skip\n // anything that isn't a media track (e.g. subtitle) and anything that\n // the user has muted. Clips whose asset has no audio track never get a\n // buffer loaded (see loadClip in playback-engine.ts), so `scheduleClip`\n // naturally skips them.\n if (track.type !== 'audio' && track.type !== 'video') continue;\n if (track.muted) continue;\n\n for (const clip of track.clips) {\n if (!isClipActiveAt(clip, fromTime)) continue;\n if (scheduledClipIds.has(clip.id)) continue;\n scheduledClipIds.add(clip.id);\n scheduleClip(clip, track, fromTime);\n }\n }\n }\n\n function startSchedulerTick(fromTime: number, composition: Composition): void {\n clearSchedulerTick();\n currentComposition = composition;\n\n // Fallback values for computing timeline time when no getCurrentTime callback\n const tickStartContextTime = audioContext.currentTime;\n const tickStartFromTime = fromTime;\n\n schedulerIntervalId = setInterval(() => {\n if (!currentComposition) return;\n\n // Use clock's getCurrentTime if available (loop-aware), otherwise fallback\n let currentTimelineTime: number;\n if (_getCurrentTime) {\n currentTimelineTime = _getCurrentTime();\n } else {\n const elapsed = (audioContext.currentTime - tickStartContextTime) * _playbackRate;\n currentTimelineTime = tickStartFromTime + elapsed;\n }\n\n // Detect loop wrap: timeline time went backwards\n if (currentTimelineTime < _prevTimelineTime) {\n scheduledClipIds.clear();\n stopAllSources();\n }\n _prevTimelineTime = currentTimelineTime;\n\n for (const track of currentComposition.tracks) {\n if (track.type !== 'audio') continue;\n if (track.muted) continue;\n\n for (const clip of track.clips) {\n if (scheduledClipIds.has(clip.id)) continue;\n if (isClipInLookAhead(clip, currentTimelineTime, LOOK_AHEAD) || isClipActiveAt(clip, currentTimelineTime)) {\n scheduledClipIds.add(clip.id);\n scheduleClip(clip, track, currentTimelineTime);\n }\n }\n }\n }, 100);\n }\n\n const scheduler: AudioScheduler = {\n get audioContext() {\n return audioContext;\n },\n\n loadClip(clipId: string, audioBuffer: AudioBuffer): void {\n clipBuffers.set(clipId, audioBuffer);\n },\n\n play(fromTime: number, composition: Composition, getCurrentTime?: () => number): void {\n stopAllSources();\n clearSchedulerTick();\n scheduledClipIds = new Set();\n _prevTimelineTime = fromTime;\n _getCurrentTime = getCurrentTime ?? null;\n _isPlaying = true;\n currentComposition = composition;\n scheduleComposition(fromTime, composition);\n startSchedulerTick(fromTime, composition);\n },\n\n pause(): void {\n clearSchedulerTick();\n stopAllSources();\n scheduledClipIds = new Set();\n _isPlaying = false;\n },\n\n seek(time: number, composition: Composition): void {\n clearSchedulerTick();\n stopAllSources();\n scheduledClipIds = new Set();\n _prevTimelineTime = time;\n scheduleComposition(time, composition);\n startSchedulerTick(time, composition);\n },\n\n setPlaybackRate(rate: number): void {\n _playbackRate = rate;\n // If playing, reschedule everything with new rate so future clip\n // contextStartTime values are recalculated correctly.\n if (_isPlaying && currentComposition && _getCurrentTime) {\n stopAllSources();\n scheduledClipIds = new Set();\n const currentTime = _getCurrentTime();\n scheduleComposition(currentTime, currentComposition);\n clearSchedulerTick();\n startSchedulerTick(currentTime, currentComposition);\n } else {\n // Not playing or no getCurrentTime — just update active sources\n for (const scheduled of activeSources) {\n scheduled.source.playbackRate.value = rate;\n }\n }\n },\n\n setTrackVolume(trackId: string, volume: number): void {\n trackVolumes.set(trackId, volume);\n const node = trackGains.get(trackId);\n if (node) {\n // Only apply gain if the track is not muted\n const isMuted = trackMutedStates.get(trackId) ?? false;\n if (!isMuted) {\n node.gain.setValueAtTime(volume, audioContext.currentTime);\n }\n }\n },\n\n setTrackMute(trackId: string, muted: boolean): void {\n trackMutedStates.set(trackId, muted);\n const node = trackGains.get(trackId);\n if (node) {\n if (muted) {\n node.gain.value = 0;\n } else {\n const vol = trackVolumes.get(trackId) ?? 1;\n node.gain.value = vol;\n }\n }\n },\n\n destroy(): void {\n clearSchedulerTick();\n stopAllSources();\n scheduledClipIds = new Set();\n currentComposition = null;\n _isPlaying = false;\n for (const node of trackGains.values()) {\n node.disconnect();\n }\n trackGains.clear();\n trackVolumes.clear();\n trackMutedStates.clear();\n clipBuffers.clear();\n masterGain.disconnect();\n },\n };\n\n return scheduler;\n}\n","import type {\n PlaybackEngine,\n PlaybackState,\n RenderedFrame,\n AssetResolver,\n MediaDecoder,\n Compositor,\n FrameRenderer,\n MasterClock,\n AudioScheduler,\n SubtitleRenderer,\n} from './types.js';\nimport type { Composition } from '@pneuma-craft/timeline';\nimport { createMediaDecoder } from './media-decoder.js';\nimport { createCompositor, type CompositorType } from './compositor.js';\nimport { createFrameRenderer } from './frame-renderer.js';\nimport { createMasterClock } from './master-clock.js';\nimport { createAudioScheduler } from './audio-scheduler.js';\n\nexport interface PlaybackEngineOptions {\n compositorType?: CompositorType;\n /** Max time a single frame decode may block before the loop skips it. Default 1000ms. */\n decodeTimeoutMs?: number;\n /**\n * Optional rasterizer for subtitle-track clips. When provided, active\n * subtitle clips are composited on top of the video layers — the same code\n * path used for export, so preview and export stay in lockstep. When\n * omitted, subtitle tracks render nothing (legacy behavior).\n */\n subtitleRenderer?: SubtitleRenderer;\n /**\n * Render preview frames (planning-layer visuals attached to time points\n * on a track) when no real clip covers the current time. Defaults to\n * `true` for the playback engine — the user scrubbing the timeline\n * should see the agent's planned vibe before any real video clips land.\n */\n includePreviewFrames?: boolean;\n}\n\n/** Default per-frame decode timeout. Decodes exceeding this are logged and skipped. */\nconst DEFAULT_DECODE_TIMEOUT_MS = 1000;\n\n/** Enables verbose rAF-loop logs for debugging playback stalls. */\nconst DEBUG_LOOP = typeof globalThis !== 'undefined'\n && (globalThis as { __PNEUMA_CRAFT_PLAYBACK_DEBUG__?: boolean }).__PNEUMA_CRAFT_PLAYBACK_DEBUG__ === true;\n\nexport function createPlaybackEngine(options?: PlaybackEngineOptions): PlaybackEngine {\n const compositorType = options?.compositorType ?? 'canvas2d';\n const decodeTimeoutMs = options?.decodeTimeoutMs ?? DEFAULT_DECODE_TIMEOUT_MS;\n const subtitleRenderer = options?.subtitleRenderer;\n const includePreviewFrames = options?.includePreviewFrames ?? true;\n\n let _state: PlaybackState = 'idle';\n let _playbackRate = 1;\n let _loop: { start: number; end: number } | null = null;\n\n // Subsystems (created on load)\n let _decoder: MediaDecoder | null = null;\n let _compositor: Compositor | null = null;\n let _frameRenderer: FrameRenderer | null = null;\n let _clock: MasterClock | null = null;\n let _audioScheduler: AudioScheduler | null = null;\n let _audioContext: AudioContext | null = null;\n let _composition: Composition | null = null;\n\n // rAF handle\n let _rafId: number | null = null;\n\n // True while a frame decode is awaiting — prevents stacking concurrent renders\n // when one decode runs slower than a rAF tick.\n let _frameInFlight = false;\n\n // Seek ID counter for discarding stale paused-seek renders\n let _seekId = 0;\n\n // Subscribers\n const stateChangeListeners = new Set<(state: PlaybackState) => void>();\n const timeUpdateListeners = new Set<(time: number) => void>();\n const frameRenderedListeners = new Set<(frame: RenderedFrame) => void>();\n\n function setState(newState: PlaybackState): void {\n if (_state === newState) return;\n _state = newState;\n for (const cb of stateChangeListeners) {\n try { cb(newState); } catch (e) { console.error('[PlaybackEngine] listener error:', e); }\n }\n }\n\n function emitTimeUpdate(time: number): void {\n for (const cb of timeUpdateListeners) {\n try { cb(time); } catch (e) { console.error('[PlaybackEngine] listener error:', e); }\n }\n }\n\n function emitFrameRendered(frame: RenderedFrame): void {\n for (const cb of frameRenderedListeners) {\n try { cb(frame); } catch (e) { console.error('[PlaybackEngine] listener error:', e); }\n }\n }\n\n function destroySubsystems(): void {\n if (_rafId !== null) {\n cancelAnimationFrame(_rafId);\n _rafId = null;\n }\n _frameInFlight = false;\n _frameRenderer?.destroy();\n _clock?.destroy();\n _audioScheduler?.destroy();\n _audioContext?.close();\n _decoder = null;\n _compositor = null;\n _frameRenderer = null;\n _clock = null;\n _audioScheduler = null;\n _audioContext = null;\n _composition = null;\n }\n\n function startRafLoop(): void {\n if (_rafId !== null) return;\n\n // The loop is designed so that time updates and rAF scheduling are INDEPENDENT\n // of frame decoding. This means:\n // 1. If a decode hangs or is slow, the clock/slider still advances and pause/seek\n // remain responsive.\n // 2. At most one decode runs at a time (no concurrent pile-up).\n // 3. A decode exceeding `decodeTimeoutMs` is logged and skipped so the loop can\n // recover from a stuck codec packet pump.\n let lastLoggedState: 'render' | 'skip-inflight' | null = null;\n const logStateChange = (state: 'render' | 'skip-inflight', time: number, extra?: string): void => {\n if (!DEBUG_LOOP) return;\n if (lastLoggedState === state) return;\n lastLoggedState = state;\n console.log(`[engine] ${state} @ t=${time.toFixed(3)}${extra ? ' ' + extra : ''}`);\n };\n\n const loop = (): void => {\n _rafId = null;\n if (_state !== 'playing' || !_clock || !_frameRenderer || !_composition) return;\n\n const time = _clock.currentTime;\n\n // Emit time update up-front — consumers see the clock tick regardless of decode state.\n emitTimeUpdate(time);\n\n // End-of-timeline check.\n if (!_clock.loop && time >= _composition.duration) {\n engine.pause();\n return;\n }\n\n // Always schedule the next rAF synchronously so timing is decoupled from decode.\n _rafId = requestAnimationFrame(loop);\n\n // Skip this tick's render if the previous decode is still in flight — back-pressure\n // so slow decodes don't stack concurrent calls. The watchdog below recovers if a\n // decode never resolves; we never get stuck.\n //\n // NOTE: we intentionally do NOT skip based on _clock.driftMs. A prior implementation\n // did, which caused a deadlock: when the first decode ran long (e.g., cold-start\n // fetch of a 50 MB blob), reportVideoTime captured a large stale-drift value, then\n // every subsequent tick skipped on drift, so reportVideoTime never ran again, and\n // drift was frozen at the stale value forever. The clock is the source of truth;\n // each render just targets its current value, which naturally catches up.\n if (_frameInFlight) {\n logStateChange('skip-inflight', time);\n return;\n }\n\n logStateChange('render', time);\n\n // Kick off the decode. A watchdog timer clears _frameInFlight so the loop can\n // recover if renderFrame never resolves. We attach .then directly (no Promise.race)\n // to preserve the microtask profile — emitFrameRendered fires on the same tick\n // that renderFrame resolves.\n _frameInFlight = true;\n const clockRef = _clock;\n const compositionRef = _composition;\n let timedOut = false;\n const timeoutId = setTimeout(() => {\n timedOut = true;\n _frameInFlight = false;\n console.warn(\n `[PlaybackEngine] decode timeout at t=${time.toFixed(3)}s (>${decodeTimeoutMs}ms) — skipping frame`,\n );\n }, decodeTimeoutMs);\n\n _frameRenderer.renderFrame(compositionRef, time).then(frame => {\n clearTimeout(timeoutId);\n if (timedOut) return;\n _frameInFlight = false;\n if (_state !== 'playing' || _clock !== clockRef) return;\n clockRef.reportVideoTime(time);\n emitFrameRendered(frame);\n }).catch(err => {\n clearTimeout(timeoutId);\n if (timedOut) return;\n _frameInFlight = false;\n console.error('[PlaybackEngine] render error:', err);\n });\n };\n\n _rafId = requestAnimationFrame(loop);\n }\n\n function stopRafLoop(): void {\n if (_rafId !== null) {\n cancelAnimationFrame(_rafId);\n _rafId = null;\n }\n _frameInFlight = false;\n }\n\n const engine: PlaybackEngine = {\n get state() { return _state; },\n\n get currentTime() {\n return _clock?.currentTime ?? 0;\n },\n\n get playbackRate() {\n return _clock?.playbackRate ?? _playbackRate;\n },\n set playbackRate(rate: number) {\n _playbackRate = rate;\n if (_clock) {\n _clock.playbackRate = rate;\n }\n if (_audioScheduler) {\n _audioScheduler.setPlaybackRate(rate);\n }\n },\n\n get loop() {\n return _clock?.loop ?? _loop;\n },\n set loop(l: { start: number; end: number } | null) {\n _loop = l;\n if (_clock) {\n _clock.loop = l;\n }\n },\n\n async load(composition: Composition, resolver: AssetResolver): Promise<void> {\n // Invalidate any pending paused-seek renders from previous session\n _seekId++;\n\n // If previously loaded, destroy old subsystems\n if (_state !== 'idle') {\n destroySubsystems();\n }\n\n setState('loading');\n\n try {\n // Create AudioContext\n _audioContext = new AudioContext();\n\n // Create subsystems\n _decoder = createMediaDecoder(resolver, _audioContext);\n _compositor = await createCompositor(\n composition.settings.width,\n composition.settings.height,\n compositorType,\n );\n _frameRenderer = createFrameRenderer(\n _decoder,\n _compositor,\n composition.settings.width,\n composition.settings.height,\n { subtitleRenderer, includePreviewFrames },\n );\n _clock = createMasterClock({\n audioContext: _audioContext,\n duration: composition.duration,\n frameRate: composition.settings.fps,\n });\n _audioScheduler = createAudioScheduler({ audioContext: _audioContext });\n\n // Apply pending settings\n _clock.playbackRate = _playbackRate;\n _clock.loop = _loop;\n\n _composition = composition;\n\n // Pre-load audio buffers from every clip on every audio OR video track.\n // Video clips carry their own embedded audio (MediaDecoder.decodeAudio\n // extracts it from the container), so they participate in the audio\n // graph just like standalone audio clips. Clips with no audio stream\n // (images, silent videos) throw — we swallow and skip so they simply\n // have no buffer loaded, which makes scheduleClip a no-op for them.\n const mediaClips = composition.tracks\n .filter(track => track.type === 'audio' || track.type === 'video')\n .flatMap(track => track.clips);\n\n for (const clip of mediaClips) {\n try {\n const audioBuffer = await _decoder.decodeAudio(clip.assetId);\n _audioScheduler.loadClip(clip.id, audioBuffer);\n } catch {\n // Clip has no audio (image asset, silent video, or decode failure).\n // Leave the clip without a buffer; audio-scheduler tolerates this.\n }\n }\n\n setState('ready');\n } catch (err) {\n // Clean up any partially-created subsystems\n destroySubsystems();\n setState('idle');\n throw err;\n }\n },\n\n play(): void {\n if (!_clock || !_audioScheduler || !_composition) {\n throw new Error('Cannot play: no composition loaded. Call load() first.');\n }\n if (_state === 'playing') return;\n\n // AudioContext may start in 'suspended' state because it was created\n // inside engine.load() after awaits, losing the user-gesture chain.\n // resume() works as long as ANY user gesture has occurred on the page —\n // clicking the play button is sufficient. Fire-and-forget: audio will\n // unmute as soon as resume settles, which in practice is <1 frame.\n if (_audioContext && _audioContext.state === 'suspended') {\n _audioContext.resume().catch((err) => {\n console.warn('[PlaybackEngine] AudioContext resume failed:', err);\n });\n }\n\n _clock.play();\n _audioScheduler.setPlaybackRate(_playbackRate);\n const clockRef = _clock;\n _audioScheduler.play(_clock.currentTime, _composition, () => clockRef.currentTime);\n setState('playing');\n startRafLoop();\n },\n\n pause(): void {\n if (_state !== 'playing') return;\n\n stopRafLoop();\n _clock?.pause();\n _audioScheduler?.pause();\n setState('paused');\n },\n\n seek(time: number): void {\n if (!_clock || !_audioScheduler || !_composition) {\n throw new Error('Cannot seek: no composition loaded. Call load() first.');\n }\n\n _clock.seek(time);\n // Use the clamped time from clock so audio/video stay in sync\n const clampedTime = _clock.currentTime;\n\n // Only reschedule audio when playing — seeking while paused should not produce sound\n if (_state === 'playing') {\n _audioScheduler.setPlaybackRate(_playbackRate);\n _audioScheduler.seek(clampedTime, _composition);\n }\n\n // If not playing, render the frame at the seeked position\n if (_state !== 'playing' && _frameRenderer) {\n const thisSeekId = ++_seekId;\n _frameRenderer.renderFrame(_composition, clampedTime).then(frame => {\n if (thisSeekId !== _seekId) return; // Stale render, discard\n emitFrameRendered(frame);\n emitTimeUpdate(clampedTime);\n }).catch(err => {\n console.error('[PlaybackEngine] seek render error:', err);\n });\n }\n },\n\n onStateChange(cb: (state: PlaybackState) => void): () => void {\n stateChangeListeners.add(cb);\n return () => { stateChangeListeners.delete(cb); };\n },\n\n onTimeUpdate(cb: (time: number) => void): () => void {\n timeUpdateListeners.add(cb);\n return () => { timeUpdateListeners.delete(cb); };\n },\n\n onFrameRendered(cb: (frame: RenderedFrame) => void): () => void {\n frameRenderedListeners.add(cb);\n return () => { frameRenderedListeners.delete(cb); };\n },\n\n destroy(): void {\n stopRafLoop();\n destroySubsystems();\n stateChangeListeners.clear();\n timeUpdateListeners.clear();\n frameRenderedListeners.clear();\n _playbackRate = 1;\n _loop = null;\n _state = 'idle';\n },\n };\n\n return engine;\n}\n","import type { OfflineAudioRenderer } from './types.js';\nimport type { Clip, Track } from '@pneuma-craft/timeline';\n\nexport function createOfflineAudioRenderer(): OfflineAudioRenderer {\n return {\n async render(composition, _resolver, decodeAudio) {\n const sampleRate = composition.settings.sampleRate ?? 48000;\n const channels = 2;\n const duration = composition.duration;\n const length = Math.ceil(duration * sampleRate);\n\n const offlineCtx = new OfflineAudioContext(channels, length, sampleRate);\n\n // Collect clips from every unmuted media track. Video clips contribute\n // their embedded audio exactly like standalone audio clips — this is\n // what keeps the exported audio in lockstep with what the preview\n // plays. Subtitle/other non-media tracks are skipped.\n const audioClips: Array<{ clip: Clip; track: Track }> = [];\n for (const track of composition.tracks) {\n if (track.type !== 'audio' && track.type !== 'video') continue;\n if (track.muted) continue;\n for (const clip of track.clips) {\n audioClips.push({ clip, track });\n }\n }\n\n // Schedule each clip\n for (const { clip, track } of audioClips) {\n let buffer: AudioBuffer;\n try {\n buffer = await decodeAudio(clip.assetId);\n } catch {\n // Clip has no audio stream (image asset, silent video) or decode\n // failed. Skip silently — video-track clips routinely have no\n // audio, so a warning here would be noise. True decode failures\n // on audio assets surface via the preview path instead.\n continue;\n }\n\n const source = offlineCtx.createBufferSource();\n source.buffer = buffer;\n\n const clipGain = offlineCtx.createGain();\n const clipVolume = clip.volume ?? 1;\n clipGain.gain.value = clipVolume;\n\n const trackGain = offlineCtx.createGain();\n trackGain.gain.value = track.volume;\n\n source.connect(clipGain);\n clipGain.connect(trackGain);\n trackGain.connect(offlineCtx.destination);\n\n // Fade automation\n const fadeIn = clip.fadeIn;\n const fadeOut = clip.fadeOut;\n\n if (fadeIn && fadeIn > 0) {\n clipGain.gain.setValueAtTime(0, clip.startTime);\n clipGain.gain.linearRampToValueAtTime(clipVolume, clip.startTime + fadeIn);\n }\n\n if (fadeOut && fadeOut > 0) {\n const fadeOutStart = clip.startTime + clip.duration - fadeOut;\n clipGain.gain.setValueAtTime(clipVolume, fadeOutStart);\n clipGain.gain.linearRampToValueAtTime(0, clip.startTime + clip.duration);\n }\n\n source.start(clip.startTime, clip.inPoint, clip.duration);\n }\n\n return offlineCtx.startRendering();\n },\n };\n}\n","import type { ExportEngine, ExportOptions, AssetResolver, SubtitleRenderer } from './types.js';\nimport type { Composition } from '@pneuma-craft/timeline';\nimport { createMediaDecoder } from './media-decoder.js';\nimport { createCompositor } from './compositor.js';\nimport { createFrameRenderer } from './frame-renderer.js';\nimport { createOfflineAudioRenderer } from './offline-audio-renderer.js';\nimport {\n Output,\n CanvasSource,\n AudioBufferSource,\n Mp4OutputFormat,\n WebMOutputFormat,\n BufferTarget,\n} from 'mediabunny';\n\nexport interface ExportEngineOptions {\n /**\n * Optional rasterizer for subtitle-track clips. Pass the same renderer used\n * by `createPlaybackEngine` so the exported video is pixel-identical to the\n * preview. When omitted, subtitle tracks are skipped during export.\n */\n subtitleRenderer?: SubtitleRenderer;\n /**\n * Include preview frames (planning-layer visuals attached to time points\n * on a track) in the exported video. Defaults to `false` because an export\n * normally represents a finished cut. Set true for review-grade exports of\n * unfinished timelines — e.g., agent producing a \"草样片\" for the user to\n * scrub before committing to expensive real renders.\n */\n includePreviewFrames?: boolean;\n}\n\nexport function createExportEngine(options?: ExportEngineOptions): ExportEngine {\n const subtitleRenderer = options?.subtitleRenderer;\n const includePreviewFrames = options?.includePreviewFrames ?? false;\n const progressListeners = new Set<(progress: number) => void>();\n let abortController: AbortController | null = null;\n\n function notifyProgress(progress: number): void {\n for (const cb of progressListeners) {\n try { cb(progress); } catch (e) { console.error('[ExportEngine]', e); }\n }\n }\n\n return {\n async export(\n composition: Composition,\n options: ExportOptions,\n resolver: AssetResolver,\n ): Promise<Blob> {\n abortController = new AbortController();\n const signal = abortController.signal;\n\n const fps = options.fps ?? composition.settings.fps;\n const width = options.width ?? composition.settings.width;\n const height = options.height ?? composition.settings.height;\n const totalFrames = Math.ceil(composition.duration * fps);\n\n // Minimal OfflineAudioContext used only for decodeAudioData during asset\n // decoding (export has its own offline context for actual audio rendering).\n const decoderAudioCtx = new OfflineAudioContext(1, 1, composition.settings.sampleRate || 48000);\n const decoder = createMediaDecoder(resolver, decoderAudioCtx);\n const compositor = await createCompositor(width, height, 'canvas2d');\n const renderer = createFrameRenderer(decoder, compositor, width, height, {\n subtitleRenderer,\n includePreviewFrames,\n });\n\n try {\n const format = options.format === 'webm'\n ? new WebMOutputFormat()\n : new Mp4OutputFormat({ fastStart: 'in-memory' });\n\n const target = new BufferTarget();\n const output = new Output({ format, target });\n\n const renderCanvas = new OffscreenCanvas(width, height);\n const videoSource = new CanvasSource(renderCanvas, {\n codec: options.videoCodec,\n bitrate: options.videoBitrate,\n });\n\n const audioSource = new AudioBufferSource({\n codec: options.audioCodec,\n bitrate: options.audioBitrate,\n });\n\n output.addVideoTrack(videoSource);\n output.addAudioTrack(audioSource);\n await output.start();\n\n // Render video frames\n const renderCtx = renderCanvas.getContext('2d')!;\n\n for (let frame = 0; frame < totalFrames; frame++) {\n if (signal.aborted) throw new Error('Export aborted');\n\n const time = frame / fps;\n const rendered = await renderer.renderFrame(composition, time);\n\n renderCtx.clearRect(0, 0, width, height);\n renderCtx.drawImage(rendered.image, 0, 0, width, height);\n rendered.image.close();\n\n await videoSource.add(time, 1 / fps);\n\n if ((frame + 1) % 5 === 0 || frame === totalFrames - 1) {\n notifyProgress((frame + 1) / totalFrames);\n }\n\n // Yield to event loop periodically\n if ((frame + 1) % 5 === 0) {\n await new Promise(r => setTimeout(r, 0));\n }\n }\n\n videoSource.close();\n if (signal.aborted) throw new Error('Export aborted');\n\n // Render audio offline\n const offlineRenderer = createOfflineAudioRenderer();\n const audioBuffer = await offlineRenderer.render(\n composition,\n resolver,\n (assetId) => decoder.decodeAudio(assetId),\n );\n if (signal.aborted) throw new Error('Export aborted');\n\n await audioSource.add(audioBuffer);\n audioSource.close();\n if (signal.aborted) throw new Error('Export aborted');\n\n await output.finalize();\n\n const buffer = target.buffer;\n if (!buffer) throw new Error('Export produced no output');\n\n const mimeType = options.format === 'webm' ? 'video/webm' : 'video/mp4';\n return new Blob([buffer], { type: mimeType });\n } finally {\n renderer.destroy();\n abortController = null;\n }\n },\n\n onProgress(cb: (progress: number) => void): () => void {\n progressListeners.add(cb);\n return () => { progressListeners.delete(cb); };\n },\n\n abort(): void {\n abortController?.abort();\n },\n };\n}\n"],"mappings":"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","names":["options"]}
1
+ {"version":3,"sources":["../src/media-decoder.ts","../src/canvas2d-compositor.ts","../src/gpu-compositor.ts","../src/compositor.ts","../src/frame-renderer.ts","../src/master-clock.ts","../src/audio-scheduler.ts","../src/playback-engine.ts","../src/offline-audio-renderer.ts","../src/export-engine.ts"],"sourcesContent":["import type { MediaDecoder, AssetResolver, MediaInfo } from './types.js';\nimport { Input, BlobSource, CanvasSink, AudioBufferSink, ALL_FORMATS } from 'mediabunny';\n\ninterface CachedAsset {\n input: InstanceType<typeof Input>;\n blob: Blob;\n videoSink: InstanceType<typeof CanvasSink> | null;\n imageBitmap: ImageBitmap | null;\n audioBuffer: AudioBuffer | null;\n mediaInfo: MediaInfo | null;\n}\n\n/**\n * Decodes an image blob and rasterizes it onto a (width × height) OffscreenCanvas\n * with \"contain\" fit (aspect-preserving scale, transparent letterbox). This matches\n * the CanvasSink's `fit: 'contain'` + `alpha: true` behavior so image layers and\n * video layers share the exact same output dimensions — important because the GPU\n * compositor copies external images into a texture of that exact size and rejects\n * mismatches. The `{ alpha: true }` context option preserves the source image's\n * alpha channel (e.g. a PNG with transparency) and leaves letterbox regions fully\n * transparent so underlying layers show through.\n */\nasync function decodeImageFitted(blob: Blob, width: number, height: number): Promise<ImageBitmap> {\n const raw = await createImageBitmap(blob);\n try {\n const canvas = new OffscreenCanvas(width, height);\n const ctx = canvas.getContext('2d', { alpha: true });\n if (!ctx) throw new Error('Failed to get 2d context for image resize');\n const scale = Math.min(width / raw.width, height / raw.height);\n const drawW = raw.width * scale;\n const drawH = raw.height * scale;\n const dx = (width - drawW) / 2;\n const dy = (height - drawH) / 2;\n ctx.drawImage(raw, 0, 0, raw.width, raw.height, dx, dy, drawW, drawH);\n return await createImageBitmap(canvas);\n } finally {\n raw.close();\n }\n}\n\nexport function createMediaDecoder(\n resolver: AssetResolver,\n audioContext: BaseAudioContext,\n): MediaDecoder {\n const cache = new Map<string, CachedAsset>();\n const initPromises = new Map<string, Promise<CachedAsset>>();\n\n async function getOrCreateAsset(assetId: string): Promise<CachedAsset> {\n const existing = cache.get(assetId);\n if (existing) return existing;\n\n const pending = initPromises.get(assetId);\n if (pending) return pending;\n\n const promise = (async () => {\n const blob = await resolver.fetchBlob(assetId);\n const input = new Input({ source: new BlobSource(blob), formats: ALL_FORMATS });\n const asset: CachedAsset = { input, blob, videoSink: null, imageBitmap: null, audioBuffer: null, mediaInfo: null };\n cache.set(assetId, asset);\n initPromises.delete(assetId);\n return asset;\n })();\n\n // Remove from initPromises on failure so future attempts can retry\n promise.catch(() => {\n initPromises.delete(assetId);\n });\n\n initPromises.set(assetId, promise);\n return promise;\n }\n\n return {\n async decodeVideoFrame(assetId, time, width, height) {\n const asset = await getOrCreateAsset(assetId);\n\n // Image fast path — cached ImageBitmap (already fitted to the target\n // dimensions), timestamp ignored. An image clip behaves as a static\n // frame that occupies its Clip's full duration.\n if (asset.imageBitmap) return asset.imageBitmap;\n\n if (!asset.videoSink) {\n let videoTrack: Awaited<ReturnType<typeof asset.input.getPrimaryVideoTrack>> | null = null;\n try {\n videoTrack = await asset.input.getPrimaryVideoTrack();\n } catch {\n // MediaBunny couldn't parse this blob as a media container — likely\n // a standalone image (png/jpg/webp/gif). Fall through to ImageBitmap.\n }\n if (!videoTrack) {\n try {\n asset.imageBitmap = await decodeImageFitted(asset.blob, width, height);\n return asset.imageBitmap;\n } catch (err) {\n throw new Error(\n `Asset ${assetId} has no video track and is not a decodable image: ${(err as Error).message}`,\n );\n }\n }\n asset.videoSink = new CanvasSink(videoTrack, {\n width,\n height,\n fit: 'contain',\n poolSize: 5,\n // Preserve source alpha — required for transparent videos (e.g. WebM\n // with VP8/VP9 alpha, HEVC/AV1 with alpha). With `alpha: false`\n // (mediabunny default) CanvasSink flattens transparent pixels to a\n // black background, breaking compositing of alpha videos.\n alpha: true,\n });\n }\n const result = await asset.videoSink.getCanvas(time);\n if (!result) throw new Error(`Failed to decode frame at ${time}s for asset ${assetId}`);\n return result.canvas;\n },\n\n async decodeAudio(assetId) {\n const asset = await getOrCreateAsset(assetId);\n if (asset.audioBuffer) return asset.audioBuffer;\n\n // Fast path: try browser-native decodeAudioData on the whole blob.\n // This works for standalone audio files (mp3/wav/flac/ogg/m4a) and is\n // much simpler and faster than iterating through MediaBunny packets.\n try {\n const arrayBuffer = await asset.blob.arrayBuffer();\n // decodeAudioData detaches the buffer, so pass a copy (slice(0) is cheap).\n const buffer = await audioContext.decodeAudioData(arrayBuffer.slice(0));\n asset.audioBuffer = buffer;\n return buffer;\n } catch {\n // Fall through to MediaBunny path — typical for video containers whose\n // audio track the browser's decodeAudioData can't extract directly.\n }\n\n const audioTrack = await asset.input.getPrimaryAudioTrack();\n if (!audioTrack) throw new Error(`No audio track in asset ${assetId}`);\n\n // Iterate the full track via AudioBufferSink.buffers() and concatenate\n // into one AudioBuffer. getBuffer(0) alone only returns the first packet.\n const sink = new AudioBufferSink(audioTrack);\n const chunks: AudioBuffer[] = [];\n let totalFrames = 0;\n let sampleRate = 0;\n let numberOfChannels = 0;\n for await (const wrapped of sink.buffers()) {\n const buf = wrapped.buffer;\n chunks.push(buf);\n totalFrames += buf.length;\n if (!sampleRate) sampleRate = buf.sampleRate;\n if (!numberOfChannels) numberOfChannels = buf.numberOfChannels;\n }\n if (chunks.length === 0 || totalFrames === 0) {\n throw new Error(`Failed to decode audio for asset ${assetId}`);\n }\n\n const merged = audioContext.createBuffer(numberOfChannels, totalFrames, sampleRate);\n let offset = 0;\n for (const chunk of chunks) {\n for (let ch = 0; ch < numberOfChannels; ch++) {\n const src = chunk.getChannelData(Math.min(ch, chunk.numberOfChannels - 1));\n merged.copyToChannel(src, ch, offset);\n }\n offset += chunk.length;\n }\n asset.audioBuffer = merged;\n return merged;\n },\n\n async getMediaInfo(assetId) {\n const asset = await getOrCreateAsset(assetId);\n if (asset.mediaInfo) return asset.mediaInfo;\n const videoTrack = await asset.input.getPrimaryVideoTrack();\n const audioTrack = await asset.input.getPrimaryAudioTrack();\n const duration = await asset.input.computeDuration();\n let fps = 0;\n if (videoTrack) {\n const stats = await videoTrack.computePacketStats(100);\n fps = stats.averagePacketRate;\n }\n const info: MediaInfo = {\n duration,\n width: videoTrack?.displayWidth ?? 0,\n height: videoTrack?.displayHeight ?? 0,\n fps,\n hasVideo: videoTrack !== null,\n hasAudio: audioTrack !== null,\n videoCodec: videoTrack?.codec ?? null,\n audioCodec: audioTrack?.codec ?? null,\n sampleRate: audioTrack?.sampleRate ?? 0,\n channels: audioTrack?.numberOfChannels ?? 0,\n };\n asset.mediaInfo = info;\n return info;\n },\n\n destroy() {\n for (const asset of cache.values()) {\n asset.input.dispose();\n asset.imageBitmap?.close();\n }\n cache.clear();\n initPromises.clear();\n },\n };\n}\n","import type { Compositor, CompositeLayer } from './types.js';\n\nexport function createCanvas2DCompositor(width: number, height: number): Compositor {\n // `{ alpha: true }` keeps the backing buffer's alpha channel, so transparent\n // pixels from source layers (alpha videos, PNGs, letterboxed content) remain\n // transparent in the composed ImageBitmap instead of being flattened to black.\n let canvas = new OffscreenCanvas(width, height);\n let ctx = canvas.getContext('2d', { alpha: true })!;\n let currentWidth = width;\n let currentHeight = height;\n\n return {\n async composite(layers: CompositeLayer[]): Promise<ImageBitmap> {\n ctx.clearRect(0, 0, currentWidth, currentHeight);\n\n // Sort by zIndex ascending (low = bottom, high = top)\n const sorted = [...layers].sort((a, b) => a.zIndex - b.zIndex);\n\n for (const layer of sorted) {\n if (layer.opacity <= 0) continue;\n\n ctx.globalAlpha = layer.opacity;\n ctx.drawImage(layer.source, 0, 0, currentWidth, currentHeight);\n }\n\n ctx.globalAlpha = 1;\n return createImageBitmap(canvas);\n },\n\n resize(w: number, h: number): void {\n currentWidth = w;\n currentHeight = h;\n canvas = new OffscreenCanvas(w, h);\n ctx = canvas.getContext('2d', { alpha: true })!;\n },\n\n destroy(): void {\n // OffscreenCanvas has no explicit cleanup needed\n },\n };\n}\n","import type { Compositor, CompositeLayer } from './types.js';\n\nconst VERTEX_SHADER = /* wgsl */ `\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) texCoord: vec2f,\n};\n\n@vertex\nfn main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {\n // Full-screen quad: 6 vertices, 2 triangles\n var positions = array<vec2f, 6>(\n vec2f(-1.0, -1.0),\n vec2f( 1.0, -1.0),\n vec2f(-1.0, 1.0),\n vec2f(-1.0, 1.0),\n vec2f( 1.0, -1.0),\n vec2f( 1.0, 1.0),\n );\n\n var texCoords = array<vec2f, 6>(\n vec2f(0.0, 1.0),\n vec2f(1.0, 1.0),\n vec2f(0.0, 0.0),\n vec2f(0.0, 0.0),\n vec2f(1.0, 1.0),\n vec2f(1.0, 0.0),\n );\n\n var output: VertexOutput;\n output.position = vec4f(positions[vertexIndex], 0.0, 1.0);\n output.texCoord = texCoords[vertexIndex];\n return output;\n}\n`;\n\nconst FRAGMENT_SHADER = /* wgsl */ `\n@group(0) @binding(0) var texSampler: sampler;\n@group(0) @binding(1) var texSource: texture_2d<f32>;\n@group(0) @binding(2) var<uniform> opacity: f32;\n\n@fragment\nfn main(@location(0) texCoord: vec2f) -> @location(0) vec4f {\n var color = textureSample(texSource, texSampler, texCoord);\n // Multiply alpha by uniform opacity\n color.a = color.a * opacity;\n // Premultiply RGB by the new alpha\n color = vec4f(color.rgb * color.a, color.a);\n return color;\n}\n`;\n\nexport async function createGPUCompositor(width: number, height: number): Promise<Compositor> {\n if (!navigator.gpu) {\n throw new Error('WebGPU not available');\n }\n const adapter = await navigator.gpu.requestAdapter();\n if (!adapter) {\n throw new Error('Failed to get WebGPU adapter');\n }\n\n const device = await adapter.requestDevice();\n\n // Create sampler\n const sampler = device.createSampler({\n magFilter: 'linear',\n minFilter: 'linear',\n });\n\n // Create uniform buffer for opacity (f32 = 4 bytes, aligned to 16)\n const opacityBuffer = device.createBuffer({\n size: 16,\n usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,\n });\n\n // Create bind group layout\n const bindGroupLayout = device.createBindGroupLayout({\n entries: [\n { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: {} },\n { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: {} },\n { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },\n ],\n });\n\n const pipelineLayout = device.createPipelineLayout({\n bindGroupLayouts: [bindGroupLayout],\n });\n\n // Create shader modules\n const vertexModule = device.createShaderModule({ code: VERTEX_SHADER });\n const fragmentModule = device.createShaderModule({ code: FRAGMENT_SHADER });\n\n // Create render pipeline with premultiplied alpha blending\n const pipeline = device.createRenderPipeline({\n layout: pipelineLayout,\n vertex: {\n module: vertexModule,\n entryPoint: 'main',\n },\n fragment: {\n module: fragmentModule,\n entryPoint: 'main',\n targets: [{\n format: 'rgba8unorm',\n blend: {\n color: {\n srcFactor: 'one',\n dstFactor: 'one-minus-src-alpha',\n operation: 'add',\n },\n alpha: {\n srcFactor: 'one',\n dstFactor: 'one-minus-src-alpha',\n operation: 'add',\n },\n },\n }],\n },\n primitive: {\n topology: 'triangle-list',\n },\n });\n\n // Create output texture\n let currentWidth = width;\n let currentHeight = height;\n let outputTexture = createOutputTexture(device, width, height);\n\n function createOutputTexture(dev: GPUDevice, w: number, h: number): GPUTexture {\n return dev.createTexture({\n size: { width: w, height: h },\n format: 'rgba8unorm',\n usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.TEXTURE_BINDING,\n });\n }\n\n return {\n async composite(layers: CompositeLayer[]): Promise<ImageBitmap> {\n const sorted = [...layers]\n .filter((l) => l.opacity > 0)\n .sort((a, b) => a.zIndex - b.zIndex);\n\n const encoder = device.createCommandEncoder();\n const outputView = outputTexture.createView();\n\n const layerTextures: GPUTexture[] = [];\n\n if (sorted.length === 0) {\n // Clear pass only\n const pass = encoder.beginRenderPass({\n colorAttachments: [{\n view: outputView,\n loadOp: 'clear',\n storeOp: 'store',\n clearValue: { r: 0, g: 0, b: 0, a: 0 },\n }],\n });\n pass.end();\n } else {\n for (let i = 0; i < sorted.length; i++) {\n const layer = sorted[i];\n\n // Create texture from source\n const layerTexture = device.createTexture({\n size: { width: currentWidth, height: currentHeight },\n format: 'rgba8unorm',\n usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,\n });\n layerTextures.push(layerTexture);\n\n device.queue.copyExternalImageToTexture(\n { source: layer.source as ImageBitmap },\n { texture: layerTexture },\n { width: currentWidth, height: currentHeight },\n );\n\n // Write opacity uniform\n const opacityData = new Float32Array([layer.opacity]);\n device.queue.writeBuffer(opacityBuffer, 0, opacityData);\n\n // Create bind group for this layer\n const bindGroup = device.createBindGroup({\n layout: bindGroupLayout,\n entries: [\n { binding: 0, resource: sampler },\n { binding: 1, resource: layerTexture.createView() },\n { binding: 2, resource: { buffer: opacityBuffer } },\n ],\n });\n\n // Render pass: first pass clears, subsequent passes load\n const loadOp: 'load' | 'clear' = i === 0 ? 'clear' : 'load';\n const pass = encoder.beginRenderPass({\n colorAttachments: [{\n view: outputView,\n loadOp,\n storeOp: 'store',\n ...(loadOp === 'clear' ? { clearValue: { r: 0, g: 0, b: 0, a: 0 } } : {}),\n }],\n });\n\n pass.setPipeline(pipeline);\n pass.setBindGroup(0, bindGroup);\n pass.draw(6);\n pass.end();\n }\n }\n\n // Copy output texture to staging buffer for CPU readback\n const bytesPerRow = Math.ceil(currentWidth * 4 / 256) * 256;\n const bufferSize = bytesPerRow * currentHeight;\n const stagingBuffer = device.createBuffer({\n size: bufferSize,\n usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,\n });\n\n encoder.copyTextureToBuffer(\n { texture: outputTexture },\n { buffer: stagingBuffer, bytesPerRow },\n { width: currentWidth, height: currentHeight },\n );\n\n device.queue.submit([encoder.finish()]);\n\n // Destroy layer textures after command submission\n for (const tex of layerTextures) {\n tex.destroy();\n }\n\n // Read back pixels from staging buffer\n await stagingBuffer.mapAsync(GPUMapMode.READ);\n const rawData = stagingBuffer.getMappedRange();\n\n // Copy pixel data row by row (bytesPerRow may have padding)\n const pixelData = new Uint8ClampedArray(currentWidth * currentHeight * 4);\n const src = new Uint8Array(rawData);\n for (let row = 0; row < currentHeight; row++) {\n const srcOffset = row * bytesPerRow;\n const dstOffset = row * currentWidth * 4;\n pixelData.set(src.subarray(srcOffset, srcOffset + currentWidth * 4), dstOffset);\n }\n\n stagingBuffer.unmap();\n stagingBuffer.destroy();\n\n // The fragment shader outputs premultiplied RGB (see FRAGMENT_SHADER),\n // so `pixelData` is in premultiplied form. `putImageData` treats its\n // input as straight (unpremultiplied) alpha — feeding premultiplied\n // bytes in would cause the canvas to double-premultiply at semi-\n // transparent pixels, producing darkened/wrong colors on alpha-blended\n // edges. Unpremultiply so the straight-alpha ImageData contract holds.\n for (let i = 0; i < pixelData.length; i += 4) {\n const a = pixelData[i + 3];\n if (a > 0 && a < 255) {\n const inv = 255 / a;\n pixelData[i] = Math.min(255, Math.round(pixelData[i] * inv));\n pixelData[i + 1] = Math.min(255, Math.round(pixelData[i + 1] * inv));\n pixelData[i + 2] = Math.min(255, Math.round(pixelData[i + 2] * inv));\n }\n }\n\n // `{ alpha: true }` keeps the readback canvas's alpha channel so a fully\n // transparent output texture stays transparent (default `alpha: false`\n // would flatten it to opaque black).\n const readbackCanvas = new OffscreenCanvas(currentWidth, currentHeight);\n const ctx = readbackCanvas.getContext('2d', { alpha: true })!;\n const imageData = new ImageData(pixelData, currentWidth, currentHeight);\n ctx.putImageData(imageData, 0, 0);\n return createImageBitmap(readbackCanvas);\n },\n\n resize(w: number, h: number): void {\n outputTexture.destroy();\n currentWidth = w;\n currentHeight = h;\n outputTexture = createOutputTexture(device, w, h);\n },\n\n destroy(): void {\n outputTexture.destroy();\n opacityBuffer.destroy();\n device.destroy();\n },\n };\n}\n","import type { Compositor } from './types.js';\nimport { createCanvas2DCompositor } from './canvas2d-compositor.js';\nimport { createGPUCompositor } from './gpu-compositor.js';\n\nexport type CompositorType = 'gpu' | 'canvas2d' | 'auto';\n\nexport async function createCompositor(\n width: number,\n height: number,\n type: CompositorType = 'auto',\n): Promise<Compositor> {\n if (type === 'canvas2d') {\n return createCanvas2DCompositor(width, height);\n }\n\n if (type === 'gpu' || type === 'auto') {\n if (typeof navigator !== 'undefined' && (navigator as unknown as { gpu?: unknown }).gpu) {\n try {\n return await createGPUCompositor(width, height);\n } catch {\n if (type === 'gpu') {\n throw new Error('WebGPU compositor requested but initialization failed');\n }\n }\n } else if (type === 'gpu') {\n throw new Error('WebGPU not available');\n }\n }\n\n return createCanvas2DCompositor(width, height);\n}\n","import type {\n FrameRenderer,\n RenderedFrame,\n MediaDecoder,\n Compositor,\n CompositeLayer,\n SubtitleRenderer,\n} from './types.js';\nimport type { Composition, ResolvedClip, ResolvedPreviewFrame } from '@pneuma-craft/timeline';\nimport { resolveFrame } from '@pneuma-craft/timeline';\n\nexport interface CreateFrameRendererOptions {\n readonly subtitleRenderer?: SubtitleRenderer;\n /**\n * Render preview frames in tracks where no real clip covers the current\n * time. Defaults to false — preview frames are a planning-layer visual\n * that should NOT appear in final exports unless explicitly requested.\n * Engines wire their own defaults: PlaybackEngine passes true, ExportEngine\n * passes false.\n */\n readonly includePreviewFrames?: boolean;\n}\n\n// Accept either the legacy positional SubtitleRenderer (pre-preview-frame\n// callers) or the new options object. Internal callers (playback-engine,\n// export-engine) and tests have been migrated to the options form.\ntype LegacyArg = SubtitleRenderer | CreateFrameRendererOptions | undefined;\n\nfunction normalizeOptions(arg: LegacyArg): CreateFrameRendererOptions {\n if (arg === undefined) return {};\n if (typeof arg === 'function') return { subtitleRenderer: arg };\n return arg;\n}\n\nexport function createFrameRenderer(\n decoder: MediaDecoder,\n compositor: Compositor,\n width: number,\n height: number,\n optionsOrLegacySubtitleRenderer?: LegacyArg,\n): FrameRenderer {\n const options = normalizeOptions(optionsOrLegacySubtitleRenderer);\n const { subtitleRenderer, includePreviewFrames = false } = options;\n\n return {\n async renderFrame(composition: Composition, time: number): Promise<RenderedFrame> {\n const resolved = resolveFrame(composition, time);\n\n // Bucket per track for O(1) lookup. resolveFrame guarantees clips and\n // previewFrames are mutually exclusive per track, so we walk\n // composition.tracks once and pick at most one bucket per track.\n const clipsByTrack = new Map<string, ResolvedClip[]>();\n for (const rc of resolved.clips) {\n const arr = clipsByTrack.get(rc.track.id);\n if (arr) arr.push(rc); else clipsByTrack.set(rc.track.id, [rc]);\n }\n const previewByTrack = new Map<string, ResolvedPreviewFrame>();\n for (const rpf of resolved.previewFrames) previewByTrack.set(rpf.track.id, rpf);\n\n const layers: CompositeLayer[] = [];\n let zIndex = 0;\n\n // Video layers — iterate tracks in array order so z-stack matches\n // composition.tracks. Per-track decision: clip wins; preview is the\n // fallback only when allowed by the renderer's includePreviewFrames flag.\n for (const track of composition.tracks) {\n if (track.type !== 'video') continue;\n\n const trackClips = clipsByTrack.get(track.id);\n if (trackClips && trackClips.length > 0) {\n for (const rc of trackClips) {\n const source = await decoder.decodeVideoFrame(\n rc.clip.assetId, rc.localTime, width, height,\n );\n layers.push({ source, opacity: 1, zIndex: zIndex++ });\n }\n continue;\n }\n\n if (includePreviewFrames) {\n const rpf = previewByTrack.get(track.id);\n if (rpf) {\n // Image asset: decoder.decodeVideoFrame uses the cached-ImageBitmap\n // fast path and ignores the time argument.\n const source = await decoder.decodeVideoFrame(\n rpf.previewFrame.assetId, 0, width, height,\n );\n layers.push({ source, opacity: 1, zIndex: zIndex++ });\n }\n }\n }\n\n // Subtitles always composite on top of every video / preview layer,\n // regardless of track ordering in the composition. Track ordering\n // between multiple subtitle tracks is preserved.\n if (subtitleRenderer) {\n const subtitleClips = resolved.clips.filter(rc => rc.track.type === 'subtitle');\n for (const rc of subtitleClips) {\n const rendered = await subtitleRenderer({\n clip: rc.clip,\n localTime: rc.localTime,\n width,\n height,\n });\n if (rendered) {\n layers.push({ source: rendered, opacity: 1, zIndex: zIndex++ });\n }\n }\n }\n\n const image = await compositor.composite(layers);\n\n return { image, time, width, height };\n },\n\n destroy(): void {\n decoder.destroy();\n compositor.destroy();\n },\n };\n}\n","import type { MasterClock, ClockState } from './types.js';\n\nexport interface MasterClockOptions {\n audioContext: AudioContext;\n duration?: number;\n frameRate?: number;\n}\n\nexport function createMasterClock(options: MasterClockOptions): MasterClock {\n const audioContext = options.audioContext;\n\n let state: ClockState = 'stopped';\n let _duration = options.duration ?? 0;\n let _playbackRate = 1;\n let _loop: { start: number; end: number } | null = null;\n\n // Timing anchors\n let startAudioContextTime = 0;\n let startTimelineTime = 0;\n let pausedAt = 0;\n\n // Drift tracking\n let _driftMs = 0;\n\n // Subscribers\n const timeUpdateListeners = new Set<(time: number) => void>();\n const stateChangeListeners = new Set<(state: ClockState) => void>();\n\n function computeCurrentTime(): number {\n if (state !== 'playing') return pausedAt;\n\n const elapsed = (audioContext.currentTime - startAudioContextTime) * _playbackRate;\n let time = startTimelineTime + elapsed;\n\n if (_loop && _loop.end > _loop.start && time >= _loop.end) {\n const loopDuration = _loop.end - _loop.start;\n time = _loop.start + ((time - _loop.start) % loopDuration);\n }\n\n return Math.max(0, Math.min(time, _duration || Infinity));\n }\n\n function notifyTimeUpdate(time: number): void {\n for (const cb of timeUpdateListeners) {\n try { cb(time); } catch (e) { console.error('[MasterClock] listener error:', e); }\n }\n }\n\n function notifyStateChange(newState: ClockState): void {\n for (const cb of stateChangeListeners) {\n try { cb(newState); } catch (e) { console.error('[MasterClock] listener error:', e); }\n }\n }\n\n function setState(newState: ClockState): void {\n if (state === newState) return;\n state = newState;\n notifyStateChange(newState);\n }\n\n const clock: MasterClock = {\n get currentTime() { return computeCurrentTime(); },\n get state() { return state; },\n get driftMs() { return _driftMs; },\n\n get playbackRate() { return _playbackRate; },\n set playbackRate(rate: number) {\n const clamped = Math.max(0.1, Math.min(rate, 16));\n if (state === 'playing') {\n const current = computeCurrentTime();\n startTimelineTime = current;\n startAudioContextTime = audioContext.currentTime;\n }\n _playbackRate = clamped;\n },\n\n get duration() { return _duration; },\n set duration(d: number) { _duration = Math.max(0, d); },\n\n get loop() { return _loop; },\n set loop(l) { _loop = l; },\n\n play(): void {\n if (state === 'playing') return;\n startTimelineTime = pausedAt;\n startAudioContextTime = audioContext.currentTime;\n setState('playing');\n if (audioContext.state === 'suspended') {\n audioContext.resume();\n }\n },\n\n pause(): void {\n if (state !== 'playing') return;\n pausedAt = computeCurrentTime();\n setState('paused');\n },\n\n seek(time: number): void {\n const clamped = Math.max(0, Math.min(time, _duration));\n if (state === 'playing') {\n startTimelineTime = clamped;\n startAudioContextTime = audioContext.currentTime;\n } else {\n pausedAt = clamped;\n }\n notifyTimeUpdate(clamped);\n },\n\n reportVideoTime(videoTime: number): void {\n const audioTime = computeCurrentTime();\n _driftMs = (audioTime - videoTime) * 1000;\n },\n\n onTimeUpdate(cb) {\n timeUpdateListeners.add(cb);\n return () => { timeUpdateListeners.delete(cb); };\n },\n\n onStateChange(cb) {\n stateChangeListeners.add(cb);\n return () => { stateChangeListeners.delete(cb); };\n },\n\n destroy(): void {\n if (state === 'playing') {\n pausedAt = computeCurrentTime();\n }\n state = 'stopped';\n timeUpdateListeners.clear();\n stateChangeListeners.clear();\n _driftMs = 0;\n },\n };\n\n return clock;\n}\n","import type { AudioScheduler } from './types.js';\nimport type { Composition, Clip, Track } from '@pneuma-craft/timeline';\n\nexport interface AudioSchedulerOptions {\n audioContext: AudioContext;\n}\n\ninterface ActiveSource {\n source: AudioBufferSourceNode;\n clipGain: GainNode;\n}\n\nexport function createAudioScheduler(options: AudioSchedulerOptions): AudioScheduler {\n const { audioContext } = options;\n\n // Buffer storage: clipId → AudioBuffer\n const clipBuffers = new Map<string, AudioBuffer>();\n\n // Per-track gain nodes: trackId → GainNode\n const trackGains = new Map<string, GainNode>();\n\n // Per-track volume (separate from mute): trackId → volume\n const trackVolumes = new Map<string, number>();\n\n // Per-track muted state: trackId → muted\n const trackMutedStates = new Map<string, boolean>();\n\n // Currently active sources\n let activeSources: ActiveSource[] = [];\n\n // Scheduler tick state for future clip scheduling\n let schedulerIntervalId: ReturnType<typeof setInterval> | null = null;\n let scheduledClipIds = new Set<string>();\n let currentComposition: Composition | null = null;\n\n // Playback rate (applied to elapsed time and source nodes)\n let _playbackRate = 1;\n\n // Whether playback is currently active\n let _isPlaying = false;\n\n // Previous timeline time for loop-wrap detection\n let _prevTimelineTime = 0;\n\n // Callback to get current timeline time from the master clock (loop-aware)\n let _getCurrentTime: (() => number) | null = null;\n\n // Look-ahead window in seconds\n const LOOK_AHEAD = 0.2;\n\n // Master gain node connected to destination\n const masterGain = audioContext.createGain();\n masterGain.gain.value = 1;\n masterGain.connect(audioContext.destination);\n\n function getOrCreateTrackGain(track: Track): GainNode {\n let node = trackGains.get(track.id);\n if (!node) {\n node = audioContext.createGain();\n // Use runtime overrides if they exist, otherwise fall back to composition data\n const vol = trackVolumes.get(track.id) ?? track.volume ?? 1;\n const muted = trackMutedStates.get(track.id) ?? track.muted;\n node.gain.value = muted ? 0 : vol;\n trackVolumes.set(track.id, vol);\n node.connect(masterGain);\n trackGains.set(track.id, node);\n }\n return node;\n }\n\n function clearSchedulerTick(): void {\n if (schedulerIntervalId !== null) {\n clearInterval(schedulerIntervalId);\n schedulerIntervalId = null;\n }\n }\n\n function stopAllSources(): void {\n for (const { source } of activeSources) {\n try {\n source.stop();\n } catch {\n // Source may already be stopped\n }\n source.disconnect();\n }\n activeSources = [];\n }\n\n function isClipActiveAt(clip: Clip, fromTime: number): boolean {\n return clip.startTime <= fromTime && fromTime < clip.startTime + clip.duration;\n }\n\n function scheduleClip(clip: Clip, track: Track, fromTime: number): void {\n const buffer = clipBuffers.get(clip.id);\n if (!buffer) return;\n\n const trackGain = getOrCreateTrackGain(track);\n\n // Create per-clip gain node\n const clipGain = audioContext.createGain();\n const clipVolume = clip.volume ?? 1;\n clipGain.gain.value = clipVolume;\n\n // Create source node\n const source = audioContext.createBufferSource();\n source.buffer = buffer;\n\n // Apply playback rate to source node\n source.playbackRate.value = _playbackRate;\n\n // Connect: source → clipGain → trackGain → masterGain → destination\n source.connect(clipGain);\n clipGain.connect(trackGain);\n\n const now = audioContext.currentTime;\n\n // Determine if the clip starts in the future relative to current timeline time\n let contextStartTime: number;\n let sourceOffset: number;\n let remainingDuration: number;\n\n if (clip.startTime > fromTime) {\n // Future clip: schedule it to start at the correct AudioContext time\n // Divide by playbackRate: at 2x rate, 1 timeline second = 0.5 real seconds\n const timeUntilClipStart = (clip.startTime - fromTime) / _playbackRate;\n contextStartTime = now + timeUntilClipStart;\n sourceOffset = clip.inPoint;\n remainingDuration = clip.duration;\n } else {\n // Already active clip: start immediately with offset\n contextStartTime = 0;\n const elapsed = fromTime - clip.startTime;\n sourceOffset = clip.inPoint + elapsed;\n remainingDuration = clip.duration - elapsed;\n }\n\n // Apply fade automations\n // Fade durations must be divided by playbackRate since AudioContext runs in real time\n if (clip.fadeIn !== undefined && clip.fadeIn > 0) {\n const realFadeIn = clip.fadeIn / _playbackRate;\n if (clip.startTime > fromTime) {\n // Future clip: anchor fade to contextStartTime\n clipGain.gain.setValueAtTime(0, contextStartTime);\n clipGain.gain.linearRampToValueAtTime(clipVolume, contextStartTime + realFadeIn);\n } else {\n const fadeInEnd = clip.startTime + clip.fadeIn;\n if (fromTime < fadeInEnd) {\n // Still within fade-in region\n const fadeElapsed = fromTime - clip.startTime;\n const currentLevel = fadeElapsed > 0 ? (fadeElapsed / clip.fadeIn) * clipVolume : 0;\n const fadeRemaining = (clip.fadeIn - fadeElapsed) / _playbackRate;\n clipGain.gain.setValueAtTime(currentLevel, now);\n clipGain.gain.linearRampToValueAtTime(clipVolume, now + fadeRemaining);\n }\n }\n }\n\n if (clip.fadeOut !== undefined && clip.fadeOut > 0) {\n const realFadeOut = clip.fadeOut / _playbackRate;\n const realDuration = clip.duration / _playbackRate;\n if (clip.startTime > fromTime) {\n // Future clip: anchor fade to contextStartTime\n const fadeOutStart = contextStartTime + realDuration - realFadeOut;\n clipGain.gain.setValueAtTime(clipVolume, fadeOutStart);\n clipGain.gain.linearRampToValueAtTime(0, contextStartTime + realDuration);\n } else {\n const fadeOutStart = clip.startTime + clip.duration - clip.fadeOut;\n const fadeOutContextTime = now + Math.max(0, (fadeOutStart - fromTime) / _playbackRate);\n clipGain.gain.setValueAtTime(clipVolume, fadeOutContextTime);\n clipGain.gain.linearRampToValueAtTime(0, fadeOutContextTime + realFadeOut);\n }\n }\n\n source.start(contextStartTime, sourceOffset, remainingDuration);\n\n activeSources.push({ source, clipGain });\n }\n\n function isClipInLookAhead(clip: Clip, currentTime: number, lookAhead: number): boolean {\n const clipEnd = clip.startTime + clip.duration;\n // Clip starts within the look-ahead window and hasn't ended\n return clip.startTime > currentTime && clip.startTime <= currentTime + lookAhead && clipEnd > currentTime;\n }\n\n function scheduleComposition(fromTime: number, composition: Composition): void {\n for (const track of composition.tracks) {\n // Both audio tracks and video tracks contribute audio — a video clip's\n // embedded audio track gets scheduled alongside pure audio clips. Skip\n // anything that isn't a media track (e.g. subtitle) and anything that\n // the user has muted. Clips whose asset has no audio track never get a\n // buffer loaded (see loadClip in playback-engine.ts), so `scheduleClip`\n // naturally skips them.\n if (track.type !== 'audio' && track.type !== 'video') continue;\n if (track.muted) continue;\n\n for (const clip of track.clips) {\n if (!isClipActiveAt(clip, fromTime)) continue;\n if (scheduledClipIds.has(clip.id)) continue;\n scheduledClipIds.add(clip.id);\n scheduleClip(clip, track, fromTime);\n }\n }\n }\n\n function startSchedulerTick(fromTime: number, composition: Composition): void {\n clearSchedulerTick();\n currentComposition = composition;\n\n // Fallback values for computing timeline time when no getCurrentTime callback\n const tickStartContextTime = audioContext.currentTime;\n const tickStartFromTime = fromTime;\n\n schedulerIntervalId = setInterval(() => {\n if (!currentComposition) return;\n\n // Use clock's getCurrentTime if available (loop-aware), otherwise fallback\n let currentTimelineTime: number;\n if (_getCurrentTime) {\n currentTimelineTime = _getCurrentTime();\n } else {\n const elapsed = (audioContext.currentTime - tickStartContextTime) * _playbackRate;\n currentTimelineTime = tickStartFromTime + elapsed;\n }\n\n // Detect loop wrap: timeline time went backwards\n if (currentTimelineTime < _prevTimelineTime) {\n scheduledClipIds.clear();\n stopAllSources();\n }\n _prevTimelineTime = currentTimelineTime;\n\n for (const track of currentComposition.tracks) {\n if (track.type !== 'audio') continue;\n if (track.muted) continue;\n\n for (const clip of track.clips) {\n if (scheduledClipIds.has(clip.id)) continue;\n if (isClipInLookAhead(clip, currentTimelineTime, LOOK_AHEAD) || isClipActiveAt(clip, currentTimelineTime)) {\n scheduledClipIds.add(clip.id);\n scheduleClip(clip, track, currentTimelineTime);\n }\n }\n }\n }, 100);\n }\n\n const scheduler: AudioScheduler = {\n get audioContext() {\n return audioContext;\n },\n\n loadClip(clipId: string, audioBuffer: AudioBuffer): void {\n clipBuffers.set(clipId, audioBuffer);\n },\n\n play(fromTime: number, composition: Composition, getCurrentTime?: () => number): void {\n stopAllSources();\n clearSchedulerTick();\n scheduledClipIds = new Set();\n _prevTimelineTime = fromTime;\n _getCurrentTime = getCurrentTime ?? null;\n _isPlaying = true;\n currentComposition = composition;\n scheduleComposition(fromTime, composition);\n startSchedulerTick(fromTime, composition);\n },\n\n pause(): void {\n clearSchedulerTick();\n stopAllSources();\n scheduledClipIds = new Set();\n _isPlaying = false;\n },\n\n seek(time: number, composition: Composition): void {\n clearSchedulerTick();\n stopAllSources();\n scheduledClipIds = new Set();\n _prevTimelineTime = time;\n scheduleComposition(time, composition);\n startSchedulerTick(time, composition);\n },\n\n setPlaybackRate(rate: number): void {\n _playbackRate = rate;\n // If playing, reschedule everything with new rate so future clip\n // contextStartTime values are recalculated correctly.\n if (_isPlaying && currentComposition && _getCurrentTime) {\n stopAllSources();\n scheduledClipIds = new Set();\n const currentTime = _getCurrentTime();\n scheduleComposition(currentTime, currentComposition);\n clearSchedulerTick();\n startSchedulerTick(currentTime, currentComposition);\n } else {\n // Not playing or no getCurrentTime — just update active sources\n for (const scheduled of activeSources) {\n scheduled.source.playbackRate.value = rate;\n }\n }\n },\n\n setTrackVolume(trackId: string, volume: number): void {\n trackVolumes.set(trackId, volume);\n const node = trackGains.get(trackId);\n if (node) {\n // Only apply gain if the track is not muted\n const isMuted = trackMutedStates.get(trackId) ?? false;\n if (!isMuted) {\n node.gain.setValueAtTime(volume, audioContext.currentTime);\n }\n }\n },\n\n setTrackMute(trackId: string, muted: boolean): void {\n trackMutedStates.set(trackId, muted);\n const node = trackGains.get(trackId);\n if (node) {\n if (muted) {\n node.gain.value = 0;\n } else {\n const vol = trackVolumes.get(trackId) ?? 1;\n node.gain.value = vol;\n }\n }\n },\n\n destroy(): void {\n clearSchedulerTick();\n stopAllSources();\n scheduledClipIds = new Set();\n currentComposition = null;\n _isPlaying = false;\n for (const node of trackGains.values()) {\n node.disconnect();\n }\n trackGains.clear();\n trackVolumes.clear();\n trackMutedStates.clear();\n clipBuffers.clear();\n masterGain.disconnect();\n },\n };\n\n return scheduler;\n}\n","import type {\n PlaybackEngine,\n PlaybackState,\n RenderedFrame,\n AssetResolver,\n MediaDecoder,\n Compositor,\n FrameRenderer,\n MasterClock,\n AudioScheduler,\n SubtitleRenderer,\n} from './types.js';\nimport type { Composition } from '@pneuma-craft/timeline';\nimport { createMediaDecoder } from './media-decoder.js';\nimport { createCompositor, type CompositorType } from './compositor.js';\nimport { createFrameRenderer } from './frame-renderer.js';\nimport { createMasterClock } from './master-clock.js';\nimport { createAudioScheduler } from './audio-scheduler.js';\n\nexport interface PlaybackEngineOptions {\n compositorType?: CompositorType;\n /** Max time a single frame decode may block before the loop skips it. Default 1000ms. */\n decodeTimeoutMs?: number;\n /**\n * Optional rasterizer for subtitle-track clips. When provided, active\n * subtitle clips are composited on top of the video layers — the same code\n * path used for export, so preview and export stay in lockstep. When\n * omitted, subtitle tracks render nothing (legacy behavior).\n */\n subtitleRenderer?: SubtitleRenderer;\n /**\n * Render preview frames (planning-layer visuals attached to time points\n * on a track) when no real clip covers the current time. Defaults to\n * `true` for the playback engine — the user scrubbing the timeline\n * should see the agent's planned vibe before any real video clips land.\n */\n includePreviewFrames?: boolean;\n}\n\n/** Default per-frame decode timeout. Decodes exceeding this are logged and skipped. */\nconst DEFAULT_DECODE_TIMEOUT_MS = 1000;\n\n/**\n * Float slack for the end-of-timeline comparison. The clock clamps\n * `currentTime` to `duration`, so reaching the end is an exact equality in\n * practice; this only absorbs rounding in a caller-supplied seek target.\n */\nconst END_EPSILON = 1e-6;\n\n/** Enables verbose rAF-loop logs for debugging playback stalls. */\nconst DEBUG_LOOP = typeof globalThis !== 'undefined'\n && (globalThis as { __PNEUMA_CRAFT_PLAYBACK_DEBUG__?: boolean }).__PNEUMA_CRAFT_PLAYBACK_DEBUG__ === true;\n\nexport function createPlaybackEngine(options?: PlaybackEngineOptions): PlaybackEngine {\n const compositorType = options?.compositorType ?? 'canvas2d';\n const decodeTimeoutMs = options?.decodeTimeoutMs ?? DEFAULT_DECODE_TIMEOUT_MS;\n const subtitleRenderer = options?.subtitleRenderer;\n const includePreviewFrames = options?.includePreviewFrames ?? true;\n\n let _state: PlaybackState = 'idle';\n let _playbackRate = 1;\n let _loop: { start: number; end: number } | null = null;\n\n // Subsystems (created on load)\n let _decoder: MediaDecoder | null = null;\n let _compositor: Compositor | null = null;\n let _frameRenderer: FrameRenderer | null = null;\n let _clock: MasterClock | null = null;\n let _audioScheduler: AudioScheduler | null = null;\n let _audioContext: AudioContext | null = null;\n let _composition: Composition | null = null;\n\n // rAF handle\n let _rafId: number | null = null;\n\n // True while a frame decode is awaiting — prevents stacking concurrent renders\n // when one decode runs slower than a rAF tick.\n let _frameInFlight = false;\n\n // Seek ID counter for discarding stale paused-seek renders\n let _seekId = 0;\n\n // Subscribers\n const stateChangeListeners = new Set<(state: PlaybackState) => void>();\n const timeUpdateListeners = new Set<(time: number) => void>();\n const frameRenderedListeners = new Set<(frame: RenderedFrame) => void>();\n\n function setState(newState: PlaybackState): void {\n if (_state === newState) return;\n _state = newState;\n for (const cb of stateChangeListeners) {\n try { cb(newState); } catch (e) { console.error('[PlaybackEngine] listener error:', e); }\n }\n }\n\n function emitTimeUpdate(time: number): void {\n for (const cb of timeUpdateListeners) {\n try { cb(time); } catch (e) { console.error('[PlaybackEngine] listener error:', e); }\n }\n }\n\n function emitFrameRendered(frame: RenderedFrame): void {\n for (const cb of frameRenderedListeners) {\n try { cb(frame); } catch (e) { console.error('[PlaybackEngine] listener error:', e); }\n }\n }\n\n function destroySubsystems(): void {\n if (_rafId !== null) {\n cancelAnimationFrame(_rafId);\n _rafId = null;\n }\n _frameInFlight = false;\n _frameRenderer?.destroy();\n _clock?.destroy();\n _audioScheduler?.destroy();\n _audioContext?.close();\n _decoder = null;\n _compositor = null;\n _frameRenderer = null;\n _clock = null;\n _audioScheduler = null;\n _audioContext = null;\n _composition = null;\n }\n\n function startRafLoop(): void {\n if (_rafId !== null) return;\n\n // The loop is designed so that time updates and rAF scheduling are INDEPENDENT\n // of frame decoding. This means:\n // 1. If a decode hangs or is slow, the clock/slider still advances and pause/seek\n // remain responsive.\n // 2. At most one decode runs at a time (no concurrent pile-up).\n // 3. A decode exceeding `decodeTimeoutMs` is logged and skipped so the loop can\n // recover from a stuck codec packet pump.\n let lastLoggedState: 'render' | 'skip-inflight' | null = null;\n const logStateChange = (state: 'render' | 'skip-inflight', time: number, extra?: string): void => {\n if (!DEBUG_LOOP) return;\n if (lastLoggedState === state) return;\n lastLoggedState = state;\n console.log(`[engine] ${state} @ t=${time.toFixed(3)}${extra ? ' ' + extra : ''}`);\n };\n\n const loop = (): void => {\n _rafId = null;\n if (_state !== 'playing' || !_clock || !_frameRenderer || !_composition) return;\n\n const time = _clock.currentTime;\n\n // Emit time update up-front — consumers see the clock tick regardless of decode state.\n emitTimeUpdate(time);\n\n // End-of-timeline check.\n if (!_clock.loop && time >= _composition.duration) {\n engine.pause();\n return;\n }\n\n // Always schedule the next rAF synchronously so timing is decoupled from decode.\n _rafId = requestAnimationFrame(loop);\n\n // Skip this tick's render if the previous decode is still in flight — back-pressure\n // so slow decodes don't stack concurrent calls. The watchdog below recovers if a\n // decode never resolves; we never get stuck.\n //\n // NOTE: we intentionally do NOT skip based on _clock.driftMs. A prior implementation\n // did, which caused a deadlock: when the first decode ran long (e.g., cold-start\n // fetch of a 50 MB blob), reportVideoTime captured a large stale-drift value, then\n // every subsequent tick skipped on drift, so reportVideoTime never ran again, and\n // drift was frozen at the stale value forever. The clock is the source of truth;\n // each render just targets its current value, which naturally catches up.\n if (_frameInFlight) {\n logStateChange('skip-inflight', time);\n return;\n }\n\n logStateChange('render', time);\n\n // Kick off the decode. A watchdog timer clears _frameInFlight so the loop can\n // recover if renderFrame never resolves. We attach .then directly (no Promise.race)\n // to preserve the microtask profile — emitFrameRendered fires on the same tick\n // that renderFrame resolves.\n _frameInFlight = true;\n const clockRef = _clock;\n const compositionRef = _composition;\n let timedOut = false;\n const timeoutId = setTimeout(() => {\n timedOut = true;\n _frameInFlight = false;\n console.warn(\n `[PlaybackEngine] decode timeout at t=${time.toFixed(3)}s (>${decodeTimeoutMs}ms) — skipping frame`,\n );\n }, decodeTimeoutMs);\n\n _frameRenderer.renderFrame(compositionRef, time).then(frame => {\n clearTimeout(timeoutId);\n if (timedOut) return;\n _frameInFlight = false;\n if (_state !== 'playing' || _clock !== clockRef) return;\n clockRef.reportVideoTime(time);\n emitFrameRendered(frame);\n }).catch(err => {\n clearTimeout(timeoutId);\n if (timedOut) return;\n _frameInFlight = false;\n console.error('[PlaybackEngine] render error:', err);\n });\n };\n\n _rafId = requestAnimationFrame(loop);\n }\n\n function stopRafLoop(): void {\n if (_rafId !== null) {\n cancelAnimationFrame(_rafId);\n _rafId = null;\n }\n _frameInFlight = false;\n }\n\n const engine: PlaybackEngine = {\n get state() { return _state; },\n\n get currentTime() {\n return _clock?.currentTime ?? 0;\n },\n\n get ended() {\n if (!_clock || !_composition) return false;\n if (_clock.loop) return false;\n const duration = _composition.duration;\n if (duration <= 0) return false;\n return _clock.currentTime >= duration - END_EPSILON;\n },\n\n get playbackRate() {\n return _clock?.playbackRate ?? _playbackRate;\n },\n set playbackRate(rate: number) {\n _playbackRate = rate;\n if (_clock) {\n _clock.playbackRate = rate;\n }\n if (_audioScheduler) {\n _audioScheduler.setPlaybackRate(rate);\n }\n },\n\n get loop() {\n return _clock?.loop ?? _loop;\n },\n set loop(l: { start: number; end: number } | null) {\n _loop = l;\n if (_clock) {\n _clock.loop = l;\n }\n },\n\n async load(composition: Composition, resolver: AssetResolver): Promise<void> {\n // Invalidate any pending paused-seek renders from previous session\n _seekId++;\n\n // If previously loaded, destroy old subsystems\n if (_state !== 'idle') {\n destroySubsystems();\n }\n\n setState('loading');\n\n try {\n // Create AudioContext\n _audioContext = new AudioContext();\n\n // Create subsystems\n _decoder = createMediaDecoder(resolver, _audioContext);\n _compositor = await createCompositor(\n composition.settings.width,\n composition.settings.height,\n compositorType,\n );\n _frameRenderer = createFrameRenderer(\n _decoder,\n _compositor,\n composition.settings.width,\n composition.settings.height,\n { subtitleRenderer, includePreviewFrames },\n );\n _clock = createMasterClock({\n audioContext: _audioContext,\n duration: composition.duration,\n frameRate: composition.settings.fps,\n });\n _audioScheduler = createAudioScheduler({ audioContext: _audioContext });\n\n // Apply pending settings\n _clock.playbackRate = _playbackRate;\n _clock.loop = _loop;\n\n _composition = composition;\n\n // Pre-load audio buffers from every clip on every audio OR video track.\n // Video clips carry their own embedded audio (MediaDecoder.decodeAudio\n // extracts it from the container), so they participate in the audio\n // graph just like standalone audio clips. Clips with no audio stream\n // (images, silent videos) throw — we swallow and skip so they simply\n // have no buffer loaded, which makes scheduleClip a no-op for them.\n const mediaClips = composition.tracks\n .filter(track => track.type === 'audio' || track.type === 'video')\n .flatMap(track => track.clips);\n\n for (const clip of mediaClips) {\n try {\n const audioBuffer = await _decoder.decodeAudio(clip.assetId);\n _audioScheduler.loadClip(clip.id, audioBuffer);\n } catch {\n // Clip has no audio (image asset, silent video, or decode failure).\n // Leave the clip without a buffer; audio-scheduler tolerates this.\n }\n }\n\n setState('ready');\n } catch (err) {\n // Clean up any partially-created subsystems\n destroySubsystems();\n setState('idle');\n throw err;\n }\n },\n\n play(): void {\n if (!_clock || !_audioScheduler || !_composition) {\n throw new Error('Cannot play: no composition loaded. Call load() first.');\n }\n if (_state === 'playing') return;\n\n // Nothing to play. Accepting the call here would start the rAF loop only\n // for it to hit the end-of-timeline check on the very first tick, which\n // shows up in consumers as a one-frame 'playing' → 'paused' flicker.\n if (_composition.duration <= 0) {\n console.warn('[PlaybackEngine] play() ignored: composition has zero duration.');\n return;\n }\n\n // Play at end-of-timeline restarts from the top, matching the HTML media\n // element algorithm (and every video transport users have ever used).\n // Without this the call would be self-absorbing: the clock resumes from\n // `duration`, the first tick satisfies the end check, and the engine\n // re-pauses having gone nowhere.\n if (engine.ended) {\n _seekId++; // discard any in-flight paused-seek render at the old position\n _clock.seek(0);\n emitTimeUpdate(_clock.currentTime);\n }\n\n // AudioContext may start in 'suspended' state because it was created\n // inside engine.load() after awaits, losing the user-gesture chain.\n // resume() works as long as ANY user gesture has occurred on the page —\n // clicking the play button is sufficient. Fire-and-forget: audio will\n // unmute as soon as resume settles, which in practice is <1 frame.\n if (_audioContext && _audioContext.state === 'suspended') {\n _audioContext.resume().catch((err) => {\n console.warn('[PlaybackEngine] AudioContext resume failed:', err);\n });\n }\n\n _clock.play();\n _audioScheduler.setPlaybackRate(_playbackRate);\n const clockRef = _clock;\n _audioScheduler.play(_clock.currentTime, _composition, () => clockRef.currentTime);\n setState('playing');\n startRafLoop();\n },\n\n pause(): void {\n if (_state !== 'playing') return;\n\n stopRafLoop();\n _clock?.pause();\n _audioScheduler?.pause();\n setState('paused');\n },\n\n seek(time: number): void {\n if (!_clock || !_audioScheduler || !_composition) {\n throw new Error('Cannot seek: no composition loaded. Call load() first.');\n }\n\n _clock.seek(time);\n // Use the clamped time from clock so audio/video stay in sync\n const clampedTime = _clock.currentTime;\n\n // Only reschedule audio when playing — seeking while paused should not produce sound\n if (_state === 'playing') {\n _audioScheduler.setPlaybackRate(_playbackRate);\n _audioScheduler.seek(clampedTime, _composition);\n }\n\n // If not playing, render the frame at the seeked position\n if (_state !== 'playing' && _frameRenderer) {\n const thisSeekId = ++_seekId;\n _frameRenderer.renderFrame(_composition, clampedTime).then(frame => {\n if (thisSeekId !== _seekId) return; // Stale render, discard\n emitFrameRendered(frame);\n emitTimeUpdate(clampedTime);\n }).catch(err => {\n console.error('[PlaybackEngine] seek render error:', err);\n });\n }\n },\n\n onStateChange(cb: (state: PlaybackState) => void): () => void {\n stateChangeListeners.add(cb);\n return () => { stateChangeListeners.delete(cb); };\n },\n\n onTimeUpdate(cb: (time: number) => void): () => void {\n timeUpdateListeners.add(cb);\n return () => { timeUpdateListeners.delete(cb); };\n },\n\n onFrameRendered(cb: (frame: RenderedFrame) => void): () => void {\n frameRenderedListeners.add(cb);\n return () => { frameRenderedListeners.delete(cb); };\n },\n\n destroy(): void {\n stopRafLoop();\n destroySubsystems();\n stateChangeListeners.clear();\n timeUpdateListeners.clear();\n frameRenderedListeners.clear();\n _playbackRate = 1;\n _loop = null;\n _state = 'idle';\n },\n };\n\n return engine;\n}\n","import type { OfflineAudioRenderer } from './types.js';\nimport type { Clip, Track } from '@pneuma-craft/timeline';\n\nexport function createOfflineAudioRenderer(): OfflineAudioRenderer {\n return {\n async render(composition, _resolver, decodeAudio) {\n const sampleRate = composition.settings.sampleRate ?? 48000;\n const channels = 2;\n const duration = composition.duration;\n const length = Math.ceil(duration * sampleRate);\n\n const offlineCtx = new OfflineAudioContext(channels, length, sampleRate);\n\n // Collect clips from every unmuted media track. Video clips contribute\n // their embedded audio exactly like standalone audio clips — this is\n // what keeps the exported audio in lockstep with what the preview\n // plays. Subtitle/other non-media tracks are skipped.\n const audioClips: Array<{ clip: Clip; track: Track }> = [];\n for (const track of composition.tracks) {\n if (track.type !== 'audio' && track.type !== 'video') continue;\n if (track.muted) continue;\n for (const clip of track.clips) {\n audioClips.push({ clip, track });\n }\n }\n\n // Schedule each clip\n for (const { clip, track } of audioClips) {\n let buffer: AudioBuffer;\n try {\n buffer = await decodeAudio(clip.assetId);\n } catch {\n // Clip has no audio stream (image asset, silent video) or decode\n // failed. Skip silently — video-track clips routinely have no\n // audio, so a warning here would be noise. True decode failures\n // on audio assets surface via the preview path instead.\n continue;\n }\n\n const source = offlineCtx.createBufferSource();\n source.buffer = buffer;\n\n const clipGain = offlineCtx.createGain();\n const clipVolume = clip.volume ?? 1;\n clipGain.gain.value = clipVolume;\n\n const trackGain = offlineCtx.createGain();\n trackGain.gain.value = track.volume;\n\n source.connect(clipGain);\n clipGain.connect(trackGain);\n trackGain.connect(offlineCtx.destination);\n\n // Fade automation\n const fadeIn = clip.fadeIn;\n const fadeOut = clip.fadeOut;\n\n if (fadeIn && fadeIn > 0) {\n clipGain.gain.setValueAtTime(0, clip.startTime);\n clipGain.gain.linearRampToValueAtTime(clipVolume, clip.startTime + fadeIn);\n }\n\n if (fadeOut && fadeOut > 0) {\n const fadeOutStart = clip.startTime + clip.duration - fadeOut;\n clipGain.gain.setValueAtTime(clipVolume, fadeOutStart);\n clipGain.gain.linearRampToValueAtTime(0, clip.startTime + clip.duration);\n }\n\n source.start(clip.startTime, clip.inPoint, clip.duration);\n }\n\n return offlineCtx.startRendering();\n },\n };\n}\n","import type { ExportEngine, ExportOptions, AssetResolver, SubtitleRenderer } from './types.js';\nimport type { Composition } from '@pneuma-craft/timeline';\nimport { createMediaDecoder } from './media-decoder.js';\nimport { createCompositor } from './compositor.js';\nimport { createFrameRenderer } from './frame-renderer.js';\nimport { createOfflineAudioRenderer } from './offline-audio-renderer.js';\nimport {\n Output,\n CanvasSource,\n AudioBufferSource,\n Mp4OutputFormat,\n WebMOutputFormat,\n BufferTarget,\n} from 'mediabunny';\n\nexport interface ExportEngineOptions {\n /**\n * Optional rasterizer for subtitle-track clips. Pass the same renderer used\n * by `createPlaybackEngine` so the exported video is pixel-identical to the\n * preview. When omitted, subtitle tracks are skipped during export.\n */\n subtitleRenderer?: SubtitleRenderer;\n /**\n * Include preview frames (planning-layer visuals attached to time points\n * on a track) in the exported video. Defaults to `false` because an export\n * normally represents a finished cut. Set true for review-grade exports of\n * unfinished timelines — e.g., agent producing a \"草样片\" for the user to\n * scrub before committing to expensive real renders.\n */\n includePreviewFrames?: boolean;\n}\n\nexport function createExportEngine(options?: ExportEngineOptions): ExportEngine {\n const subtitleRenderer = options?.subtitleRenderer;\n const includePreviewFrames = options?.includePreviewFrames ?? false;\n const progressListeners = new Set<(progress: number) => void>();\n let abortController: AbortController | null = null;\n\n function notifyProgress(progress: number): void {\n for (const cb of progressListeners) {\n try { cb(progress); } catch (e) { console.error('[ExportEngine]', e); }\n }\n }\n\n return {\n async export(\n composition: Composition,\n options: ExportOptions,\n resolver: AssetResolver,\n ): Promise<Blob> {\n abortController = new AbortController();\n const signal = abortController.signal;\n\n const fps = options.fps ?? composition.settings.fps;\n const width = options.width ?? composition.settings.width;\n const height = options.height ?? composition.settings.height;\n const totalFrames = Math.ceil(composition.duration * fps);\n\n // Minimal OfflineAudioContext used only for decodeAudioData during asset\n // decoding (export has its own offline context for actual audio rendering).\n const decoderAudioCtx = new OfflineAudioContext(1, 1, composition.settings.sampleRate || 48000);\n const decoder = createMediaDecoder(resolver, decoderAudioCtx);\n const compositor = await createCompositor(width, height, 'canvas2d');\n const renderer = createFrameRenderer(decoder, compositor, width, height, {\n subtitleRenderer,\n includePreviewFrames,\n });\n\n try {\n const format = options.format === 'webm'\n ? new WebMOutputFormat()\n : new Mp4OutputFormat({ fastStart: 'in-memory' });\n\n const target = new BufferTarget();\n const output = new Output({ format, target });\n\n const renderCanvas = new OffscreenCanvas(width, height);\n const videoSource = new CanvasSource(renderCanvas, {\n codec: options.videoCodec,\n bitrate: options.videoBitrate,\n });\n\n const audioSource = new AudioBufferSource({\n codec: options.audioCodec,\n bitrate: options.audioBitrate,\n });\n\n output.addVideoTrack(videoSource);\n output.addAudioTrack(audioSource);\n await output.start();\n\n // Render video frames\n const renderCtx = renderCanvas.getContext('2d')!;\n\n for (let frame = 0; frame < totalFrames; frame++) {\n if (signal.aborted) throw new Error('Export aborted');\n\n const time = frame / fps;\n const rendered = await renderer.renderFrame(composition, time);\n\n renderCtx.clearRect(0, 0, width, height);\n renderCtx.drawImage(rendered.image, 0, 0, width, height);\n rendered.image.close();\n\n await videoSource.add(time, 1 / fps);\n\n if ((frame + 1) % 5 === 0 || frame === totalFrames - 1) {\n notifyProgress((frame + 1) / totalFrames);\n }\n\n // Yield to event loop periodically\n if ((frame + 1) % 5 === 0) {\n await new Promise(r => setTimeout(r, 0));\n }\n }\n\n videoSource.close();\n if (signal.aborted) throw new Error('Export aborted');\n\n // Render audio offline\n const offlineRenderer = createOfflineAudioRenderer();\n const audioBuffer = await offlineRenderer.render(\n composition,\n resolver,\n (assetId) => decoder.decodeAudio(assetId),\n );\n if (signal.aborted) throw new Error('Export aborted');\n\n await audioSource.add(audioBuffer);\n audioSource.close();\n if (signal.aborted) throw new Error('Export aborted');\n\n await output.finalize();\n\n const buffer = target.buffer;\n if (!buffer) throw new Error('Export produced no output');\n\n const mimeType = options.format === 'webm' ? 'video/webm' : 'video/mp4';\n return new Blob([buffer], { type: mimeType });\n } finally {\n renderer.destroy();\n abortController = null;\n }\n },\n\n onProgress(cb: (progress: number) => void): () => void {\n progressListeners.add(cb);\n return () => { progressListeners.delete(cb); };\n },\n\n abort(): void {\n abortController?.abort();\n },\n };\n}\n"],"mappings":"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","names":["options"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pneuma-craft/video",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Video engine — decode, composite, preview, export (built on MediaBunny)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",