@pictor-sdk/video-android 0.1.3 → 0.1.5
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/android/src/main/java/dev/pictor/video/CompositionSeek.kt +8 -0
- package/android/src/main/java/dev/pictor/video/CompositionSpike.kt +23 -13
- package/android/src/main/java/dev/pictor/video/CompositionVideoItem.kt +16 -0
- package/android/src/main/java/dev/pictor/video/CompositionWindow.kt +17 -0
- package/android/src/main/java/dev/pictor/video/FrameClock.kt +6 -1
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +23 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/compositionSpike.ts +9 -0
- package/src/useCompositionPlayback.ts +55 -1
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/memoryGuard.ts","../src/syncTrace.ts","../src/NativePictorVideo.ts","../src/player.ts","../src/media3.ts","../src/playlistSession.ts","../src/surfaceOwner.ts","../src/useVideoSurface.ts","../src/useCompositionPlayback.ts","../src/compositionSpike.ts","../src/playbackLifecycle.ts","../src/useMedia3Playback.ts","../src/surfaceRecovery.ts","../src/thumbnailProvider.ts","../src/convergence.ts","../src/exportJob.ts","../src/mixSources.ts","../src/exportDestination.ts","../src/exportProgress.ts","../src/exportProgressPump.ts","../src/exporter.ts","../src/mediaStore.ts","../src/overlayStaging.ts","../src/audioProbe.ts","../src/managedSources.ts","../src/previewMix.ts","../src/previewAudioSession.ts","../src/tracePhases.ts","../src/spikeExcursions.ts","../src/previewCadence.ts","../src/submissionScoring.ts","../src/usePreviewAudio.ts","../src/previewAudioDiagnostics.ts","../src/encoderGate.ts","../src/previewTrace.ts"],"sourcesContent":["/**\n * A debug safety cutoff on native memory.\n *\n * **Remove before production.** This exists because an earlier revision drove\n * the native heap to 1.5 GB in sixty seconds and the process was killed —\n * taking other applications on the device with it, since Android reclaims from\n * everything before it gives up on the foreground app. A diagnostic run must\n * not be able to do that again.\n *\n * Two independent triggers, because a leak can be fast or merely relentless:\n *\n * - **Ceiling.** An absolute limit, well under anything that would put the\n * device under pressure.\n * - **Sustained rise.** Memory that climbs across every sample of a window with\n * no plateau, which is what a slow leak looks like and what a ceiling alone\n * would take minutes to catch.\n *\n * Pure, so the rule is tested rather than trusted: a cutoff that misfires stops\n * a good run, and one that never fires is not a cutoff.\n *\n * @experimental\n */\n\nexport interface MemoryGuard {\n readonly ceilingBytes: number\n readonly windowSamples: number\n /** Minimum total climb across the window to count as a rise, in bytes. */\n readonly riseBytes: number\n samples: number[]\n tripped: null | 'ceiling' | 'sustained-rise'\n}\n\n/**\n * 250 MB. Healthy single-clip playback plateaus near 65 MB.\n *\n * Was 400 MB, which a 1 MB/s leak takes six minutes to reach — long enough for\n * a five-minute run to finish, look successful, and leave the process fat\n * enough that the next run pushed the device into reclaiming from other\n * applications.\n */\nexport const DEFAULT_CEILING_BYTES = 250 * 1024 * 1024\n\n/** Forty samples at 4 Hz — ten seconds of uninterrupted climbing. */\nexport const DEFAULT_WINDOW_SAMPLES = 40\n\n/**\n * 8 MB across the window: ~0.8 MB/s.\n *\n * Was 24 MB over three seconds — 8 MB/s, calibrated against a leak that hit\n * 1.5 GB in a minute. It was blind to the 1.07 MB/s leak that actually got the\n * process killed, so the cutoff never fired. A cutoff tuned only to the last\n * failure is not a cutoff.\n *\n * A threshold this small is safe only because the rise must also be *strictly*\n * increasing at every one of the forty samples. Real memory jitters; a leak\n * does not.\n */\nexport const DEFAULT_RISE_BYTES = 8 * 1024 * 1024\n\nexport function createMemoryGuard(\n ceilingBytes = DEFAULT_CEILING_BYTES,\n windowSamples = DEFAULT_WINDOW_SAMPLES,\n riseBytes = DEFAULT_RISE_BYTES,\n): MemoryGuard {\n return {\n ceilingBytes,\n windowSamples: Math.max(3, Math.floor(windowSamples)),\n riseBytes,\n samples: [],\n tripped: null,\n }\n}\n\n/**\n * Record a reading and decide whether to stop.\n *\n * Once tripped it stays tripped: the caller has already torn the session down,\n * and a guard that untrips would invite it to start again into the same leak.\n */\nexport function observeMemory(guard: MemoryGuard, bytes: number): MemoryGuard {\n if (guard.tripped) return guard\n if (!Number.isFinite(bytes) || bytes <= 0) return guard\n\n if (bytes >= guard.ceilingBytes) {\n return { ...guard, samples: [...guard.samples, bytes].slice(-guard.windowSamples), tripped: 'ceiling' }\n }\n\n const samples = [...guard.samples, bytes].slice(-guard.windowSamples)\n if (samples.length < guard.windowSamples) return { ...guard, samples }\n\n // **Strictly** increasing across the whole window, and a meaningful total\n // climb. Both conditions matter, and strictness is the subtle one: allowing\n // equal consecutive samples made a single large allocation followed by a flat\n // plateau — a decoder starting up — read as a leak. A leak climbs at every\n // sample; an allocation climbs once and then stops.\n let climbing = true\n for (let i = 1; i < samples.length; i++) {\n if (samples[i]! <= samples[i - 1]!) {\n climbing = false\n break\n }\n }\n const climb = samples[samples.length - 1]! - samples[0]!\n if (climbing && climb >= guard.riseBytes) {\n return { ...guard, samples, tripped: 'sustained-rise' }\n }\n return { ...guard, samples }\n}\n\n/** A sentence a human can act on, or null while nothing is wrong. */\nexport function describeTrip(guard: MemoryGuard): string | null {\n if (!guard.tripped) return null\n const mb = (b: number) => (b / (1024 * 1024)).toFixed(0)\n const latest = guard.samples[guard.samples.length - 1] ?? 0\n return guard.tripped === 'ceiling'\n ? `native heap reached ${mb(latest)} MB (ceiling ${mb(guard.ceilingBytes)} MB) — session released`\n : `native heap rose ${mb(latest - (guard.samples[0] ?? 0))} MB across ${guard.samples.length} samples without plateauing — session released`\n}\n","/**\n * Per-tick synchronisation forensics.\n *\n * A single offset number cannot say *why* video is late, and optimising against\n * it invites exactly the wrong fixes — presenting future frames, nudging the\n * clock, delaying audio. Each of those improves the number and worsens the\n * playback.\n *\n * So every tick is decomposed into causes that can be acted on separately:\n *\n * - **Frame quantisation** — video only changes every frame period, so the\n * picture is always somewhat behind the audio instant. Bounded by one frame\n * by definition, and not a defect.\n * - **Decoder lateness** — the frame that *should* be showing had not been\n * decoded yet. Fix by adjusting the bounded lead or queue depth.\n * - **Scheduler lateness** — the right frame *was* available and was not\n * selected. Fix the missed tick or the contention causing it.\n * - **Clock correction** — how far each raw `AudioTrack` report moves the\n * extrapolated position. Large jumps mean stale timestamps or a bad baseline.\n *\n * Pure, so the decomposition is tested rather than trusted.\n *\n * @experimental\n */\n\nexport interface TickSample {\n /** Authoritative extrapolated audio time. */\n audioUs: number\n /** Age of the newest raw `getTimestamp()` report at this tick. */\n rawAgeUs: number\n /** How far the newest raw report moved the extrapolated position. */\n correctionUs: number\n /** Newest decoded frame at or before `audioUs`, or null if none is eligible. */\n newestEligiblePtsUs: number | null\n /** What was actually presented, or null. */\n presentedPtsUs: number | null\n /** Interval since the previous tick. */\n tickIntervalUs: number\n queueDepth: number\n decoderLeadUs: number\n}\n\nexport interface SyncTrace {\n readonly frameUs: number\n quantisation: number[]\n decoderLate: number[]\n schedulerLate: number[]\n correction: number[]\n presentedOffset: number[]\n tickInterval: number[]\n ticks: number\n decoderLateTicks: number\n schedulerLateTicks: number\n}\n\nconst MAX_SAMPLES = 20_000\n\nexport function createSyncTrace(fps: number): SyncTrace {\n const rate = Number.isFinite(fps) && fps > 0 ? fps : 30\n return {\n frameUs: Math.round(1_000_000 / rate),\n quantisation: [],\n decoderLate: [],\n schedulerLate: [],\n correction: [],\n presentedOffset: [],\n tickInterval: [],\n ticks: 0,\n decoderLateTicks: 0,\n schedulerLateTicks: 0,\n }\n}\n\n/**\n * The frame that ought to be on screen at `audioUs`.\n *\n * Assumes a regular frame grid anchored at zero, which the controlled fixture\n * has. On a variable-rate source this is an approximation and the quantisation\n * figure should be read as indicative — which is why it is stated here rather\n * than left for a reader to infer from a clean-looking number.\n */\nexport function expectedPtsAt(audioUs: number, frameUs: number): number {\n if (audioUs <= 0 || frameUs <= 0) return 0\n return Math.floor(audioUs / frameUs) * frameUs\n}\n\nexport function record(trace: SyncTrace, sample: TickSample): SyncTrace {\n if (trace.ticks >= MAX_SAMPLES) return { ...trace, ticks: trace.ticks + 1 }\n\n const expected = expectedPtsAt(sample.audioUs, trace.frameUs)\n const next: SyncTrace = {\n ...trace,\n ticks: trace.ticks + 1,\n quantisation: [...trace.quantisation, sample.audioUs - expected],\n correction: [...trace.correction, Math.abs(sample.correctionUs)],\n tickInterval: [...trace.tickInterval, sample.tickIntervalUs],\n decoderLate: trace.decoderLate,\n schedulerLate: trace.schedulerLate,\n presentedOffset: trace.presentedOffset,\n decoderLateTicks: trace.decoderLateTicks,\n schedulerLateTicks: trace.schedulerLateTicks,\n }\n\n // Decoder lateness: the frame that should be showing was not available.\n // Measured as how far short of `expected` the newest eligible frame fell.\n const eligible = sample.newestEligiblePtsUs\n if (eligible === null || eligible < expected) {\n const shortfall = eligible === null ? sample.audioUs : expected - eligible\n next.decoderLate = [...trace.decoderLate, shortfall]\n next.decoderLateTicks = trace.decoderLateTicks + 1\n }\n\n // Scheduler lateness: an eligible frame existed and something older — or\n // nothing — was shown instead. This is the category that means a tick was\n // missed rather than a frame being absent.\n if (eligible !== null && (sample.presentedPtsUs === null || sample.presentedPtsUs < eligible)) {\n const behind = eligible - (sample.presentedPtsUs ?? 0)\n next.schedulerLate = [...trace.schedulerLate, behind]\n next.schedulerLateTicks = trace.schedulerLateTicks + 1\n }\n\n if (sample.presentedPtsUs !== null) {\n next.presentedOffset = [...trace.presentedOffset, sample.presentedPtsUs - sample.audioUs]\n }\n return next\n}\n\nexport interface Distribution {\n n: number\n p50: number\n p95: number\n p99: number\n max: number\n}\n\nexport function distribution(values: readonly number[]): Distribution {\n if (values.length === 0) return { n: 0, p50: 0, p95: 0, p99: 0, max: 0 }\n const abs = values.map(Math.abs).sort((a, b) => a - b)\n const at = (q: number) => abs[Math.min(abs.length - 1, Math.floor(q * abs.length))]!\n return { n: abs.length, p50: at(0.5), p95: at(0.95), p99: at(0.99), max: abs[abs.length - 1]! }\n}\n\nexport interface SyncReport {\n quantisation: Distribution\n decoderLate: Distribution\n schedulerLate: Distribution\n correction: Distribution\n presentedOffset: Distribution\n tickInterval: Distribution\n ticks: number\n decoderLateTicks: number\n schedulerLateTicks: number\n frameUs: number\n /** Which cause dominates, so the fix is chosen by evidence. */\n dominant: 'none' | 'decoder' | 'scheduler' | 'quantisation'\n}\n\nexport function syncReport(trace: SyncTrace): SyncReport {\n const decoderLate = distribution(trace.decoderLate)\n const schedulerLate = distribution(trace.schedulerLate)\n const quantisation = distribution(trace.quantisation)\n const presentedOffset = distribution(trace.presentedOffset)\n\n // Attribute by how often each fires and how badly, not by which p95 is\n // numerically larger: a rare huge decoder stall and a constant one-frame\n // scheduler lag need different fixes.\n const decoderShare = trace.ticks === 0 ? 0 : trace.decoderLateTicks / trace.ticks\n const schedulerShare = trace.ticks === 0 ? 0 : trace.schedulerLateTicks / trace.ticks\n let dominant: SyncReport['dominant'] = 'none'\n if (presentedOffset.p95 > trace.frameUs) {\n if (decoderShare >= schedulerShare && decoderShare > 0.01) dominant = 'decoder'\n else if (schedulerShare > 0.01) dominant = 'scheduler'\n else dominant = 'quantisation'\n }\n\n return {\n quantisation,\n decoderLate,\n schedulerLate,\n correction: distribution(trace.correction),\n presentedOffset,\n tickInterval: distribution(trace.tickInterval),\n ticks: trace.ticks,\n decoderLateTicks: trace.decoderLateTicks,\n schedulerLateTicks: trace.schedulerLateTicks,\n frameUs: trace.frameUs,\n dominant,\n }\n}\n","/* eslint-disable @typescript-eslint/no-wrapper-object-types --\n * React Native's codegen recognises the upper-cased `Object` as its\n * `UnsafeObject` type and rejects the primitive `object`. This file is a\n * codegen spec before it is TypeScript, so the generator's spelling wins.\n */\nimport { TurboModuleRegistry, type TurboModule } from 'react-native'\n\n/**\n * The native surface. **Internal** until the API stabilises.\n *\n * Numbers, strings and plain objects only. `ExoPlayer`, `MediaItem`, `Surface`\n * and every other Android or Media3 type stops at the Kotlin side of this\n * boundary and never appears in a Pictor signature.\n *\n * Nothing here is per frame. Frames reach the renderer through the\n * `__pictorVideoSurface` host object on the UI runtime, so the JS thread is not\n * in the picture's path at all; these calls are controls and a throttled\n * snapshot.\n */\nexport interface Spec extends TurboModule {\n /** Publishes `__pictorVideoSurface` on the JS runtime. */\n installVideoSurface(): boolean\n /**\n * Publishes `__pictorOverlayStaging` on the JS runtime.\n *\n * Native chooses the directory; JS never supplies a destination.\n */\n installOverlayStaging(): boolean\n\n /** Open a source — local `android.resource:`/`file:` or remote — and render it. */\n media3Prepare(uri: string): boolean\n /** Diagnostics: a player with no video output, for heap attribution. */\n media3PrepareNoSurface(uri: string): boolean\n /** Diagnostics: point the existing player at another source. */\n media3Rebind(uri: string): boolean\n /** Diagnostics: end the source, keep the player. */\n media3StopKeepingPlayer(): void\n /**\n * Replace the playlist. Two parallel arrays: sources, then four numbers per\n * entry — clipping start and end in microseconds, speed in milli-units, and\n * muted as 0 or 1.\n */\n media3SetPlaylist(\n uris: string[],\n bounds: number[],\n startWindow: number,\n startPositionUs: number,\n ): boolean\n /** Seek in the player's coordinates: window index plus offset. */\n media3SeekToWindow(windowIndex: number, positionUs: number): void\n /**\n * **V3.3 gate only.** Encode one source through Transformer, once.\n *\n * Not an exporter and not reachable from `VideoExporter`: it takes a single\n * URI rather than a project, writes to an adapter-private temporary, and\n * exists to answer whether Skia can draw inside Transformer's frame processor\n * without changing the picture. `withSkia` selects the effect chain — false is\n * the vanilla run that establishes what the codec alone costs.\n *\n * Returns a JSON string rather than a shaped object because the shape is a\n * gate's report, not an API, and freezing it here would be pretending\n * otherwise.\n *\n * @experimental\n */\n exportGateRun(uri: string, withSkia: boolean, timeoutSeconds: number): string\n /**\n * **V3.3 gate only.** Execute one serialized, versioned `VideoScene`.\n *\n * Native receives a scene, never a project: ids, indices and numbers that\n * `resolveVideoScene` already settled. It validates the schema version, the\n * command order and every identity before Transformer starts, and refuses\n * anything it cannot execute faithfully rather than skipping it.\n *\n * @experimental\n */\n exportGateRunScene(\n uri: string,\n sceneJson: string,\n timeoutSeconds: number,\n /** `-1` never fails; `0` fails after GL init before the first frame; `n` after n frames. */\n failAfterFrames: number,\n ): string\n /**\n * **V3.3 gate only.** Execute a compiled scene *program* over a sequence.\n *\n * One document for the whole export: sorted, contiguous, half-open segments\n * compiled from the project's clip placements and overlay spans. Native\n * validates coverage, then selects a segment per frame by binary search — a\n * lookup, not an interpretation.\n *\n * @experimental\n */\n exportGateRunProgram(uris: string[], programJson: string, timeoutSeconds: number): string\n /** **V3.3 gate only.** Delete every temporary the gate created. @experimental */\n /**\n * **V3.3 gate only.** A sequence whose clipping and speed TypeScript chose.\n *\n * `itemsJson` is `{ uri, startMs, endMs, speed, durationUs?, forceAudioTrack? }[]`.\n * Native hands these to Media3 and computes none of them: which part of a\n * source plays, and how fast, is timeline interpretation and belongs to the\n * compiler.\n *\n * `forceAudioTrack` is sequence-level and honoured if any item sets it. A\n * source with no audio track fails outright at any rate other than 1 without\n * it — Media3 drives its speed path through audio.\n *\n * @experimental\n */\n exportGateRunItems(itemsJson: string, programJson: string, timeoutSeconds: number): string\n exportGateReleaseTemporary(): number\n /**\n * **V3.3 gate only.** Compare two files in **display space**.\n *\n * Display space is the only space the question means anything in: a file\n * storing 1080×1920 with a rotation flag and one storing 1920×1080 with the\n * orientation baked in display identically, and comparing stored dimensions\n * would call that a difference.\n *\n * Returns a JSON report of per-timestamp statistics. It states no verdict —\n * the threshold is frozen by the caller, from the control, before the thing\n * being tested is looked at.\n *\n * @experimental\n */\n exportGateCompare(leftUri: string, rightUri: string, timestampsUs: number[]): string\n /**\n * **V3.3 gate only.** Mean colour of a central region, per timestamp.\n *\n * Reports numbers and draws no conclusion. Which source a colour implies, and\n * which frame of it, is arithmetic the TypeScript oracle owns.\n *\n * @experimental\n */\n exportGateSample(uri: string, timestampsUs: number[]): string\n /**\n * **V3.3 gate only.** Muxed sample timestamps and per-track durations.\n *\n * A frame count and a whole-file duration cannot tell a held tail frame from a\n * correctly-timed one, or a gap from a slow rate. The written timestamps can.\n * Reports them; judges nothing.\n *\n * @experimental\n */\n /**\n * **V3.4 gate only.** Mean and extremes over caller-chosen rectangles.\n *\n * `probesJson` is `{ timeUs, x, y, w, h }[]`. The fixed centre region\n * {@link exportGateSample} uses straddles a transformed clip's edge and\n * averages source pixels with cleared ones; the caller has the corner oracle\n * and passes rectangles it has confirmed are covered.\n *\n * @experimental\n */\n /**\n * **V3.5 gate only.** Decoded audio: loudness and spectrum per window.\n *\n * A pitch-shifting speed change and a pitch-preserving one produce files whose\n * duration and sample count are identical, so counting cannot tell them apart.\n * Also reports AAC encoder delay, padding and the first presentation time,\n * which a seam alignment check needs.\n *\n * @experimental\n */\n exportGateAudio(uri: string, windowsJson: string): string\n exportGateProbe(uri: string, probesJson: string): string\n exportGateTracks(uri: string): string\n /**\n * **V3.4 gate only.** One decoded frame as base64 PNG, in display space.\n *\n * @experimental\n */\n exportGateFramePng(uri: string, timeUs: number): string\n /**\n * **V3.4 gate only.** Encode a still through the treatment's encoder.\n *\n * A comparison between two renderers is only about the renderers if\n * everything after them is the same, so the reference is encoded and decoded\n * exactly as the treatment is.\n *\n * @experimental\n */\n exportGateEncodeStill(pngBase64: string, frames: number): string\n /**\n * **V3.4 gate only.** Geometry and interior colour, measured separately.\n *\n * A maximum channel delta is near-useless at a transformed edge: one row of\n * pixels on the far side of a bright/black boundary reads 255 while the\n * picture is right. Returns mask overlap, edge displacement and interior\n * colour error, and draws no conclusion about any of them.\n *\n * @experimental\n */\n exportGateGeometry(\n leftUri: string,\n leftUs: number,\n rightUri: string,\n rightUs: number,\n maskThreshold: number,\n ): string\n /**\n * **V3.3 gate only.** Per-sample checksums of the compressed video payload.\n *\n * Transmuxing must move encoded bytes and change only timestamps and container\n * metadata. Decoded pixels cannot show that — a re-encode that happens to look\n * identical would pass — so the payload itself is checksummed.\n *\n * @experimental\n */\n exportGateSampleHashes(uri: string): string\n /**\n * **V3.3 gate only.** One Transformer job per clip, then a transmux concat.\n *\n * `shardsJson` is `{ uri, startMs, endMs, speed, timelineOffsetUs,\n * forceAudioTrack, frameDropFps?, encoderRepeatUs?, emitsPerInput?,\n * outputPtsUs?, cadenceFailAfterEmits? }[]`. The cadence arrays come straight\n * from `resampleClip`; native walks them and computes no grid arithmetic. Native adds `timelineOffsetUs` to each local\n * presentation timestamp and does segment lookup; it computes no placement,\n * duration or rate. Intermediates are deleted on every path.\n *\n * @experimental\n */\n exportGateRunShards(\n shardsJson: string,\n programJson: string,\n timeoutSeconds: number,\n overlaysJson: string,\n ): string\n /**\n * **V3.4 gate only.** Write raw RGBA bytes to a sidecar and checksum them.\n *\n * Pixels never travel in scene JSON. This stages them beside it and returns\n * the path, length and CRC32 a descriptor must declare.\n *\n * @experimental\n */\n /**\n * **V3.5 gate only.** Video shards, an audio timeline, then a muxer copy.\n *\n * The two pipelines never meet until the last step, and that step decodes\n * nothing: `MediaMuxer` writes both sets of encoded samples unchanged, so\n * neither takes a second generation.\n *\n * @experimental\n */\n /**\n * Run an export off the bridge so it can be cancelled while it runs.\n *\n * Resolves with the same JSON outcome the blocking call returns. @experimental\n */\n exportRunProjectAsync(\n jobId: string,\n shardsJson: string,\n programJson: string,\n overlaysJson: string,\n mixProgramJson: string,\n mixUrisJson: string,\n timeoutSeconds: number,\n /** `\"\"` for a temporary, or `{kind:'file',path,overwrite}`. @experimental */\n destinationJson: string,\n ): Promise<string>\n /**\n * Save a finished export into the device's video collection.\n *\n * Inserts pending, streams, verifies, then publishes. Settles once: the\n * published `content:` uri, or an error with the pending row already\n * deleted. @experimental\n */\n mediaSaveAsync(jobId: string, sourceUri: string): Promise<string>\n /**\n * Offer a saved video to the system share sheet.\n *\n * The durable `content:` uri only, with a read grant and nothing else.\n * Returns what the intent carried, so a gate can inspect it. @experimental\n */\n mediaShare(uri: string): string\n /** Ask a running save to stop. Idempotent. @experimental */\n mediaSaveCancel(jobId: string): string\n /** Delete this app's own stale pending rows. Never a published item. @experimental */\n mediaSaveSweepPending(): string\n /** Confirm a saved item still opens. @experimental */\n mediaSaveReadable(uri: string): string\n /**\n * Release one temporary output this module produced. Idempotent.\n *\n * Refuses any path it did not create, so it cannot delete a caller's file.\n * @experimental\n */\n exportReleaseTemporaryOutput(uri: string): string\n /**\n * Remove `.pictor-staging-*.tmp` orphans a killed process left behind.\n *\n * Positively identified by prefix and suffix; never a wildcard. @experimental\n */\n exportSweepStagingOrphans(directory: string): string\n /** Counted work plus the beat total, for the progress pump. @experimental */\n exportProgressSnapshot(): string\n /**\n * What a picked audio file actually contains.\n *\n * Duration, audio-track presence, seekability and MIME, read by opening the\n * file. The host asks this before a sound may enter a project: the mix\n * compiler has no decoder and believes whatever the document declares, and a\n * document that claimed audio a file lacked cost a whole export. Returns\n * `{ok, durationUs, hasAudioTrack, seekable, mimeType}` or `{ok:false,\n * reason}` — never throws, because an unreadable pick is an answer.\n * @experimental\n */\n /**\n * Prepare the audio a preview plays, off the JS thread.\n *\n * The same mixer an export runs, over the same compiled program. Resolves\n * `{ok, uri, key, cached, byteLength}`, or `{ok:false, superseded}` when a\n * newer request replaced it — which a caller must never publish.\n * @experimental\n */\n previewMixPrepare(\n jobId: string,\n programJson: string,\n urisJson: string,\n ): Promise<string>\n /**\n * Load a prepared mix into the mixed-audio player, stamped with an epoch.\n *\n * The epoch is how a start or a seek proves it belongs to the mix currently\n * playing; one carrying an older epoch is dropped. @experimental\n */\n previewAudioLoad(uri: string, epoch: number): string\n /** Start, if the epoch still matches. @experimental */\n previewAudioPlay(epoch: number): string\n /** @experimental */\n previewAudioPause(): string\n /** Seek to a global timeline position, if the epoch still matches. @experimental */\n previewAudioSeek(positionUs: number, epoch: number): string\n /** Preview rate. Clip speed is already compiled into the mix. @experimental */\n previewAudioSetRate(rate: number): string\n /** Position and the wall clock it was sampled at, for drift. @experimental */\n previewAudioState(): string\n /**\n * Silence the video player's own audio while a mixed preview plays.\n *\n * The mix contains the clips' audio too, so both audible plays the timeline\n * twice. @experimental\n */\n previewMuteSourceAudio(muted: boolean): string\n /** @experimental */\n previewAudioRelease(): string\n /**\n * Gate-only: encode a fixed, natively generated PCM fixture and describe the\n * result — access units, timestamps, CSD, payload hashes and decoded metrics.\n *\n * The inputs are formulas rather than files, so a baseline taken before a\n * change to the encoder loop is comparable with one taken after it.\n * @experimental\n */\n encoderGateRun(fixture: string, maxInputSize: number): string\n /** Gate-only: where samples go, stage by stage, for one marker source. @experimental */\n primingAttribution(): string\n /** Gate-only: the file-measurable Music gates, in one run. @experimental */\n musicGates(): string\n\n /**\n * Gate-only: mix the marker fixture at a chosen timeline start, decode the\n * result and report where the markers actually landed. @experimental\n */\n previewAlignmentGate(startUs: number): string\n\n /** Gate-only: arm the paired player trace and sample every `intervalMs`. @experimental */\n previewTraceArm(on: boolean, intervalMs: number): string\n /** Gate-only: label the trace so a harness can find its own cases. @experimental */\n previewTraceMark(label: string): string\n /** Gate-only: drain trace events from a sequence number, paged. @experimental */\n previewTraceDrain(fromSeq: number, limit: number): string\n /**\n * Compare both players on one clock and correct the mix past a tolerance.\n *\n * Native because that is where both anchors are published; measuring across\n * the bridge would carry the hop's own jitter into the answer. @experimental\n */\n previewDriftCheck(toleranceUs: number): string\n /**\n * Stop playback for a host reason — background, picker, unmount.\n *\n * Overrides any automatic resume the stall coordinator is holding, so\n * playback cannot restart at a moment nobody asked for. @experimental\n */\n previewSyncStop(reason: string): string\n /** What the stall coordinator has done, for assertions. @experimental */\n previewSyncCounters(): string\n /**\n * Frames offered, queued and drawn.\n *\n * Read-only accounting, available in release, so the question \"is the trace\n * disturbing the picture?\" can be asked without the trace. @experimental\n */\n previewFramePipeline(): string\n /**\n * Gate-only: the shared-clock playback spike.\n *\n * `spikeStart` takes `{items:[{uri,startUs,endUs,speed}], mixUri, width,\n * height}` straight from `playlistOf()`, so the compiler decides the timing\n * and native maps it onto media items without interpreting the project.\n */\n spikeStart(specJson: string): string\n spikePlay(): string\n spikePause(): string\n spikeSeek(positionUs: number): string\n spikeClaim(): string\n spikeState(): string\n spikeDrain(fromSeq: number, limit: number): string\n spikeRelease(): string\n /**\n * Gate-only: turn parts of the music preview off, to bisect a failure.\n *\n * `{loadPlayer, startMix, positionPump, coordinator}`, all true in\n * production. @experimental\n */\n previewConfigSet(json: string): string\n /** What the configuration currently is. @experimental */\n previewConfigGet(): string\n /** Every codec mix preparation owns, and what became of it. @experimental */\n previewCodecLedger(): string\n /** Stop whatever is preparing. @experimental */\n previewMixCancel(): string\n /** The cache key for a program, without preparing it. @experimental */\n previewMixKey(programJson: string, urisJson: string): string\n /** Drop one prepared mix. @experimental */\n previewMixRelease(key: string): string\n /** Keep only these keys; remove the rest and every partial. @experimental */\n previewMixSweep(keepKeysJson: string): string\n /** What is prepared, for assertions. @experimental */\n previewMixLedger(): string\n /**\n * Copy a picked source into app-owned durable storage.\n *\n * A picker returns a reference, not a file: a `content://` grant dies with\n * the activity result and a cache copy dies whenever Android wants storage\n * back. The project persists what this returns, never what a picker did.\n * Content-addressed, so the same song added twice is stored once. Returns\n * `{ok, uri, token, byteLength, sha256, created}` or `{ok:false, reason}`.\n * @experimental\n */\n ingestManagedSource(uri: string): string\n /** Drop one managed source by token. @experimental */\n releaseManagedSource(token: string): string\n /**\n * Remove every managed source not named in `keepTokensJson`, and every\n * partial a killed process left. The only thing that deletes media.\n * @experimental\n */\n sweepManagedSources(keepTokensJson: string): string\n /**\n * Every filename in the managed directory, including partials and strays.\n *\n * Read-only accounting. `managedSourceLedger` reports only well-formed\n * sources, which cannot show what a sweep left behind. @experimental\n */\n managedSourceList(): string\n /** Gate-only: sweep with an unparsed keep list. @experimental */\n managedSourceGateSweepRaw(rawKeep: string): string\n /** Gate-only: seed a file with an exact name, to exercise the sweep. @experimental */\n managedSourceGateSeed(name: string, bytes: number): string\n /** Tokens and sizes of what is stored, for assertions. @experimental */\n managedSourceLedger(): string\n probeAudioSource(uri: string): string\n /** Confirm the destination is closed and parseable. @experimental */\n exportProbeOutput(uri: string): string\n /** Gate-only: hold inside the media copy, so a kill can land there. @experimental */\n exportGateHoldMediaCopy(enabled: boolean): string\n /** Gate-only: wedge the cadence program after N frames. -1 is off. @experimental */\n exportGateStallAfterFrames(frames: number): string\n /**\n * Gate-only: the smallest failing Media3 export, one stage at a time.\n *\n * Takes a JSON spec — uri, source span in microseconds, the configured speed\n * and the emission plan `resampleClip` compiled — and runs a plain\n * Transformer, then the same run with speed mapping, cadence and Skia added\n * individually. Starts in the background and returns immediately; each rung\n * and the whole ladder are reported to logcat under `PictorLadderRung` and\n * `PictorLadder`. @experimental\n */\n exportGateLadder(specJson: string): string\n /**\n * Arm the gate-only controls. Everything gate-only is inert until this is\n * called, and the production exporter never calls it. @experimental\n */\n exportGateEnableDiagnostics(enabled: boolean): string\n /** Gate-only: fail publication at a chosen point. @experimental */\n exportGatePublishFault(mode: string): string\n /** Gate-only: does this directory's filesystem have an atomic no-replace? @experimental */\n exportGateProbeNoReplace(directory: string): string\n /** Gate-only: size and CRC32, to prove a file did or did not change. @experimental */\n exportGateFileDigest(path: string): string\n /** Gate-only: which temporaries are still registered, by name. @experimental */\n exportGateLiveTemporaries(): string\n /** Gate-only: remove a file, so a case can start from a known state. @experimental */\n exportGateDeleteFile(path: string): string\n /** Gate-only: a writable directory that is not the gallery. @experimental */\n exportGateExternalFilesDir(): string\n /** Gate-only: the current heartbeat epoch. @experimental */\n exportGateBeatEpoch(): string\n /** Gate-only: offer a beat for `epoch`; reports acceptance. @experimental */\n exportGateTestBeat(source: string, epoch: number): string\n /** Gate-only: enable sample hashing for conformance checks. @experimental */\n exportGateProvenance(enabled: boolean): string\n /** Heartbeat counts and the maximum gap per source. @experimental */\n exportGateHeartbeats(): string\n /** Gate-only: started/completed/failed/released accounting. @experimental */\n exportGateAccounting(): string\n /** Stop `jobId` if it is the one running. Idempotent. @experimental */\n exportCancelJob(jobId: string): string\n exportGateRunProject(\n shardsJson: string,\n programJson: string,\n overlaysJson: string,\n mixProgramJson: string,\n mixUrisJson: string,\n timeoutSeconds: number,\n destinationJson: string,\n ): string\n /**\n * **V3.5 gate only.** Mix a compiled `AudioMixProgram` and encode it once.\n *\n * `urisJson` maps span id to a host-resolved URI. Native reads only the plan:\n * no audio kinds, no duration policy, no project.\n *\n * @experimental\n */\n exportGateMixAudio(programJson: string, urisJson: string): string\n /**\n * **V3.5 gate only.** Decode one window and report every stage.\n *\n * Running the identical window at 1x and another rate settles which stage is\n * responsible: matching cropped counts with differing stretched counts point\n * at the stretch, differing cropped counts at the decode.\n *\n * @experimental\n */\n exportGateDecodeProbe(uri: string, fromUs: number, toUs: number, speed: number): string\n /** Per-window dominant-frequency trajectory and harmonic band energies. @experimental */\n exportGateTrajectory(uri: string, spec: string): string\n /** Copy a gate output to external files, for the human gates. @experimental */\n exportGateStage(uri: string, name: string): string\n /** Measure this encoder configuration's priming from a burst at `at`. @experimental */\n exportGatePrimingProbe(rate: number, channels: number, bitrate: number, at: number): string\n /** Decode a window under a simulated decoder warm-up requirement. @experimental */\n exportGatePrerollProbe(uri: string, fromUs: number, toUs: number, warmupUs: number): string\n /**\n * **V3.5 gate only.** Run the time-stretch alone on synthetic PCM.\n *\n * @experimental\n */\n exportGateStretchProbe(rate: number, channels: number, frames: number, speed: number): string\n exportGateStageResource(base64: string, width: number, height: number): string\n media3Play(): void\n media3Pause(): void\n /** Microseconds, like every other time in Pictor. */\n media3Seek(positionUs: number): void\n media3SetRate(rate: number): void\n /** Media3's own recovery after a source failure: prepare again. */\n media3Retry(): void\n media3Release(): void\n /** A throttled snapshot: position, size, counters and any source error. */\n media3State(): Object\n\n /**\n * Start decoding one still frame. Returns a token to poll or cancel with.\n *\n * Not a Promise: a strip issues and abandons requests in bursts, and a\n * rejected Promise per abandoned request is noisy and hard to cancel. A\n * token the caller owns makes \"stop this one\" expressible.\n */\n thumbnailRequest(uri: string, sourceTimeUs: number, widthPx: number): number\n thumbnailCancel(token: number): void\n /** Base64 JPEG, or null while it is still decoding. Consumes the result. */\n thumbnailTake(token: number): string | null\n thumbnailError(token: number): string | null\n thumbnailRunning(token: number): boolean\n thumbnailReleaseSource(uri: string): void\n thumbnailReleaseAll(): void\n /** `{ decoded, cancelled, failed, running, retrievers }`. */\n thumbnailCounters(): Object\n}\n\nexport default TurboModuleRegistry.get<Spec>('PictorVideo')\n","/**\n * Reaching the native adapter.\n *\n * This file used to hold `AndroidVideoPlayer`: a full player over `MediaExtractor`,\n * `MediaCodec`, `AudioTrack` and an `ImageReader` pool of `AHardwareBuffer`s.\n * That path reached one-frame synchronisation and is gone. Media3's ExoPlayer\n * rendering into the persistent external-OES `SurfaceTexture` replaced it, for\n * the reasons in `docs/research/media3-mapping.md` — codec pacing, buffer\n * ownership, async-flush races and per-device workarounds are ExoPlayer's\n * problem to have already solved, not Pictor's to solve again.\n *\n * What remains is the module accessor and the one call that is not playback:\n * publishing the presentation surface.\n *\n * @experimental\n */\nimport Native from './NativePictorVideo'\n\n/** @experimental */\nexport const AndroidVideoPlayer = {\n /**\n * Publish the presentation surface host object on the JS runtime.\n *\n * It is captured into a worklet closure from there; the UI runtime has its\n * own global and does not see this one.\n */\n installVideoSurface(): boolean {\n return requireNative().installVideoSurface()\n },\n}\n\nexport function requireNative() {\n if (!Native) {\n throw new Error(\n '@pictor-sdk/video-android: the native module is not installed. It is Android-only and requires minSdkVersion 29.',\n )\n }\n return Native\n}\n","import { type PlaylistResult } from '@pictor-sdk/video-core'\nimport { requireNative } from './player'\n\n/**\n * Playback backed by Media3's ExoPlayer.\n *\n * Media3 owns decoding, buffering, audio focus, synchronisation, seeking, speed\n * and lifecycle. Pictor owns the document, the timeline and the Skia\n * composition. This is the whole of the seam between them.\n *\n * **No Media3 or Android type appears here.** Positions are microseconds\n * because that is Pictor's unit; the adapter converts.\n *\n * Video is rendered into the persistent external texture, so the renderer holds\n * one `SkImage` for a whole session and nothing is imported per frame.\n *\n * @experimental\n */\nexport interface Media3State {\n positionUs: number\n durationUs: number\n playing: boolean\n buffering: boolean\n ready: boolean\n ended: boolean\n rate: number\n width: number\n height: number\n released: boolean\n error: string\n /** Bumped on seek, source change and release; pairs frames with a clip. */\n generation: number\n /** Frame metadata awaiting a renderer. Bounded. */\n queueDepth: number\n /** Metadata dropped because the queue was full. Should stay zero. */\n overflowed: number\n /** Frames consumed with no sample to pair. Expected briefly after a seek. */\n unpaired: number\n /** Samples discarded because their generation was superseded. */\n stale: number\n queueHighWater: number\n /** Live threads in the process. Diagnostics for leak attribution. */\n threads: number\n /** Allocated native heap, for the safety cutoff. */\n nativeHeapBytes: number\n /** The anchor's own position, before extrapolation. Diagnostics. */\n anchorPositionUs: number\n /** Frames the buffer queue dropped because the renderer was behind. */\n coalesced: number\n /** How coarse `Player.currentPosition` is, measured rather than assumed. */\n anchorPublications: number\n anchorChanges: number\n anchorMaxStepUs: number\n anchorMeanStepUs: number\n anchorMaxStaleNs: number\n /** Which playlist window is playing, and how many there are. */\n windowIndex: number\n windowCount: number\n /** Offset inside the current window, in microseconds. */\n windowPositionUs: number\n /** Window transitions so far — seams crossed. Diagnostics. */\n transitions: number\n /**\n * Frames whose latch failed and was retried on the next tick.\n *\n * `updateTexImage` returns quietly when Skia's GL context is not current on\n * the calling thread, so this counts the retries rather than letting the\n * frames disappear. A handful around startup and playlist changes is normal;\n * a rate that tracks the frame rate is not.\n */\n missedLatches: number\n /**\n * What the single latch owner did, per frame.\n *\n * `skipped` counts ticks refused because the surface was retired or replaced\n * — the invalid call, prevented rather than caught. `failed` must stay\n * visible: a latch that quietly does nothing looks exactly like a paused\n * video, which is how an earlier defect hid for a whole session.\n */\n updatesAttempted: number\n updatesSucceeded: number\n updatesSkipped: number\n updatesFailed: number\n /**\n * Times a requested rate exceeded the range and had to be clamped.\n *\n * Should stay zero: the UI only offers rates that compose within range. A\n * non-zero value means the picture and the timeline are counting differently.\n */\n rateConstrained: number\n /** The rate actually in force: clip speed times preview rate, clamped. */\n effectiveRate: number\n /**\n * The rate asked for, before clamping.\n *\n * Beside `effectiveRate` this says whether a ceiling was *selected* or merely\n * reached: 10 requested and 10 effective is a legitimate extreme, while 25\n * requested and 10 effective is a refusal.\n */\n requestedRate: number\n /**\n * Window indices that had no speed. Should stay zero.\n *\n * Non-zero means the playlist and the window index were read at different\n * moments — the shape that left a cleared playlist running at the rate of the\n * playlist before it.\n */\n staleWindowSpeeds: number\n /** How many per-window speeds the player holds. Zero when idle. */\n windowSpeedCount: number\n /**\n * The two halves of frame delivery, counted separately.\n *\n * `framesOffered` is Media3 telling us a frame is about to be released;\n * `framesAvailable` is the SurfaceTexture saying one arrived. If the first\n * climbs while the second does not, the decoder is running but its output is\n * not reaching our surface — which is a completely different fault from the\n * renderer failing to pull.\n */\n framesOffered: number\n framesAvailable: number\n /**\n * Frames waiting to be latched, right now.\n *\n * A level rather than an edge, so it survives the consumer being disarmed.\n * Usually 0 or 1; larger after a background, and it must fall to 0 within a\n * tick of the consumer re-arming.\n */\n pendingDepth: number\n /** Availability announced for a generation already retired. Should stay 0. */\n framesRejected: number\n /**\n * The generation the pending counters are keeping for.\n *\n * Must equal the consumer's generation. A frame pending for a different one\n * is a frame nobody will ever take.\n */\n pendingGeneration: number\n /** Producer identity, for proving which object a consumer is holding. */\n controllerId: number\n pendingId: number\n orphaned: number\n /** `mime WxH rotN @fps` for the active item. Empty when there is no video. */\n videoFormat: string\n /** The decoder Media3 selected, e.g. `c2.exynos.h264.decoder`. */\n decoderName: string\n /**\n * What the decoder was configured for, and what it produced.\n *\n * Read from the player's own events rather than inferred from the device: a\n * mixed-resolution seam is the one moment input and output can disagree, and\n * naming the decoder from the model instead of asking is how an attribution\n * survives without ever being checked.\n */\n inputFormats: number\n inputWidth: number\n inputHeight: number\n inputRotation: number\n /** Media3's decoder-reuse verdict across the last format change; -1 if none. */\n decoderReuse: number\n outputSizes: number\n outputWidth: number\n outputHeight: number\n /** Increments on each decoder init — makes re-attachment visible. */\n surfaceGeneration: number\n /**\n * Seek bookkeeping for the cross-window freeze soak.\n *\n * `seeksRequested` counts calls; `seeksAccepted` counts those the player's\n * own thread acted on. A gap between them, or a `pendingSeek` that stays\n * true, is a seek that never reached the player — which is what the observed\n * freeze would look like from this side.\n */\n /**\n * Surface attachment bookkeeping, for diagnostics only.\n *\n * The adapter attaches automatically the instant a surface becomes live, so a\n * host never calls anything. These exist so the invariant can be *checked*:\n * `attachedSurfaceGeneration` trailing `liveSurfaceGeneration` means the\n * player is rendering into a surface nobody is watching.\n */\n surfaceAttachments: number\n attachedSurfaceGeneration: number\n liveSurfaceGeneration: number\n seeksRequested: number\n seeksAccepted: number\n pendingSeek: boolean\n /** Media3's own playback state constant. 1 idle, 2 buffering, 3 ready, 4 ended. */\n playbackState: number\n /** PTS of the last frame the renderer actually paired, or -1. */\n lastPairedPtsUs: number\n /**\n * Source failures reported by the player, not inferred from the state.\n *\n * A failed source otherwise looks exactly like an idle one — nothing playing,\n * nothing buffering, no duration — so the count is what distinguishes \"never\n * started\" from \"started and broke\".\n */\n errorCount: number\n /** Media3's error code, and its name plus message. Empty when healthy. */\n errorCode: number\n errorMessage: string\n /** Whether Media3 classifies the failure as worth retrying. */\n errorRecoverable: boolean\n}\n\nconst EMPTY: Media3State = {\n positionUs: 0,\n durationUs: 0,\n playing: false,\n buffering: false,\n ready: false,\n ended: false,\n rate: 1,\n width: 0,\n height: 0,\n released: true,\n error: '',\n generation: 0,\n queueDepth: 0,\n overflowed: 0,\n unpaired: 0,\n stale: 0,\n queueHighWater: 0,\n threads: 0,\n nativeHeapBytes: 0,\n anchorPositionUs: 0,\n coalesced: 0,\n anchorPublications: 0,\n anchorChanges: 0,\n anchorMaxStepUs: 0,\n anchorMeanStepUs: 0,\n anchorMaxStaleNs: 0,\n windowIndex: 0,\n windowCount: 0,\n windowPositionUs: 0,\n transitions: 0,\n missedLatches: 0,\n updatesAttempted: 0,\n updatesSucceeded: 0,\n updatesSkipped: 0,\n updatesFailed: 0,\n rateConstrained: 0,\n effectiveRate: 1,\n requestedRate: 1,\n staleWindowSpeeds: 0,\n windowSpeedCount: 0,\n framesOffered: 0,\n framesAvailable: 0,\n pendingDepth: 0,\n framesRejected: 0,\n pendingGeneration: 0,\n controllerId: 0,\n pendingId: 0,\n orphaned: 0,\n videoFormat: '',\n decoderName: '',\n inputFormats: 0,\n inputWidth: 0,\n inputHeight: 0,\n inputRotation: 0,\n decoderReuse: -1,\n outputSizes: 0,\n outputWidth: 0,\n outputHeight: 0,\n surfaceGeneration: 0,\n surfaceAttachments: 0,\n attachedSurfaceGeneration: 0,\n liveSurfaceGeneration: 0,\n seeksRequested: 0,\n seeksAccepted: 0,\n pendingSeek: false,\n playbackState: 0,\n lastPairedPtsUs: -1,\n errorCount: 0,\n errorCode: 0,\n errorMessage: '',\n errorRecoverable: false,\n}\n\n/** @experimental */\nexport const Media3 = {\n /**\n * Open a local clip and render it into the presentation surface.\n *\n * The surface must already exist: it is created on the UI runtime, where\n * Skia's GL context lives, and preparing without one is an error rather than\n * a silently black picture.\n */\n prepare(uri: string): boolean {\n return requireNative().media3Prepare(uri)\n },\n\n /** Diagnostics: a player with no video output, for heap attribution. */\n prepareNoSurface(uri: string): boolean {\n return requireNative().media3PrepareNoSurface(uri)\n },\n\n /** Diagnostics: reuse the existing player for another source. */\n rebind(uri: string): boolean {\n return requireNative().media3Rebind(uri)\n },\n\n /** Diagnostics: end the source, keep the player. */\n stopKeepingPlayer(): void {\n requireNative().media3StopKeepingPlayer()\n },\n\n /**\n * Replace the playlist from a project's playable windows.\n *\n * Takes the `PlaylistResult` straight from `@pictor-sdk/video-core` so the\n * project → window mapping has exactly one definition, tested there as a\n * round trip.\n * The sources must already be URI strings; this adapter does not interpret a\n * host's media descriptors, and a non-URI source is a host normalisation bug\n * rather than something to guess at.\n *\n * The OES texture and the `SkImage` survive this. Changing the playlist is\n * not a reason to rebuild what the renderer holds.\n */\n setPlaylist(\n playlist: PlaylistResult,\n start: { windowIndex: number; positionUs: number } = { windowIndex: 0, positionUs: 0 },\n options: { allowDivergence?: boolean } = {},\n ): boolean {\n // Refuse a playlist that would not play what the timeline shows.\n //\n // An unsupported clip keeps its span on the timeline while contributing\n // nothing to playback, so the two clocks diverge by exactly its duration\n // and every seam after it lands wrong. That is far worse than an error: it\n // looks like a working editor that is slightly out of sync. A host that\n // genuinely wants the playable remainder must ask for it.\n if (playlist.unsupported.length > 0 && options.allowDivergence !== true) {\n throw new Error(\n '@pictor-sdk/video-android: refusing to build a playlist that would diverge from the ' +\n `timeline. ${playlist.unsupported.length} clip(s) cannot play:\\n` +\n playlist.unsupported.map((u) => ` · ${u.message}`).join('\\n') +\n '\\nPass { allowDivergence: true } to play the remainder anyway.',\n )\n }\n const uris: string[] = []\n const bounds: number[] = []\n for (const item of playlist.items) {\n uris.push(uriOf(item.source))\n bounds.push(item.startUs, item.endUs, Math.round(item.speed * 1000), item.muted ? 1 : 0)\n }\n return requireNative().media3SetPlaylist(uris, bounds, start.windowIndex, start.positionUs)\n },\n\n /** Seek in the player's coordinates. Pair with `projectTimeToWindow`. */\n seekToWindow(windowIndex: number, positionUs: number): void {\n requireNative().media3SeekToWindow(windowIndex, positionUs)\n },\n\n play(): void {\n requireNative().media3Play()\n },\n\n pause(): void {\n requireNative().media3Pause()\n },\n\n /** Microseconds, like every other time in Pictor. */\n seek(positionUs: number): void {\n requireNative().media3Seek(positionUs)\n },\n\n setRate(rate: number): void {\n requireNative().media3SetRate(rate)\n },\n\n /**\n * Try the current source again after a failure.\n *\n * This is Media3's own recovery path: it re-resolves the source and resumes\n * from the current position. Worth calling when `errorRecoverable` is true —\n * a network source that went away and came back — and pointless otherwise.\n */\n retry(): void {\n requireNative().media3Retry()\n },\n\n release(): void {\n requireNative().media3Release()\n },\n\n /** A snapshot for throttled reads. Never call this per frame. */\n state(): Media3State {\n const raw = requireNative().media3State() as Partial<Media3State> | null\n return raw ? { ...EMPTY, ...raw } : EMPTY\n },\n}\n\n/**\n * A source as a URI string, or a readable failure.\n *\n * `MediaSource` is deliberately open — the timeline stores whatever descriptor\n * a host uses — so the adapter is where it has to become something Android can\n * open. Failing loudly here is better than handing `undefined` to `Uri.parse`\n * and getting a black picture with no explanation.\n */\nfunction uriOf(source: unknown): string {\n if (typeof source === 'string') return source\n if (source && typeof source === 'object' && 'uri' in source) {\n const uri = (source as { uri?: unknown }).uri\n if (typeof uri === 'string') return uri\n }\n throw new Error(\n `@pictor-sdk/video-android: a clip's source is not a URI. Normalise host media to { kind: 'uri', uri } before opening a project. Received: ${JSON.stringify(source)}`,\n )\n}\n","/**\n * Which playlist Media3 is holding, and against which surface.\n *\n * The bug this exists to prevent, in order:\n *\n * 1. The editor opens and builds surface A.\n * 2. The system picker opens. The app backgrounds; A is no longer usable.\n * 3. Two clips are added to the document while the picker is closing.\n * 4. The editor returns and builds surface B.\n * 5. Nothing prepares, because the only thing that ever triggered preparation\n * was the *playlist changing*, and it changed in step 3 — when there was no\n * surface to prepare against. `setPlaylist` was refused, the signature was\n * recorded as applied anyway, and the editor sat there with two clips on the\n * timeline and a player holding nothing.\n *\n * Two corrections follow from that. Readiness is **level-triggered**: the\n * question is \"does the playlist Media3 holds match the document, against the\n * surface that is live *now*\", asked whenever either side moves — not \"did the\n * playlist just change\". And a refusal is **not** recorded as applied, so it is\n * retried rather than remembered as done.\n *\n * A generation is what makes \"the surface that is live now\" expressible. Without\n * it a caller can only know that *a* surface exists, which cannot distinguish\n * the one it prepared against from the one that replaced it.\n *\n * Pure, so the sequence above is a test rather than a device run.\n *\n * @experimental\n */\n\n/** No surface. Distinct from every real generation, which start at 1. */\nexport const NO_SURFACE = 0\n\n/** @experimental */\nexport interface PlaylistSession {\n /** The playlist Media3 is holding, or null if it holds none. */\n appliedSignature: string | null\n /** The surface generation it was prepared against. */\n appliedGeneration: number\n /**\n * A preparation was attempted and refused.\n *\n * Kept so the retry is driven by this rather than by hoping the document\n * changes again. A user who has added their clips will not add them twice.\n */\n pending: boolean\n}\n\n/** @experimental */\nexport function idlePlaylistSession(): PlaylistSession {\n return { appliedSignature: null, appliedGeneration: NO_SURFACE, pending: false }\n}\n\n/** What the document and the device currently want. @experimental */\nexport interface PlaylistIntent {\n /** A stable description of the playlist the document implies. */\n signature: string\n /** The live surface generation, or {@link NO_SURFACE}. */\n generation: number\n}\n\n/**\n * Whether to call `setPlaylist` now.\n *\n * @experimental\n */\nexport function shouldPrepare(session: PlaylistSession, intent: PlaylistIntent): boolean {\n // Nothing to prepare against. Attempting it is the refusal that started all\n // of this, and it would poison the session by recording a signature.\n if (intent.generation === NO_SURFACE) return false\n if (session.pending) return true\n if (session.appliedSignature !== intent.signature) return true\n // The playlist is unchanged but the surface underneath it is not. A player\n // attached to a retired surface decodes, releases frames and reports no\n // error while the live texture receives nothing.\n return session.appliedGeneration !== intent.generation\n}\n\n/**\n * Record the outcome.\n *\n * A refusal leaves the session **unchanged except for `pending`**. Recording the\n * signature on failure is precisely the mistake that made the editor wait for an\n * edit that was never going to come.\n *\n * @experimental\n */\nexport function afterPrepare(\n session: PlaylistSession,\n intent: PlaylistIntent,\n ok: boolean,\n): PlaylistSession {\n if (!ok) return { ...session, pending: true }\n return {\n appliedSignature: intent.signature,\n appliedGeneration: intent.generation,\n pending: false,\n }\n}\n\n/**\n * The surface went away — backgrounded, replaced, released.\n *\n * The signature is kept: it still describes what Media3 was asked to hold, and\n * the comparison that matters next is against the *generation*, which is now\n * guaranteed to differ from any live one.\n *\n * @experimental\n */\nexport function retireSurface(session: PlaylistSession): PlaylistSession {\n return { ...session, appliedGeneration: NO_SURFACE, pending: true }\n}\n\n/**\n * Whether a callback carrying `generation` is still worth acting on.\n *\n * A screen's teardown routinely runs after the next screen's setup, and an\n * asynchronous surface creation can land after the surface it created has been\n * replaced. Both arrive looking exactly like a valid result.\n *\n * @experimental\n */\nexport function isCurrentGeneration(generation: number, live: number): boolean {\n return generation !== NO_SURFACE && generation === live\n}\n","import { NO_SURFACE } from './playlistSession'\n\n/**\n * Who is allowed to create a presentation surface, and when.\n *\n * There is one external texture in the process, so there is one of these. Not\n * one per hook instance: a hook is remounted by a navigation, a fast refresh or\n * a rotation, and each remount would bring its own lock and its own idea of\n * what is in flight. Two creates then overlap, each registers a generation, and\n * the second overwrites the native handles the first is still holding — the GL\n * texture and `SurfaceTexture` from the first are then unreachable and never\n * released. The symptom is a hook reporting one generation while the adapter\n * has moved on to another, with an abandoned surface in between.\n *\n * ## The generation is native's, and only native's\n *\n * `create()` returns the generation `VideoSurface.register` assigned. This\n * module publishes that number unchanged and keeps no counter of its own — a\n * second counter is a second answer to \"which surface is live\", and the whole\n * class of bug here is two answers to one question.\n *\n * ## A superseded attempt is retired, not ignored\n *\n * An attempt whose owner went away while it was on the UI runtime still\n * *succeeds*: the texture exists, the `Surface` is registered, Media3 could\n * attach to it. Ignoring its report leaves exactly that — a live, registered\n * surface nobody will ever draw or release. So it is released, on the runtime\n * that owns the GL context, before anything else is started.\n *\n * ## Serialised, and retirement does not unlock\n *\n * A request that arrives while an attempt is unresolved is queued, and\n * backgrounding or unmounting does **not** release the lock: the earlier\n * attempt is still out there on the UI runtime and will still come back. The\n * queue is one deep, because every waiting request wants the same thing.\n *\n * Effects are injected so the interleavings are deterministic in a test rather\n * than a matter of timing on a device.\n *\n * @experimental\n */\n\n/** What native `create()` reports. @experimental */\nexport interface SurfaceCreateResult {\n ok: boolean\n /** The generation `register` assigned, or 0 on failure. */\n generation: number\n textureId: number\n error: string\n}\n\n/** @experimental */\nexport interface SurfaceOwnerEffects {\n /**\n * Schedule a create on the UI runtime.\n *\n * Must eventually call {@link SurfaceOwner.complete}, whatever happens.\n */\n create(): void\n /**\n * Release a specific generation, on the runtime that owns the GL context.\n *\n * Takes a generation rather than \"the current surface\" so a late attempt\n * cannot release the surface that replaced it.\n */\n retire(generation: number): void\n}\n\n/** @experimental */\nexport interface SurfaceReadiness {\n /** The live generation, exactly as native reported it. 0 when there is none. */\n generation: number\n /** The last failure, or empty. */\n error: string\n}\n\n/** @experimental */\nexport interface SurfaceOwner {\n readonly readiness: SurfaceReadiness\n /** Whether an attempt is on the UI runtime right now. */\n readonly busy: boolean\n /** Generations created by a superseded attempt and released. Diagnostics. */\n readonly retired: number\n /**\n * Ask for a surface on behalf of `token`.\n *\n * Returns false when the request was queued or refused rather than started.\n */\n request(token: number): boolean\n /** That owner is gone. Does not unlock: an unresolved attempt is still out there. */\n retireOwner(token: number): void\n /** An attempt finished. */\n complete(result: SurfaceCreateResult): void\n /** Adopt a surface that already exists, without creating one. */\n adopt(generation: number, token: number): void\n subscribe(listener: () => void): () => void\n /** Point the owner at the newest caller's effects. */\n useEffects(next: SurfaceOwnerEffects): void\n}\n\n/** @experimental */\nexport function createSurfaceOwner(initial: SurfaceOwnerEffects): SurfaceOwner {\n // Mutable, because the singleton outlives every hook that uses it and each\n // one brings its own closure over the published handle and the texture size.\n // The newest is the right one: they all drive the same native object.\n let effects = initial\n let readiness: SurfaceReadiness = { generation: NO_SURFACE, error: '' }\n let busy = false\n let retired = 0\n /** The owner the in-flight attempt was started for. */\n let activeToken: number | null = null\n /**\n * Whether that owner went away while its attempt was on the UI runtime.\n *\n * A flag rather than a comparison against whoever is current now: a *second*\n * hook asking for a surface does not invalidate the one being built, because\n * they share it. Only the asking owner disappearing does. Comparing against\n * the current token retired a perfectly good surface every time a remount\n * overlapped a create, which is a leak dressed as caution.\n */\n let activeRetired = false\n /** The owner currently entitled to a surface. */\n let currentToken: number | null = null\n /** One waiting request. Every waiter wants the same thing. */\n let queuedToken: number | null = null\n const listeners = new Set<() => void>()\n\n const emit = () => {\n for (const listener of listeners) listener()\n }\n\n const publish = (next: SurfaceReadiness) => {\n if (next.generation === readiness.generation && next.error === readiness.error) return\n readiness = next\n emit()\n }\n\n const start = (token: number) => {\n busy = true\n activeToken = token\n activeRetired = false\n effects.create()\n }\n\n const owner: SurfaceOwner = {\n get readiness() {\n return readiness\n },\n get busy() {\n return busy\n },\n get retired() {\n return retired\n },\n\n request(token) {\n currentToken = token\n if (readiness.generation !== NO_SURFACE) return false\n if (busy) {\n queuedToken = token\n return false\n }\n start(token)\n return true\n },\n\n retireOwner(token) {\n if (activeToken === token) activeRetired = true\n if (currentToken === token) currentToken = null\n if (queuedToken === token) queuedToken = null\n // Deliberately not `busy = false`. The attempt is on the UI runtime and\n // will come back; unlocking here is what lets a second create overlap it.\n },\n\n adopt(generation, token) {\n currentToken = token\n if (generation === NO_SURFACE) return\n publish({ generation, error: '' })\n },\n\n complete(result) {\n const wasRetired = activeRetired\n const hasOwner = currentToken !== null\n busy = false\n activeToken = null\n activeRetired = false\n\n if (result.ok && result.generation > NO_SURFACE) {\n // Superseded means nobody is left who asked for this: the owner that\n // started it retired, or every owner has gone. Not \"somebody else asked\n // in the meantime\" — they want the same surface.\n const superseded = wasRetired || !hasOwner\n const stale = result.generation < readiness.generation\n if (superseded || stale) {\n // It exists and is registered, so it has to be released rather than\n // forgotten. Releasing by generation means a late attempt cannot take\n // down the surface that replaced it.\n retired += 1\n effects.retire(result.generation)\n } else {\n publish({ generation: result.generation, error: '' })\n }\n } else {\n publish({\n generation: readiness.generation,\n error: result.error || 'surface create failed',\n })\n }\n\n const next = queuedToken\n queuedToken = null\n // Only if there is still no surface: a queued request that has since been\n // satisfied by the attempt that just finished wants nothing more.\n if (next !== null && readiness.generation === NO_SURFACE && next === currentToken) {\n start(next)\n }\n },\n\n useEffects(next) {\n effects = next\n },\n\n subscribe(listener) {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n }\n\n return owner\n}\n\n/**\n * The process-wide owner, created on first use.\n *\n * A module-level singleton because the thing it guards — the one external\n * texture — is one per process. `resetSurfaceOwnerForTests` exists so a test can\n * start from a known state; nothing in production calls it.\n *\n * @experimental\n */\nlet singleton: SurfaceOwner | null = null\n\n/** @experimental */\nexport function surfaceOwner(effects: SurfaceOwnerEffects): SurfaceOwner {\n if (!singleton) singleton = createSurfaceOwner(effects)\n else singleton.useEffects(effects)\n return singleton\n}\n\n/** @experimental */\nexport function resetSurfaceOwnerForTests(): void {\n singleton = null\n}\n\n/** Fresh token for a hook instance. Only ever increases. @experimental */\nlet nextToken = 0\n\n/** @experimental */\nexport function nextOwnerToken(): number {\n nextToken += 1\n return nextToken\n}\n","import { useCallback, useEffect, useRef, useState } from 'react'\nimport { AppState, type AppStateStatus } from 'react-native'\nimport { runOnJS, runOnUI, useSharedValue } from 'react-native-reanimated'\nimport { NO_SURFACE } from './playlistSession'\nimport { AndroidVideoPlayer } from './player'\nimport { nextOwnerToken, surfaceOwner, type SurfaceCreateResult } from './surfaceOwner'\n\n/**\n * The presentation surface, and whether it is usable *right now*.\n *\n * This was host code, and every host would have got it wrong in the same way.\n * `create()` must run on the UI runtime, where Skia's GL context lives, and\n * `runOnUI` **schedules** that rather than performing it — so a host that sets\n * its state beside the call declares the surface ready a frame or two before the\n * texture exists. A playlist built in that window is refused outright, and the\n * editor shows clips on the timeline with a player holding nothing.\n *\n * Readiness is therefore reported from *inside* the worklet, after the texture\n * and the native `Surface` both exist, and it carries **the generation native\n * assigned**. This hook keeps no counter of its own: a second counter is a\n * second answer to \"which surface is live\", and the fault it replaced was\n * exactly two answers disagreeing.\n *\n * ## Level-triggered, not one-shot\n *\n * The surface is re-examined whenever the app returns to the foreground, and\n * marked unavailable when it leaves. Backgrounding is the ordinary case, not an\n * edge case: choosing media means going to another activity and coming back, and\n * the clips arrive in the document *while there is no surface to prepare\n * against*. Anything that only reacts to the document changing has already\n * missed its chance by the time it returns.\n *\n * ## Serialisation is process-wide\n *\n * Creation is arbitrated by {@link surfaceOwner}, a module-level singleton,\n * because the thing it guards — the one external texture — is one per process.\n * A lock held per hook instance is no lock at all: a remount brings a fresh one,\n * two creates overlap, and the second overwrites the native handles the first is\n * still holding. Nor does unmounting release it: the earlier attempt is still\n * out on the UI runtime and will still come back.\n *\n * A surface built by an attempt whose owner has since gone is **released**, not\n * ignored. It exists, it is registered, and Media3 could attach to it.\n *\n * @experimental\n */\n\n/** How often to retry creating a surface while there is none, in milliseconds. */\nconst CREATE_RETRY_MS = 250\n\n/**\n * The one stable slot, installed on first ask.\n *\n * Synchronous, and callable during a render, because the consumer has to be able\n * to capture it *before* any surface exists. An effect is too late: the frame\n * callback is built on the first render, and anything it captures then is what\n * it keeps.\n *\n * The object never changes. `create` fills its target in place and retirement\n * clears it, so every call through it resolves to whatever the slot points at\n * now. There is deliberately nothing replaceable to hold.\n *\n * @experimental\n */\nexport function videoSurfaceSlot(): VideoSurfaceHandle | undefined {\n const published = (globalThis as unknown as { __pictorVideoSurface?: VideoSurfaceHandle })\n .__pictorVideoSurface\n if (published) return published\n if (!AndroidVideoPlayer.installVideoSurface()) return undefined\n return (globalThis as unknown as { __pictorVideoSurface?: VideoSurfaceHandle })\n .__pictorVideoSurface\n}\n\n/** @experimental */\nexport interface VideoSurfaceOptions {\n /** Backing size of the texture. Frames are scaled into it. */\n width: number\n height: number\n enabled?: boolean\n}\n\n/**\n * The surface as the UI runtime sees it.\n *\n * Structural rather than an imported Android type: nothing platform-specific\n * reaches Pictor's public API.\n *\n * @experimental\n */\nexport interface VideoSurfaceHandle {\n create(\n width: number,\n height: number,\n ): { textureId: number; ok: boolean; generation: number; error?: string }\n generation(): number\n alive(): boolean\n /** Release a named generation. Does nothing if it is not the one held. */\n releaseGeneration(generation: number): boolean\n}\n\n/** @experimental */\nexport interface VideoSurface {\n /**\n * The stable slot.\n *\n * Available from the first render and never replaced, so a consumer captures\n * it once and reads through it. It answers `generation() === 0` while empty,\n * which is how a renderer knows there is nothing to draw — a null would have\n * to be captured, and a captured null is what it stays.\n */\n surface: unknown | undefined\n /**\n * The live generation, exactly as native reported it. 0 when there is none.\n *\n * Feed to `useMedia3Playback`. Every change is a reason to reconcile the\n * playlist, including the change back to 0.\n */\n generation: number\n /** Failure to install or create, for a host's error slot. Empty when healthy. */\n error: string\n}\n\n/** @experimental */\nexport function useVideoSurface(options: VideoSurfaceOptions): VideoSurface {\n const { width, height, enabled = true } = options\n const [readiness, setReadiness] = useState({ generation: NO_SURFACE, error: '' })\n // Resolved during render, not in an effect: a consumer building a frame\n // callback on its first render must have the slot to capture by then.\n const handleRef = useRef<VideoSurfaceHandle | undefined>(undefined)\n if (!handleRef.current) handleRef.current = videoSurfaceSlot()\n const textureId = useSharedValue(0)\n\n /** This hook instance's identity, so the owner can tell it from a remount. */\n const tokenRef = useRef(0)\n if (tokenRef.current === 0) tokenRef.current = nextOwnerToken()\n const token = tokenRef.current\n\n const [attempt, setAttempt] = useState(0)\n\n const ownerRef = useRef<ReturnType<typeof surfaceOwner> | undefined>(undefined)\n\n /**\n * Resolve an attempt on the process-wide owner.\n *\n * Reached through `runOnJS` from inside the worklet, which is the only place\n * that can honestly say the texture and the `Surface` exist.\n */\n const complete = useCallback((result: SurfaceCreateResult) => {\n ownerRef.current?.complete(result)\n }, [])\n\n const owner = surfaceOwner({\n create() {\n const published = handleRef.current\n if (!published) {\n complete({ ok: false, generation: 0, textureId: 0, error: '__pictorVideoSurface missing' })\n return\n }\n runOnUI(() => {\n 'worklet'\n const created = published.create(width, height)\n textureId.value = created.textureId\n if (created.ok && !(created.generation > 0)) {\n runOnJS(complete)({\n ok: false,\n generation: 0,\n textureId: created.textureId,\n error: 'adapter too old: create() reports no generation',\n })\n return\n }\n runOnJS(complete)({\n ok: created.ok && created.textureId > 0,\n generation: created.generation,\n textureId: created.textureId,\n // The native reason, not a summary of it. \"No EGL context current on\n // this thread\" and \"no JNIEnv\" send a reader to different places.\n error: created.ok ? '' : created.error || 'surface create failed',\n })\n })()\n },\n retire(generation: number) {\n const published = handleRef.current\n if (!published || typeof published.releaseGeneration !== 'function') return\n // On the UI runtime, because deleting a GL texture needs the context that\n // owns it to be current — which is true on exactly one thread.\n runOnUI(() => {\n 'worklet'\n published.releaseGeneration(generation)\n })()\n },\n })\n ownerRef.current = owner\n\n useEffect(() => {\n const unsubscribe = owner.subscribe(() => setReadiness(owner.readiness))\n setReadiness(owner.readiness)\n return unsubscribe\n }, [owner])\n\n const ensure = useCallback(() => {\n if (!enabled) return\n // Through the same accessor the first render used. Calling\n // `installVideoSurface` again here is what produced two host objects: the\n // consumer captured the one installed during render, `create` ran on the one\n // this effect installed over it, and the two reported different generations\n // forever with neither being wrong about itself.\n const published = videoSurfaceSlot()\n if (!published) {\n setReadiness((prev) => ({ ...prev, error: '__pictorVideoSurface missing' }))\n return\n }\n handleRef.current = published\n\n // Already live: adopt it rather than build a second. Guarded because an\n // adapter built before these accessors answers `undefined`, and calling that\n // throws inside an effect — which presents as a blank screen rather than as\n // the version mismatch it is.\n if (\n typeof published.alive === 'function' &&\n typeof published.generation === 'function' &&\n published.alive() &&\n published.generation() !== NO_SURFACE\n ) {\n owner.adopt(published.generation(), token)\n return\n }\n\n owner.request(token)\n }, [enabled, owner, token])\n\n useEffect(() => {\n ensure()\n }, [ensure, attempt])\n\n /**\n * Keep asking until there is a surface.\n *\n * `create` needs Skia's GL context current on the UI runtime, and that is only\n * true once a canvas has drawn — which has not happened when this hook's first\n * effect runs. Measured on a Pixel 7: the first attempt returns \"no EGL\n * context current on this thread\", and a later one succeeds.\n *\n * A retry rather than a wait on some readiness signal, because there is no\n * such signal: the context becoming current is a side effect of rendering, not\n * an event anything publishes. The owner refuses to overlap attempts, so\n * asking often is safe.\n */\n useEffect(() => {\n if (!enabled) return\n if (readiness.generation !== NO_SURFACE) return\n const id = setInterval(() => setAttempt((n) => n + 1), CREATE_RETRY_MS)\n return () => clearInterval(id)\n }, [enabled, readiness.generation])\n\n useEffect(() => {\n if (!enabled) return\n const onChange = (status: AppStateStatus) => {\n if (status === 'active') {\n // Level-triggered: ask again rather than assume the answer survived.\n ensure()\n return\n }\n // This owner is gone for now. The lock is deliberately not released — an\n // attempt on the UI runtime is still out there — and a surface it produces\n // will be retired rather than published to nobody.\n owner.retireOwner(token)\n }\n const subscription = AppState.addEventListener('change', onChange)\n return () => subscription.remove()\n }, [enabled, ensure, owner, token])\n\n useEffect(() => () => owner.retireOwner(token), [owner, token])\n\n return {\n // Always the slot, never undefined once installed. Gating this on readiness\n // is what put a null into the consumer's closure and kept it there.\n surface: handleRef.current,\n generation: readiness.generation,\n error: readiness.error,\n }\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n playIntent,\n playlistOf,\n projectDuration,\n type PlayIntent,\n type UnsupportedSource,\n type VideoProject,\n} from '@pictor-sdk/video-core'\nimport {\n compositionSpikeSpec,\n type CompositionSpikeState,\n spikePause,\n spikePlay,\n spikeRelease,\n spikeSeek,\n spikeStart,\n spikeState,\n} from './compositionSpike'\nimport { createPlaybackLifecycle, type PlaybackLifecycle } from './playbackLifecycle'\nimport type { Media3Playback } from './useMedia3Playback'\n\n/**\n * The preview, on one clock.\n *\n * ## What this replaces\n *\n * Two players kept in step: a video player and a separate player for the\n * prepared audio mix, with a drift check correcting between them. That\n * architecture was measured to its limit and could not hold synchronisation\n * across a clip seam — the correction is a seek, a seek costs a decode, and a\n * decode at the moment of a seam is what the correction was trying to cover.\n *\n * A `CompositionPlayer` has one clock, one playback thread and one audio graph.\n * The prepared mix becomes an audio-only sequence beside the video sequence, so\n * the two cannot drift: there is nothing to drift against.\n *\n * ## What it does not fix\n *\n * The clip seam still costs a picture. Media3 tears down the video decoder at\n * an item boundary and builds another, and for 119 to 168 ms nothing reaches\n * the renderer while the clock runs on — the audio is continuous and the\n * picture holds. That behaviour was watched on a device and accepted for v1;\n * see `docs/research/preview-acceptance.md` for what was accepted and what was\n * explicitly not.\n *\n * ## Why the same contract\n *\n * It returns {@link Media3Playback}, unchanged. The editor, the transport and\n * the gates were all written against it, and swapping the engine underneath is\n * a smaller and more reviewable change than rewriting every caller to a shape\n * that would end up equivalent.\n *\n * @experimental\n */\nexport interface UseCompositionPlaybackOptions {\n project: VideoProject\n /** Timeline time the host wants shown. */\n playheadUs: number\n /**\n * The prepared audio program, from `usePreviewAudio`.\n *\n * Null while it is being prepared, after it has failed, and — legitimately —\n * when the document has no audio at all.\n *\n * Those are different situations and the hook has to tell them apart. A\n * document *with* audio waits: building the composition without its sound\n * would play a silent preview and gain the music a second later, which is two\n * timelines to anyone watching. A document *without* audio has nothing to\n * wait for, and refusing to build would leave the picture dead.\n */\n mix: { uri: string; durationUs: number } | null\n /** Turns a document source into something the player can open. */\n resolveSource: (source: { kind: string; uri?: string }) => string | null\n /** @see UseMedia3PlaybackOptions.surfaceGeneration */\n surfaceGeneration?: number\n enabled?: boolean\n}\n\n/** How often the composition's state is read, in milliseconds. */\nconst POLL_MS = 100\n\n/**\n * What a composition knows about itself.\n *\n * Deliberately not `Media3State`. That type is the shape the video player's\n * native state comes back in — forty fields about queue depth, anchors, native\n * heap and window indices — and a composition has none of them. Synthesising\n * one would be inventing forty numbers to satisfy a type, and a reader would\n * have no way to tell the invented ones from the measured ones.\n *\n * @experimental\n */\nexport interface CompositionPlaybackState {\n playing: boolean\n ready: boolean\n buffering: boolean\n ended: boolean\n released: boolean\n /** Project time, which for a single composition is the only time there is. */\n positionUs: number\n durationUs: number\n /** The presentation size, or 0 while nothing has been measured. */\n width: number\n height: number\n error: string | null\n}\n\nfunction composedState(snapshot: CompositionSpikeState): CompositionPlaybackState {\n const playbackState = snapshot.playbackState ?? 1\n return {\n playing: snapshot.playing === true,\n ready: playbackState === 3,\n buffering: playbackState === 2,\n ended: playbackState === 4,\n // `released` counts releases; a positive count with nothing attached is the\n // released state this means.\n released: snapshot.attached !== true && (snapshot.released ?? 0) > 0,\n positionUs: snapshot.positionUs ?? 0,\n durationUs: snapshot.durationUs ?? 0,\n /*\n Zero, and honestly so.\n\n A composition renders into the presentation surface at the size the host\n gave it; it does not report a decoded size the way the video player did.\n The host already knows the size it asked for, and a fabricated number here\n would be worse than none.\n */\n width: 0,\n height: 0,\n error: snapshot.error ?? null,\n }\n}\n\n/** @experimental */\nexport type CompositionPlayback = Omit<Media3Playback, 'state'> & {\n state: CompositionPlaybackState | null\n}\n\n/** @experimental */\nexport function useCompositionPlayback(\n options: UseCompositionPlaybackOptions,\n): CompositionPlayback {\n const { project, mix, enabled = true } = options\n const surfaceGeneration = options.surfaceGeneration ?? 0\n const resolveSource = useRef(options.resolveSource)\n resolveSource.current = options.resolveSource\n\n const [state, setState] = useState<CompositionPlaybackState | null>(null)\n const [playing, setPlaying] = useState(false)\n const playheadRef = useRef(options.playheadUs)\n playheadRef.current = options.playheadUs\n\n const playlist = useMemo(() => playlistOf(project), [project])\n const items = playlist.items\n const durationUs = useMemo(() => projectDuration(project), [project])\n\n /*\n What the composition is made of, as a string.\n\n Every edit makes a new project object and most of them — a selection, a\n playhead move — change nothing the player should be playing. The mix is part\n of it: a rebuilt mix is a different composition even when the video is\n identical, and starting the old one would play the sound the user just\n changed.\n */\n // A document with audio has a mix to wait for; one without does not.\n const needsMix = project.audio.length > 0\n const signature = useMemo(() => {\n if (needsMix && mix === null) return ''\n const parts = items.map(\n (i) => `${JSON.stringify(i.source)}|${i.startUs}|${i.endUs}|${i.speed}|${i.muted}`,\n )\n const sound = mix === null ? 'silent' : `${mix.uri}:${mix.durationUs}`\n return `${parts.join(';')}#${playlist.unsupported.length}@${sound}`\n }, [items, playlist.unsupported.length, mix, needsMix])\n\n const [unsupported, setUnsupported] = useState<readonly UnsupportedSource[]>([])\n const lastSeekRef = useRef<Media3Playback['lastSeek']>(null)\n\n /*\n This screen's ownership, unchanged from the path it replaces.\n\n The player outlives any screen: leaving the editor while playing left the\n sound running, and backgrounding did the same. One lifecycle per hook\n instance, and a screen mounted later starts from nothing.\n */\n const lifecycleRef = useRef<PlaybackLifecycle | null>(null)\n if (lifecycleRef.current === null) {\n lifecycleRef.current = createPlaybackLifecycle({\n pause: () => void spikePause(),\n release: () => void spikeRelease(),\n })\n }\n const lifecycle = lifecycleRef.current\n const [lifecycleEpoch, setLifecycleEpoch] = useState(lifecycle.epoch)\n\n const loaded = useRef('')\n useEffect(() => {\n if (!enabled || signature === '') return\n if (surfaceGeneration === 0) return\n if (loaded.current === signature) return\n /*\n An empty URI is a composition with no audio sequence, which the adapter\n builds and plays. It is not a missing mix — that case never reaches here,\n because the signature is empty until one exists.\n */\n const { spec, unsupported: refused } = compositionSpikeSpec(\n project,\n mix ?? { uri: '', durationUs: 0 },\n (s) => resolveSource.current(s),\n )\n setUnsupported(\n refused.map((reason) => ({ clipId: reason.split(':')[0] ?? '', reason }) as UnsupportedSource),\n )\n if (spec.items.length === 0) return\n const started = spikeStart(spec) as { ok?: boolean }\n // A refusal is not a load. Recording it as one is what left an editor\n // waiting for an edit the person had already made.\n if (started.ok === true) loaded.current = signature\n }, [enabled, signature, surfaceGeneration, project, mix])\n\n useEffect(() => {\n return () => {\n lifecycle.teardown('unmount')\n }\n }, [lifecycle])\n\n useEffect(() => {\n if (!enabled) return\n // Captured now and checked on every tick: an interval is not cleared the\n // instant a screen goes, and a poll landing afterwards must not write into\n // one that is no longer there.\n const epoch = lifecycle.epoch\n const id = setInterval(() => {\n if (!lifecycle.accepts(epoch)) return\n const next = composedState(spikeState())\n /*\n Only a change of meaning re-renders.\n\n Replacing the state object every tick re-renders the editor ten times a\n second to move a line, and at ten hertz the line moves visibly in steps.\n A host that wants the position drives it from `positionUs()`.\n */\n setState((previous: CompositionPlaybackState | null) =>\n previous !== null &&\n previous.playing === next.playing &&\n previous.ready === next.ready &&\n previous.buffering === next.buffering &&\n previous.ended === next.ended &&\n previous.released === next.released &&\n previous.error === next.error\n ? previous\n : next,\n )\n setPlaying(next.playing)\n }, POLL_MS)\n return () => clearInterval(id)\n }, [enabled, lifecycle, lifecycleEpoch])\n\n const seek = useCallback((timeUs: number) => {\n const at = Math.max(0, Math.round(timeUs))\n lastSeekRef.current = { globalUs: at, windowIndex: 0, localUs: at }\n spikeSeek(at)\n }, [])\n\n const positionUs = useCallback((): number | null => {\n const snapshot = spikeState()\n if (!snapshot.attached) return null\n const at = snapshot.positionUs\n return typeof at === 'number' ? at : null\n }, [])\n\n const play = useCallback((): PlayIntent => {\n const snapshot = spikeState()\n const intent = playIntent({\n positionUs: typeof snapshot.positionUs === 'number' ? snapshot.positionUs : 0,\n durationUs,\n playbackState: typeof snapshot.playbackState === 'number' ? snapshot.playbackState : 1,\n ended: snapshot.playbackState === 4,\n })\n if (intent.seekToUs !== null) seek(intent.seekToUs)\n if (!intent.play) return intent\n // Refused once the screen has given the player back: a late press from a\n // control that has not unmounted yet must not start a released player.\n if (lifecycle.released) return intent\n spikePlay()\n setPlaying(true)\n return intent\n }, [durationUs, lifecycle, seek])\n\n useEffect(() => {\n setLifecycleEpoch(lifecycle.epoch)\n }, [lifecycle, surfaceGeneration])\n\n return {\n playing,\n play,\n get lastSeek() {\n return lastSeekRef.current\n },\n positionUs,\n effectiveRate: 1,\n pause: useCallback(() => {\n spikePause()\n setPlaying(false)\n }, []),\n /*\n Rate is the document's, not the transport's.\n\n Clip speed is compiled into the composition and into the mix, and a\n preview-wide rate on top of it would be a second speed nobody asked for.\n The contract keeps the method; this path has nothing to do with it.\n */\n setRate: useCallback(() => {}, []),\n seek,\n state,\n items,\n unsupported,\n divergenceUs: 0,\n lifecycle,\n // One player, so there is no half to stall: the recovery this reported was\n // the video player running while the audio player had stopped, or the\n // reverse. @see docs/research/preview-acceptance.md\n stall: null,\n }\n}\n","import { playlistOf, type VideoProject } from '@pictor-sdk/video-core'\nimport { requireNative } from './player'\n\n/**\n * The shared-clock playback spike, from the host's side.\n *\n * ## What it is testing\n *\n * Whether one playback-clock authority can drive the picture and a prepared,\n * project-wide mix at once. The two-player preview cannot meet the 60 ms gate\n * and the reasons are structural: anchors polled at 50 ms make a single\n * comparison uncertain by up to ±56 ms against a 60 ms tolerance, the\n * correction loop runs at 500 ms while each corrective seek costs 200-400 ms,\n * and at a seam the project clock divides by a speed the player applies about\n * 450 ms later.\n *\n * ## Why the spec is built here\n *\n * `playlistOf` is the shared compiler, and it already decides every number this\n * needs: the source bounds a clip plays between, its speed, and where the\n * window sits on the project timeline. Passing its output through verbatim is\n * what keeps the adapter free of a second interpretation of the document —\n * native maps items onto media items and nothing else.\n *\n * Diagnostics-only, and it says nothing about live PCM gain or fades: it plays\n * the same prepared mix file the current preview plays.\n *\n * @experimental\n */\n\n/** One clip, as the compiler described it. @experimental */\nexport interface CompositionSpikeItem {\n uri: string\n /** Clipping bounds in **source** time, which is where speed is applied. */\n startUs: number\n endUs: number\n speed: number\n}\n\n/** @experimental */\nexport interface CompositionSpikeSpec {\n /**\n * Hold the shared clock at each clip boundary.\n *\n * Off, and measured off for a reason: with `playWhenReady` false\n * `CompositionPlayer` does not advance across an item boundary, so the\n * incoming frame the hold waits for is never produced and the hold runs to\n * its bound. Holding the clock and preparing the next stream are mutually\n * exclusive on this player.\n */\n seamHold?: boolean\n /**\n * Prepare the incoming item with an explicit seek while the clock is held.\n *\n * Only meaningful with `seamHold`. Pausing natural traversal and seeking to a\n * target are different operations: the seek path resets the renderers, which\n * is what makes a paused player produce a frame, and is the same path the\n * existing seek readiness already passes on.\n */\n seamSeek?: boolean\n items: CompositionSpikeItem[]\n /** The prepared mix. Empty plays the picture with no sound at all. */\n mixUri: string\n /**\n * The mix's duration.\n *\n * Required whenever `mixUri` is set: the player computes each sequence's\n * duration before playing, and an audio-only item cannot report one itself.\n */\n mixDurationUs: number\n width: number\n height: number\n /**\n * The request this attachment belongs to.\n *\n * Claimed before preparation starts. Native refuses an attachment whose id is\n * older than the newest claim, so a preparation that finishes late cannot put\n * a stale mix on screen for even one frame. Omit for runs with no race.\n */\n requestId?: number\n}\n\n/** @experimental */\nexport interface CompositionSpikeState {\n ok: boolean\n attached: boolean\n built: number\n released: number\n error: string | null\n /**\n * Readiness latency, reported apart from synchronisation.\n *\n * A preparation pause is a latency figure and an A/V offset is an error;\n * averaging one into the other is how a slow start reads as drift.\n */\n startupMs: number\n seekMs: number\n /** What the person asked for, independent of whether the transport is running. */\n intentPlaying?: boolean\n /** A hold waiting for the first frame of the current epoch. */\n releasePending?: boolean\n releases?: number\n readinessTimeouts?: number\n readinessFailure?: string | null\n /** How long the last hold lasted before playback was released. */\n holdMs?: number\n /**\n * Wall-clock milliseconds spent holding the clock at each clip boundary.\n *\n * Listed, never averaged. Each entry is a pause the person sees: the decoder\n * is torn down and rebuilt at a boundary and the clock waits rather than the\n * picture falling behind it. **This is not seamless playback**, which is why\n * it is reported apart from every synchronisation figure.\n */\n seamHoldMs?: number[]\n /** Holds that reached their bound without a correctly associated frame. */\n seamHoldTimeouts?: number\n seamHoldActive?: boolean\n atNs?: number\n positionUs?: number\n playing?: boolean\n playbackState?: number\n durationUs?: number\n presentedUs?: number | null\n /** How old the presented frame was when it was read, so a stalled sample can be excluded. */\n presentedAgeNs?: number\n /** How long the paired read itself took. Bounds the pairing error. */\n readSpanNs?: number\n /** How stale the pairing was when it was read out. */\n pairingAgeNs?: number\n}\n\n/**\n * Build the spike's spec from a project and a prepared mix.\n *\n * Unsupported clips are not silently dropped: `playlistOf` reports them, and a\n * spec built from a playlist missing a window would put every later seam in the\n * wrong place — which is exactly the class of error this spike exists to\n * measure.\n *\n * @experimental\n */\nexport function compositionSpikeSpec(\n project: VideoProject,\n mix: { uri: string; durationUs: number },\n resolve: (source: { kind: string; uri?: string }) => string | null,\n): { spec: CompositionSpikeSpec; unsupported: string[] } {\n const playlist = playlistOf(project)\n const items: CompositionSpikeItem[] = []\n const unsupported = playlist.unsupported.map((u) => `${u.clipId}: ${u.reason}`)\n for (const item of playlist.items) {\n const uri = resolve(item.source as unknown as { kind: string; uri?: string })\n if (uri === null) {\n unsupported.push(`${item.clipId}: unresolved`)\n continue\n }\n items.push({\n uri,\n startUs: Number(item.startUs),\n endUs: Number(item.endUs),\n speed: item.speed,\n })\n }\n return {\n spec: {\n items,\n mixUri: mix.uri,\n mixDurationUs: mix.durationUs,\n width: project.size.width,\n height: project.size.height,\n },\n unsupported,\n }\n}\n\nfunction call(fn: () => string, label: string): Record<string, unknown> {\n try {\n return JSON.parse(fn()) as Record<string, unknown>\n } catch (error) {\n return { ok: false, error: `${label}: ${String(error)}` }\n }\n}\n\n/** Claim the next request id, before preparing. @experimental */\nexport function spikeClaim(): number {\n const result = call(() => requireNative().spikeClaim(), 'spikeClaim')\n return typeof result.requestId === 'number' ? result.requestId : 0\n}\n\n/** @experimental */\nexport function spikeStart(spec: CompositionSpikeSpec): Record<string, unknown> {\n return call(() => requireNative().spikeStart(JSON.stringify(spec)), 'spikeStart')\n}\n\n/** @experimental */\nexport function spikePlay(): Record<string, unknown> {\n return call(() => requireNative().spikePlay(), 'spikePlay')\n}\n\n/** @experimental */\nexport function spikePause(): Record<string, unknown> {\n return call(() => requireNative().spikePause(), 'spikePause')\n}\n\n/** @experimental */\nexport function spikeSeek(positionUs: number): Record<string, unknown> {\n return call(() => requireNative().spikeSeek(positionUs), 'spikeSeek')\n}\n\n/** @experimental */\nexport function spikeState(): CompositionSpikeState {\n return call(() => requireNative().spikeState(), 'spikeState') as unknown as CompositionSpikeState\n}\n\n/**\n * One retained sample, unpacked.\n *\n * Packed across the boundary because a gate reads thousands of these, and named\n * here because a gate that indexes into an array of eighteen longs is one\n * transposition away from a confident wrong answer.\n *\n * @experimental\n */\nexport interface SpikeSample {\n seq: number\n atNs: number\n positionUs: number\n playing: boolean\n ready: boolean\n epoch: number\n sinceEpochNs: number\n frameId: number\n frameGeneration: number\n frameWindow: number\n framePtsUs: number\n /**\n * When Media3's renderer said this frame was about to go to the surface.\n *\n * The earliest boundary observable here, and the one that separates the\n * owners of a gap: nothing before this is the decoder or the item's\n * preparation, this to {@link SpikeSample.frameAvailableNs} is the video\n * graph, and that to {@link SpikeSample.frameConsumedNs} is this consumer.\n */\n frameMetadataNs: number\n /** The time Media3 *asked* the surface to present at. An intention. */\n frameReleaseNs: number\n /** The time the consumer latched it. The last boundary observable here. */\n frameConsumedNs: number\n frameAvailableNs: number\n coalesced: number\n offered: number\n available: number\n taken: number\n readSpanNs: number\n}\n\n/** @experimental */\nexport function spikeDrain(fromSeq: number, limit = 400): {\n ok: boolean\n rows: SpikeSample[]\n nextSeq: number\n total: number\n dropped: number\n} {\n const page = call(() => requireNative().spikeDrain(fromSeq, limit), 'spikeDrain') as {\n ok?: boolean\n rows?: number[][]\n nextSeq?: number\n total?: number\n dropped?: number\n }\n return {\n ok: page.ok === true,\n rows: (page.rows ?? []).map((r) => ({\n seq: r[0]!,\n atNs: r[1]!,\n positionUs: r[2]!,\n playing: (r[3]! & 1) === 1,\n ready: (r[3]! & 2) === 2,\n epoch: r[4]!,\n sinceEpochNs: r[5]!,\n frameId: r[6]!,\n frameGeneration: r[7]!,\n frameWindow: r[8]!,\n framePtsUs: r[9]!,\n frameReleaseNs: r[10]!,\n frameConsumedNs: r[11]!,\n frameAvailableNs: r[12]!,\n coalesced: r[13]!,\n offered: r[14]!,\n available: r[15]!,\n taken: r[16]!,\n readSpanNs: r[17]!,\n frameMetadataNs: r[18] ?? -1,\n })),\n nextSeq: page.nextSeq ?? fromSeq,\n total: page.total ?? 0,\n dropped: page.dropped ?? 0,\n }\n}\n\n/** @experimental */\nexport function spikeRelease(): Record<string, unknown> {\n return call(() => requireNative().spikeRelease(), 'spikeRelease')\n}\n","/**\n * Who owns playback, and until when.\n *\n * Media3's player is a **process-wide singleton**. Nothing about it ends when\n * the screen that started it goes away, so on a Pixel 7 leaving the editor while\n * playing left the audio running, and backgrounding the app did the same: the\n * picture stopped because its surface was retired, and the sound carried on\n * because nobody had told the player anything.\n *\n * A singleton with no owner is the whole problem, so this gives it one. A screen\n * takes ownership when it mounts and gives it back exactly once — and a screen\n * that mounts afterwards starts from nothing rather than inheriting whatever the\n * last one was doing.\n *\n * ## Two kinds of stopping, deliberately different\n *\n * **Suspend** is temporary: the app went to the background, or the picker came\n * up over the editor. Playback pauses and the player is kept, because the screen\n * is coming back and rebuilding a player it never lost would be a stall the user\n * did nothing to earn.\n *\n * **Teardown** is final: the screen unmounted, or navigation left it. The player\n * is paused *and then* released, in that order, so nothing is audible in the\n * gap. After a teardown this object does nothing at all — a late suspend from a\n * listener that has not been removed yet must not resurrect anything.\n *\n * Neither is a reason to touch the document. Playback stopping says nothing\n * about what the project is or where its playhead should be, and clearing either\n * would turn leaving a screen into an edit.\n *\n * ## Epochs\n *\n * Every stop bumps an epoch, and callbacks carry the epoch they were scheduled\n * under. A poll that fires after the screen backgrounded still runs — clearing\n * an interval is not instantaneous — and `accepts` is how it finds out that the\n * answer it is about to deliver is no longer wanted.\n *\n * @experimental\n */\n\n/** Why playback stopped. Recorded so a log can say which path ran. @experimental */\nexport type PlaybackStopReason = 'unmount' | 'background' | 'blur'\n\n/** @experimental */\nexport interface PlaybackLifecycleEffects {\n /** Stop the sound. Must be safe to call when already paused. */\n pause(): void\n /** Give up the native player, and with it audio focus. */\n release(): void\n}\n\n/** @experimental */\nexport interface PlaybackLifecycle {\n /** Whether this screen currently owns running playback. */\n readonly active: boolean\n /** Whether the player has been given back. Final. */\n readonly released: boolean\n /** Why it last stopped, or `null`. */\n readonly reason: PlaybackStopReason | null\n /** Bumped by every stop; callbacks carry the value they were scheduled under. */\n readonly epoch: number\n /** Whether work scheduled under `epoch` may still act. */\n accepts(epoch: number): boolean\n /** Pause and keep the player. Idempotent within one suspension. */\n suspend(reason: PlaybackStopReason): void\n /** Become active again. Idempotent; ignored after teardown. */\n resume(): void\n /** Pause, then release, exactly once, forever. */\n teardown(reason: PlaybackStopReason): void\n}\n\n/** @experimental */\nexport function createPlaybackLifecycle(\n effects: PlaybackLifecycleEffects,\n): PlaybackLifecycle {\n let active = true\n let released = false\n let reason: PlaybackStopReason | null = null\n let epoch = 1\n\n return {\n get active() {\n return active\n },\n get released() {\n return released\n },\n get reason() {\n return reason\n },\n get epoch() {\n return epoch\n },\n\n accepts(at) {\n // Never after teardown, whatever epoch is offered: the player is gone and\n // there is nothing a late callback could correctly do with it.\n if (released) return false\n return active && at === epoch\n },\n\n suspend(why) {\n if (released || !active) return\n active = false\n reason = why\n epoch += 1\n effects.pause()\n },\n\n resume() {\n if (released || active) return\n active = true\n reason = null\n epoch += 1\n },\n\n teardown(why) {\n if (released) return\n const wasActive = active\n released = true\n active = false\n reason = why\n epoch += 1\n /*\n Pause first, then release — and only if it was still running.\n\n Releasing a playing player is the one ordering that can be heard: the\n release is not instantaneous, and the gap between \"still playing\" and\n \"gone\" is exactly the audio that outlived the screen. A screen torn down\n while already suspended was paused on the way in, so pausing it again\n would be a native call that says nothing.\n */\n if (wasActive) effects.pause()\n effects.release()\n },\n }\n}\n","import {\n playIntent,\n playableDuration,\n playlistOf,\n projectDuration,\n projectTimeToWindow,\n windowToProjectTime,\n type PlayIntent,\n type PlaylistItem,\n type UnsupportedSource,\n type VideoProject,\n} from '@pictor-sdk/video-core'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { AppState } from 'react-native'\nimport { Media3, type Media3State } from './media3'\nimport { createPlaybackLifecycle, type PlaybackLifecycle } from './playbackLifecycle'\nimport {\n classifyStall,\n shouldRecover,\n type RecoverySample,\n type StallKind,\n} from './surfaceRecovery'\nimport {\n NO_SURFACE,\n afterPrepare,\n idlePlaylistSession,\n shouldPrepare,\n} from './playlistSession'\n\n/**\n * Keep a Media3 playlist in step with a project, and the playhead with playback.\n *\n * Two directions, deliberately separated:\n *\n * - **Project → player.** When the clips, their order or their trims change,\n * the playlist is replaced. The current *project time* is preserved across\n * that, not the window index — deleting the second of four clips must not\n * leave the playhead in whatever the second window now happens to be.\n * - **Player → playhead.** While playing, the player's position is polled at a\n * readable rate and mapped back to project time. This is a **throttled\n * semantic update**, not a per-frame one: the picture is composed on the UI\n * runtime from the external texture and never waits for React.\n *\n * The playlist is only replaced when it genuinely differs. A trim drag\n * dispatches a command per finger sample, and rebuilding the playlist on each\n * would restart decoding many times a second.\n *\n * @experimental\n */\n\n/** @experimental */\nexport interface UseMedia3PlaybackOptions {\n project: VideoProject\n /** Timeline time the host wants shown. */\n playheadUs: number\n /** Called with the project time as playback advances. Throttled. */\n onPositionUs?: (timeUs: number) => void\n /** How often to poll the player, in milliseconds. */\n pollMs?: number\n /**\n * The live presentation surface generation, from `useVideoSurface`.\n *\n * Not a boolean. A playlist prepared against a retired surface renders into\n * something nobody is watching — the player decodes, releases frames and\n * reports no error — so the question is *which* surface, not whether one\n * exists. Every change is a reason to reconcile, including the change to 0.\n */\n surfaceGeneration?: number\n enabled?: boolean\n}\n\n/** @experimental */\nexport interface Media3Playback {\n playing: boolean\n /**\n * Start or resume playback, replaying from zero when there is nothing left.\n *\n * Returns what it decided, so a host can show it or count it. See\n * `playIntent` in `@pictor-sdk/video-core` for why the end is a special case.\n */\n play(): PlayIntent\n pause(): void\n setRate(rate: number): void\n /** Seek in project time. Mapped to the player's own coordinates. */\n seek(timeUs: number): void\n state: Media3State | null\n /** The windows currently loaded, for diagnostics and gates. */\n items: readonly PlaylistItem[]\n /**\n * Clips the player cannot open. Non-empty means playback is **not** loaded.\n *\n * Surfaced rather than swallowed: the alternative is an editor that plays a\n * shorter timeline than it draws, which reads as a synchronisation bug.\n */\n unsupported: readonly UnsupportedSource[]\n /**\n * Which half of the pipeline stalled after a surface restoration, or `null`.\n *\n * Non-null means a black picture was detected and the handshake re-run — a\n * host should surface it rather than show a silent audio-only result.\n */\n stall: StallKind | null\n /**\n * This screen's ownership of the shared player, for diagnostics and gates.\n *\n * Exposed so a host can log which path stopped playback and assert that a\n * screen released exactly once.\n */\n lifecycle: PlaybackLifecycle\n /**\n * How far the timeline and the player disagree, in microseconds. Zero when\n * everything is playable, and asserted by the device gates.\n */\n divergenceUs: number\n /**\n * The player's position in project time, read now, without rendering.\n *\n * For a host driving a UI-thread playhead: it samples this a few times a\n * second and advances between samples itself. Returns `null` when the player\n * has no position to give — before a playlist is loaded, or after release.\n */\n positionUs(): number | null\n /**\n * The rate actually in force: clip speed and preview rate, composed and\n * clamped by the adapter. A host showing a rate should show this one.\n */\n effectiveRate: number\n /**\n * The last seek this hook actually issued.\n *\n * Global time in, and the `{windowIndex, localUs}` it was converted to. A\n * player reporting an unexpected position is either being sent the wrong\n * thing or mishandling the right thing, and those need different fixes — this\n * is what tells them apart instead of leaving it to arithmetic on a\n * screenshot.\n */\n lastSeek: { globalUs: number; windowIndex: number; localUs: number } | null\n}\n\nconst DEFAULT_POLL_MS = 100\n\n/** How often a refused preparation is retried, in milliseconds. */\nconst RETRY_INTERVAL_MS = 250\n\n/** @experimental */\nexport function useMedia3Playback(options: UseMedia3PlaybackOptions): Media3Playback {\n const { project, playheadUs, enabled = true } = options\n const surfaceGeneration = options.surfaceGeneration ?? NO_SURFACE\n const pollMs = options.pollMs ?? DEFAULT_POLL_MS\n const [state, setState] = useState<Media3State | null>(null)\n const [playing, setPlaying] = useState(false)\n\n const playlist = useMemo(() => playlistOf(project), [project])\n const items = playlist.items\n const divergenceUs = useMemo(\n () => projectDuration(project) - playableDuration(playlist),\n [project, playlist],\n )\n\n /**\n * A stable description of the playlist.\n *\n * Compared as a string rather than by reference: every edit produces a new\n * project object, but most edits — a selection, a playhead move, an opacity\n * change — do not change what the player should be playing.\n */\n const signature = useMemo(\n () =>\n items\n .map((i) => `${JSON.stringify(i.source)}|${i.startUs}|${i.endUs}|${i.speed}|${i.muted}`)\n .join(';') +\n // Unsupported clips belong in the signature even though they are not\n // windows. Adding a photo between two videos leaves the video windows\n // identical, so a signature built from items alone would not change and\n // the player would keep playing a timeline that had grown underneath it —\n // the exact divergence this is all meant to prevent.\n `!${playlist.unsupported.map((u) => `${u.clipId}:${u.reason}`).join(',')}`,\n [items, playlist.unsupported],\n )\n\n const playheadRef = useRef(playheadUs)\n playheadRef.current = playheadUs\n const itemsRef = useRef(items)\n itemsRef.current = items\n\n const session = useRef(idlePlaylistSession())\n /** Bumped by the retry timer, purely to run the reconciliation again. */\n const [retry, setRetry] = useState(0)\n\n /**\n * Reconcile what Media3 holds with what the document and the surface imply.\n *\n * Level-triggered. The old version fired on the playlist *changing*, which is\n * the one moment it must not rely on: clips are added while the system picker\n * is closing and there is no surface to prepare against, so the change has\n * already happened by the time preparation becomes possible. See\n * `playlistSession.ts`.\n */\n useEffect(() => {\n if (!enabled) return\n const intent = { signature, generation: surfaceGeneration }\n if (!shouldPrepare(session.current, intent)) return\n\n let ok: boolean\n try {\n if (playlist.unsupported.length > 0 || items.length === 0) {\n // Deliberately not loaded. The host is told why through `unsupported`,\n // and the timeline keeps drawing — what it must not do is play\n // something shorter than what it shows and call that working.\n ok = Media3.setPlaylist({ items: [], unsupported: [] }) !== false\n } else {\n // Preserve project time, not window index. The window that held the\n // playhead may not be the same window — or may not exist — after the\n // edit.\n const start = projectTimeToWindow(items, playheadRef.current) ?? {\n windowIndex: 0,\n positionUs: 0,\n }\n ok = Media3.setPlaylist(playlist, start) !== false\n }\n } catch {\n // A refusal, not a crash. Recording it as applied is what left an editor\n // waiting for an edit the user had already made.\n ok = false\n }\n session.current = afterPrepare(session.current, intent, ok)\n }, [signature, items, playlist, enabled, surfaceGeneration, retry])\n\n /**\n * Retry a refused preparation without waiting for anything else to move.\n *\n * A surface can arrive without its generation changing again, and a user who\n * has added their clips will not add them a second time to prod it.\n */\n useEffect(() => {\n if (!enabled) return\n if (!session.current.pending) return\n const id = setInterval(() => setRetry((n) => n + 1), RETRY_INTERVAL_MS)\n return () => clearInterval(id)\n }, [enabled, retry, surfaceGeneration, signature])\n\n /*\n This screen's ownership of the process-wide player.\n\n Media3's player outlives any screen, so leaving the editor while playing\n left the audio running and backgrounding the app did the same — the picture\n stopped because its surface was retired, and nothing had told the player.\n One lifecycle per hook instance: a screen mounted later starts from nothing\n rather than inheriting what the last one was doing.\n */\n const stallRef = useRef<StallKind | null>(null)\n const surfaceGenerationRef = useRef(surfaceGeneration ?? 0)\n surfaceGenerationRef.current = surfaceGeneration ?? 0\n const lifecycleRef = useRef<PlaybackLifecycle | null>(null)\n if (lifecycleRef.current === null) {\n lifecycleRef.current = createPlaybackLifecycle({\n pause: () => Media3.pause(),\n release: () => Media3.release(),\n })\n }\n const lifecycle = lifecycleRef.current\n /*\n The epoch, made visible to React.\n\n The polls capture an epoch when they start and compare on every tick, so\n resuming has to restart them under the new one — otherwise a screen that\n came back from the background would poll forever under an epoch that no\n longer matches and never report a position again.\n */\n const [lifecycleEpoch, setLifecycleEpoch] = useState(lifecycle.epoch)\n\n /*\n Backgrounding pauses; returning re-arms.\n\n Suspend rather than teardown: the picker comes up over the editor as a\n background transition, and rebuilding a player the screen never lost would\n be a stall nobody asked for. The document is not touched either way —\n playback stopping says nothing about where the playhead should be.\n */\n useEffect(() => {\n const subscription = AppState.addEventListener('change', (next) => {\n if (next === 'active') {\n lifecycle.resume()\n setLifecycleEpoch(lifecycle.epoch)\n return\n }\n lifecycle.suspend('background')\n setLifecycleEpoch(lifecycle.epoch)\n setPlaying(false)\n })\n return () => subscription.remove()\n }, [lifecycle])\n\n /*\n The screen going is the end of its playback, exactly once.\n\n Last of the effects declared here, so it is the first to be cleaned up: a\n teardown that ran after the polls were cleared would release a player the\n polls were still reading.\n */\n useEffect(() => {\n return () => {\n lifecycle.teardown('unmount')\n }\n }, [lifecycle])\n\n /*\n The watchdog for a surface that came back half-attached.\n\n The failure it exists for reports nothing: after a background or a picker\n the clock advances, audio plays, no error is raised and the picture is\n black. A sample is kept from the last time the pipeline looked healthy, and\n when the clock has moved for long enough without a frame being latched the\n handshake is run again. `classifyStall` names which half is broken, because\n re-preparing fixes a stale generation and does nothing for a consumer that\n has stopped pulling.\n */\n const healthy = useRef<RecoverySample | null>(null)\n const [stall, setStall] = useState<StallKind | null>(null)\n\n const lastMeaning = useRef('')\n\n useEffect(() => {\n if (!enabled) return\n // Captured now, checked on every tick: an interval is not cleared the\n // instant the screen goes, and a poll that lands afterwards must not write\n // a position into a screen that is no longer there.\n const epoch = lifecycle.epoch\n const id = setInterval(() => {\n if (!lifecycle.accepts(epoch)) return\n const snapshot = Media3.state()\n // Position is not part of this. Replacing the state object on every tick\n // re-renders the whole editor ten times a second to move a line, which is\n // both wasteful and — because it lands at ten hertz — visibly steppy. The\n // meaning of the state changes far less often than its numbers do, so\n // only a change in meaning is worth a render, and a host that wants the\n // position drives it from `positionUs()` on the UI thread.\n const meaning = [\n snapshot.playing,\n snapshot.ready,\n snapshot.buffering,\n snapshot.ended,\n snapshot.released,\n snapshot.width,\n snapshot.height,\n snapshot.windowIndex,\n snapshot.windowCount,\n snapshot.errorCount,\n snapshot.errorMessage,\n snapshot.rateConstrained,\n ].join('|')\n if (meaning !== lastMeaning.current) {\n lastMeaning.current = meaning\n setState(snapshot)\n setPlaying(snapshot.playing)\n }\n const sample: RecoverySample = {\n atMs: Date.now(),\n positionUs: snapshot.windowPositionUs,\n framesOffered: snapshot.framesOffered,\n framesAvailable: snapshot.framesAvailable,\n // Latched is not reported directly: what arrived, less what is still\n // waiting, is the count the consumer has actually taken.\n framesLatched: snapshot.framesAvailable - snapshot.pendingDepth,\n framesRejected: snapshot.framesRejected,\n pendingGeneration: snapshot.pendingGeneration,\n consumerGeneration: surfaceGenerationRef.current,\n }\n const since = healthy.current\n if (since === null) {\n healthy.current = sample\n } else if (classifyStall(since, sample) === null) {\n // Healthy, or simply not running: either way this is the new baseline.\n healthy.current = sample\n if (stallRef.current !== null) setStall(null)\n } else if (shouldRecover(since, sample)) {\n const kind = classifyStall(since, sample)\n healthy.current = sample\n stallRef.current = kind\n setStall(kind)\n // Re-run the surface/playlist handshake. Never silently: a plausible\n // audio-only result is the thing this refuses to leave alone.\n session.current = { ...session.current, pending: true }\n setRetry((n) => n + 1)\n }\n\n if (!snapshot.playing) return\n const projectUs = windowToProjectTime(\n itemsRef.current,\n snapshot.windowIndex,\n snapshot.windowPositionUs,\n )\n if (projectUs !== null) options.onPositionUs?.(projectUs)\n }, pollMs)\n return () => clearInterval(id)\n // `onPositionUs` is read through the closure on each tick; depending on it\n // would restart the interval whenever the host re-created the callback.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [enabled, pollMs, lifecycle, lifecycleEpoch])\n\n const lastSeekRef = useRef<Media3Playback['lastSeek']>(null)\n\n /**\n * Seek in **global** timeline time.\n *\n * The store's playhead is global and stays global; the conversion to a window\n * and a position inside it happens here, once, through `projectTimeToWindow` —\n * the same mapping `play` and the playlist load use. Handing a player a global\n * time as though it were window-local is silent: it clamps to the end of\n * whichever clip it is in and reports a perfectly plausible position.\n */\n const seek = useCallback((timeUs: number) => {\n const target = projectTimeToWindow(itemsRef.current, timeUs)\n if (!target) return\n lastSeekRef.current = {\n globalUs: Math.round(timeUs),\n windowIndex: target.windowIndex,\n localUs: target.positionUs,\n }\n Media3.seekToWindow(target.windowIndex, target.positionUs)\n }, [])\n\n const positionUs = useCallback((): number | null => {\n const snapshot = Media3.state()\n if (snapshot.released || itemsRef.current.length === 0) return null\n return windowToProjectTime(\n itemsRef.current,\n snapshot.windowIndex,\n snapshot.windowPositionUs,\n )\n }, [])\n\n const durationUsRef = useRef(0)\n durationUsRef.current = projectDuration(project)\n\n /**\n * Play, replaying from the start if there is nothing left to play.\n *\n * The decision is `playIntent` in `@pictor-sdk/video-core`, not an `if` here: it\n * is the same question a host's own transport has to answer, and two answers\n * would eventually disagree. Read fresh from the player rather than from\n * `state`, which only re-renders on a change of meaning and can be a poll\n * behind at the moment of the press.\n */\n const play = useCallback(() => {\n const snapshot = Media3.state()\n const intent = playIntent({\n positionUs:\n windowToProjectTime(\n itemsRef.current,\n snapshot.windowIndex,\n snapshot.windowPositionUs,\n ) ?? 0,\n durationUs: durationUsRef.current,\n playbackState: snapshot.playbackState,\n ended: snapshot.ended,\n })\n if (intent.seekToUs !== null) {\n const target = projectTimeToWindow(itemsRef.current, intent.seekToUs)\n if (target) Media3.seekToWindow(target.windowIndex, target.positionUs)\n }\n if (!intent.play) return intent\n // Refused once the screen has given the player back: a late press from a\n // control that has not unmounted yet must not start a released player.\n if (lifecycle.released) return intent\n Media3.play()\n setPlaying(true)\n return intent\n }, [lifecycle])\n\n return {\n playing,\n play,\n get lastSeek() {\n return lastSeekRef.current\n },\n positionUs,\n effectiveRate: state?.effectiveRate ?? 1,\n pause: useCallback(() => {\n Media3.pause()\n setPlaying(false)\n }, []),\n setRate: useCallback((rate: number) => Media3.setRate(rate), []),\n seek,\n state,\n items,\n unsupported: playlist.unsupported,\n divergenceUs,\n lifecycle,\n stall,\n }\n}\n","/**\n * Deciding that a restored surface is not actually showing anything.\n *\n * After a background, a picker, or any other trip away from the editor, the\n * player and the surface have to find each other again. When that handshake\n * half-succeeds the result is the worst kind of failure: audio plays, the clock\n * advances, no error is reported, and the picture is black. Nothing in the\n * player's own state says anything is wrong, because from its point of view\n * nothing is.\n *\n * So it is decided from the counters instead, and the counters are already\n * separated into the four things that can go wrong — see `Media3State`, where\n * `framesOffered` is the decoder saying a frame is coming and `framesAvailable`\n * is the surface saying one arrived.\n *\n * The point of naming the branch is that the repairs differ. Re-running the\n * handshake fixes a stale generation and does nothing at all for a consumer\n * that has stopped pulling, and \"retry everything\" would have made both look\n * like the same intermittent bug.\n *\n * @experimental\n */\n\n/** How long a stall must persist before it counts. @experimental */\nexport const RECOVERY_GRACE_MS = 1500\n\n/** What a sample of the pipeline looked like at one instant. @experimental */\nexport interface RecoverySample {\n atMs: number\n /** The player's own clock. Advancing means audio is being produced. */\n positionUs: number\n /** Media3 announcing frames. */\n framesOffered: number\n /** The SurfaceTexture receiving them. */\n framesAvailable: number\n /** Frames the consumer actually took. */\n framesLatched: number\n /** Announcements for a generation already retired. */\n framesRejected: number\n /** The generation the producer is filling. */\n pendingGeneration: number\n /** The generation the consumer is watching. */\n consumerGeneration: number\n}\n\n/**\n * Which half of the pipeline is broken, or `null` when it is healthy.\n *\n * @experimental\n */\nexport type StallKind =\n /** Frames arrive and nobody takes them: the consumer or its frame loop. */\n | 'consumer'\n /** Producer and consumer disagree about which surface: a stale handshake. */\n | 'handshake'\n /** The player is running but nothing reaches the surface: attachment. */\n | 'attachment'\n /** Frames are latched and the picture is still wrong: scene or image. */\n | 'resource'\n\n/**\n * Name the branch from two samples.\n *\n * `null` means nothing is wrong *that this can see* — including the ordinary\n * case of a player that is simply not advancing, which is what a paused editor\n * looks like and is not a fault.\n *\n * @experimental\n */\nexport function classifyStall(since: RecoverySample, now: RecoverySample): StallKind | null {\n // A clock that is not moving is not a stall. Nothing is expected of a player\n // that is paused, ended or buffering, and calling that a fault would make\n // every pause look like a broken surface.\n if (now.positionUs <= since.positionUs) return null\n\n // Checked before anything else: when the two sides disagree about the\n // generation, every other counter is describing a surface nobody is watching.\n if (\n now.pendingGeneration !== now.consumerGeneration ||\n now.framesRejected > since.framesRejected\n ) {\n return 'handshake'\n }\n\n const latched = now.framesLatched > since.framesLatched\n if (latched) {\n // The pipeline is delivering. Whatever is wrong is further along — the\n // scene, the image, the matrix — and re-running the handshake would only\n // interrupt something that is working.\n return 'resource'\n }\n\n const arrived = now.framesAvailable > since.framesAvailable\n if (arrived) return 'consumer'\n\n // The decoder may or may not be announcing; either way nothing reached the\n // surface, so the player is not attached to the one being watched.\n return 'attachment'\n}\n\n/**\n * Whether the stall has lasted long enough to act on.\n *\n * A grace period because none of this is instantaneous: a surface is recreated,\n * a playlist is reconciled and the first frame decoded, and a check that fired\n * inside that window would recover a pipeline that was about to work.\n *\n * @experimental\n */\nexport function shouldRecover(\n since: RecoverySample,\n now: RecoverySample,\n graceMs: number = RECOVERY_GRACE_MS,\n): boolean {\n if (now.atMs - since.atMs < graceMs) return false\n const kind = classifyStall(since, now)\n // `resource` is deliberately excluded: frames are being latched, so the\n // handshake this recovers is not what is wrong, and retrying it would blank a\n // working picture on a timer.\n return kind === 'consumer' || kind === 'handshake' || kind === 'attachment'\n}\n","import {\n createThumbnailPolicy,\n thumbnailKey,\n type MediaSource,\n type AbortLike,\n type Thumbnail,\n type ThumbnailProvider,\n type ThumbnailRequest,\n} from '@pictor-sdk/video-core'\nimport { requireNative } from './player'\n\n/**\n * `ThumbnailProvider` over Media3's retriever, with the core policy on top.\n *\n * The division is the point: `@pictor-sdk/video-core` owns deduplication, the\n * bounded cache and visible-range ordering, all of which are pure; this file\n * owns the part that needs a codec. A host that writes its own provider — for a\n * remote thumbnail service, say — reuses the policy and replaces only this.\n *\n * Results arrive as base64 JPEG rather than as a native image handle. A strip\n * thumbnail is a few kilobytes and it is drawn by React Native's own image\n * pipeline, so there is nothing here to leak and no ownership to transfer. The\n * `AHardwareBuffer` lesson applies to full-resolution frames at 30 per second,\n * not to a 64-pixel square that changes when the user scrolls.\n *\n * @experimental\n */\n\n/** @experimental */\nexport interface AndroidThumbnailOptions {\n /**\n * How many decoded thumbnails to keep.\n *\n * 240 covers several screens of a dense strip at 64 px — enough that\n * scrolling back does not re-decode, bounded enough that a long project\n * cannot grow without limit.\n */\n capacity?: number\n /** How often to check whether a decode has finished, in milliseconds. */\n pollMs?: number\n}\n\nconst DEFAULT_CAPACITY = 240\nconst DEFAULT_POLL_MS = 16\n\n/** @experimental */\nexport function createAndroidThumbnailProvider(\n options: AndroidThumbnailOptions = {},\n): ThumbnailProvider {\n const policy = createThumbnailPolicy<string>({\n capacity: options.capacity ?? DEFAULT_CAPACITY,\n })\n const pollMs = options.pollMs ?? DEFAULT_POLL_MS\n /** Tokens in flight, by request key, so a cancel can reach the native side. */\n const tokens = new Map<string, number>()\n\n function uriOf(source: MediaSource): string {\n if (typeof source === 'string') return source\n if (source && typeof source === 'object' && 'uri' in source) {\n const uri = (source as { uri?: unknown }).uri\n if (typeof uri === 'string') return uri\n }\n throw new Error(\n `@pictor-sdk/video-android: a thumbnail source is not a URI. Normalise host media to { kind: 'uri', uri } first. Received: ${JSON.stringify(source)}`,\n )\n }\n\n return {\n async request(request: ThumbnailRequest, signal?: AbortLike): Promise<Thumbnail | null> {\n const key = thumbnailKey(request)\n const cached = policy.get(request)\n if (cached !== undefined) {\n return { image: cached, sourceTimeUs: request.sourceTimeUs, size: { width: 0, height: 0 } }\n }\n if (signal?.aborted) return null\n\n const native = requireNative()\n policy.begin(request)\n const token = native.thumbnailRequest(\n uriOf(request.source),\n request.sourceTimeUs,\n request.widthPx,\n )\n tokens.set(key, token)\n\n const stop = () => {\n native.thumbnailCancel(token)\n tokens.delete(key)\n policy.cancel(request)\n }\n signal?.addEventListener('abort', stop, { once: true })\n\n try {\n // Poll rather than await a bridged Promise: the native side is a token\n // API precisely so a request can be abandoned, and a Promise that\n // cannot be cancelled would decode frames nobody will see.\n for (;;) {\n if (signal?.aborted) return null\n const data = native.thumbnailTake(token)\n if (data !== null && data !== undefined) {\n tokens.delete(key)\n const evicted = policy.store(request, `data:image/jpeg;base64,${data}`)\n // Base64 strings are garbage-collected; eviction is bookkeeping\n // rather than disposal, and the returned values are ignored on\n // purpose. A provider handing back native images would dispose here.\n void evicted\n return {\n image: policy.get(request)!,\n sourceTimeUs: request.sourceTimeUs,\n size: { width: request.widthPx, height: 0 },\n }\n }\n const error = native.thumbnailError(token)\n if (error !== null && error !== undefined) {\n tokens.delete(key)\n policy.cancel(request)\n return null\n }\n if (!native.thumbnailRunning(token)) {\n // Neither a result nor an error, and not running: the frame could\n // not be produced. A source can legitimately have no frame at a\n // time — past the end of a variable-rate clip, for instance.\n tokens.delete(key)\n policy.cancel(request)\n return null\n }\n await new Promise<void>((resolve) => setTimeout(() => resolve(), pollMs))\n }\n } finally {\n signal?.removeEventListener('abort', stop)\n }\n },\n\n releaseSource(source: MediaSource) {\n policy.evictSource(source)\n requireNative().thumbnailReleaseSource(uriOf(source))\n },\n\n release() {\n for (const token of tokens.values()) requireNative().thumbnailCancel(token)\n tokens.clear()\n policy.clear()\n requireNative().thumbnailReleaseAll()\n },\n }\n}\n\n/** Decode counters, for gates and diagnostics. @experimental */\nexport function thumbnailCounters(): {\n decoded: number\n cancelled: number\n failed: number\n running: number\n retrievers: number\n} {\n return requireNative().thumbnailCounters() as {\n decoded: number\n cancelled: number\n failed: number\n running: number\n retrievers: number\n }\n}\n","/**\n * Deciding when playback has settled, instead of assuming it after a timeout.\n *\n * The first attempt excluded a fixed 1.5 s after every transition. That is a\n * guess in both directions: too short for a seek, whose tail leaked into the\n * steady figures, and too long for a rate change that settles immediately. It\n * also hides the thing worth knowing — how long recovery actually took.\n *\n * So a transition opens a phase, and the phase closes when the offsets say so:\n * [CONVERGED_RUN] consecutive samples inside the expected bound. The recovery\n * duration and the worst offset seen on the way are kept, because a transient\n * that is excluded but not reported is a failure nobody can see.\n *\n * [MAX_RECOVERY_SAMPLES] is a safety net, not a policy. A phase that never\n * converges is closed anyway and marked, so a permanent failure surfaces as a\n * failure instead of an ever-growing exclusion window.\n *\n * Pure, so the rule is tested rather than trusted.\n *\n * @experimental\n */\n\n/** What kind of transition opened a phase. Steady is the absence of one. */\nexport type Phase =\n | 'steady'\n | 'startup'\n | 'seek'\n | 'source'\n | 'pauseResume'\n | 'rateChange'\n\n/**\n * Consecutive in-bound samples that end a transient.\n *\n * Eight at 30 fps is about a quarter second of agreement — long enough that a\n * single lucky frame cannot close a phase, short enough to measure recovery\n * rather than round it up.\n */\nexport const CONVERGED_RUN = 8\n\n/** A phase is closed after this many samples whatever they say. */\nexport const MAX_RECOVERY_SAMPLES = 600\n\nexport interface PhaseState {\n phase: Phase\n /** Offsets collected while this phase was open. */\n samples: number[]\n /** Consecutive in-bound samples so far. */\n run: number\n /** Worst absolute offset seen during the transient, in microseconds. */\n worstUs: number\n /** Samples taken before it converged. */\n recoverySamples: number\n /** True once converged; further samples belong to steady state. */\n converged: boolean\n /** True when the safety net closed it instead of convergence. */\n timedOut: boolean\n}\n\nexport function beginPhase(phase: Phase): PhaseState {\n return {\n phase,\n samples: [],\n run: 0,\n worstUs: 0,\n recoverySamples: 0,\n converged: phase === 'steady',\n timedOut: false,\n }\n}\n\n/**\n * Feed one offset. Returns the updated phase and where the sample belongs.\n *\n * A sample taken while a transient is open belongs to that transient, never to\n * steady state — merging them would report the transitions as if they were\n * playback.\n */\nexport function observe(\n state: PhaseState,\n offsetUs: number,\n boundUs: number,\n): { state: PhaseState; bucket: Phase } {\n if (state.converged) {\n return { state, bucket: 'steady' }\n }\n\n const magnitude = Math.abs(offsetUs)\n const next: PhaseState = {\n ...state,\n samples: [...state.samples, offsetUs],\n recoverySamples: state.recoverySamples + 1,\n worstUs: Math.max(state.worstUs, magnitude),\n run: magnitude <= boundUs ? state.run + 1 : 0,\n }\n\n if (next.run >= CONVERGED_RUN) {\n next.converged = true\n } else if (next.recoverySamples >= MAX_RECOVERY_SAMPLES) {\n // Closed by the safety net. Marked, so it reads as a failure to converge\n // rather than quietly becoming steady state.\n next.converged = true\n next.timedOut = true\n }\n\n return { state: next, bucket: state.phase }\n}\n\n/** How long recovery took, in frames, or null while still recovering. */\nexport function recoveryFrames(state: PhaseState): number | null {\n return state.converged ? state.recoverySamples : null\n}\n","import { VideoExportCancelled } from '@pictor-sdk/video-core'\n\n/**\n * One export at a time, settled exactly once, cancellable anywhere.\n *\n * The experimental exporter blocked the bridge for a whole encode and threw\n * from `cancel()`. That was honest — a silent no-op behind a cancel button is\n * worse than a refusal — but it is not something a host can ship, so this is the\n * orchestration that makes cancellation real.\n *\n * ## Why cancellation cannot just reject the promise\n *\n * A Transformer nobody is listening to still encodes, still writes and still\n * holds its codecs. Rejecting in JavaScript would hand the host a cancelled\n * state over an export that is still running, still filling the cache and still\n * about to produce a file nobody is waiting for. So `cancel` reaches native\n * first and the promise settles from the job's own checkpoints.\n *\n * ## Checkpoints\n *\n * Cancellation is cooperative. The work is a sequence of stages, each of which\n * calls {@link ExportJob.checkpoint} on the way past, and the first checkpoint\n * after a cancel throws. That is why the stages are named: a cancellation\n * carries where it stopped, which is the difference between a log that says\n * \"cancelled\" and one that says which resources were live at the time.\n *\n * ## Settling once\n *\n * Every path — success, failure, cancellation — clears the active job, and\n * nothing may be delivered afterwards. A progress callback that arrives after a\n * host has shown a result is a UI that goes backwards from \"done\".\n *\n * @experimental\n */\n\n/** The lifecycle points an export can be stopped at. @experimental */\nexport const EXPORT_STAGES = [\n 'resolving',\n 'overlays',\n 'shards',\n 'cadence',\n 'audio',\n 'concatenating',\n 'finalising',\n 'publishing',\n] as const\n\n/** @experimental */\nexport type ExportStage = (typeof EXPORT_STAGES)[number]\n\n/** @experimental */\nexport interface ExportJob {\n readonly id: string\n /**\n * Give cancellation a chance to take effect.\n *\n * Throws {@link VideoExportCancelled} if the job has been cancelled. Called\n * between stages and inside long loops, because a cancel that can only be\n * observed when the work finishes is not a cancel.\n */\n checkpoint(stage: ExportStage): void\n /** Deliver progress, unless the job has settled. */\n progress(value: number, deliver: (value: number) => void): void\n}\n\n/** @experimental */\nexport interface ExportJobsOptions {\n /**\n * Stop the native side of a job.\n *\n * Given the job id so a late cancel cannot stop a *different* export that\n * started in the meantime.\n */\n cancelNative(jobId: string): void\n /** Injected in tests. */\n now?: () => number\n}\n\n/**\n * The job domain's ledger.\n *\n * Kept apart from the native run counters on purpose. One export job drives\n * several Transformer runs, so adding a job total to a run total produces a\n * number that reconciles with nothing — and reads like a reconciliation, which\n * is worse than reporting neither.\n *\n * @experimental\n */\nexport interface ExportJobCounts {\n /** Jobs that began. Refused starts are not accepted jobs. */\n accepted: number\n succeeded: number\n cancelled: number\n failed: number\n /** Exactly 0 or 1: one job at a time. */\n active: number\n}\n\n/** @experimental */\nexport interface ExportJobs {\n /** The running job's id, or `null`. */\n readonly active: string | null\n /**\n * `accepted === succeeded + cancelled + failed + active`, always.\n *\n * Terminal outcomes are mutually exclusive: a cancelled job is not also a\n * failed one, however it unwound.\n */\n readonly counts: ExportJobCounts\n /** Run `body` as the one active job. */\n run<T>(body: (job: ExportJob) => Promise<T>): Promise<T>\n /** Ask the running job to stop. Idempotent; a no-op when none is running. */\n cancel(): void\n}\n\nlet counter = 0\n\n/** @experimental */\nexport function createExportJobs(options: ExportJobsOptions): ExportJobs {\n const now = options.now ?? (() => Date.now())\n let active: string | null = null\n let cancelledAt: ExportStage | null = null\n let cancelRequested = false\n let settled = false\n let accepted = 0\n let succeeded = 0\n let cancelled = 0\n let failed = 0\n\n const clear = () => {\n active = null\n cancelRequested = false\n cancelledAt = null\n settled = true\n }\n\n return {\n get active() {\n return active\n },\n\n get counts() {\n return {\n accepted,\n succeeded,\n cancelled,\n failed,\n active: active === null ? 0 : 1,\n }\n },\n\n async run<T>(body: (job: ExportJob) => Promise<T>): Promise<T> {\n if (active !== null) {\n /*\n Refused, not queued.\n\n The native module runs one export per process. Interleaving two would\n have them share a Transformer, a muxer and a cache directory, and the\n first symptom would be a corrupt file rather than an error.\n */\n throw new Error(\n `createMedia3VideoExporter: an export is already running (${active}); ` +\n 'cancel it before starting another',\n )\n }\n\n counter += 1\n accepted += 1\n const id = `export_${now().toString(36)}_${counter.toString(36)}`\n active = id\n cancelRequested = false\n cancelledAt = null\n settled = false\n\n const job: ExportJob = {\n id,\n checkpoint(stage) {\n if (cancelRequested) {\n cancelledAt = stage\n throw new VideoExportCancelled(id, stage)\n }\n },\n progress(value, deliver) {\n if (settled || cancelRequested) return\n deliver(value)\n },\n }\n\n try {\n const result = await body(job)\n /*\n A cancel that arrived while the last stage was finishing still wins.\n\n The alternative is resolving with a file the user asked us not to\n make, which is the outcome a cancel button exists to prevent.\n */\n if (cancelRequested) {\n throw new VideoExportCancelled(id, cancelledAt ?? 'finalising')\n }\n clear()\n succeeded += 1\n return result\n } catch (error) {\n const wasCancelled = cancelRequested\n const stage = cancelledAt ?? 'finalising'\n clear()\n if (wasCancelled) cancelled += 1\n else failed += 1\n if (wasCancelled) {\n /*\n The cancellation is the outcome, whatever the body threw.\n\n Tearing down a Transformer mid-encode raises its own errors, and\n reporting one of those would tell a host the export failed when the\n user had simply stopped it.\n */\n if (error instanceof VideoExportCancelled) throw error\n throw new VideoExportCancelled(id, stage)\n }\n throw error\n }\n },\n\n cancel() {\n // No job, nothing to stop, nothing to say. Repeated presses reach native\n // once, because a second stop for the same job is not a second request.\n if (active === null || cancelRequested) return\n cancelRequested = true\n options.cancelNative(active)\n },\n }\n}\n","import type {\n AudioMixProgram,\n ExportSourceContext,\n MediaSource,\n VideoProject,\n} from '@pictor-sdk/video-core'\n\n/**\n * Turning a compiled mix program's spans into files to decode.\n *\n * Shared by the exporter and by preview preparation, deliberately. The two must\n * agree about which file a span means, or a preview would be mixed from one set\n * of sources and the export from another — and the difference would only be\n * audible in the finished video.\n *\n * @experimental\n */\n\n/**\n * What a span is, so a host can resolve it as what it actually is.\n *\n * A span carved from a clip's own audio resolves as that clip — the same\n * context the video pass used, so the same media resolves the same way. A span\n * from a music item resolves as that item. The id alone cannot say which, and a\n * host that guessed would resolve the first thing that matched.\n */\nexport function mixSpanContext(\n project: VideoProject,\n spanId: string,\n): ExportSourceContext | null {\n const isClip = project.clips.some((clip) => clip.id === spanId)\n const isAudio = project.audio.some((item) => item.id === spanId)\n // Ambiguous or absent are the same answer: there is no one owner to resolve\n // against, and picking either would be a guess.\n if (isClip === isAudio) return null\n return isClip ? { kind: 'clip', clipId: spanId } : { kind: 'audio', audioId: spanId }\n}\n\n/** @experimental */\nexport interface ResolvedMixSources {\n /** Span id to readable URI, for every span that needs decoding. */\n uris: Record<string, string>\n /** One sentence per span that could not be resolved. Empty means usable. */\n unresolved: string[]\n}\n\n/**\n * Resolve every span that needs decoding.\n *\n * Silent spans are skipped: they occupy their time and contribute no samples,\n * so asking a host to resolve one would be asking for a file that is not\n * needed — and a host that returned nothing would look like a failure.\n *\n * @experimental\n */\nexport function resolveMixSources(\n project: VideoProject,\n program: AudioMixProgram,\n resolveSource: (source: MediaSource, context: ExportSourceContext) => string | null | undefined,\n): ResolvedMixSources {\n const uris: Record<string, string> = {}\n const unresolved: string[] = []\n\n for (const span of program.spans) {\n if (span.silent || span.source === null) continue\n const context = mixSpanContext(project, span.id)\n if (context === null) {\n unresolved.push(\n `audio span '${span.id}' matches no clip or audio item, so it has no owner to resolve against`,\n )\n continue\n }\n const uri = resolveSource(span.source, context)\n if (typeof uri !== 'string' || uri.length === 0) {\n unresolved.push(`audio span '${span.id}' could not be resolved to a readable source`)\n continue\n }\n uris[span.id] = uri\n }\n\n return { uris, unresolved }\n}\n","import type { VideoExportDestination } from '@pictor-sdk/video-core'\n\n/**\n * Where the file goes, and what is refused before anything is encoded.\n *\n * ## Refusal, never fallback\n *\n * A destination that cannot be honoured fails the export naming the reason. The\n * tempting alternative — write to the cache and return that instead — produces\n * an export that reports success while the file is nowhere the caller asked for,\n * and nothing downstream can tell that apart from having worked. A host that\n * wants a temporary can ask for one.\n *\n * ## `file:` only, for now\n *\n * A `content:` uri is not a path. A document provider need not be a filesystem,\n * so there is no directory to stage in and no rename to be atomic about;\n * publishing would have to be a copy into an opaque stream, with no way to leave\n * the previous contents intact if it failed halfway. That is a different design,\n * not a missing branch, so it is refused by name.\n *\n * ## Paths do not appear in errors\n *\n * A message that quotes `/data/user/0/com.host.app/files/private/...` puts an\n * app-private path into a log, a crash report and whatever the host displays.\n * Every reason here names the file and nothing above it.\n *\n * @experimental\n */\n\n/** A destination this adapter can honour. @experimental */\nexport type ResolvedDestination =\n | { kind: 'temporary' }\n | { kind: 'file'; path: string; overwrite: boolean }\n\n/** @experimental */\nexport interface DestinationDecision {\n destination: ResolvedDestination | null\n /** Empty when `destination` is set. Never contains a full path. */\n reasons: string[]\n}\n\n/**\n * The last segment, for a message that has to name something.\n *\n * Directories are the private part. A filename the caller chose is already\n * theirs, and without it a refusal cannot say which destination it refused.\n */\nexport function describeTarget(path: string): string {\n const trimmed = path.replace(/\\/+$/, '')\n const name = trimmed.slice(trimmed.lastIndexOf('/') + 1)\n return name.length > 0 ? name : '(unnamed)'\n}\n\nconst KNOWN_SCHEMES: Record<string, string> = {\n 'content:': 'a content uri is not a filesystem path, so it cannot be published atomically',\n 'http:': 'a remote destination cannot be written to directly',\n 'https:': 'a remote destination cannot be written to directly',\n 'android.resource:': 'app resources are read-only',\n 'asset:': 'bundled assets are read-only',\n}\n\n/** @experimental */\nexport function resolveDestination(destination?: VideoExportDestination): DestinationDecision {\n if (destination === undefined) return { destination: { kind: 'temporary' }, reasons: [] }\n if (destination.kind === 'temporary') return { destination: { kind: 'temporary' }, reasons: [] }\n if (destination.kind !== 'file') {\n return {\n destination: null,\n reasons: [`destination kind '${String((destination as { kind: string }).kind)}' is not supported`],\n }\n }\n\n const uri = destination.uri\n if (typeof uri !== 'string' || uri.trim().length === 0) {\n return { destination: null, reasons: ['the file destination has no uri'] }\n }\n\n const scheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.exec(uri)?.[0]?.toLowerCase()\n if (scheme !== undefined && scheme !== 'file:') {\n const known = KNOWN_SCHEMES[scheme]\n return {\n destination: null,\n reasons: [\n known !== undefined\n ? `destination scheme '${scheme}' is not supported: ${known}`\n : `destination scheme '${scheme}' is not supported; only file: is`,\n ],\n }\n }\n if (scheme === undefined) {\n // A bare path is ambiguous — relative to what? — and accepting it would\n // silently resolve against a working directory nobody set.\n return { destination: null, reasons: ['the file destination must be a file: uri'] }\n }\n\n let path: string\n try {\n // `file:///a/b` and `file:/a/b` both name an absolute path; `file://host/a`\n // names another machine's, which is not something to guess at.\n const withoutScheme = uri.slice('file:'.length)\n const rest = withoutScheme.startsWith('//') ? withoutScheme.slice(2) : withoutScheme\n if (withoutScheme.startsWith('//') && !rest.startsWith('/')) {\n return {\n destination: null,\n reasons: ['the file destination names a remote host, which is not supported'],\n }\n }\n path = decodeURIComponent(rest)\n } catch {\n return { destination: null, reasons: ['the file destination uri is malformed'] }\n }\n\n if (!path.startsWith('/')) {\n return { destination: null, reasons: ['the file destination must be an absolute path'] }\n }\n if (path.endsWith('/')) {\n return {\n destination: null,\n reasons: ['the file destination is a directory; it must name the file to write'],\n }\n }\n if (path.includes('\\0')) {\n return { destination: null, reasons: ['the file destination uri is malformed'] }\n }\n /*\n `..` is refused rather than normalised.\n\n Resolving it would mean a destination that names one directory and writes to\n another, and the staging file — which must live beside the target for the\n publication to be a same-filesystem rename — would be placed by arithmetic\n the caller did not do.\n */\n if (path.split('/').some((segment) => segment === '..')) {\n return { destination: null, reasons: ['the file destination must not contain \"..\"'] }\n }\n const name = describeTarget(path)\n if (name === '.' || name === '(unnamed)') {\n return {\n destination: null,\n reasons: ['the file destination is a directory; it must name the file to write'],\n }\n }\n\n return {\n destination: { kind: 'file', path, overwrite: destination.overwrite === true },\n reasons: [],\n }\n}\n","import type { VideoExportProgress } from '@pictor-sdk/video-core'\nimport type { ExportStage } from './exportJob'\n\n/**\n * How far an export has got, and how to tell slow from stuck.\n *\n * ## Weighting is derived, not decided\n *\n * Equal percentages per stage would be a lie the bar tells: a 0.1× shard writes\n * three hundred frames while its neighbour writes sixty, and splitting them\n * evenly makes the first appear stuck and the second appear instant. The weights\n * come from the work the compiler already planned — output frames per shard,\n * audio samples, the mux — so the bar moves at the rate the encoder does.\n *\n * Output frames, specifically. Cadence changes how many frames a shard *writes*\n * for a given source, and writing is what the progress is measuring the passage\n * of; counting source frames would make slow motion look fast and fast motion\n * look stuck.\n *\n * ## What 1 means\n *\n * One is not \"the encoder finished\". It is \"the destination is complete and\n * readable\", which is a later moment: there is still a mux, and after that a\n * publication. Reporting 1 at the end of encoding produces a \"done\" that cannot\n * yet be opened, which is worse than a bar that sits at 97 for a moment.\n *\n * ## Progress and heartbeat are different signals\n *\n * **Progress** is completed work shown to a person, throttled to a handful of\n * updates a second because nobody can read more than that and every update\n * costs a bridge crossing.\n *\n * **Heartbeat** is evidence that work is still happening. It is not throttled,\n * it does not have to move the visible percentage, and it is counted natively:\n * a single muxed sample batch or a block of copied destination bytes is a\n * heartbeat even when the bar does not visibly change for a second.\n *\n * Conflating them is what makes a watchdog wrong in both directions. Keyed on\n * visible progress, a long mux or a large destination copy looks stalled\n * because its percentage barely moves; keyed on elapsed time per stage, a 0.1×\n * shard that legitimately runs for minutes looks stalled too. What actually\n * marks a wedge is **silence** — no heartbeat from any source at all.\n *\n * @experimental\n */\n\n/** What a shard will write. @experimental */\nexport interface ShardPlan {\n /** Output frames, after cadence. */\n frames: number\n}\n\n/** @experimental */\nexport interface ProgressPlanInput {\n shards: readonly ShardPlan[]\n /** Mixed audio samples the timeline will produce. Zero when silent. */\n audioSamples: number\n durationUs: number\n}\n\n/** Fractions of the whole export, summing to 1. @experimental */\nexport interface ProgressPlan {\n shardWeights: number[]\n audioWeight: number\n /** Concatenation and mux: real work, and the last thing before publication. */\n muxWeight: number\n /**\n * Publication: writing the destination and making it readable.\n *\n * Held back deliberately so the encoded-and-muxed state cannot reach 1. A bar\n * that fills before the file exists produces a \"done\" that cannot be opened,\n * which is a worse lie than sitting at 97 for a moment.\n */\n publishWeight: number\n durationUs: number\n /** The totals the fractions are measured against. */\n shardFrames: number[]\n audioSamples: number\n}\n\n/**\n * How long without a heartbeat counts as wedged.\n *\n * Derived from the longest silence real work produces, not from how long the\n * platform takes to notice a wedge. The first version had that backwards — it\n * justified twelve seconds by sitting *past* Media3's observed 10.5 s stall\n * detection, which makes a watchdog decorative: it can only ever be the second\n * thing to notice.\n *\n * The maximum legitimate heartbeat gap, measured across **three passes of seven\n * cases** on a Pixel 7 — short and long 1×, 0.1× and 10× cadence, mixed-speed\n * multi-clip, muted, and project audio — is **2046 ms**, in the 0.1× case. Seven\n * seconds is 3.4× that, and 3.5 s ahead of the platform's own 10.5 s stall\n * detection. Both margins are the point: too close to the first and it fires on\n * healthy work, too close to the second and it is not worth having.\n *\n * Three passes rather than one because a single pass is not a maximum. The same\n * 0.1× case measured 1668, 1426 and 2046 ms in three different sessions, and a\n * threshold frozen from the lowest of those would have been derived from a run\n * that happened to go well.\n *\n * ## Two corrections this number carries\n *\n * The first series measured 1668 ms with gate provenance hashing running — every\n * sample of both intermediates and the result read and digested, beating as it\n * went. A threshold calibrated against work the user never pays for is\n * calibrated against the wrong export, so the hashing moved behind a conformance\n * flag. The second series missed the *per-shard* hashing, which was still\n * running. With all of it gated the worst source moved from `muxBatch` to\n * `audioBlock`: the mux beats on its own sample batches, and once the hashing\n * that was reading them stopped, the audio mix became the quietest stage again.\n *\n * ## The headroom here is small, and that is the finding\n *\n * 3.4× the worst gap and 3.5 s before the platform leaves roughly one doubling\n * of the worst legitimate silence before the two constraints meet. On a slower\n * encoder there may be no gap between them at all, which is why\n * `Media3ExporterOptions.stallGraceMs` exists.\n *\n * Any of this was only measurable after the silence was instrumented rather\n * than tolerated. Before, the maximum gap was 5.4 s and no useful threshold\n * existed between it and the platform's timeout; the gap turned out to be the\n * Sonic time-stretch and AAC encode inside the audio mix, which at 0.1× turn one\n * second of source into ten with nothing beating.\n *\n * @experimental\n */\nexport const STALL_GRACE_MS = 7_000\n\n/** The measured maximum, kept beside the threshold it justifies. @experimental */\nexport const MAX_OBSERVED_HEARTBEAT_GAP_MS = 2_046\n\n/** A unit of work, in the same currency as the weights. */\nconst AUDIO_UNIT = 1 / 512\nconst MUX_UNITS = 24\nconst PUBLISH_UNITS = 8\n\n/** @experimental */\nexport function planProgressWeights(input: ProgressPlanInput): ProgressPlan {\n const shardUnits = input.shards.map((s) => Math.max(0, s.frames))\n const audioUnits = input.audioSamples > 0 ? input.audioSamples * AUDIO_UNIT : 0\n const total =\n shardUnits.reduce((a, b) => a + b, 0) + audioUnits + MUX_UNITS + PUBLISH_UNITS\n // The fixed mux and publish units keep this above zero, so an empty timeline\n // weighs its publication rather than dividing by nothing.\n return {\n shardWeights: shardUnits.map((u) => u / total),\n audioWeight: audioUnits / total,\n muxWeight: MUX_UNITS / total,\n publishWeight: PUBLISH_UNITS / total,\n durationUs: Math.max(0, input.durationUs),\n shardFrames: shardUnits,\n audioSamples: Math.max(0, input.audioSamples),\n }\n}\n\n/** @experimental */\nexport interface ProgressReporterOptions {\n plan: ProgressPlan\n durationUs: number\n onProgress?: (progress: VideoExportProgress) => void\n /** Bridge crossings are bounded to roughly this spacing. */\n throttleMs?: number\n now?: () => number\n}\n\n/** @experimental */\nexport interface ProgressReporter {\n /** Emit 0, once, when work begins. */\n begin(): void\n /** Report a shard's completed output frames. */\n shard(index: number, framesDone: number): void\n /** Report mixed audio samples. */\n audio(samplesDone: number): void\n /** Concatenation and mux finished. Not 1. */\n muxed(): void\n /** The destination is complete and readable. Emits 1, once. */\n complete(): void\n /**\n * Apply several updates and emit at most once.\n *\n * A poll reads every shard, the audio and the mux together, and without this\n * the first of those spends the throttle slot and the rest are dropped until\n * the next tick — so the last shard's frames and the whole audio mix could\n * arrive a tick late or, at the end of an export, never. One read of native\n * state is one update to the bar.\n */\n batch(body: () => void): void\n /** Deliver nothing further. */\n stop(): void\n}\n\n/** @experimental */\nexport function createProgressReporter(options: ProgressReporterOptions): ProgressReporter {\n const now = options.now ?? (() => Date.now())\n const throttleMs = options.throttleMs ?? 120\n const plan = options.plan\n const shardDone = plan.shardWeights.map(() => 0)\n let audioDone = 0\n let muxDone = 0\n let publishDone = 0\n let highest = 0\n let began = false\n let finished = false\n let stopped = false\n let lastEmitMs = Number.NEGATIVE_INFINITY\n let batching = 0\n let batchForce = false\n\n const fraction = () => {\n let sum = 0\n for (let i = 0; i < shardDone.length; i += 1) {\n sum += plan.shardWeights[i]! * shardDone[i]!\n }\n return sum + plan.audioWeight * audioDone + plan.muxWeight * muxDone + plan.publishWeight * publishDone\n }\n\n const emit = (value: number, force: boolean) => {\n /*\n Nothing crosses before the export is declared started.\n\n Everything that can refuse an export runs before `begin`, and a bar that\n appears and then vanishes because a photo clip was rejected claims work\n that never happened.\n */\n if (stopped || !began) return\n if (batching > 0) {\n // Held until the batch closes. `force` survives, because a completion\n // inside a batch is still a completion.\n batchForce = batchForce || force\n return\n }\n /*\n Monotonic by construction.\n\n A native counter that restarts — a retried shard, a stage re-entered —\n must not drag the bar backwards, so what is reported is the high-water\n mark rather than the latest computation.\n */\n const next = Math.max(highest, Math.min(1, value))\n const at = now()\n if (!force && at - lastEmitMs < throttleMs) return\n /*\n A repeat is not an update.\n\n Polling reports the same fraction every tick while a shard encodes, and\n delivering it is a bridge crossing that tells a host nothing — measured on\n device, a one-second export spent three of its ten callbacks re-sending 0.\n Liveness is the heartbeat's job, and it does not go through here.\n */\n if (!force && began && next === highest) return\n highest = next\n lastEmitMs = at\n /*\n A host's callback is not allowed to end the export.\n\n It runs on the caller's side and can throw for reasons that have nothing\n to do with encoding — a render error, a disposed component. Aborting a\n working encode because a progress indicator failed would be the wrong\n trade in every case.\n */\n try {\n options.onProgress?.({\n progress: highest,\n time: Math.min(plan.durationUs, Math.round(plan.durationUs * highest)),\n })\n } catch {\n // Deliberately swallowed; see above.\n }\n }\n\n return {\n begin() {\n if (began || stopped) return\n began = true\n emit(0, true)\n },\n shard(index, framesDone) {\n if (finished || stopped) return\n if (index < 0 || index >= shardDone.length) return\n const total = Math.max(1, plan.shardFrames[index] ?? 1)\n shardDone[index] = Math.max(shardDone[index]!, Math.min(1, framesDone / total))\n emit(fraction(), false)\n },\n audio(samplesDone) {\n if (finished || stopped) return\n const total = Math.max(1, plan.audioSamples)\n audioDone = Math.max(audioDone, Math.min(1, samplesDone / total))\n emit(fraction(), false)\n },\n muxed() {\n if (finished || stopped) return\n muxDone = 1\n emit(fraction(), true)\n },\n complete() {\n if (finished || stopped) return\n finished = true\n publishDone = 1\n emit(1, true)\n },\n batch(body) {\n batching += 1\n try {\n body()\n } finally {\n batching -= 1\n }\n const force = batchForce\n batchForce = false\n emit(finished ? 1 : fraction(), force)\n },\n stop() {\n stopped = true\n },\n }\n}\n\n/** What a wedged export looked like when it was noticed. @experimental */\nexport interface StallVerdict {\n stage: ExportStage | string\n units: number\n idleMs: number\n}\n\n/** @experimental */\nexport interface StallWatchdogOptions {\n thresholdMs?: number\n}\n\n/** Where a heartbeat came from, so a gap can be attributed. @experimental */\nexport const HEARTBEAT_SOURCES = [\n 'videoFrame',\n 'cadence',\n 'audioBlock',\n 'muxBatch',\n 'publishBytes',\n] as const\n\n/** @experimental */\nexport type HeartbeatSource = (typeof HEARTBEAT_SOURCES)[number]\n\n/** @experimental */\nexport interface StallWatchdog {\n /**\n * Record a heartbeat. Any beat counts, whether or not it moved the bar.\n *\n * `units` is carried for the report only — a repeated count is still a\n * heartbeat, because the pipeline spoke.\n */\n advanced(stage: ExportStage | string, units: number, atMs: number): void\n /** The verdict if nothing has reported for the threshold, else `null`. */\n stalledFor(atMs: number): StallVerdict | null\n}\n\n/**\n * Slow is not stuck.\n *\n * Keyed on *any* report rather than on elapsed time per stage: a 0.1× export\n * spends minutes inside one shard and is perfectly healthy while doing it, and\n * a stage timeout would either fire on that or be too long to catch anything.\n * What distinguishes a wedge is silence.\n *\n * Fires once. A wedged export is one event, and the cleanup it triggers is the\n * same path cancellation already proved.\n *\n * @experimental\n */\nexport function createStallWatchdog(options: StallWatchdogOptions = {}): StallWatchdog {\n const thresholdMs = options.thresholdMs ?? STALL_GRACE_MS\n let lastStage: ExportStage | string | null = null\n let lastUnits = 0\n let lastAtMs = 0\n let fired = false\n\n return {\n advanced(stage, units, atMs) {\n lastStage = stage\n lastUnits = units\n lastAtMs = atMs\n },\n stalledFor(atMs) {\n // Nothing has started, so nothing is stalled.\n if (fired || lastStage === null) return null\n const idleMs = atMs - lastAtMs\n if (idleMs < thresholdMs) return null\n fired = true\n return { stage: lastStage, units: lastUnits, idleMs }\n },\n }\n}\n","import type { VideoExportProgress } from '@pictor-sdk/video-core'\nimport type { ExportStage } from './exportJob'\nimport {\n STALL_GRACE_MS,\n createProgressReporter,\n createStallWatchdog,\n type ProgressPlan,\n type StallVerdict,\n} from './exportProgress'\n\n/**\n * Reading native counters, and turning them into a progress stream.\n *\n * The native run is one call that resolves when the whole export is done, so\n * there is nothing to subscribe to: progress has to be *read*. That is the whole\n * reason this exists as a pump rather than an event stream, and it is also why\n * the two signals stay separate on either side of the boundary.\n *\n * **Natively, nothing is throttled.** Every emitted frame, every block of mixed\n * audio and every muxed sample batch increments a counter or beats, at whatever\n * rate the encoder runs. Throttling there would be throttling the evidence.\n *\n * **Across the bridge, everything is throttled.** This polls at 8 Hz and the\n * reporter emits at most that often, because nobody can read a progress bar\n * faster and every update costs a crossing. A 0.1× export that beats a thousand\n * times a second still produces eight callbacks.\n *\n * The watchdog reads the *beat total* rather than the progress fraction. A long\n * mux moves the bar by almost nothing while beating steadily, and treating that\n * as a stall would kill a healthy export — which is the failure mode the\n * separation exists to prevent.\n *\n * @experimental\n */\n\n/** What native reports about work completed so far. @experimental */\nexport interface ProgressSnapshot {\n /** Output frames written, per shard index. */\n shards: readonly number[]\n /** Mixed audio samples handed to the encoder. */\n audioSamples: number\n /** What `audioSamples` counts up to. Zero before the mix starts. */\n audioPlanned: number\n /** Concatenation and mux finished. */\n muxed: boolean\n /**\n * Every heartbeat from every source, summed.\n *\n * Liveness only. It is deliberately not a work total — a mux batch and an\n * output frame are not the same amount of anything — so it feeds the watchdog\n * and never the bar.\n */\n beats: number\n}\n\n/** @experimental */\nexport interface ProgressPumpOptions {\n plan: ProgressPlan\n durationUs: number\n /** Read the native counters. Called on every tick. */\n read(): ProgressSnapshot\n onProgress?: (progress: VideoExportProgress) => void\n /** Called once, when nothing has reported for the threshold. */\n onStall(verdict: StallVerdict): void\n /** Bridge delivery rate. 125 ms is 8 Hz. */\n pollMs?: number\n thresholdMs?: number\n now?: () => number\n}\n\n/** @experimental */\nexport interface ProgressPump {\n /** Emit `{ progress: 0, time: 0 }`, once. */\n begin(): void\n /** Read native once and report what changed. */\n tick(): void\n /**\n * The destination is closed and verified readable. Emits 1, once.\n *\n * Separate from the mux on purpose: a bar that fills when the encoder stops\n * produces a \"done\" over a file that cannot yet be opened.\n */\n complete(): void\n /** Deliver nothing further, whatever arrives. */\n stop(): void\n /** How often {@link tick} expects to be called. */\n readonly pollMs: number\n}\n\n/**\n * Where the export was when it went quiet.\n *\n * Derived from what has finished rather than from a native stage string, so a\n * stall report cannot claim a stage the counters contradict.\n */\nfunction stageOf(snapshot: ProgressSnapshot, plan: ProgressPlan): ExportStage {\n if (snapshot.muxed) return 'publishing'\n const shardsDone = plan.shardFrames.every(\n (total, index) => (snapshot.shards[index] ?? 0) >= total,\n )\n if (!shardsDone) return 'shards'\n if (snapshot.audioPlanned > 0 && snapshot.audioSamples < snapshot.audioPlanned) return 'audio'\n return 'concatenating'\n}\n\n/** @experimental */\nexport function createProgressPump(options: ProgressPumpOptions): ProgressPump {\n const now = options.now ?? (() => Date.now())\n const pollMs = options.pollMs ?? 125\n const plan = options.plan\n const reporter = createProgressReporter({\n plan,\n durationUs: options.durationUs,\n onProgress: options.onProgress,\n // The reporter's own throttle is the poll interval: a tick that finds\n // nothing new should not be able to spend a crossing saying so.\n throttleMs: pollMs,\n now,\n })\n const watchdog = createStallWatchdog({ thresholdMs: options.thresholdMs ?? STALL_GRACE_MS })\n let lastBeats = -1\n let stopped = false\n let stalled = false\n\n return {\n pollMs,\n\n begin() {\n reporter.begin()\n // Seeded here rather than on the first tick. Without it the threshold\n // would be measured from whenever polling happened to start, which for a\n // slow first shard is a different moment from when work began.\n watchdog.advanced('resolving', 0, now())\n },\n\n tick() {\n if (stopped || stalled) return\n const snapshot = options.read()\n /*\n One read of native state is one update to the bar.\n\n Without the batch the first shard spends the throttle slot and the rest\n — including the whole audio mix — are dropped until the next tick, so\n the last thing to change before an export ends could never be delivered\n at all.\n */\n reporter.batch(() => {\n for (let i = 0; i < plan.shardFrames.length; i += 1) {\n reporter.shard(i, snapshot.shards[i] ?? 0)\n }\n if (snapshot.audioPlanned > 0 && plan.audioSamples > 0) {\n /*\n Rescaled rather than counted twice.\n\n Native counts interleaved samples at its own rate and channel count;\n the plan counts what the compiler predicted. Passing native's number\n straight through would weight audio by whichever of the two happened\n to be larger, so what crosses is the fraction.\n */\n const fraction = Math.min(1, snapshot.audioSamples / snapshot.audioPlanned)\n reporter.audio(plan.audioSamples * fraction)\n }\n if (snapshot.muxed) reporter.muxed()\n })\n\n const at = now()\n if (snapshot.beats !== lastBeats) {\n lastBeats = snapshot.beats\n watchdog.advanced(stageOf(snapshot, plan), snapshot.beats, at)\n }\n const verdict = watchdog.stalledFor(at)\n if (verdict) {\n // Once. The export is torn down from here and there is nothing further\n // to observe; continuing to poll a dead run would report it again.\n stalled = true\n options.onStall(verdict)\n }\n },\n\n complete() {\n if (stopped) return\n reporter.complete()\n },\n\n stop() {\n stopped = true\n reporter.stop()\n },\n }\n}\n","import { createExportJobs } from './exportJob'\nimport { resolveMixSources } from './mixSources'\nimport { describeTarget, resolveDestination } from './exportDestination'\nimport { planProgressWeights } from './exportProgress'\nimport { createProgressPump } from './exportProgressPump'\nimport {\n VideoExportCancelled,\n VideoExportFailed,\n VideoExportStalled,\n compileAudioMixProgram,\n compileVideoSceneProgram,\n isPlayableWindow,\n layout,\n planClipShards,\n projectDuration,\n validateAudioMixProgram,\n validateProject,\n VIDEO_SCENE_PROGRAM_VERSION,\n type ExportSourceContext,\n type MediaSource,\n type ResampleRequest,\n type VideoExportProgress,\n type VideoExportSpec,\n type VideoExporter,\n type VideoProject,\n} from '@pictor-sdk/video-core'\nimport NativePictorVideoModule from './NativePictorVideo'\n\n/** The turbo module, or a failure that says so rather than a null dereference. */\nfunction nativeModule(): NonNullable<typeof NativePictorVideoModule> {\n if (!NativePictorVideoModule) {\n throw new VideoExportRejected(['the Pictor video native module is not installed'])\n }\n return NativePictorVideoModule\n}\n\n/**\n * The Media3 exporter.\n *\n * Honours `VideoExporter` in full: asynchronous, cancellable while it runs,\n * monotonic progress, temporary and file destinations, one export at a time,\n * typed refusal, cancellation, stall and failure, and deterministic cleanup on\n * every one of those paths.\n *\n * Nothing constructs it for you. It is not on `VideoRuntime`, not behind the\n * editor's Next button, and not reachable from `@pictor-sdk/video-ui` — a host\n * builds it explicitly, which is what keeps the surface reviewable. Wiring it\n * to a button is Slice 5's job, not this file's.\n *\n * ## The gate boundary\n *\n * Nothing here reaches a `exportGate*` method. Provenance hashing, fault\n * injection, artificial holds and test beats live behind\n * `exportGateEnableDiagnostics`, which this never calls, and which leaves them\n * inert even when a payload asks for them.\n *\n * ## The whole project is interpreted here, in TypeScript\n *\n * `planClipShards` and `compileVideoSceneProgram` run once, sources are resolved\n * once, overlays are rasterised once, and what crosses to native is a list of\n * shards, a compiled program and a resource manifest — all JSON and file paths.\n * Native never sees a `VideoProject`, a `VideoClip`, a trim, a speed or a\n * transform field. It looks up segments by timestamp and draws what they say.\n *\n * ## It refuses rather than approximating\n *\n * Every clip kind, spec field and overlay state this slice does not implement is\n * rejected by name before anything is encoded. A photo clip silently skipped, or\n * a bitrate silently ignored, produces a file that is plausible and wrong — and\n * a plausible wrong file is worse than no file, because nothing downstream can\n * tell it apart from a correct one.\n *\n * @experimental\n */\n\n/**\n * An overlay raster, described structurally.\n *\n * Deliberately not imported from `@pictor-sdk/video-skia`. The adapter has no\n * opinion about how an overlay is rendered — that is the whole point of the\n * boundary — and importing the renderer would drag Skia into a package whose\n * job is to talk to Media3. What it needs is the shape of a descriptor, which\n * is JSON.\n *\n * @experimental\n */\nexport interface OverlayResourceDescriptor {\n overlayId: string\n resourceId: string\n size: { width: number; height: number }\n rasterBounds: { x: number; y: number; width: number; height: number }\n rasterScale: number\n rasterWidth: number\n rasterHeight: number\n format: string\n premultiplied: boolean\n colorSpace: string\n file: string\n byteLength: number\n checksum: number\n}\n\n/** What preparing a program's overlays produced. @experimental */\nexport interface OverlayPreparationResult {\n resources: readonly OverlayResourceDescriptor[]\n /** Still resolving. An export must not proceed and omit them. */\n pending: readonly string[]\n /** Cannot be exported at all. */\n invalid: readonly { overlayId: string; kind: string; reason: string }[]\n}\n\n/** Everything the host must supply. @experimental */\nexport interface Media3ExporterOptions {\n /**\n * Turn an opaque `MediaSource` into something Android can open.\n *\n * The exporter never inspects a descriptor: `MediaSource` is `JsonValue` and\n * only the host knows whether it is an asset id, a content URI or a row in a\n * database. Returning `null` fails the export naming the clip or the span.\n *\n * `context` says which part of the project is being resolved. It used to be\n * the clip, which left the audio path with nothing honest to pass: every\n * span, project music included, was resolved with the *first video clip* as\n * context. A resolver that keys on the clip returned that clip's media for\n * the whole timeline's audio — a plausible file with the wrong sound in it.\n */\n resolveSource(source: MediaSource, context: ExportSourceContext): string | null\n /**\n * Rasterise every overlay the compiled program names.\n *\n * The host wires its registry, its prepared resources and its raster sink\n * behind this — typically `prepareProgramOverlayExports` from\n * `@pictor-sdk/video-skia`. Injecting the whole step rather than its three inputs\n * is what keeps this package free of a renderer dependency, and it is the same\n * reason `resolveSource` is injected rather than a media library being.\n *\n * Omitted means \"no overlays are exportable\": a project that has any then\n * fails naming each one, rather than exporting without them.\n */\n prepareOverlays?(\n program: { segments: readonly { commands: readonly unknown[] }[] },\n projectSize: { width: number; height: number },\n rasterScale: number,\n ): OverlayPreparationResult\n /**\n * Called after an export finishes, successfully or not.\n *\n * The sidecars belong to the host's raster sink, so the host removes them.\n * Passing the list rather than the sink keeps this from having to know how\n * they were stored.\n */\n releaseResources?(files: readonly string[]): void\n /**\n * How long without any heartbeat counts as wedged.\n *\n * Defaults to {@link STALL_GRACE_MS}, which is **Pixel 7 evidence**: the worst\n * legitimate silence measured across seven production-path cases on that\n * device, with margin. It is not a claim about Android. A slower encoder, a\n * thermally throttled phone or a codec that batches differently can produce a\n * longer honest gap, and the number to change is this one — raising it costs\n * only how late a genuine wedge is noticed, while leaving it too low kills\n * healthy exports.\n */\n stallGraceMs?: number\n /**\n * How often native counters are read, and progress delivered.\n *\n * Defaults to 125 ms — 8 Hz, inside the 5–10 Hz a person can actually read.\n * Native counting is unthrottled regardless; this is only the bridge rate.\n */\n progressPollMs?: number\n /**\n * Which mechanism re-times a clip's **video** when its speed is not 1.\n *\n * `'cadence'` (the default, and what has shipped) compiles an exact emit plan\n * in `resampleClip` and a GL effect drops or repeats frames to match it. The\n * plan's length is the clip's length by construction, so project time is\n * exact — and above 1× the decoder still reads every source frame, which is\n * what produces a burst of output followed immediately by end-of-stream.\n *\n * `'media3'` sends no plan, so no cadence effect is built and\n * `EditedMediaItem.setSpeed` carries the video timing on its own.\n *\n * Exposed because the burst is implicated in a vendor encoder that drops its\n * output end-of-stream at high rates — see\n * `docs/research/export-speed-stall.md`. It is a comparison switch, not a\n * recommendation: the two paths select different frames, and which one is\n * correct is what the comparison is for.\n *\n * @experimental\n */\n videoSpeedVia?: 'cadence' | 'media3'\n}\n\n/** Why an export will not start. @experimental */\nexport class VideoExportRejected extends Error {\n readonly reasons: readonly string[]\n constructor(reasons: readonly string[]) {\n super(`export rejected: ${reasons.join('; ')}`)\n this.name = 'VideoExportRejected'\n this.reasons = reasons\n }\n}\n\n/** The frame rate an export runs at when the spec does not say. */\nconst DEFAULT_FPS = 30\n\n\n/**\n * What this slice cannot encode, stated rather than skipped.\n *\n * Each of these has a milestone. Until then the export fails naming the clip or\n * the field, because the alternative is a file that looks finished.\n */\nfunction describeUnsupported(project: VideoProject, spec: VideoExportSpec): string[] {\n const reasons: string[] = []\n\n for (const clip of project.clips) {\n if (clip.kind === 'photo') {\n reasons.push(`clip '${clip.id}' is a photo, which this exporter does not encode yet`)\n continue\n }\n }\n\n if (spec.format !== undefined && spec.format !== 'mp4') {\n reasons.push(`format '${spec.format}' is not supported; this exporter writes mp4`)\n }\n if (spec.scale !== undefined && spec.scale !== 1) {\n // Honoured or rejected, never quietly ignored.\n reasons.push(`scale ${spec.scale} is not supported yet; only 1 is`)\n }\n if (spec.fps !== undefined && spec.fps !== DEFAULT_FPS) {\n reasons.push(`fps ${spec.fps} is not supported yet; only ${DEFAULT_FPS} is`)\n }\n if (spec.bitrate !== undefined) {\n reasons.push('bitrate is not supported yet; the encoder default is used')\n }\n return reasons\n}\n\n/**\n * Build an exporter.\n *\n * @experimental\n */\nexport function createMedia3VideoExporter(options: Media3ExporterOptions): VideoExporter {\n /*\n One job at a time, cancellable at every stage.\n\n The registry owns identity, exclusivity and settling exactly once; the body\n below only has to call `job.checkpoint` where stopping is safe. See\n `exportJob.ts` for why cancellation cannot simply reject the promise.\n */\n const jobs = createExportJobs({\n cancelNative: (jobId) => {\n nativeModule().exportCancelJob(jobId)\n },\n })\n\n return {\n async export(\n project: VideoProject,\n spec: VideoExportSpec,\n onProgress?: (progress: VideoExportProgress) => void,\n ) {\n return jobs.run(async (job) => {\n // First thing, so a cancel pressed before any work is observed before\n // the compile, the overlay pass and the source resolution rather than\n // after them. Measured: without it a pre-native cancel took 4.7 s to\n // settle, because the first checkpoint was past all of that.\n job.checkpoint('resolving')\n const issues = validateProject(project)\n if (issues.length > 0) {\n throw new VideoExportRejected(issues.map((issue) => issue.message))\n }\n const unsupported = describeUnsupported(project, spec)\n if (unsupported.length > 0) throw new VideoExportRejected(unsupported)\n\n /*\n Refused, never redirected.\n\n Falling back to a temporary would produce an export that reports success\n while the file is nowhere the caller asked for — and nothing downstream\n can tell that apart from having worked.\n */\n const decision = resolveDestination(spec.destination)\n if (decision.destination === null) throw new VideoExportRejected(decision.reasons)\n const destination = decision.destination\n\n const playable = project.clips.filter((clip) => isPlayableWindow(clip))\n if (playable.length === 0) throw new VideoExportRejected(['the project has no playable clips'])\n if (!(projectDuration(project) > 0)) {\n throw new VideoExportRejected(['the project has no duration'])\n }\n\n // Resolved without inspecting the descriptor: only the host knows what a\n // `MediaSource` is, and this must not learn.\n const uris: string[] = []\n const unresolved: string[] = []\n for (const clip of playable) {\n const uri = options.resolveSource(clip.source, { kind: 'clip', clipId: clip.id })\n if (typeof uri !== 'string' || uri.length === 0) {\n unresolved.push(`clip '${clip.id}' could not be resolved to a readable source`)\n continue\n }\n uris.push(uri)\n }\n if (unresolved.length > 0) throw new VideoExportRejected(unresolved)\n\n const fps = DEFAULT_FPS\n const placements = layout(project)\n const requests: ResampleRequest[] = playable.map((clip) => ({\n sourceStartUs: clip.sourceIn,\n sourceEndUs: clip.sourceOut,\n speed: clip.speed,\n sourceFps: clip.metadata.fps ?? fps,\n targetFps: fps,\n }))\n const plan = planClipShards(requests, fps)\n const program = compileVideoSceneProgram(project)\n if (program.segments.length === 0) {\n throw new VideoExportRejected(['the compiled program has no segments'])\n }\n void placements\n\n /*\n Overlays are rasterised and checked **before** any native work.\n\n A resource discovered missing once encoding has started leaves a partial\n file, and the frames already written are silently missing something the\n timeline said was on screen.\n */\n const named = new Set<string>()\n for (const segment of program.segments) {\n for (const command of segment.commands) {\n if (command.op === 'overlay') named.add(command.overlayId)\n }\n }\n const overlayPlan: OverlayPreparationResult = options.prepareOverlays\n ? options.prepareOverlays(program, project.size, 1)\n : {\n resources: [],\n pending: [],\n invalid: [...named].map((overlayId) => ({\n overlayId,\n kind: 'unknown',\n reason: 'no prepareOverlays was supplied, so this overlay has no portable form',\n })),\n }\n // Before the plan is judged: a cancel raised from inside the host's\n // preparation must be reported as `overlays`, not as whatever the\n // validation below happens to say first.\n job.checkpoint('overlays')\n\n /*\n Anything already staged is released on every failure path below.\n\n A rejection after rasterising leaves sidecars nothing else will come back\n for, and the run that failed is exactly the one nobody cleans up after.\n */\n const stagedNow = overlayPlan.resources.map((resource) => resource.file)\n if (overlayPlan.pending.length > 0) {\n options.releaseResources?.(stagedNow)\n throw new VideoExportRejected(\n overlayPlan.pending.map(\n (id: string) => `overlay '${id}' is still preparing; export would omit it`,\n ),\n )\n }\n if (overlayPlan.invalid.length > 0) {\n options.releaseResources?.(stagedNow)\n throw new VideoExportRejected(\n overlayPlan.invalid.map(\n (bad: { overlayId: string; kind: string; reason: string }) =>\n `overlay '${bad.overlayId}' (${bad.kind}): ${bad.reason}`,\n ),\n )\n }\n const resources = overlayPlan.resources\n\n /*\n Whether a re-timed clip gets an emit plan at all.\n\n Omitting it is how the cadence effect is left out: native builds one\n only when both arrays arrive, so there is no second switch to keep in\n step. A clip at 1× has nothing to re-time either way.\n */\n const viaMedia3 = options.videoSpeedVia === 'media3'\n const shards = plan.map((shard, index) => {\n const clip = playable[index]!\n const cadence =\n viaMedia3 && clip.speed !== 1 ? null : cadenceFor(shard.plan.frames)\n return {\n uri: uris[index]!,\n startMs: Math.round(clip.sourceIn / 1000),\n endMs: Math.round(clip.sourceOut / 1000),\n speed: clip.speed,\n timelineOffsetUs: shard.timelineOffsetUs,\n /*\n The shard's own audio is dropped and replaced with generated silence.\n\n Not because anything wants that silence — the final mux takes only\n the video track from this pipeline — but because Media3 drives its\n speed path through audio, and a video-only item at any rate other\n than 1 stalls with \"no output sample written\". So the track exists to\n satisfy the re-timing and is discarded.\n\n What matters is that the *source's* audio never enters this pipeline:\n a per-shard audio track has to be re-encoded to concatenate, which is\n what put a second generation on everything the user hears.\n\n `ExportGate` notes that `setRemoveAudio` beside a speed change is a\n combination Media3 warns about, and that was tried as a candidate\n fix for the high-rate export stall: giving the re-timing the real\n stream instead of silence. It made no difference to the fault — the\n encoder stopped at exactly the same place — so the change was\n reverted rather than kept for a benefit that could not be shown.\n See `docs/research/export-speed-stall.md`.\n */\n forceAudioTrack: true,\n removeAudio: true,\n emitsPerInput: cadence?.emitsPerInput,\n outputPtsUs: cadence?.outputPtsUs,\n spanUs: Math.round((shard.plan.frameCount * 1_000_000) / fps),\n }\n })\n\n /*\n The whole timeline's audio, compiled once and resolved once.\n\n `compileAudioMixProgram` settles placements, trims, gains and fades; the\n backend mixes and encodes exactly what it says. Sources are resolved by\n span id through the same host resolver the video pipeline uses, and a\n silent span needs none.\n */\n const mixProgram = compileAudioMixProgram(project)\n const mixProblems = validateAudioMixProgram(mixProgram)\n if (mixProblems.length > 0) throw new VideoExportRejected(mixProblems)\n /*\n Resolved as what each span actually is, not as whichever clip came\n first — and by the same function preview preparation calls, so the two\n cannot disagree about which file a span means. A disagreement there\n would be audible only in the finished video.\n */\n const resolved = resolveMixSources(project, mixProgram, options.resolveSource)\n const mixUris = resolved.uris\n if (resolved.unresolved.length > 0) throw new VideoExportRejected(resolved.unresolved)\n job.checkpoint('resolving')\n\n job.checkpoint('overlays')\n const staged = resources.map((resource) => resource.file)\n\n /*\n Progress starts here, after everything that can refuse.\n\n A bar that appears and then vanishes because a photo clip was rejected\n is worse than one that never appeared: the export never began, and\n showing 0% claims otherwise. Everything above this line either throws or\n establishes the plan the weights are computed from.\n */\n const durationUs = projectDuration(project)\n const progressPlan = planProgressWeights({\n shards: plan.map((shard) => ({ frames: shard.plan.frameCount })),\n audioSamples: mixProgram.spans.some((span) => !span.silent)\n ? Math.max(0, mixProgram.durationUs)\n : 0,\n durationUs,\n })\n // A box rather than a `let`: the assignment happens in a callback, and\n // TypeScript narrows a closed-over `let` to `never` at the read.\n const stalled: { at: { stage: string; idleMs: number } | null } = { at: null }\n const pump = createProgressPump({\n plan: progressPlan,\n durationUs,\n read: () => JSON.parse(nativeModule().exportProgressSnapshot()) as never,\n // Delivered through the job, so nothing arrives after settlement: a\n // host that has already shown a result must not be told 90%.\n onProgress: onProgress\n ? (value) => { job.progress(value.progress, () => onProgress(value)) }\n : undefined,\n onStall: (verdict) => {\n stalled.at = { stage: String(verdict.stage), idleMs: verdict.idleMs }\n /*\n Torn down through the path cancellation already proved.\n\n A wedged export holds a Transformer, a muxer and its codecs, and the\n only mechanism in this process that reliably lets go of all three is\n the native cancel. What differs is what the caller is told: nobody\n asked to stop, so this settles as `VideoExportStalled`.\n */\n nativeModule().exportCancelJob(job.id)\n },\n thresholdMs: options.stallGraceMs,\n pollMs: options.progressPollMs,\n })\n pump.begin()\n const ticker = setInterval(() => { pump.tick() }, pump.pollMs)\n\n let outcome: { ok: boolean; uri: string; error: string }\n try {\n outcome = JSON.parse(\n await nativeModule().exportRunProjectAsync(\n job.id,\n JSON.stringify(shards),\n JSON.stringify({\n version: VIDEO_SCENE_PROGRAM_VERSION,\n projectSize: { width: project.size.width, height: project.size.height },\n durationUs: program.durationUs,\n segments: program.segments,\n }),\n JSON.stringify(resources),\n JSON.stringify(mixProgram),\n JSON.stringify(mixUris),\n /* timeoutSeconds= */ 600,\n destination.kind === 'file'\n ? JSON.stringify({\n kind: 'file',\n path: destination.path,\n overwrite: destination.overwrite,\n })\n : '',\n ),\n ) as { ok: boolean; uri: string; error: string }\n } finally {\n clearInterval(ticker)\n // Every path, including the one that threw: the sidecars are this\n // export's and nothing else will come back for them.\n options.releaseResources?.(staged)\n }\n\n /*\n A stall outranks whatever native said on the way out.\n\n Tearing down a wedged Transformer reports itself as a cancellation,\n because a cancellation is how it was torn down. Reporting that would\n tell a host the user pressed stop when nothing of the sort happened.\n */\n const wedged = stalled.at\n if (wedged) {\n pump.stop()\n throw new VideoExportStalled(job.id, wedged.stage, wedged.idleMs)\n }\n\n /*\n A native cancellation is a cancellation, with the stage native recorded.\n\n Without this the job registry falls back to its own default and every\n cancellation reports the same stage, which is worse than no stage at\n all: it looks like information.\n */\n const cancelledDuring = /^cancelled during (\\w+)/.exec(outcome.error ?? '')\n if (!outcome.ok && cancelledDuring) {\n pump.stop()\n throw new VideoExportCancelled(job.id, cancelledDuring[1]!)\n }\n\n if (!outcome.ok) {\n /*\n No uri either way.\n\n A partial file is worse than none — it plays, it is the wrong length,\n and a caller that took the uri would have no way to know. Native\n deletes what it created on every one of its own failure paths; nothing\n is swept here, because a sweep would also take the temporary a\n *previous* export handed the caller.\n */\n pump.stop()\n const error = outcome.error || 'the export failed'\n if (isRefusal(error)) throw new VideoExportRejected([error])\n throw new VideoExportFailed(job.id, 'encoding', error)\n }\n job.checkpoint('publishing')\n\n /*\n One is the destination being readable, not the encoder stopping.\n\n The container is closed after the last sample is written, and a file\n that is closed is not necessarily one that parses. Probing it costs a\n `MediaExtractor` open and turns \"the encode finished\" into \"there is a\n file you can play\", which is the only claim a bar reaching 1 should be\n making.\n */\n const probe = JSON.parse(nativeModule().exportProbeOutput(outcome.uri)) as {\n readable: boolean\n error?: string\n tracks?: number\n }\n if (!probe.readable) {\n pump.stop()\n // Written and unopenable: the work happened and the platform produced\n // something unusable, which is a failure rather than a refusal.\n throw new VideoExportFailed(\n job.id,\n 'publishing',\n `${describeTarget(outcome.uri)} could not be read back: ${probe.error ?? 'no video track'}`,\n )\n }\n pump.complete()\n pump.stop()\n\n /*\n Ownership, stated in the type.\n\n A temporary is the adapter's until the caller says otherwise, so it\n carries the one call that disposes of it. A file destination is the\n caller's — they chose the path — so that branch has no `release` at all,\n rather than one that quietly does nothing. An optional cleanup method on\n a single shape would put \"delete the user's video\" one typo away from\n every success path.\n */\n if (destination.kind === 'file') return { kind: 'file', uri: outcome.uri }\n const temporaryUri = outcome.uri\n return {\n kind: 'temporary',\n uri: temporaryUri,\n release: async () => {\n // Idempotent, and scoped to this file: native refuses any path it did\n // not create, so a second call — or one after the cache was cleared —\n // is not an error.\n await Promise.resolve()\n nativeModule().exportReleaseTemporaryOutput(temporaryUri)\n },\n }\n })\n },\n\n /**\n * Ask the running export to stop.\n *\n * Idempotent, and a no-op when nothing is running. Reaches the native job\n * before the promise settles: a Transformer nobody is listening to still\n * encodes, still writes and still holds its codecs, so rejecting in\n * JavaScript alone would show a cancelled state over a running export.\n */\n cancel(): void {\n jobs.cancel()\n },\n }\n}\n\n/**\n * Refusal or failure — the distinction a caller acts on.\n *\n * A refusal means the project, the destination or a resource was not something\n * this adapter could encode, decided before or instead of doing work: change\n * the input. A failure means the work started and the platform gave up: retry,\n * check storage, or report a bug. Reporting one as the other sends a caller to\n * the wrong place, which is worse than an unhelpful message.\n *\n * Matched on the prefixes native produces rather than on a code, because the\n * native side reports strings — and the list is exhaustive over the reasons\n * `runProject` can refuse, so anything unrecognised is a failure. That default\n * is the safe one: a new encoder error misreported as \"unsupported\" tells a user\n * to edit a project that was fine.\n */\nconst REFUSAL_PATTERNS = [\n /^program rejected/,\n /^overlay resources? /,\n /^overlay resource /,\n /^no shards/,\n /already exists; pass overwrite/,\n /^the directory for /,\n /^the destination /,\n /cannot be written without overwrite/,\n /^the staged file could not be read back/,\n]\n\nfunction isRefusal(error: string): boolean {\n return REFUSAL_PATTERNS.some((pattern) => pattern.test(error))\n}\n\n\n/**\n * The cadence transport: copies owed per arriving input, and their timestamps.\n *\n * Straight from `resampleClip`'s plan. Native walks these arrays and computes no\n * grid arithmetic of its own.\n */\nfunction cadenceFor(\n frames: readonly { outputPtsUs: number; sourceFrameIndex: number }[],\n): { emitsPerInput: number[]; outputPtsUs: number[] } {\n const first = frames[0]!.sourceFrameIndex\n const last = frames[frames.length - 1]!.sourceFrameIndex\n const emitsPerInput: number[] = []\n for (let i = 0; i <= last - first; i += 1) emitsPerInput.push(0)\n const outputPtsUs: number[] = []\n for (const frame of frames) {\n const slot = frame.sourceFrameIndex - first\n emitsPerInput[slot] = (emitsPerInput[slot] ?? 0) + 1\n outputPtsUs.push(frame.outputPtsUs)\n }\n return { emitsPerInput, outputPtsUs }\n}\n","import type { MediaSaver, SavedMedia } from '@pictor-sdk/video-core'\nimport NativePictorVideoModule from './NativePictorVideo'\n\n/**\n * Saving a finished export into Android's video collection.\n *\n * A thin boundary. The insert-pending / stream / verify / publish sequence is\n * native, because it is where the file descriptors and the cancellation live;\n * this turns one JSON result into either a {@link SavedMedia} or a typed error,\n * and gives every save an id so a cancel reaches the right one.\n *\n * ## What a failure guarantees\n *\n * Nothing partial. Every unsuccessful native path — a short write, a full disk,\n * a cancellation, an unreadable result — deletes the pending row before\n * returning, and a pending row was never visible to the gallery in the first\n * place. So a failed save leaves the collection exactly as it was.\n *\n * ## What it never does\n *\n * Delete a published item. Releasing the export's temporary is a separate act\n * on a separate file, and a saved video belongs to the user from the moment it\n * is published.\n *\n * @experimental\n */\n\n/** Why a save did not happen. @experimental */\nexport class MediaSaveFailed extends Error {\n /** Safe to display: never a path, a stack, or a platform message. */\n readonly reason: string\n constructor(reason: string) {\n super(`save failed: ${reason}`)\n this.name = 'MediaSaveFailed'\n this.reason = reason\n }\n}\n\n/** A save the user asked to stop. @experimental */\nexport class MediaSaveCancelled extends Error {\n constructor() {\n super('save cancelled')\n this.name = 'MediaSaveCancelled'\n }\n}\n\n/**\n * Offer a saved video to the system share sheet.\n *\n * Only after a successful save, and only ever the durable `content:` uri. The\n * cache path the export produced is app-private: a receiver could not open it,\n * and handing one out is the mistake `FileUriExposedException` exists to catch.\n *\n * Sharing changes nothing. The same row is offered every time — no copy, no\n * rename, no re-publish, and nothing released.\n *\n * Returns `false` when the device has nothing that accepts a video, which is an\n * outcome rather than a failure: the saved video is untouched either way.\n *\n * @experimental\n */\nexport function shareSavedMedia(uri: string): { ok: boolean; noTargets: boolean; reason: string } {\n if (!NativePictorVideoModule) {\n return { ok: false, noTargets: false, reason: 'sharing is not available' }\n }\n if (!uri.startsWith('content://')) {\n /*\n Refused rather than attempted.\n\n The only uri worth sharing is the published one. A cache path reaching\n here would mean the save had not happened, and offering it would either\n throw on a modern target or hand a receiver something it cannot read.\n */\n return { ok: false, noTargets: false, reason: 'only a saved video can be shared' }\n }\n const result = JSON.parse(NativePictorVideoModule.mediaShare(uri)) as {\n ok?: boolean\n noTargets?: boolean\n error?: string\n }\n return {\n ok: result.ok === true,\n noTargets: result.noTargets === true,\n reason: result.error ?? '',\n }\n}\n\nlet counter = 0\n\n/** @experimental */\nexport function createMediaStoreSaver(): MediaSaver {\n /*\n The job whose cancel is live.\n\n Held so a cancel arriving after one save settled cannot stop the next: the\n id is checked natively, and clearing it here means a stray press reaches\n nothing at all.\n */\n let active: string | null = null\n\n const native = () => {\n if (!NativePictorVideoModule) {\n throw new MediaSaveFailed('the Pictor video native module is not installed')\n }\n return NativePictorVideoModule\n }\n\n return {\n async save(sourceUri: string): Promise<SavedMedia> {\n if (active !== null) {\n // Refused rather than queued: two saves of the same export would insert\n // two rows, and the user asked for one video.\n throw new MediaSaveFailed('a save is already running')\n }\n counter += 1\n const id = `save_${counter.toString(36)}`\n active = id\n let outcome: {\n ok: boolean\n uri?: string\n displayName?: string\n location?: string\n error?: string\n cancelled?: boolean\n }\n try {\n outcome = JSON.parse(await native().mediaSaveAsync(id, sourceUri))\n } finally {\n active = null\n }\n if (outcome.cancelled === true) throw new MediaSaveCancelled()\n if (!outcome.ok || typeof outcome.uri !== 'string') {\n throw new MediaSaveFailed(outcome.error ?? 'the file could not be saved')\n }\n return {\n uri: outcome.uri,\n displayName: outcome.displayName ?? '',\n location: outcome.location ?? '',\n }\n },\n\n cancel(): void {\n if (active === null) return\n native().mediaSaveCancel(active)\n },\n }\n}\n\n/**\n * Delete pending rows a killed process left behind.\n *\n * Worth calling once at startup. A pending row is invisible to the gallery and\n * still occupies storage, so nothing will ever prompt a user to clean one up.\n *\n * Scoped to this app's own still-pending rows under its own folder: a published\n * item is never a candidate, and neither is anything another app wrote.\n *\n * @experimental\n */\nexport function sweepPendingMediaSaves(): {\n swept: number\n inspected: number\n /** Pending rows seen and deliberately left alone. */\n skipped: number\n} {\n if (!NativePictorVideoModule) return { swept: 0, inspected: 0, skipped: 0 }\n const result = JSON.parse(NativePictorVideoModule.mediaSaveSweepPending()) as {\n swept?: number\n inspected?: number\n skipped?: number\n }\n return {\n swept: result.swept ?? 0,\n inspected: result.inspected ?? 0,\n skipped: result.skipped ?? 0,\n }\n}\n","import { requireNative } from './player'\n\n/**\n * Where an export's overlay rasters are written.\n *\n * ## Why the adapter owns this\n *\n * `OverlayResources.kt`, in this package, is what reads these files back and\n * checks their length and CRC. The writer belongs beside the reader: a host\n * that produced the bytes *and* chose where they lived would need a filesystem\n * dependency and a path convention, and the two ends of one format would be\n * maintained apart.\n *\n * It also keeps the promise the rest of the library makes. `@pictor-sdk/video-core`,\n * `@pictor-sdk/video-text` and the example app acquire no filesystem package for\n * this; they receive a structural sink and never learn a path.\n *\n * ## Why it is synchronous\n *\n * `OverlayRasterSink.stage` must return a descriptor before the renderer can\n * continue building its preparation, and React Native has no synchronous\n * filesystem. The bytes are written by a JSI host function, which runs on the\n * calling thread and returns a value like any other function — no bridge round\n * trip, no second event loop, no waiting on a promise that cannot be awaited.\n *\n * That is a real cost: the JS thread is blocked for the write. Measured on a\n * Pixel 7, release build, at roughly 5 ms per megabyte:\n *\n * | raster | bytes | stall |\n * |---------------|--------|----------|\n * | 671 x 114 | 299 KB | 2 ms |\n * | 1080 x 400 | 1.7 MB | 7 ms |\n * | 1080 x 1920 | 8.1 MB | 36–48 ms |\n * | 3240 x 5760 | 71 MB | 357 ms |\n *\n * The first three are the operating point: a raster covers the overlay's own\n * bounds at export scale, not the project, and the two overlays in the Text\n * acceptance document staged 426 KB and 306 KB. A full-bleed overlay in a\n * 1080x1920 export is the realistic worst case at ~40 ms. The last row is the\n * pessimistic bound — a project-sized raster at 3x — and it is what the note\n * below is about.\n *\n * All of it is paid once per overlay during preparation, before the export\n * begins and before there is a progress bar to stutter. It is not a per-frame\n * cost. So the synchronous contract stands at these sizes; see\n * `measureStagingStall` for when it should not.\n *\n * @experimental\n */\n\n/** What native returns for one staged raster. */\ninterface StagedDescriptor {\n /** Opaque ownership handle. The only thing that may be released. */\n token: string\n /** Where it went. Read by the export reader; not shown to anyone. */\n file: string\n byteLength: number\n checksum: number\n}\n\ninterface StagingHost {\n stage(bytes: ArrayBuffer, width: number, height: number): StagedDescriptor\n release(token: string): boolean\n releaseAll(): number\n sweep(): number\n ledger(): { created: number; released: number; live: number }\n}\n\ndeclare const global: { __pictorOverlayStaging?: StagingHost }\n\nlet installed = false\n\n/**\n * Install the staging primitive, once.\n *\n * Idempotent, and safe to call before an export rather than at module load: a\n * host that never exports an overlay never pays for it.\n */\nexport function installOverlayStaging(): boolean {\n if (installed && global.__pictorOverlayStaging !== undefined) return true\n installed = requireNative().installOverlayStaging()\n return installed && global.__pictorOverlayStaging !== undefined\n}\n\nfunction host(): StagingHost {\n const h = global.__pictorOverlayStaging\n if (h === undefined) {\n throw new Error('@pictor-sdk/video-android: overlay staging is not installed')\n }\n return h\n}\n\n/** What a sink hands back, plus the token that owns it. @experimental */\nexport interface StagedRaster {\n file: string\n byteLength: number\n checksum: number\n}\n\n/**\n * A sink, and the tokens it is holding.\n *\n * The token list is the whole point of the wrapper: the structural\n * `OverlayRasterSink` contract identifies a staged raster by its file, and a\n * caller that released by path would be naming something it was never given\n * authority over. Every release here goes through the token native issued.\n *\n * @experimental\n */\nexport interface OverlayStagingSink {\n stage(\n bytes: Uint8Array,\n meta: { overlayId: string; resourceId: string; rasterWidth: number; rasterHeight: number },\n ): StagedRaster\n /** Discard one staged raster, by the file the descriptor carried. */\n remove(file: string): void\n /** Discard everything this sink still holds. Idempotent. */\n releaseAll(): number\n /** For assertions: created, released and still live. */\n ledger(): { created: number; released: number; live: number }\n}\n\n/**\n * Remove sidecars a previous process left behind.\n *\n * Scoped by native to this module's own directory and file suffix, so it cannot\n * take a cache file belonging to anything else. Returns how many it removed.\n *\n * @experimental\n */\nexport function sweepOverlayStaging(): number {\n if (!installOverlayStaging()) return 0\n return host().sweep()\n}\n\n/** @experimental */\nexport function createOverlayStagingSink(): OverlayStagingSink {\n if (!installOverlayStaging()) {\n throw new Error('@pictor-sdk/video-android: overlay staging could not be installed')\n }\n /** file → token, so a structural `remove(file)` can release exactly. */\n const tokens = new Map<string, string>()\n\n return {\n stage(bytes, meta) {\n /*\n A view over a larger buffer is copied rather than passed whole.\n\n `Uint8Array` may be a window onto a bigger `ArrayBuffer`, and handing\n native the underlying buffer would stage the whole thing — the right\n length by accident when the view happens to cover it, and silently wrong\n when it does not.\n */\n const exact =\n bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength\n ? (bytes.buffer as ArrayBuffer)\n : (bytes.slice().buffer as ArrayBuffer)\n\n const staged = host().stage(exact, meta.rasterWidth, meta.rasterHeight)\n tokens.set(staged.file, staged.token)\n return {\n file: staged.file,\n byteLength: staged.byteLength,\n checksum: staged.checksum,\n }\n },\n\n remove(file) {\n const token = tokens.get(file)\n // Releasing something never staged, or already released, is not an error:\n // the caller's cleanup paths overlap on purpose, and each must be safe to\n // run after the others.\n if (token === undefined) return\n tokens.delete(file)\n host().release(token)\n },\n\n releaseAll() {\n tokens.clear()\n return host().releaseAll()\n },\n\n ledger() {\n return host().ledger()\n },\n }\n}\n\n/**\n * How long a synchronous stage blocks the JS thread, for a raster this size.\n *\n * Exists because the synchronous contract is only defensible if the number is\n * small. If a maximum-size raster stalls long enough to drop frames in the\n * export sheet, the answer is to make `OverlayRasterSink.stage` asynchronous —\n * not to keep a blocking write behind a synchronous signature because it is\n * convenient.\n *\n * @experimental\n */\nexport function measureStagingStall(\n sink: OverlayStagingSink,\n width: number,\n height: number,\n): { ms: number; bytes: number } {\n const bytes = new Uint8Array(width * height * 4)\n const at = Date.now()\n const staged = sink.stage(bytes, {\n overlayId: 'measure',\n resourceId: 'measure',\n rasterWidth: width,\n rasterHeight: height,\n })\n const ms = Date.now() - at\n sink.remove(staged.file)\n return { ms, bytes: bytes.byteLength }\n}\n","import type { AudioProbe } from '@pictor-sdk/video-core'\nimport { requireNative } from './player'\n\n/**\n * Reading what a picked audio file actually contains.\n *\n * The structural half of {@link AudioProbe} lives in `@pictor-sdk/video-core`,\n * which has no decoder and never will; this is the adapter that can answer it.\n * A host calls this once, before adding, and passes the result to\n * `acceptAudioSource`.\n *\n * Nothing here identifies the file. The display name is the host's to carry —\n * a picker URI embeds an id and sometimes a user's own folder names.\n *\n * @experimental\n */\n\n/** A probe that failed, with a reason fit for a log rather than a person. @experimental */\nexport interface AudioProbeFailure {\n ok: false\n /** Why the file could not be read. Diagnostic, not a user-facing sentence. */\n reason: string\n}\n\n/** What native learned, minus the display name only the host knows. @experimental */\nexport type AudioProbeResult = ({ ok: true } & Omit<AudioProbe, 'displayName'>) | AudioProbeFailure\n\n/**\n * Open `uri` and report its audio.\n *\n * Never throws for an unreadable file: a pick that cannot be read is an answer,\n * and a host that had to wrap this in a `try` would be handling a normal\n * outcome as an exception. It throws only if the adapter is not installed,\n * which is a wiring mistake rather than a file.\n *\n * @experimental\n */\nexport function probeAudioSource(uri: string): AudioProbeResult {\n let raw: string\n try {\n raw = requireNative().probeAudioSource(uri)\n } catch (error) {\n return { ok: false, reason: error instanceof Error ? error.message : String(error) }\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return { ok: false, reason: 'probe returned unparseable JSON' }\n }\n\n const value = parsed as Partial<AudioProbe> & { ok?: boolean; reason?: string }\n if (value.ok !== true) {\n return { ok: false, reason: typeof value.reason === 'string' ? value.reason : 'probe failed' }\n }\n\n /*\n Normalised here, not trusted as-is.\n\n Everything downstream treats these as decided facts — `hasAudioTrack` ends\n up in `metadata.hasAudio`, which is what the encoder is told. A missing or\n malformed field must not arrive as `undefined` and read as permission.\n */\n return {\n ok: true,\n durationUs: Number.isFinite(value.durationUs) ? Number(value.durationUs) : 0,\n hasAudioTrack: value.hasAudioTrack === true,\n seekable: value.seekable === true,\n mimeType: typeof value.mimeType === 'string' ? value.mimeType : '',\n }\n}\n","import { requireNative } from './player'\n\n/**\n * Media the app owns, copied out of wherever it was picked from.\n *\n * A picker returns a reference, not a file. A `content://` grant is scoped to\n * the activity result that produced it; `copyToCacheDirectory` improves on that\n * by writing into **cache**, which Android reclaims whenever it wants storage\n * back. Either way a project persisted with a picker URI reopens with media\n * that has quietly gone, and nothing says so until an export fails.\n *\n * So ingestion copies into `filesDir` and the project stores that. See\n * `ManagedSources.kt` for why the name is the content hash and why nothing is\n * ever deleted implicitly.\n *\n * @experimental\n */\n\n/** Where a managed source lives, and what it is. @experimental */\nexport interface ManagedSource {\n /** A `file://` URI in app-owned durable storage. Safe to persist. */\n uri: string\n /** The content hash. Names the file; never derived from the user's filename. */\n token: string\n byteLength: number\n sha256: string\n /**\n * Whether this call brought the file into existence.\n *\n * False means the bytes were already stored and something else may be\n * pointing at them — a caller unwinding its own work must not release a\n * source it did not create.\n */\n created: boolean\n}\n\n/** @experimental */\nexport interface ManagedSourceFailure {\n ok: false\n /** Diagnostic. Never shown to a person: it may name a path. */\n reason: string\n}\n\n/** @experimental */\nexport type IngestResult = ({ ok: true } & ManagedSource) | ManagedSourceFailure\n\n/**\n * Copy a picked source into durable, app-owned storage.\n *\n * Never throws for an unreadable source: a pick that cannot be copied is an\n * answer. It throws only if the adapter is not installed.\n *\n * @experimental\n */\nexport function ingestManagedSource(uri: string): IngestResult {\n return parse<IngestResult>(\n () => requireNative().ingestManagedSource(uri),\n (value) => ({\n ok: true,\n uri: String(value.uri ?? ''),\n token: String(value.token ?? ''),\n byteLength: Number(value.byteLength ?? 0),\n sha256: String(value.sha256 ?? ''),\n created: value.created === true,\n }),\n )\n}\n\n/**\n * Drop one managed source.\n *\n * For unwinding an ingestion whose item was never created. Not for Undo: an\n * Undo that deleted the source would leave Redo restoring an item that points\n * at nothing.\n *\n * @experimental\n */\nexport function releaseManagedSource(token: string): boolean {\n try {\n const value = JSON.parse(requireNative().releaseManagedSource(token)) as { removed?: boolean }\n return value.removed === true\n } catch {\n return false\n }\n}\n\n/**\n * Remove every managed source not named in `keepTokens`.\n *\n * The only thing that deletes media, and it is always given the full set of\n * what the project still references. Also removes partials a killed process\n * left behind.\n *\n * @experimental\n */\nexport function sweepManagedSources(keepTokens: readonly string[]): {\n removed: number\n kept: number\n partials: number\n /** Files that are not ours, counted so the accounting matches a listing. */\n skipped: number\n} {\n try {\n const value = JSON.parse(\n requireNative().sweepManagedSources(JSON.stringify([...keepTokens])),\n ) as { ok?: boolean; removed?: number; kept?: number; partials?: number; skipped?: number }\n if (value.ok !== true) return { removed: 0, kept: 0, partials: 0, skipped: 0 }\n return {\n removed: Number(value.removed ?? 0),\n kept: Number(value.kept ?? 0),\n partials: Number(value.partials ?? 0),\n skipped: Number(value.skipped ?? 0),\n }\n } catch {\n // A sweep that cannot be understood removes nothing. The opposite default\n // would delete a project's media on a parse error.\n return { removed: 0, kept: 0, partials: 0, skipped: 0 }\n }\n}\n\n/**\n * Every filename in the managed directory, including partials and strays.\n *\n * Read-only accounting, so it is available in release for the same reason the\n * ledgers are: it is how a sweep is proven rather than asserted.\n * {@link managedSourceLedger} reports only well-formed sources, which cannot\n * show what a sweep left behind.\n *\n * @experimental\n */\nexport function managedSourceList(): { name: string; byteLength: number }[] {\n try {\n const value = JSON.parse(requireNative().managedSourceList()) as {\n items?: { name: string; byteLength: number }[]\n }\n return value.items ?? []\n } catch {\n return []\n }\n}\n\n/**\n * Gate-only: put a file in the managed directory with an exact name.\n *\n * Ingestion only ever produces well-formed, referenced sources, so the cases a\n * sweep exists to handle — an orphan, a stale partial, a file that is not ours\n * — cannot be reached through the normal API. Refused in a release build built\n * without diagnostics.\n *\n * @experimental\n */\nexport function seedManagedSourceForGate(name: string, bytes: number): boolean {\n try {\n const value = JSON.parse(requireNative().managedSourceGateSeed(name, bytes)) as { ok?: boolean }\n return value.ok === true\n } catch {\n return false\n }\n}\n\n/**\n * Gate-only: sweep with a keep list handed over exactly as written.\n *\n * The production path serialises its own list, so an unparseable one can never\n * reach native — which makes \"an unreadable keep list deletes nothing\"\n * unprovable through it. Returns what native reported.\n *\n * @experimental\n */\nexport function sweepManagedSourcesRawForGate(rawKeep: string): {\n ok: boolean\n removed: number\n kept: number\n partials: number\n skipped: number\n} {\n try {\n const value = JSON.parse(requireNative().managedSourceGateSweepRaw(rawKeep)) as {\n ok?: boolean\n removed?: number\n kept?: number\n partials?: number\n skipped?: number\n }\n return {\n ok: value.ok === true,\n removed: Number(value.removed ?? 0),\n kept: Number(value.kept ?? 0),\n partials: Number(value.partials ?? 0),\n skipped: Number(value.skipped ?? 0),\n }\n } catch {\n return { ok: false, removed: 0, kept: 0, partials: 0, skipped: 0 }\n }\n}\n\n/** What is stored, for assertions. Tokens and sizes, never names. @experimental */\nexport function managedSourceLedger(): {\n count: number\n bytes: number\n items: { token: string; byteLength: number }[]\n} {\n try {\n const value = JSON.parse(requireNative().managedSourceLedger()) as {\n count?: number\n bytes?: number\n items?: { token: string; byteLength: number }[]\n }\n return {\n count: Number(value.count ?? 0),\n bytes: Number(value.bytes ?? 0),\n items: value.items ?? [],\n }\n } catch {\n return { count: 0, bytes: 0, items: [] }\n }\n}\n\n/**\n * The token a managed URI names, or null if it is not one of ours.\n *\n * Lets a host collect the keep list for {@link sweepManagedSources} straight\n * from a project's sources without storing a second index that could drift.\n *\n * @experimental\n */\nexport function managedSourceToken(uri: string): string | null {\n const match = /\\/([0-9a-f]{64})\\.bin$/.exec(uri)\n return match ? match[1]! : null\n}\n\nfunction parse<T>(\n call: () => string,\n build: (value: Record<string, unknown>) => T,\n): T | ManagedSourceFailure {\n let raw: string\n try {\n raw = call()\n } catch (error) {\n return { ok: false, reason: error instanceof Error ? error.message : String(error) }\n }\n let value: Record<string, unknown>\n try {\n value = JSON.parse(raw) as Record<string, unknown>\n } catch {\n return { ok: false, reason: 'native returned unparseable JSON' }\n }\n if (value.ok !== true) {\n return { ok: false, reason: typeof value.reason === 'string' ? value.reason : 'failed' }\n }\n return build(value)\n}\n","import { requireNative } from './player'\n\n/**\n * The audio a preview plays.\n *\n * ## Preparation, not playback\n *\n * A preview mix is produced by the same code an export runs: every contributing\n * source is decoded, re-timed by its clip's speed, ramped, summed and saturated\n * once, then encoded. That takes real time proportional to the project, and no\n * amount of naming it \"preview\" makes it realtime. A caller has to show that it\n * is preparing and must not pretend otherwise.\n *\n * ## Why not mix in the player\n *\n * Because then there would be two definitions of what a project sounds like,\n * and the one a person hears while editing would be the one that was never\n * tested against an export. Gain, fades, trims, speed and overlap are decided\n * by `compileAudioMixProgram` and executed by `AudioMixer` — here and in the\n * exporter, from the same plan.\n *\n * ## Superseding\n *\n * One job at a time. A newer request cancels the one in flight, which unwinds\n * at its next checkpoint, deletes its partial and reports `superseded`. A\n * superseded result is not a failure and must never be published: it is the\n * answer to a question nobody is asking any more.\n *\n * @experimental\n */\n\n/** A prepared mix. @experimental */\nexport interface PreparedMix {\n ok: true\n /** A `file://` URI in app-private storage. */\n uri: string\n /** The content key: mix program, sources and mixer configuration. */\n key: string\n /** True when nothing had to be encoded — the file was already there. */\n cached: boolean\n byteLength: number\n /**\n * Where the time went, when it was actually prepared.\n *\n * Milliseconds per stage and frames per source. Absent for a cached mix,\n * which did no work to attribute.\n */\n profile?: {\n stageMs: Record<string, number>\n counts: Record<string, number>\n sources: Record<string, Record<string, number>>\n }\n}\n\n/** @experimental */\nexport type PreviewMixResult =\n | PreparedMix\n /** A newer request replaced this one. Not a failure; never publish it. */\n | { ok: false; superseded: true; key: string }\n /** It ran and failed. The reason is diagnostic and may name a path. */\n | { ok: false; superseded?: false; key?: string; reason: string }\n\n/**\n * Prepare the mix for a compiled program, or hand back the one already\n * prepared.\n *\n * `urisJson` maps span id to a resolved, durable source URI — the same\n * resolution an export performs, so the two agree about which file a span\n * means.\n *\n * Never rejects: a mix that cannot be produced is an outcome.\n *\n * @experimental\n */\nexport async function preparePreviewMix(\n jobId: string,\n programJson: string,\n urisJson: string,\n): Promise<PreviewMixResult> {\n let raw: string\n try {\n raw = await requireNative().previewMixPrepare(jobId, programJson, urisJson)\n } catch (error) {\n return { ok: false, reason: error instanceof Error ? error.message : String(error) }\n }\n\n let value: Record<string, unknown>\n try {\n value = JSON.parse(raw) as Record<string, unknown>\n } catch {\n return { ok: false, reason: 'native returned unparseable JSON' }\n }\n\n if (value.ok === true) {\n return {\n ok: true,\n uri: String(value.uri ?? ''),\n key: String(value.key ?? ''),\n cached: value.cached === true,\n byteLength: Number(value.byteLength ?? 0),\n ...(value.profile ? { profile: value.profile as PreparedMix['profile'] } : {}),\n }\n }\n if (value.superseded === true) {\n return { ok: false, superseded: true, key: String(value.key ?? '') }\n }\n return {\n ok: false,\n ...(typeof value.key === 'string' ? { key: value.key } : {}),\n reason: typeof value.error === 'string' ? value.error : 'the mix could not be prepared',\n }\n}\n\n/**\n * Stop whatever is preparing.\n *\n * Safe to call when nothing is: cancelling twice, or cancelling after a job\n * already finished, is how a caller's teardown paths overlap on purpose.\n *\n * @experimental\n */\nexport function cancelPreviewMix(): void {\n try {\n requireNative().previewMixCancel()\n } catch {\n // Nothing to stop, or no adapter. Neither is worth raising during teardown.\n }\n}\n\n/**\n * The key a program would produce, without preparing it.\n *\n * Lets a caller ask \"is this the mix I am already playing?\" before starting a\n * job — which is what stops a selection change, or any edit that does not\n * change the sound, from rebuilding anything.\n *\n * @experimental\n */\nexport function previewMixKey(programJson: string, urisJson: string): string {\n try {\n return requireNative().previewMixKey(programJson, urisJson)\n } catch {\n return ''\n }\n}\n\n/** Drop one prepared mix. @experimental */\nexport function releasePreviewMix(key: string): boolean {\n try {\n const value = JSON.parse(requireNative().previewMixRelease(key)) as { removed?: boolean }\n return value.removed === true\n } catch {\n return false\n }\n}\n\n/**\n * Keep only these mixes; remove the rest and every partial.\n *\n * The bound on the cache. A sweep that cannot read its own result removes\n * nothing, rather than assuming the directory should be empty.\n *\n * @experimental\n */\nexport function sweepPreviewMixes(keepKeys: readonly string[]): {\n removed: number\n kept: number\n partials: number\n skipped: number\n} {\n const none = { removed: 0, kept: 0, partials: 0, skipped: 0 }\n try {\n const value = JSON.parse(requireNative().previewMixSweep(JSON.stringify([...keepKeys]))) as {\n ok?: boolean\n removed?: number\n kept?: number\n partials?: number\n skipped?: number\n }\n if (value.ok !== true) return none\n return {\n removed: Number(value.removed ?? 0),\n kept: Number(value.kept ?? 0),\n partials: Number(value.partials ?? 0),\n skipped: Number(value.skipped ?? 0),\n }\n } catch {\n return none\n }\n}\n\n/** What is prepared, for assertions. @experimental */\nexport function previewMixLedger(): {\n count: number\n bytes: number\n items: { name: string; byteLength: number }[]\n} {\n try {\n const value = JSON.parse(requireNative().previewMixLedger()) as {\n count?: number\n bytes?: number\n items?: { name: string; byteLength: number }[]\n }\n return {\n count: Number(value.count ?? 0),\n bytes: Number(value.bytes ?? 0),\n items: value.items ?? [],\n }\n } catch {\n return { count: 0, bytes: 0, items: [] }\n }\n}\n","import { compileAudioMixProgram, type VideoProject } from '@pictor-sdk/video-core'\n\n/**\n * Deciding what a preview should be doing about audio.\n *\n * Pure, and separate from the hook that acts on it, because every rule worth\n * getting right here is a rule about state rather than about React: whether a\n * project needs a mix at all, whether the mix on hand is the one the document\n * describes, whether a result that just arrived is still wanted, and whether\n * two players have drifted far enough to be worth correcting.\n *\n * ## Two sources of sound, never both\n *\n * Without music, the video player's own audio is the preview and nothing here\n * applies. With music, the mix covers the *whole* timeline — including the\n * audio of the clips themselves — so playing the clips' audio as well would\n * play everything twice. There is no state in which both are audible, and the\n * transition between them is the thing this file exists to sequence.\n *\n * @experimental\n */\n\n/** What a preview's audio is doing. @experimental */\nexport type PreviewAudioStatus =\n /** No music in the project. The video player's own audio is the preview. */\n | 'source'\n /** Music exists and the mix for *this* document is not ready. */\n | 'preparing'\n /** The mix on hand matches the document. Source audio is muted. */\n | 'ready'\n /** The mix could not be prepared. Nothing is played rather than the wrong thing. */\n | 'failed'\n\n/** @experimental */\nexport interface PreviewAudioDecision {\n status: PreviewAudioStatus\n /** The key the document wants. Empty when there is no music. */\n wantedKey: string\n /** Whether a job should be started for `wantedKey`. */\n shouldPrepare: boolean\n /** Whether the video player's own audio must be silenced. */\n muteSourceAudio: boolean\n /** Whether playback may run right now. */\n canPlay: boolean\n}\n\n/** @experimental */\nexport interface PreviewAudioInputs {\n /** The key the current document compiles to, or '' when it has no music. */\n wantedKey: string\n /** The key of the mix already prepared and loaded, if any. */\n readyKey: string | null\n /** The key a job is currently preparing, if any. */\n preparingKey: string | null\n /** The key whose preparation failed, if any. */\n failedKey: string | null\n}\n\n/**\n * Whether a project needs a mixed preview at all.\n *\n * True when anything in the compiled program is audible *and comes from an\n * audio item* — a project whose only sound is clip audio does not need a mix,\n * because the video player already plays exactly that and does it in realtime.\n *\n * @experimental\n */\nexport function previewAudioNeeded(project: VideoProject): boolean {\n return project.audio.length > 0\n}\n\n/**\n * What to do, given what is wanted and what is on hand.\n *\n * @experimental\n */\nexport function decidePreviewAudio(inputs: PreviewAudioInputs): PreviewAudioDecision {\n const { wantedKey, readyKey, preparingKey, failedKey } = inputs\n\n if (wantedKey === '') {\n // No music. The video player's audio is the preview, exactly as it was\n // before any of this existed.\n return {\n status: 'source',\n wantedKey,\n shouldPrepare: false,\n muteSourceAudio: false,\n canPlay: true,\n }\n }\n\n if (readyKey === wantedKey) {\n return {\n status: 'ready',\n wantedKey,\n shouldPrepare: false,\n // The mix contains the clips' audio too. Leaving the source audible\n // would play the whole timeline twice.\n muteSourceAudio: true,\n canPlay: true,\n }\n }\n\n if (failedKey === wantedKey) {\n /*\n Refusing rather than falling back.\n\n The tempting fallback is \"play the clip audio instead\", and it is wrong:\n the person would hear a preview missing its music, with no reliable way to\n tell that from a project whose music is silent. A named failure is worse\n to look at and better to act on.\n */\n return {\n status: 'failed',\n wantedKey,\n shouldPrepare: false,\n muteSourceAudio: true,\n canPlay: false,\n }\n }\n\n /*\n Preparing. A mix for an older document may still be loaded, and it must not\n play: it is the previous edit's sound, which is the most convincing kind of\n wrong. Source audio stays muted throughout, so the transition is silence\n rather than a burst of un-mixed clip audio.\n */\n return {\n status: 'preparing',\n wantedKey,\n shouldPrepare: preparingKey !== wantedKey,\n muteSourceAudio: true,\n canPlay: false,\n }\n}\n\n/** The key a project's audio compiles to, given resolved sources. @experimental */\nexport function previewProgramJson(project: VideoProject): string {\n return JSON.stringify(compileAudioMixProgram(project))\n}\n\n/*\n The drift comparison itself lives in native, not here.\n\n It used to be a function in this file, and it was never wired to a caller —\n measured, a clip seam left the mix 240 ms ahead of the picture and it stayed\n there. Reimplementing it here now would be worse than that: both anchors are\n published on one monotonic clock in native, and a JavaScript twin would have\n no way to read them at the same instant, so the two would disagree about what\n \"at the same time\" meant. See `PreviewTrace.driftCheck`.\n*/\n\n/**\n * The default tolerance.\n *\n * The drift this used to chase is prevented rather than corrected: `PreviewSync`\n * holds both transports through a stall and releases them aligned, which is\n * where all of it came from. A correcting seek during playback costs 200-340 ms\n * on a Pixel 7 — the same size as the error it was chasing, so it oscillated\n * between +264 ms and -198 ms — and realignment now happens while everything is\n * held, where a seek has nothing to chase.\n *\n * So this is back to the tolerance the gates were written against. It is a\n * backstop for something nobody has seen, not the mechanism.\n *\n * @experimental\n */\nexport const PREVIEW_DRIFT_TOLERANCE_US = 60_000\n","import type { TraceEvent } from './previewTrace'\n\n/**\n * The offset, segmented by the marks a gate put in the run.\n *\n * ## Why a whole-run summary is not enough\n *\n * `summariseOffsets` answers \"how bad did it get\", and a gate needs \"where\".\n * Measured on a Pixel 7, a run whose worst excursion was 266 ms spent every\n * moment between its transitions inside ±40 ms: the whole-run worst described\n * three settling transients and nothing else, and a single number cannot tell\n * that from a steady 266 ms error. They are not the same defect and they do not\n * have the same fix.\n *\n * So a run is cut at its marks, and each segment is summarised on its own. The\n * gate verdict is still the run's worst — this localises a failure, it does not\n * soften one.\n *\n * ## Fresh and stale are reported, never traded\n *\n * A stale presented frame is what a person actually sees, so its offset is real\n * and is counted in `worstAbsUs`. But a stale frame's offset grows purely\n * because audio advanced while the picture did not, which is a different fault\n * from two clocks disagreeing — so `freshWorstAbsUs` is reported beside it\n * rather than instead of it. Neither is allowed to stand alone: quoting only\n * the fresh figure is how a run with a nine-second frozen picture passes a\n * synchronisation gate.\n *\n * @experimental\n */\nexport interface PhaseSummary {\n /** The mark that opened this segment, or `'start'` before the first one. */\n label: string\n index: number\n /** Samples in the segment, whatever their state. */\n samples: number\n /** Samples with both players playing — the only ones an offset means anything for. */\n playingSamples: number\n /** Of those, the ones whose presented frame had advanced. */\n freshPlayingSamples: number\n stalePlayingSamples: number\n /** The longest unbroken run of stale samples, in samples. */\n longestStaleRun: number\n minUs: number\n maxUs: number\n meanUs: number\n /** Worst absolute offset over every playing sample, stale included. */\n worstAbsUs: number\n /** Worst over fresh samples only. Reported beside `worstAbsUs`, never instead. */\n freshWorstAbsUs: number\n /** Playing samples outside the tolerance. */\n overToleranceSamples: number\n /**\n * How long the segment took to come inside tolerance and stay there.\n *\n * `-1` when it never did, which is the difference between a transient and a\n * failure that happens to start at a transition. Measured from the segment's\n * first playing sample, so it is a settling time rather than a wall clock.\n */\n settledAfterUs: number\n}\n\n/** @experimental */\nexport interface PhaseReport {\n toleranceUs: number\n phases: PhaseSummary[]\n /** The run's worst, over every phase. This is the gate. */\n worstAbsUs: number\n freshWorstAbsUs: number\n overToleranceSamples: number\n /** Whether every playing sample in the run was inside tolerance. */\n pass: boolean\n}\n\n/**\n * The prefix a gate's own marks carry.\n *\n * Every mark is recorded as a `mark` event carrying its label, and the trace is\n * also full of system events — holds, releases, drift corrections, transport\n * transitions. Cutting on \"not a sample\" would start a new phase at every one\n * of those, and cutting on every mark would let an ad-hoc label reshape a gate's\n * phases. So only marks whose label carries this prefix cut the run.\n *\n * @experimental\n */\nexport const PHASE_MARK_PREFIX = 'phase:'\n\nfunction num(v: unknown): number {\n return typeof v === 'number' ? v : Number(v)\n}\n\nfunction empty(label: string, index: number): PhaseSummary {\n return {\n label,\n index,\n samples: 0,\n playingSamples: 0,\n freshPlayingSamples: 0,\n stalePlayingSamples: 0,\n longestStaleRun: 0,\n minUs: 0,\n maxUs: 0,\n meanUs: 0,\n worstAbsUs: 0,\n freshWorstAbsUs: 0,\n overToleranceSamples: 0,\n settledAfterUs: 0,\n }\n}\n\n/**\n * Summarise a drained run, cut at its mark events.\n *\n * @experimental\n */\nexport function summarisePhases(\n events: readonly TraceEvent[],\n toleranceUs: number,\n): PhaseReport {\n const phases: PhaseSummary[] = []\n let current = empty('start', 0)\n // Accumulators kept outside the summary so the published shape stays plain\n // data — a gate serialises this straight into a log line.\n let sum = 0\n let staleRun = 0\n let firstPlayingNs = 0\n let lastOverNs = 0\n let sawPlaying = false\n let stillOver = false\n\n const close = () => {\n current.meanUs = current.playingSamples > 0 ? Math.round(sum / current.playingSamples) : 0\n /*\n Settling is measured to the *last* excursion, not the first return.\n\n A transient that goes out, comes back and goes out again has not settled\n at the first return, and reporting that would turn the worst case into the\n best-looking number available. -1 for a phase still outside tolerance at\n its final sample: that is not a transient at all.\n */\n current.settledAfterUs =\n current.overToleranceSamples === 0\n ? 0\n : stillOver\n ? -1\n : Math.round((lastOverNs - firstPlayingNs) / 1000)\n phases.push(current)\n sum = 0\n staleRun = 0\n firstPlayingNs = 0\n lastOverNs = 0\n sawPlaying = false\n stillOver = false\n }\n\n for (const e of events) {\n if (e.type !== 'sample') {\n const label = e.type === 'mark' ? String(e.v.label ?? '') : ''\n if (label.startsWith(PHASE_MARK_PREFIX)) {\n close()\n current = empty(label.slice(PHASE_MARK_PREFIX.length), phases.length)\n }\n continue\n }\n current.samples += 1\n if (e.v.videoPlaying !== true || e.v.audioPlaying !== true) {\n staleRun = 0\n continue\n }\n const offset = num(e.v.offsetUs)\n if (!Number.isFinite(offset)) continue\n current.playingSamples += 1\n if (!sawPlaying) {\n sawPlaying = true\n firstPlayingNs = num(e.atNs)\n }\n sum += offset\n if (current.playingSamples === 1) {\n current.minUs = offset\n current.maxUs = offset\n } else {\n current.minUs = Math.min(current.minUs, offset)\n current.maxUs = Math.max(current.maxUs, offset)\n }\n const abs = Math.abs(offset)\n if (abs > current.worstAbsUs) current.worstAbsUs = abs\n if (abs > toleranceUs) {\n current.overToleranceSamples += 1\n lastOverNs = num(e.atNs)\n stillOver = true\n } else {\n stillOver = false\n }\n if (e.v.pfFresh === true) {\n current.freshPlayingSamples += 1\n if (abs > current.freshWorstAbsUs) current.freshWorstAbsUs = abs\n staleRun = 0\n } else {\n current.stalePlayingSamples += 1\n staleRun += 1\n if (staleRun > current.longestStaleRun) current.longestStaleRun = staleRun\n }\n }\n close()\n\n const worstAbsUs = phases.reduce((w, p) => Math.max(w, p.worstAbsUs), 0)\n const freshWorstAbsUs = phases.reduce((w, p) => Math.max(w, p.freshWorstAbsUs), 0)\n const overToleranceSamples = phases.reduce((n, p) => n + p.overToleranceSamples, 0)\n return {\n toleranceUs,\n phases,\n worstAbsUs,\n freshWorstAbsUs,\n overToleranceSamples,\n pass: overToleranceSamples === 0,\n }\n}\n","import type { SpikeSample } from './compositionSpike'\n\n/**\n * Where a shared-clock run left the tolerance, and why.\n *\n * ## What is being compared\n *\n * The audio clock's position and the presented frame's timestamp, brought to\n * one instant. The frame was latched at `frameConsumedNs`; the position was\n * read at `atNs`. Rolling the position back to the latch is what puts them on\n * the same monotonic instant — the composition runs at project rate 1x, so the\n * roll-back is the elapsed time itself.\n *\n * ## Which boundary this observes\n *\n * The **latch**, not the display. `frameConsumedNs` is when the consumer called\n * `updateTexImage`; `frameReleaseNs` is the time Media3 *asked* the surface to\n * present at, which is an intention and not an acknowledgement. Actual scanout\n * happens one compositor pass later and is not observed anywhere in this path.\n * So an offset here is \"audio against latched picture\", and the display adds a\n * further unmeasured delay common to every sample.\n *\n * ## Uncertainty is stated, never subtracted\n *\n * Comparing a 30 fps frame's timestamp against a continuous clock carries an\n * irreducible ±16.7 ms, and the paired read adds a few more. That is reported\n * beside the verdict. It is not deducted from an excursion to bring it under\n * the line: a 95 ms excursion is not a 78 ms excursion because the instrument\n * is imperfect.\n *\n * @experimental\n */\n\n/** @experimental */\nexport const SPIKE_TOLERANCE_US = 60_000\n\n/**\n * Why one sample left the tolerance.\n *\n * Ordered by precedence, because more than one can be true and the first is the\n * one that explains the sample: a frame from before a seek is cross-epoch\n * whatever else is also true of it.\n *\n * @experimental\n */\nexport type ExcursionCause =\n /**\n * The latched frame predates the moment playback was released.\n *\n * Not a filter and not a drift. The readiness gate releases on a frame whose\n * *position* matches the epoch, and that frame may have been latched long\n * before — measured at 13.9 s, the whole interval between attaching the\n * composition and the person pressing play. The picture was right; the\n * frame's wall-clock age simply includes a pause, and charging that to\n * synchronisation measures the pause.\n *\n * Reported rather than excluded, because the size of it is the size of the\n * interval in which the first sample after a release is not yet a\n * synchronisation reading.\n */\n | 'pre-release-frame'\n /** The frame belongs to a different transition epoch or frame generation. */\n | 'cross-epoch'\n /** The frame was latched long enough ago that it is not a current reading. */\n | 'stale-pairing'\n /** The first frames after a seek or after readiness, before the picture caught up. */\n | 'first-frame-after-seek'\n /** The frame was latched well after the time it was asked to be presented at. */\n | 'delayed-presentation'\n /** The audio clock moved discontinuously between adjacent samples. */\n | 'audio-discontinuity'\n /** The mapped project time disagrees with the frame's own window and PTS. */\n | 'mapping'\n /** None of the above fitted. Deliberately not folded into a neighbour. */\n | 'unexplained'\n\n/** @experimental */\nexport interface Excursion {\n seq: number\n /** Against the latch: observed, and earlier than display. */\n offsetUs: number\n /** Against the time the frame was asked to appear. An intention. */\n offsetAtIntendedUs: number\n cause: ExcursionCause\n epoch: number\n sinceEpochMs: number\n frameWindow: number\n framePtsUs: number\n /** Latched minus asked-for. Large means the picture arrived late. */\n latchLagUs: number\n /** How old the latched frame was when the position was read. */\n frameAgeUs: number\n /** The audio clock's step from the previous sample, in microseconds. */\n positionStepUs: number\n /** The wall-clock step from the previous sample. */\n wallStepUs: number\n}\n\n/** @experimental */\nexport interface ExcursionReport {\n toleranceUs: number\n /** Samples where playback was declared active. Only these are judged. */\n activeSamples: number\n /** Samples before readiness, exposed rather than dropped. */\n preparingSamples: number\n excursions: Excursion[]\n byCause: Record<string, number>\n /**\n * Samples still showing a frame latched before the release.\n *\n * Reported apart, and *retained* in the acceptance set: they are real\n * playback, and if the picture never catches up they are the only thing that\n * says so.\n */\n preReleaseSamples: number\n preReleaseWorstUs: number\n worstAbsUs: number\n /** The same run judged at the intended-presentation boundary. */\n worstAtIntendedUs: number\n overAtIntended: number\n passAtIntended: boolean\n /** Mean latch-minus-intended over the excursions. Negative means early. */\n meanLatchLagUs: number\n pass: boolean\n uncertainty: {\n /** Half a frame interval at the measured rate. Irreducible. */\n frameQuantumUs: number\n /** The widest paired read in the run. */\n readSpanUs: number\n positionQuantumUs: number\n }\n}\n\n/**\n * The offset at each observable boundary.\n *\n * They are not the same instant and the difference is not noise. Measured on a\n * Pixel 7, the consumer latches a buffer about 34 ms *before* the time Media3\n * asked the surface to present it — `releaseOutputBuffer(index, releaseTimeNs)`\n * queues immediately with a desired present time in the future, and the\n * SurfaceTexture consumer takes it as soon as it is queued.\n *\n * So `atLatch` compares the audio clock against a frame that is in the texture\n * but has not been displayed, and is systematically early by that gap;\n * `atIntended` compares it against when that frame was *meant* to appear, which\n * is the best available estimate of what a viewer experiences and is still an\n * intention rather than an acknowledgement. The true display instant lies at or\n * after `atIntended`, and nothing in this process observes it.\n *\n * Both are returned because choosing one silently is how a boundary decision\n * becomes a verdict.\n *\n * @experimental\n */\nexport function offsetAt(\n sample: SpikeSample,\n /**\n * When the current playing interval was released, if known.\n *\n * A frame latched while paused carries the pause in its age. Charging that to\n * synchronisation measured a 13.9 second \"error\" for a picture that was\n * showing exactly the right position. Clamping the reference to the release\n * instant charges only the time playback has actually been running — so at\n * release the offset is (audio position - displayed PTS), and it grows from\n * there if the audio advances while the picture stays put.\n *\n * That growth is the point: this corrects the baseline without removing\n * coverage. A frame that is never replaced still accrues the offset it\n * deserves.\n */\n releaseAtNs = 0,\n): { atLatch: number; atIntended: number } {\n /*\n A frame already on screen when playback began has no age to subtract.\n\n For a frame presented during playback, the sound being ahead of it by the\n time since it appeared is not error — the frame is a sample of the recent\n past. Subtracting that age is what makes the measure a synchronisation\n error rather than a restatement of the frame rate.\n\n A pre-release frame is different in kind. It was displayed before the clock\n started, so there is no elapsed-since-display to discount: at the moment of\n release the comparison is simply \"what is on screen\" against \"where the\n sound is\", which is near zero when the readiness gate did its job. And if\n the picture then stays put while the sound runs on, the gap is exactly the\n staleness a viewer sees — so it is counted, not cancelled. Subtracting the\n age here would have the two grow together and report zero forever.\n */\n if (releaseAtNs > 0 && sample.frameConsumedNs < releaseAtNs) {\n const raw = sample.positionUs - sample.framePtsUs\n return { atLatch: raw, atIntended: raw }\n }\n return {\n atLatch: sample.positionUs - (sample.atNs - sample.frameConsumedNs) / 1000 - sample.framePtsUs,\n atIntended:\n sample.positionUs - (sample.atNs - sample.frameReleaseNs) / 1000 - sample.framePtsUs,\n }\n}\n\n/**\n * Classify a run's over-tolerance samples.\n *\n * `frameIntervalUs` is the source's nominal frame interval; half of it is the\n * irreducible quantisation of comparing a discrete frame against a continuous\n * clock.\n *\n * @experimental\n */\nexport function classifyExcursions(\n samples: readonly SpikeSample[],\n options: { toleranceUs?: number; frameIntervalUs?: number } = {},\n): ExcursionReport {\n const toleranceUs = options.toleranceUs ?? SPIKE_TOLERANCE_US\n const frameIntervalUs = options.frameIntervalUs ?? 33_333\n\n /*\n Judged only once playback is active, and the rest is exposed rather than\n dropped. A position that has not started moving is not synchronised or\n unsynchronised; counting it either way is a statement about nothing.\n */\n /*\n A frame from before this epoch is not a reading about this epoch.\n\n After a start or a seek the clock moves immediately while the first frame\n for the new position is still being decoded, so the newest latched frame is\n the one from *before* the transition. Pairing it against the moving clock\n produces an excursion the size of the seek — measured here at up to 13.8 s\n — and no classification of that number is about synchronisation.\n\n So the interval is exposed as preparing rather than judged, which is also\n what makes \"include every sample once playback is active\" mean something:\n active begins when the picture for this epoch has actually arrived.\n */\n /*\n And the clock has to be moving.\n\n `isPlaying` and `STATE_READY` both go true before the composition's clock\n starts: measured, the player reported both for fourteen seconds with its\n position at zero and exactly one frame ever latched. Judging that interval\n produced a 13.9 second \"excursion\" that is not a synchronisation reading at\n all — it is the preparation the brief asks to be exposed rather than scored.\n\n Observed rather than assumed: the position must have changed somewhere in\n the preceding window, so the test is \"this clock is running\" and not \"this\n player says it is\".\n */\n const ADVANCE_WINDOW_NS = 200_000_000\n const advancing = samples.map((s, i) => {\n for (let j = i - 1; j >= 0; j -= 1) {\n if (s.atNs - samples[j]!.atNs > ADVANCE_WINDOW_NS) break\n if (samples[j]!.positionUs !== s.positionUs) return true\n }\n return false\n })\n\n /*\n When the current playing interval began.\n\n A frame latched before it carries the pause in its age, so the first\n samples after a release are not yet synchronisation readings. Tracked so\n they can be *named* rather than dropped.\n */\n const releaseAtNs: number[] = []\n let intervalStart = 0\n for (let i = 0; i < samples.length; i += 1) {\n if (!advancing[i]) intervalStart = 0\n else if (intervalStart === 0) intervalStart = samples[i]!.atNs\n releaseAtNs.push(intervalStart)\n }\n\n const isActive = (s: SpikeSample, i: number) =>\n s.playing &&\n s.ready &&\n s.framePtsUs >= 0 &&\n s.frameConsumedNs > 0 &&\n // The picture for this epoch has arrived.\n s.frameConsumedNs >= s.atNs - s.sinceEpochNs &&\n advancing[i] === true\n const active = samples.filter(isActive)\n const preparing = samples.length - active.length\n\n const excursions: Excursion[] = []\n const byCause: Record<string, number> = {}\n let worstAbsUs = 0\n let worstAtIntendedUs = 0\n let overAtIntended = 0\n let readSpanUs = 0\n let latchLagSum = 0\n let preReleaseSamples = 0\n let preReleaseWorstUs = 0\n\n // Indexed by the sample's own position in the full run, because the release\n // boundary was computed there and `active` has holes.\n const releaseFor = new Map<number, number>()\n samples.forEach((s, i) => releaseFor.set(s.seq, releaseAtNs[i]!))\n\n for (let i = 0; i < active.length; i += 1) {\n const s = active[i]!\n const previous = i > 0 ? active[i - 1]! : null\n readSpanUs = Math.max(readSpanUs, s.readSpanNs / 1000)\n const releasedAt = releaseFor.get(s.seq) ?? 0\n const both = offsetAt(s, releasedAt)\n const offsetUs = both.atLatch\n const abs = Math.abs(offsetUs)\n const absIntended = Math.abs(both.atIntended)\n if (releasedAt > 0 && s.frameConsumedNs < releasedAt) {\n preReleaseSamples += 1\n if (abs > preReleaseWorstUs) preReleaseWorstUs = abs\n }\n if (abs > worstAbsUs) worstAbsUs = abs\n if (absIntended > worstAtIntendedUs) worstAtIntendedUs = absIntended\n if (absIntended > toleranceUs) overAtIntended += 1\n if (abs <= toleranceUs) continue\n\n const frameAgeUs = (s.atNs - s.frameConsumedNs) / 1000\n const latchLagUs = (s.frameConsumedNs - s.frameReleaseNs) / 1000\n latchLagSum += latchLagUs\n const positionStepUs = previous ? s.positionUs - previous.positionUs : 0\n const wallStepUs = previous ? (s.atNs - previous.atNs) / 1000 : 0\n\n /*\n First match wins, and the order is the point.\n\n A frame carried across a seek is cross-epoch whether or not it is also\n old and also late; calling it \"delayed presentation\" would send the fix\n to the renderer instead of to the epoch check.\n */\n let cause: ExcursionCause\n if (releasedAt > 0 && s.frameConsumedNs < releasedAt) {\n cause = 'pre-release-frame'\n } else if (s.frameConsumedNs > 0 && s.frameConsumedNs < s.atNs - s.sinceEpochNs) {\n cause = 'cross-epoch'\n } else if (previous && s.frameGeneration !== previous.frameGeneration) {\n cause = 'cross-epoch'\n } else if (frameAgeUs > 2 * frameIntervalUs) {\n cause = 'stale-pairing'\n } else if (s.sinceEpochNs / 1000 < 500_000) {\n cause = 'first-frame-after-seek'\n } else if (latchLagUs > frameIntervalUs) {\n cause = 'delayed-presentation'\n } else if (previous && Math.abs(positionStepUs - wallStepUs) > toleranceUs) {\n cause = 'audio-discontinuity'\n } else if (previous && s.frameWindow !== previous.frameWindow) {\n cause = 'mapping'\n } else {\n cause = 'unexplained'\n }\n\n byCause[cause] = (byCause[cause] ?? 0) + 1\n excursions.push({\n seq: s.seq,\n offsetUs: Math.round(offsetUs),\n offsetAtIntendedUs: Math.round(both.atIntended),\n cause,\n epoch: s.epoch,\n sinceEpochMs: Math.round(s.sinceEpochNs / 1_000_000),\n frameWindow: s.frameWindow,\n framePtsUs: s.framePtsUs,\n latchLagUs: Math.round(latchLagUs),\n frameAgeUs: Math.round(frameAgeUs),\n positionStepUs: Math.round(positionStepUs),\n wallStepUs: Math.round(wallStepUs),\n })\n }\n\n return {\n toleranceUs,\n activeSamples: active.length,\n preparingSamples: preparing,\n excursions,\n byCause,\n preReleaseSamples,\n preReleaseWorstUs: Math.round(preReleaseWorstUs),\n worstAbsUs: Math.round(worstAbsUs),\n worstAtIntendedUs: Math.round(worstAtIntendedUs),\n overAtIntended,\n passAtIntended: active.length > 0 && overAtIntended === 0,\n meanLatchLagUs: excursions.length\n ? Math.round(latchLagSum / excursions.length)\n : 0,\n // No samples is not a pass. A run that never played has not been judged.\n pass: active.length > 0 && excursions.length === 0,\n uncertainty: {\n frameQuantumUs: Math.round(frameIntervalUs / 2),\n readSpanUs: Math.round(readSpanUs),\n positionQuantumUs: 1000,\n },\n }\n}\n\n/**\n * The samples around one excursion, for looking at rather than inferring from.\n *\n * @experimental\n */\nexport function windowAround(\n samples: readonly SpikeSample[],\n seq: number,\n spanUs = 250_000,\n): SpikeSample[] {\n const centre = samples.find((s) => s.seq === seq)\n if (!centre) return []\n const spanNs = spanUs * 1000\n return samples.filter((s) => Math.abs(s.atNs - centre.atNs) <= spanNs)\n}\n","import { resampleClip, type ResampleRequest } from '@pictor-sdk/video-core'\nimport type { SpikeSample } from './compositionSpike'\nimport type { SubmissionRecord } from './submissionScoring'\n\n/**\n * Did the preview show the right frame at each output instant?\n *\n * ## The oracle this replaces\n *\n * The first version counted *source* frames and expected every one to appear.\n * That reported a 2x clip — 6 seconds of source occupying 3 seconds — as\n * delivering 98 of 180 frames with 58 missed intervals. The 180 was wrong: a\n * preview has its own output rate, and at 30 fps three seconds of timeline has\n * about 90 output instants, at each of which the resampling rule *skips* every\n * second source frame by design. Requiring all 180 asked the preview to run at\n * 60 fps and then scored it for refusing.\n *\n * So the oracle is the shared rule itself. `resampleClip` already decides which\n * source frame belongs at each output instant, at a stated target rate, and it\n * is the same function the exporter's contract is written against. Using it\n * here means the preview and the export cannot disagree about what \"correct\"\n * means.\n *\n * ## What is checked\n *\n * At each expected output instant, whether the picture on screen is the frame\n * the rule names. Not whether every source frame appeared — above 1x most of\n * them must not.\n *\n * @experimental\n */\n\n/** One clip, as the playlist compiled it. @experimental */\nexport interface CadenceClip {\n name: string\n /** Where the window sits on the project timeline. */\n timelineStartUs: number\n timelineEndUs: number\n /** Clipping bounds in source time. */\n sourceStartUs: number\n sourceEndUs: number\n speed: number\n sourceFps: number\n}\n\n/**\n * How far the preview may sit from the export's frame selection.\n *\n * The tolerance the rest of the video path is held to. A preview picture is\n * temporally accurate when the frame it shows is within this of the frame the\n * rule names, converted to project time — not when it is that exact frame.\n *\n * @experimental\n */\nexport const PREVIEW_TOLERANCE_US = 60_000\n\n/**\n * How far a correctly rendered picture may be from its own frame's content.\n *\n * The level tolerance absorbs the codec: a flat grey through H.264 at 4:2:0 and\n * back through a scaler does not return the exact value it went in as. The bar\n * tolerance is a little over one source row at the fixture's height, which is\n * what an interpolating scaler can move an edge by. Neither absorbs a flip, an\n * offset or a wrong scale, which move the bar by whole per cents.\n */\nconst PARITY_LEVEL_TOLERANCE = 12\nconst PARITY_BAR_TOLERANCE = 4 / 960\n\n/**\n * What a run established, as three outcomes rather than two.\n *\n * `invalid` is not a soft failure — it is the absence of a measurement, and it\n * must never be reachable by a run that simply had the probe switched off. A\n * boolean cannot say that: `false` reads as \"measured and wrong\", and three\n * device runs once reported `wrong=0, worstOffset=0.0 ms` because a shifted\n * button meant nothing was scorable at all. That was a clean pass by absence of\n * evidence and one paste from being reported as a fix working.\n *\n * @experimental\n */\nexport type Verdict = 'pass' | 'fail' | 'invalid'\n\n/** Rendering correctness, per clip. @experimental */\nexport interface CadenceParity {\n /** Submissions that carried a parity measurement. */\n measured: number\n /** Worst background-level error, 0..255. */\n worstLevelDelta: number\n /** Worst bar-position error, in 0..1 of height. */\n worstBarDelta: number\n /** Submissions whose rendering was outside tolerance. */\n failures: number\n pass: boolean\n}\n\n/** @experimental */\nexport interface CadenceResult {\n clip: string\n /** Output instants the rule names for the whole clip. */\n nominalInstants: number\n /** Output instants the rule expects across the span actually covered. */\n expectedInstants: number\n /**\n * Instants that could not be placed on the wall clock.\n *\n * No bracketing pair of player-clock samples inside one segment — the\n * transport was paused or seeking across them. Counted and excluded from\n * scoring rather than placed by extrapolation.\n */\n unplaceableInstants: number\n /**\n * How far the frame-derived clock sits from the player's, in microseconds.\n *\n * A diagnostic, never the anchor. Positive means pictures are handed over\n * later than the project time they carry — the pipeline's systematic\n * presentation delay.\n *\n * Anchoring on the frame-derived value would subtract this from every\n * measurement and call a uniformly late pipeline punctual, which is why\n * instants are placed from the player's clock and this is only reported.\n */\n frameDerivedOffsetUs: number\n /**\n * Instants before the first submission, and after the last.\n *\n * Named rather than absorbed. Scoring a clip over its nominal length would\n * count every instant a run never reached as a missed deadline; excluding\n * them silently would let a picture that arrived late or stopped early\n * disappear from coverage instead. They are excluded from the identity check\n * and reported here, and `leadingExcluded` in particular is the interval\n * where the previous clip was still on screen — which is the seam, and is\n * scored by the submission metric rather than by this one.\n */\n leadingExcluded: number\n trailingExcluded: number\n /** Instants where some frame was on screen. */\n coveredInstants: number\n /**\n * Instants showing a different source frame from the one the rule names.\n *\n * The real failure: a missed deadline is the picture being wrong at a moment\n * somebody was looking, not a source frame going unused.\n */\n wrongFrameInstants: number\n /** Instants with no frame at all — the picture had not caught up. */\n uncoveredInstants: number\n /** Unique pictures submitted inside the span. */\n submittedUnique: number\n /**\n * Source frames the rule says to skip.\n *\n * Above 1x this is most of them and is correct. Reported so a count of\n * \"missing\" frames cannot be read as loss.\n */\n ruleSkippedSourceFrames: number\n /** The interval between output instants, from the declared preview rate. */\n outputIntervalUs: number\n medianHoldUs: number\n maxHoldUs: number\n /**\n * How long one picture should stay up, from the rule.\n *\n * At 2x a new source frame is due at every output instant; at 0.5x the rule\n * repeats each one, so a hold of two instants is correct rather than a stall.\n */\n expectedHoldUs: number\n /**\n * Pictures held past their turn.\n *\n * Counted from the holds rather than from the covered span, because a stall\n * at the end of a run produces no further submissions and would otherwise\n * simply shorten the span it was scored over.\n */\n missedDeadlines: number\n /**\n * How far off the shown frame was, counted in source frames.\n *\n * Keyed by `shown - expected`, over every scored instant whose identity was\n * established — the correct ones included, so the shape of the whole run is\n * visible rather than only its tail. This is what separates a picture that is\n * one source frame early from one that is a third of a second stale, and at\n * 2x those are 8.3 ms and 330 ms of project time: the same count of \"wrong\n * instants\" and completely different defects.\n */\n offsets: Record<string, number>\n /**\n * The largest offset, in project time rather than in source frames.\n *\n * The conversion that makes the count mean something. One source frame at\n * speed `s` is `1 / sourceFps / s` of project time — 16.7 ms at 2x, 66.7 ms\n * at 0.5x — so the same offset of three frames is 50 ms in one clip and 200\n * ms in another. Judged against the synchronisation tolerance, not against\n * zero: a preview consumes a live decoder at its own display rate and cannot\n * seek to an exact source frame per instant the way an export does.\n */\n worstOffsetUs: number\n /**\n * Rendering correctness, against the frame each picture's own band names.\n *\n * Separate from everything above it. The rest of this result is about *which*\n * frame was shown; this is about whether that frame was drawn correctly —\n * colour, orientation and scale. A picture one source frame early and\n * rendered perfectly scores zero here.\n */\n parity: CadenceParity\n /**\n * Instants that showed exactly the frame the export rule names.\n *\n * Reported, never required. It is the export contract's measure, kept here so\n * the preview's tolerance is visible as a decision rather than as the absence\n * of a check.\n */\n exactInstants: number\n /**\n * What this run established. @see Verdict\n *\n * `pass` only when identity was read from pixels, parity was measured, and\n * both were clean. Anything unmeasured is `invalid`.\n */\n verdict: Verdict\n /** Why, when the verdict is not a pass. */\n verdictReason: string\n /** Whether the acquired-identity evidence was usable at all. */\n identity: CadenceIdentity\n /**\n * Wall-clock gaps between this clip's submissions.\n *\n * Per clip because the aggregate mixes rates: a 0.5x clip legitimately holds\n * each picture for two output intervals, so a global p95 at 66.7 ms is the\n * 0.5x clip behaving correctly and says nothing about whether the 2x clip\n * stalled. Reported separately so a delay cannot hide inside a hold.\n */\n frameTimeUs: { p50: number; p95: number; p99: number; max: number; samples: number }\n /**\n * Every instant whose picture was not the one the rule names.\n *\n * Including the boundary instants the scored span excludes: those are the\n * seam, and leaving them out of the identity check is how a wrong picture at\n * a clip change disappears from both metrics at once.\n */\n wrong: WrongInstant[]\n pass: boolean\n}\n\n/**\n * Why an instant showed the wrong picture.\n *\n * The first three are decided in pixels and name one place to look. The fourth\n * is what the metadata alone can support, which is less than it appears: on\n * this device `SurfaceTexture` reports a system-clock value, so an instant\n * classified without the marker cannot separate a frame acquired wrongly from a\n * frame chosen wrongly, and says so rather than picking one.\n *\n * @experimental\n */\nexport type WrongInstantCause =\n /**\n * The expected frame's pixels were not in the ring when the instant needed\n * them.\n *\n * Nothing downstream could have shown it. The fault is decode, acquisition or\n * the copy into the ring — and the test is by that instant, not by the end of\n * the clip, because every frame arrives eventually and a clip-global check\n * would charge a frame that had simply not arrived yet to selection.\n */\n | 'not-delivered'\n /**\n * The expected frame's pixels were in the ring, and a different frame was\n * submitted.\n *\n * The fault is the choice, not the supply.\n */\n | 'wrong-selection'\n /**\n * The submitted picture no longer held the frame its slot was filled with.\n *\n * A wrapped texture aliases storage the ring may redraw. This is that storage\n * changing under a picture composition still holds, and it is the one cause\n * here that is a correctness defect in the ring's own contract.\n */\n | 'storage-aliased'\n /**\n * The wrong picture, and the evidence cannot say whether it was acquired or\n * chosen wrongly.\n *\n * What the metadata supports with the probe off. Not a third fault: a\n * placeholder for an attribution not yet made.\n */\n | 'wrong-submitted-identity'\n /**\n * The right frame, submitted after the instant it belonged to.\n *\n * Only reachable without pixel identity. When the marker is readable, a frame\n * matching the rule makes the instant correct however late it was submitted,\n * so lateness stops being an identity fault and becomes a frame-time one.\n * Its evidence — the ring's own submission time and project timestamp — is\n * independent of the disputed pairing, which is why it survives there.\n */\n | 'late-delivery'\n /** No submission covered the instant at all. */\n | 'uncovered'\n\n/** @experimental */\nexport interface WrongInstant {\n projectUs: number\n /** From the shared rule: the source frame that belongs at this instant. */\n expectedSourceFrame: number\n expectedSourcePtsUs: number\n /** What was on screen, mapped back through the same speed. */\n shownSourceFrame: number\n shownPtsUs: number\n /** What the texture said it held when that frame was acquired. */\n acquiredTexturePtsUs: number\n /** The frame index in the pixels of the slot, and of the submitted picture. */\n acquiredMarker: number\n submittedMarker: number\n slot: number\n frameId: number\n submittedNs: number\n cause: WrongInstantCause\n /** True when the instant lies outside the scored span. */\n boundary: boolean\n}\n\n/** What the texture timestamp turned out to be. @experimental */\nexport type TimestampDomain =\n /** Comparable with the clip's media time. */\n | 'media'\n /** A system-clock value: `System.nanoTime()`, as measured on this device. */\n | 'system'\n /** Nothing was recorded, so there is nothing to establish. */\n | 'unknown'\n\n/** @experimental */\nexport interface CadenceIdentity {\n /**\n * The domain the texture timestamps were established to be in.\n *\n * Recorded rather than assumed, and recorded even when it is the one that\n * makes the check weaker. It was `media` by assumption for one whole run and\n * the assumption was wrong by forty-eight hours.\n */\n timestampDomain: TimestampDomain\n medianTextureSkewUs: number\n /**\n * Whether identity came from the pixels.\n *\n * When false, the fallback compares submitted timestamps — which on a\n * `system` domain cannot uniquely associate a buffer with a frame. Every\n * instant is then counted `identityUnverified` and the clip cannot pass. A\n * fallback that quietly declared those instants correct would be restoring\n * confidence the evidence does not support.\n */\n pixelIdentity: boolean\n /** Instants whose identity could not be established either way. */\n identityUnverified: number\n /** Distinct frame indices read out of the ring for this clip. */\n deliveredFrames: number\n /** Bands that were read, and bands that failed. */\n markersRead: number\n markersFailed: number\n}\n\n/**\n * The preview's own output rate.\n *\n * Stated rather than inferred from what arrived: an oracle that takes its\n * expectation from the measurement cannot fail.\n *\n * @experimental\n */\nexport const PREVIEW_OUTPUT_FPS = 30\n\n/**\n * Which source frame was on screen at `projectUs`, from what was submitted.\n *\n * The last submission at or before the instant — the same \"last one at or\n * before\" rule `resampleClip` samples the source with, because a nearest-match\n * would credit a frame that had not been shown yet.\n */\n/**\n * When an instant of project time happened, on the monotonic clock.\n *\n * Built from the player's own reported position paired with the monotonic time\n * it was read at — two values the pump records together and neither derived\n * from a frame. That independence is the point: an offset computed from\n * submission records folds any systematic presentation delay into the anchor,\n * so a pipeline uniformly late looks punctual to itself.\n *\n * Segmented by epoch and by whether the transport was running. A pause or a\n * seek breaks the correspondence between project time and the wall clock\n * entirely, and interpolating across one would place an instant at a moment\n * that never occurred. Instants with no bracketing pair inside a single segment\n * return null rather than a guess.\n */\nfunction wallClockAt(\n samples: readonly SpikeSample[],\n projectUs: number,\n intervalUs: number,\n): number | null {\n let before: SpikeSample | null = null\n let after: SpikeSample | null = null\n for (const s of samples) {\n if (!s.playing || s.positionUs < 0 || s.atNs <= 0) continue\n if (s.positionUs <= projectUs && (before === null || s.positionUs > before.positionUs)) before = s\n if (s.positionUs >= projectUs && (after === null || s.positionUs < after.positionUs)) after = s\n }\n /*\n One interval past the last sample, and no further.\n\n The final instant of a clip has nothing after it to bracket with, and\n refusing it would make every real run end in an unplaceable instant and\n report `invalid`. Extrapolating one interval at the project rate is exact\n enough for a span that short; beyond it the transport may have stopped and\n there is no evidence either way.\n */\n if (after === null) {\n if (before === null || projectUs - before.positionUs > intervalUs) return null\n return before.atNs + (projectUs - before.positionUs) * 1000\n }\n /*\n And one interval before the first sample, symmetrically.\n\n An instant can land just ahead of the earliest reading — the output grid and\n the poller are not aligned, and the grid's rounding puts one instant of a\n clip on the wrong side of it. The same bound applies: inside an interval the\n project rate places it exactly, beyond it there is no evidence.\n */\n if (before === null) {\n if (after.positionUs - projectUs > intervalUs) return null\n return after.atNs - (after.positionUs - projectUs) * 1000\n }\n // Different segments: a pause or a seek sits between them.\n if (before.epoch !== after.epoch) return null\n if (after.positionUs === before.positionUs) return before.atNs\n const t = (projectUs - before.positionUs) / (after.positionUs - before.positionUs)\n return before.atNs + t * (after.atNs - before.atNs)\n}\n\n/**\n * The picture on screen at an instant — as of that instant.\n *\n * Selected by when it was submitted, not only by what time it claims. Choosing\n * the greatest `ptsUs` at or before the instant let a frame submitted long\n * afterwards be credited to it: the screen showed something older at the time,\n * and the late arrival made the instant correct retroactively. A frame that had\n * not been handed to composition yet was not on screen, whatever its timestamp\n * says.\n *\n * The instant's wall-clock time is derived from the clip's first submission and\n * the project rate, which is 1x for the composition — the same assumption the\n * audio pairing documents and relies on.\n */\nfunction displayedAt(\n submissions: readonly SubmissionRecord[],\n projectUs: number,\n atNs: number | null,\n intervalNs: number,\n): SubmissionRecord | null {\n /*\n One output interval of slack, because an instant is an interval.\n\n An output instant is not a point in time — it is the span during which its\n picture should be on screen. A frame handed over anywhere inside that span\n was shown for part of it, and excluding it would charge the grid's own\n resolution to the pipeline. A frame arriving after the span has closed was\n never on screen for it, which is the case this rule exists to exclude.\n */\n if (atNs === null) return null\n const deadlineNs = atNs + intervalNs\n let best: SubmissionRecord | null = null\n for (const s of submissions) {\n if (s.ptsUs > projectUs) continue\n // Not yet submitted when the instant passed: it was not the picture.\n if (s.submittedNs > deadlineNs) continue\n if (best === null || s.submittedNs > best.submittedNs) best = s\n }\n return best\n}\n\n/** @experimental */\nexport function checkCadence(\n clip: CadenceClip,\n submissions: readonly SubmissionRecord[],\n outputFps = PREVIEW_OUTPUT_FPS,\n /**\n * How far the shown frame may be from the named one, in project time.\n *\n * Zero scores the export contract — exact selection, no tolerance — and the\n * default scores the preview's. @see PREVIEW_TOLERANCE_US\n */\n toleranceUs = PREVIEW_TOLERANCE_US,\n /**\n * The player's own clock, paired with monotonic time.\n *\n * Where instants are placed on the wall clock. Empty means no instant can be\n * placed, and the result is `invalid` rather than scored against a guess.\n */\n samples: readonly SpikeSample[] = [],\n): CadenceResult {\n const request: ResampleRequest = {\n sourceStartUs: clip.sourceStartUs as never,\n sourceEndUs: clip.sourceEndUs as never,\n speed: clip.speed,\n sourceFps: clip.sourceFps,\n targetFps: outputFps,\n }\n const plan = resampleClip(request)\n const outputIntervalUs = Math.round(1_000_000 / outputFps)\n\n const inClip = submissions\n .filter((s) => s.ptsUs >= clip.timelineStartUs && s.ptsUs < clip.timelineEndUs)\n .sort((a, b) => a.ptsUs - b.ptsUs)\n\n /*\n Is the texture's timestamp even in media time?\n\n On this device it is not: `SurfaceTexture.getTimestamp` returns a\n `System.nanoTime`-based value, about forty-nine hours of uptime, in the\n domain of the release time rather than of the presentation timestamp.\n Treating it as media time called every instant a wrong acquisition — a\n broken check reporting a broken pipeline.\n\n So the domain is tested rather than assumed, and when it does not hold the\n identity check falls back to the paired metadata and says it could not\n separate a wrong acquisition from a wrong promotion.\n */\n const skews = inClip\n .filter((s) => s.texturePtsUs >= 0)\n .map((s) => Math.abs(s.texturePtsUs - s.ptsUs))\n .sort((a, b) => a - b)\n const medianSkewUs = skews.length > 0 ? skews[Math.floor(skews.length / 2)]! : -1\n const timestampDomain: TimestampDomain =\n skews.length === 0 ? 'unknown' : medianSkewUs < 1_000_000 ? 'media' : 'system'\n const unique = new Set(inClip.map((s) => s.ptsUs))\n\n /*\n What the pixels say arrived, and what they say was shown.\n\n `delivered` is every distinct frame index the ring actually held during this\n clip. It is what separates a frame that was never supplied from a frame that\n was supplied and not chosen — the distinction the timestamps could not make,\n and the reason for the band in the first place.\n */\n const delivered = new Set<number>()\n let markersRead = 0\n let markersFailed = 0\n for (const record of inClip) {\n if (record.acquiredMarker >= 0) delivered.add(record.acquiredMarker)\n if (record.submittedMarker >= 0) markersRead += 1\n else if (record.markerFailure !== '' && record.markerFailure !== 'unread') markersFailed += 1\n }\n const pixelIdentity = markersRead > 0\n\n /*\n Scored across the span actually covered, not the clip's nominal length.\n\n A run that stopped early, or a seek that landed mid-clip, would otherwise\n report every instant it never reached as a missed deadline.\n */\n const first = inClip.length > 0 ? inClip[0]!.ptsUs : 0\n const last = inClip.length > 0 ? inClip[inClip.length - 1]!.ptsUs : 0\n /*\n The frame-derived offset, kept only as a cross-check.\n\n It was the anchor once, and it should not have been: an offset computed from\n submission records folds any systematic presentation delay into itself, so a\n pipeline uniformly late measures as punctual. Instants are placed from the\n player's own clock instead, and this is reported beside it — a growing\n difference between the two *is* the presentation delay, which is worth\n seeing rather than absorbing.\n */\n const clockOffsets = inClip\n .filter((r) => r.submittedNs > 0)\n .map((r) => r.submittedNs - r.ptsUs * 1000)\n .sort((a, b) => a - b)\n const playerOffsets = samples\n .filter((s) => s.playing && s.atNs > 0 && s.positionUs >= 0)\n .map((s) => s.atNs - s.positionUs * 1000)\n .sort((a, b) => a - b)\n /*\n The difference between the two clocks, not either one's absolute value.\n\n Both are monotonic-minus-project offsets and both are dominated by uptime —\n reported raw, one read as 203,687 seconds, which says nothing. What is worth\n seeing is how far apart they sit: the player says when a moment of project\n time occurred, the frames say when a picture for it was handed over, and the\n gap is the pipeline's systematic presentation delay. Anchoring on the frames\n would subtract exactly that and call the result punctual.\n */\n const frameDerivedOffsetNs =\n clockOffsets.length > 0 && playerOffsets.length > 0\n ? clockOffsets[Math.floor(clockOffsets.length / 2)]! -\n playerOffsets[Math.floor(playerOffsets.length / 2)]!\n : 0\n\n let expectedInstants = 0\n let leadingExcluded = 0\n let trailingExcluded = 0\n let covered = 0\n let wrong = 0\n let unverified = 0\n let unplaceable = 0\n let exact = 0\n const offsets: Record<string, number> = {}\n // One source frame, in project time: 16.7 ms at 2x, 66.7 ms at 0.5x. The\n // conversion that makes an offset comparable with the tolerance.\n const sourceIntervalUs = clip.sourceFps > 0 ? 1_000_000 / clip.sourceFps / clip.speed : 0\n const wrongInstants: WrongInstant[] = []\n for (const frame of plan.frames) {\n const projectUs = clip.timelineStartUs + Number(frame.outputPtsUs)\n if (inClip.length === 0) continue\n const boundary = projectUs < first || projectUs > last\n if (projectUs < first) leadingExcluded += 1\n else if (projectUs > last) trailingExcluded += 1\n else expectedInstants += 1\n\n const instantNs = wallClockAt(samples, projectUs, outputIntervalUs)\n if (instantNs === null && !boundary) unplaceable += 1\n const shown =\n instantNs === null ? null : displayedAt(inClip, projectUs, instantNs, outputIntervalUs * 1000)\n if (shown === null) {\n /*\n The boundary instants are checked too.\n\n They are excluded from the *coverage* arithmetic because the run never\n reached them, but their identity still matters: a leading instant is the\n seam, where the previous clip is still on screen, and dropping it from\n both metrics is how a wrong picture at a clip change goes unseen.\n */\n if (boundary) {\n wrongInstants.push({\n projectUs,\n expectedSourceFrame: frame.sourceFrameIndex,\n expectedSourcePtsUs: Number(frame.sourceTimeUs),\n shownSourceFrame: -1,\n shownPtsUs: -1,\n acquiredTexturePtsUs: -1,\n acquiredMarker: -1,\n submittedMarker: -1,\n slot: -1,\n frameId: -1,\n submittedNs: 0,\n cause: 'uncovered',\n boundary,\n })\n }\n continue\n }\n if (!boundary) covered += 1\n\n /*\n Identity by source frame, not by timestamp.\n\n The submitted PTS is project time and the rule names a source frame, so\n the comparison maps the shown picture back through the same speed the\n compiler applied. A tolerance of one source frame absorbs the rounding\n between the two directions.\n */\n /*\n What the texture holds, not what the record claims it holds.\n\n The two are usually the same and the case where they are not is exactly a\n wrong acquisition: the picture on screen is the texture's content, and\n scoring the record's timestamp instead would call that instant correct.\n */\n const shownProjectUs = timestampDomain === 'media' ? shown.texturePtsUs : shown.ptsUs\n const shownSourceUs = clip.sourceStartUs + (shownProjectUs - clip.timelineStartUs) * clip.speed\n /*\n The marker when there is one, the timestamp only when there is not.\n\n A read band names the frame outright; the derived index is an inference\n from the very association under test. When neither is available the\n instant is unverified rather than assumed correct.\n */\n const derivedIndex = Math.floor((shownSourceUs * clip.sourceFps) / 1_000_000)\n const shownIndex = shown.submittedMarker >= 0 ? shown.submittedMarker : derivedIndex\n const verified = shown.submittedMarker >= 0 || timestampDomain === 'media'\n if (!verified) {\n if (!boundary) unverified += 1\n continue\n }\n /*\n Counted before it is judged, and counted whether or not it passes.\n\n A pixel oracle can be exact, so it is: the rule names one source frame and\n the band says which one arrived. But \"wrong\" spans a frame that is 8.3 ms\n early and a frame that is a third of a second stale, and a count alone\n cannot tell them apart.\n */\n const offset = shownIndex - frame.sourceFrameIndex\n /*\n Over the scored span only.\n\n A boundary instant is one the run never reached, and what is on screen\n there is the previous clip or the last picture of this one — an offset of\n a few hundred frames that is not jitter and would swamp the distribution.\n Those instants are still checked; they appear in `wrong` flagged\n `boundary`, which is where they belong.\n */\n if (!boundary) {\n const key = String(offset)\n offsets[key] = (offsets[key] ?? 0) + 1\n }\n\n /*\n Two contracts, and this is the preview's.\n\n An export selects the exact frame `resampleClip` names: it drives the\n decoder to a source time and gets that frame. A preview consumes a live\n decoder at its own display rate, and above 1x the frame the rule names is\n usually never produced at the instant it is wanted — measured at 2x, 31 to\n 37 of about 90 instants wanted a frame no slot ever held, against 0 to 1\n the ring genuinely mis-chose. Requiring exact identity would be asking the\n preview to seek per frame.\n\n So the preview is judged temporally: the picture must be within\n `PREVIEW_TOLERANCE_US` of project time from the frame the rule names, and\n dropping frames during accelerated playback is permitted. This is an\n acceptance decision, not a discovery that the identity check passed —\n `exactInstants` and the offset distribution are reported either way, and\n an export must still be scored against exact selection.\n */\n const offsetUs = Math.abs(offset) * sourceIntervalUs\n // The derived index still needs its rounding slack; a band is exact.\n const slack = shown.submittedMarker >= 0 ? 0 : sourceIntervalUs\n if (offset === 0) exact += 1\n if (offsetUs <= Math.max(toleranceUs, slack)) continue\n if (!boundary) wrong += 1\n\n /*\n Three different faults, and they send the fix to three different places.\n\n The texture's own timestamp is what separates them: if it disagrees with\n the metadata the frame was recorded under, the pairing was wrong before\n the ring saw it. If it agrees and a different frame is on screen, the ring\n chose wrongly. If the right frame is on screen but arrived after its\n instant, nothing chose wrongly and it was simply late.\n */\n const acquired = shown.texturePtsUs\n /*\n Late delivery first, because its evidence stands on its own.\n\n Both terms are the ring's own records — when it submitted, and what\n project time it recorded the frame under — and neither comes from the\n texture. It survives the timestamp-domain problem intact, which is why it\n stays a separate cause rather than folding into the unresolved one.\n */\n const late = shown.submittedNs > 0 && shown.ptsUs < projectUs - outputIntervalUs\n /*\n Available *by this instant*, not somewhere in the clip.\n\n A clip-global check answers the wrong question: every frame arrives\n eventually, so nothing is ever \"not delivered\" and a picture that simply\n had not arrived yet is charged to selection. What decides the fault is\n whether the named frame was in the ring when the instant needed it.\n */\n const hadItByThen = inClip.some(\n (r) =>\n r.acquiredMarker === frame.sourceFrameIndex &&\n r.acquiredNs > 0 &&\n r.acquiredNs <= shown.submittedNs,\n )\n /*\n Three outcomes in pixels, and only three.\n\n With identity read from the picture, \"the right frame submitted late\"\n cannot produce a wrong instant: if the marker matches the rule's frame the\n instant is correct, whenever it was submitted. So the pixel path asks only\n whether the storage changed, whether the frame was there to show, and\n whether something else was shown instead. `late-delivery` belongs to the\n timestamp path, where identity is inferred from when a frame was recorded\n rather than from what it contains.\n */\n let cause: WrongInstantCause\n if (shown.submittedMarker >= 0) {\n if (shown.acquiredMarker >= 0 && shown.submittedMarker !== shown.acquiredMarker) {\n cause = 'storage-aliased'\n } else if (!hadItByThen) cause = 'not-delivered'\n else cause = 'wrong-selection'\n } else if (late) cause = 'late-delivery'\n else cause = 'wrong-submitted-identity'\n\n wrongInstants.push({\n projectUs,\n expectedSourceFrame: frame.sourceFrameIndex,\n expectedSourcePtsUs: Number(frame.sourceTimeUs),\n shownSourceFrame: shownIndex,\n shownPtsUs: shownProjectUs,\n acquiredTexturePtsUs: acquired,\n acquiredMarker: shown.acquiredMarker,\n submittedMarker: shown.submittedMarker,\n slot: shown.slot,\n frameId: shown.frameId,\n submittedNs: shown.submittedNs,\n cause,\n boundary,\n })\n }\n\n const walls: number[] = []\n for (let i = 1; i < inClip.length; i += 1) {\n const gap = (inClip[i]!.submittedNs - inClip[i - 1]!.submittedNs) / 1000\n if (gap > 0) walls.push(gap)\n }\n walls.sort((a, b) => a - b)\n const tail = (fraction: number) =>\n walls.length === 0\n ? 0\n : Math.round(walls[Math.min(walls.length - 1, Math.floor(walls.length * fraction))]!)\n const frameTimes = {\n p50: tail(0.5),\n p95: tail(0.95),\n p99: tail(0.99),\n max: walls.length > 0 ? Math.round(walls[walls.length - 1]!) : 0,\n samples: walls.length,\n }\n\n /*\n Parity over the submissions, not over the instants.\n\n Rendering is a property of a picture, and the same picture can cover several\n instants; scoring it once per instant would weight a held frame more heavily\n than a fleeting one for no reason.\n */\n let parityMeasured = 0\n let worstLevelDelta = 0\n let worstBarDelta = 0\n let parityFailures = 0\n for (const record of inClip) {\n if (record.levelDelta < 0 || record.barDelta < 0) continue\n parityMeasured += 1\n if (record.levelDelta > worstLevelDelta) worstLevelDelta = record.levelDelta\n if (record.barDelta > worstBarDelta) worstBarDelta = record.barDelta\n if (record.levelDelta > PARITY_LEVEL_TOLERANCE || record.barDelta > PARITY_BAR_TOLERANCE) {\n parityFailures += 1\n }\n }\n\n const stamps = [...unique].sort((a, b) => a - b)\n const holds: number[] = []\n for (let i = 1; i < stamps.length; i += 1) holds.push(stamps[i]! - stamps[i - 1]!)\n const repeats = Math.max(1, Math.round(1 / clip.speed))\n const expectedHoldUs = outputIntervalUs * repeats\n const missedDeadlines = holds.filter((h) => h > expectedHoldUs * 1.5).length\n holds.sort((a, b) => a - b)\n\n /*\n Nothing measured is not a failing measurement.\n\n Checked before anything is judged: a run with no submissions, no readable\n band, or no parity reading has established nothing about this clip, and\n saying `fail` would be as wrong as saying `pass`.\n */\n let verdict: Verdict = 'pass'\n let verdictReason = ''\n if (expectedInstants === 0) {\n verdict = 'invalid'\n verdictReason = 'no instants covered'\n } else if (!pixelIdentity) {\n verdict = 'invalid'\n verdictReason = 'identity not read from pixels'\n } else if (parityMeasured === 0) {\n verdict = 'invalid'\n verdictReason = 'rendering parity not measured'\n } else if (unplaceable > 0) {\n verdict = 'invalid'\n verdictReason = `${unplaceable} instants could not be placed on the clock`\n } else if (unverified > 0) {\n verdict = 'invalid'\n verdictReason = `${unverified} instants unverified`\n } else if (wrong > 0) {\n verdict = 'fail'\n verdictReason = `${wrong} wrong instants`\n } else if (covered !== expectedInstants) {\n verdict = 'fail'\n verdictReason = `${expectedInstants - covered} instants uncovered`\n } else if (missedDeadlines > 0) {\n verdict = 'fail'\n verdictReason = `${missedDeadlines} missed deadlines`\n } else if (parityFailures > 0) {\n verdict = 'fail'\n verdictReason = `${parityFailures} parity failures`\n }\n\n return {\n clip: clip.name,\n nominalInstants: plan.frames.length,\n leadingExcluded,\n trailingExcluded,\n expectedInstants,\n coveredInstants: covered,\n wrongFrameInstants: wrong,\n uncoveredInstants: expectedInstants - covered,\n submittedUnique: unique.size,\n ruleSkippedSourceFrames: plan.droppedSourceFrameCount,\n outputIntervalUs,\n medianHoldUs: holds.length > 0 ? holds[Math.floor(holds.length / 2)]! : 0,\n maxHoldUs: holds.length > 0 ? holds[holds.length - 1]! : 0,\n expectedHoldUs,\n missedDeadlines,\n parity: {\n measured: parityMeasured,\n worstLevelDelta,\n worstBarDelta: Number(worstBarDelta.toFixed(5)),\n failures: parityFailures,\n // No measurement is not a pass: a run that never looked has not looked.\n pass: parityMeasured > 0 && parityFailures === 0,\n },\n offsets,\n unplaceableInstants: unplaceable,\n frameDerivedOffsetUs: Math.round(frameDerivedOffsetNs / 1000),\n exactInstants: exact,\n worstOffsetUs: Math.round(\n Object.keys(offsets).reduce((worst, k) => Math.max(worst, Math.abs(Number(k))), 0) *\n (clip.sourceFps > 0 ? 1_000_000 / clip.sourceFps / clip.speed : 0),\n ),\n identity: {\n timestampDomain,\n medianTextureSkewUs: medianSkewUs,\n pixelIdentity,\n identityUnverified: unverified,\n deliveredFrames: delivered.size,\n markersRead,\n markersFailed,\n },\n frameTimeUs: frameTimes,\n wrong: wrongInstants,\n // Every expected instant covered, by the frame the shared rule names, and\n // no picture held past its turn.\n /*\n An unverified instant is not a passing one.\n\n With the probe off and the timestamps in the system domain there is no\n evidence about which frame was shown, and a check that reported that\n absence as success would be the same mistake as the one the band was\n built to correct.\n */\n /*\n The preview contract: temporally accurate, correctly rendered, covered.\n\n Exact export-frame identity is deliberately absent. `exactInstants` and\n `offsets` record it, and `@see PREVIEW_TOLERANCE_US` for why requiring it\n here would be requiring a per-frame seek.\n */\n pass: verdict === 'pass',\n verdict,\n verdictReason,\n }\n}\n","import type { SpikeSample } from './compositionSpike'\n\n/**\n * Score the picture where it is handed to composition.\n *\n * ## The boundary, stated\n *\n * **Submission**, not scanout. A frame is scored at the instant composition\n * begins consuming it; the compositor presents it some time later and nothing\n * in this process observes that. The delay is common to every frame, so it\n * shifts the whole series rather than spreading it — but it is not measured\n * here and is not subtracted from anything.\n *\n * ## Why an interval rather than a point\n *\n * A frame is not a sample of one instant; it is the correct picture for a span\n * of project time. Comparing a continuous audio clock against a single\n * timestamp charges the frame's own duration to synchronisation, which is why\n * the 0.5x clip — whose frames are correct for 66.7 ms each — looked worse than\n * the 1x clip at identical quality.\n *\n * So a submitted frame carries `[ptsUs, ptsUs + intervalUs)`, and the error is\n * how far outside that span the audio clock sits. Zero inside, and the distance\n * to the nearer edge outside. The interval comes from the measured cadence,\n * which for a clip of speed `s` and source interval `i` is `i / s` — 33.3 ms at\n * 1x, 66.7 ms at 0.5x.\n *\n * @experimental\n */\n\n/** One submission, as the consumer recorded it. @experimental */\nexport interface SubmissionRecord {\n frameId: number\n epoch: number\n ptsUs: number\n intervalUs: number\n submittedNs: number\n /** What the texture reported holding at acquisition. */\n texturePtsUs: number\n /** Which ring slot carried it. */\n slot: number\n occupancy: number\n /**\n * The frame index read out of the slot's pixels when it was acquired.\n *\n * -1 when the probe was off or the band could not be read; `markerFailure`\n * says which.\n */\n acquiredMarker: number\n /** The frame index read out of the picture actually handed to composition. */\n submittedMarker: number\n /** Empty when the band was read; otherwise why it was not. */\n markerFailure: string\n /**\n * The picture's whole life, so one row can be attributed.\n *\n * When it was acquired, when it was due for presentation, when it was\n * promoted. Without these, a late submission cannot be separated into a late\n * arrival, a long wait for its due time, and a slow promotion.\n */\n acquiredNs: number\n dueNs: number\n promotedNs: number\n /**\n * Rendering error against the frame the submitted band names: background\n * level in 0..255, bar position in 0..1 of height. -1 when not measured.\n */\n levelDelta: number\n barDelta: number\n}\n\n/** @experimental */\nexport interface ScoredSubmission {\n frameId: number\n ptsUs: number\n intervalUs: number\n submittedNs: number\n /** The audio clock at the submission instant, on the same monotonic timeline. */\n audioUs: number\n /** 0 inside the interval; otherwise the distance to the nearer edge. */\n errorUs: number\n /** Positive when the sound is past the frame's span — the picture is behind. */\n signedUs: number\n occupancy: number\n}\n\n/** @experimental */\nexport interface SubmissionReport {\n toleranceUs: number\n scored: number\n /** Submissions with no audio sample close enough to pair with. */\n unpaired: number\n excursions: ScoredSubmission[]\n worstErrorUs: number\n pass: boolean\n /**\n * What this run established, as three outcomes.\n *\n * A score computed from nothing is not a pass. Zero scored submissions means\n * the run did not observe playback, and that has to be distinguishable from\n * a run that observed it and found it clean.\n */\n verdict: 'pass' | 'fail' | 'invalid'\n verdictReason: string\n /** Gaps between consecutive submitted timestamps: the delivered cadence. */\n cadenceUs: { median: number; min: number; max: number; samples: number }\n /**\n * Wall-clock gaps between consecutive submissions.\n *\n * The tail is the point: a median at the output interval says nothing about\n * the frame that took four times as long, and that frame is the one somebody\n * sees. Taken from `submittedNs`, which is the instant composition began\n * consuming each picture.\n */\n frameTimeUs: { p50: number; p95: number; p99: number; max: number; samples: number }\n peakOccupancy: number\n}\n\n/**\n * The audio clock at an arbitrary instant, from the paired samples around it.\n *\n * The composition runs at project rate 1x, so the clock is linear between\n * samples and interpolation is exact rather than an approximation. Returns null\n * when there is no sample near enough to interpolate from, which is reported\n * rather than guessed.\n */\nfunction audioAt(\n samples: readonly SpikeSample[],\n atNs: number,\n windowNs: number,\n): number | null {\n let before: SpikeSample | null = null\n let after: SpikeSample | null = null\n for (const s of samples) {\n if (!s.playing) continue\n if (s.atNs <= atNs && (before === null || s.atNs > before.atNs)) before = s\n if (s.atNs >= atNs && (after === null || s.atNs < after.atNs)) after = s\n }\n if (before !== null && after !== null && after.atNs > before.atNs) {\n const spanUs = (after.atNs - before.atNs) / 1000\n const movedUs = after.positionUs - before.positionUs\n /*\n Never interpolate across a seek.\n\n Between two samples the clock advances with the wall clock, because the\n composition runs at project rate 1x. When it does not, it was *moved* —\n and interpolating across that produced a 2.7 second \"error\" for a frame\n that was correct when it was handed over: the picture was at 23.9 s and\n the position it was paired with had already jumped back to 21.2 s.\n\n Detected from the clock itself rather than from an epoch number, because\n the consumer's frame generation and the player's transition counter are\n different counters and equal values between them mean nothing.\n */\n if (Math.abs(movedUs - spanUs) <= DISCONTINUITY_US) {\n const t = (atNs - before.atNs) / (after.atNs - before.atNs)\n return before.positionUs + t * movedUs\n }\n // A jump: use the sample on the submission's own side of it.\n return before.positionUs + (atNs - before.atNs) / 1000\n }\n const nearest = before ?? after\n if (nearest === null) return null\n if (Math.abs(nearest.atNs - atNs) > windowNs) return null\n // Rate 1x, so extrapolating the short distance to the edge sample is exact.\n return nearest.positionUs + (atNs - nearest.atNs) / 1000\n}\n\n/**\n * How far the clock may diverge from the wall clock and still be running.\n *\n * Beyond this it was moved rather than advanced, which is a seek.\n */\nconst DISCONTINUITY_US = 100_000\n\n/** @experimental */\nexport function scoreSubmissions(\n submissions: readonly SubmissionRecord[],\n samples: readonly SpikeSample[],\n options: { toleranceUs?: number; pairWindowNs?: number } = {},\n): SubmissionReport {\n const toleranceUs = options.toleranceUs ?? 60_000\n const pairWindowNs = options.pairWindowNs ?? 50_000_000\n\n const scored: ScoredSubmission[] = []\n const excursions: ScoredSubmission[] = []\n let unpaired = 0\n let worstErrorUs = 0\n let peakOccupancy = 0\n\n for (const record of submissions) {\n if (record.ptsUs < 0 || record.submittedNs <= 0) continue\n if (record.occupancy > peakOccupancy) peakOccupancy = record.occupancy\n const audioUs = audioAt(samples, record.submittedNs, pairWindowNs)\n if (audioUs === null) {\n unpaired += 1\n continue\n }\n /*\n Inside the span is not an error of any size.\n\n The frame is the correct picture for the whole interval, so a clock\n anywhere within it is synchronised. Scoring the distance to `ptsUs`\n instead would report the frame's own duration as drift.\n */\n const start = record.ptsUs\n const end = record.ptsUs + Math.max(record.intervalUs, 0)\n const signedUs = audioUs < start ? audioUs - start : audioUs > end ? audioUs - end : 0\n const errorUs = Math.abs(signedUs)\n const entry: ScoredSubmission = {\n frameId: record.frameId,\n ptsUs: record.ptsUs,\n intervalUs: record.intervalUs,\n submittedNs: record.submittedNs,\n audioUs: Math.round(audioUs),\n errorUs: Math.round(errorUs),\n signedUs: Math.round(signedUs),\n occupancy: record.occupancy,\n }\n scored.push(entry)\n if (errorUs > worstErrorUs) worstErrorUs = errorUs\n if (errorUs > toleranceUs) excursions.push(entry)\n }\n\n const gaps: number[] = []\n const frameTimes: number[] = []\n for (let i = 1; i < scored.length; i += 1) {\n const gap = scored[i]!.ptsUs - scored[i - 1]!.ptsUs\n if (gap > 0) gaps.push(gap)\n const wall = (scored[i]!.submittedNs - scored[i - 1]!.submittedNs) / 1000\n if (wall > 0) frameTimes.push(wall)\n }\n gaps.sort((a, b) => a - b)\n frameTimes.sort((a, b) => a - b)\n const at = (fraction: number) =>\n frameTimes.length === 0\n ? 0\n : Math.round(frameTimes[Math.min(frameTimes.length - 1, Math.floor(frameTimes.length * fraction))]!)\n\n return {\n toleranceUs,\n scored: scored.length,\n unpaired,\n excursions,\n worstErrorUs: Math.round(worstErrorUs),\n // No submissions is not a pass: a run that showed nothing was not judged.\n pass: scored.length > 0 && excursions.length === 0,\n verdict: scored.length === 0 ? 'invalid' : excursions.length === 0 ? 'pass' : 'fail',\n verdictReason:\n scored.length === 0\n ? 'no submissions scored'\n : excursions.length === 0\n ? ''\n : `${excursions.length} excursions, worst ${Math.round(worstErrorUs / 1000)} ms`,\n cadenceUs: {\n median: gaps.length > 0 ? gaps[Math.floor(gaps.length / 2)]! : 0,\n min: gaps.length > 0 ? gaps[0]! : 0,\n max: gaps.length > 0 ? gaps[gaps.length - 1]! : 0,\n samples: gaps.length,\n },\n frameTimeUs: {\n p50: at(0.5),\n p95: at(0.95),\n p99: at(0.99),\n max: frameTimes.length > 0 ? Math.round(frameTimes[frameTimes.length - 1]!) : 0,\n samples: frameTimes.length,\n },\n peakOccupancy,\n }\n}\n\n/** One submission, with its whole life and the clock it was scored against. @experimental */\nexport interface TraceRow {\n frameId: number\n /** Offsets in ms from the window's first row, so the numbers are readable. */\n acquiredMs: number\n dueMs: number\n promotedMs: number\n submittedMs: number\n /**\n * Acquisition to due, due to promotion, promotion to submission, in us.\n *\n * A negative `waitUs` means the presentation time had already passed when the\n * frame was acquired — it was late before the ring saw it.\n */\n waitUs: number\n promoteUs: number\n submitUs: number\n ptsUs: number\n audioUs: number\n errorUs: number\n slot: number\n occupancy: number\n marker: number\n}\n\n/** @experimental */\nexport interface SubmissionTrace {\n /** The frame the window is centred on. */\n frameId: number\n rows: TraceRow[]\n /**\n * Where the centre frame's budget went, as the largest single interval.\n *\n * `arrival` when it was acquired late, `due` when it sat waiting for its\n * presentation time, `promotion` when it was due and not chosen, `submission`\n * when promotion and submission diverged. Empty when the row is incomplete.\n */\n spentOn: 'arrival' | 'due' | 'promotion' | 'submission' | 'unknown'\n /** The dominant leg's size, so a label cannot imply a magnitude it lacks. */\n spentUs: number\n /** True when the window could not be built from complete records. */\n invalid: boolean\n reason: string\n}\n\n/**\n * The life of one submission and its neighbours.\n *\n * Built for attribution, not for scoring: a submission excursion says a picture\n * was wrong at an instant, and this says where its time went. Acquisition, due\n * time, promotion and submission are four stamps on one frame, and the largest\n * gap between consecutive ones is the interval to investigate.\n *\n * Incomplete rows are reported rather than dropped. A ring buffer that wrapped,\n * a record written before the ring carried these stamps, or a frame that never\n * reached submission all produce a window that cannot be attributed, and a\n * silently shortened window would read as a clean one.\n *\n * @experimental\n */\nexport function traceSubmission(\n records: readonly SubmissionRecord[],\n scored: readonly ScoredSubmission[],\n frameId: number,\n span = 8,\n): SubmissionTrace {\n const ordered = records.filter((r) => r.submittedNs > 0).sort((a, b) => a.submittedNs - b.submittedNs)\n const centre = ordered.findIndex((r) => r.frameId === frameId)\n if (centre < 0) {\n return {\n frameId,\n rows: [],\n spentOn: 'unknown',\n spentUs: 0,\n invalid: true,\n reason: 'frame not retained',\n }\n }\n const from = Math.max(0, centre - span)\n const window = ordered.slice(from, centre + span + 1)\n const base = window[0]!.submittedNs\n const ms = (ns: number) => (ns > 0 ? Math.round((ns - base) / 1e6) : -1)\n\n const rows: TraceRow[] = window.map((r) => {\n const match = scored.find((s) => s.frameId === r.frameId)\n return {\n frameId: r.frameId,\n acquiredMs: ms(r.acquiredNs),\n dueMs: ms(r.dueNs),\n promotedMs: ms(r.promotedNs),\n submittedMs: ms(r.submittedNs),\n waitUs: r.acquiredNs > 0 && r.dueNs > 0 ? Math.round((r.dueNs - r.acquiredNs) / 1000) : -1,\n promoteUs: r.dueNs > 0 && r.promotedNs > 0 ? Math.round((r.promotedNs - r.dueNs) / 1000) : -1,\n submitUs:\n r.promotedNs > 0 && r.submittedNs > 0 ? Math.round((r.submittedNs - r.promotedNs) / 1000) : -1,\n ptsUs: r.ptsUs,\n audioUs: match === undefined ? -1 : match.audioUs,\n errorUs: match === undefined ? -1 : match.errorUs,\n slot: r.slot,\n occupancy: r.occupancy,\n marker: r.submittedMarker,\n }\n })\n\n const target = rows.find((r) => r.frameId === frameId)!\n /*\n Presence is checked on the raw stamps, not on the offsets.\n\n An offset is relative to the window's first submission, so a frame acquired\n before that submission has a negative one — which is ordinary, and which an\n earlier version of this check read as \"not recorded\" and refused to\n attribute. Zero nanoseconds means never stamped; a negative offset means\n earlier than the window.\n */\n const raw = window.find((r) => r.frameId === frameId)!\n if (raw.acquiredNs <= 0 || raw.dueNs <= 0 || raw.promotedNs <= 0) {\n return {\n frameId,\n rows,\n spentOn: 'unknown',\n spentUs: 0,\n invalid: true,\n reason: 'incomplete stamps',\n }\n }\n /*\n A frame acquired after its own due time was late before the ring saw it.\n\n `waitUs` is due minus acquired, so a negative value means the presentation\n time had already passed on arrival. Measuring promotion from that stale due\n time then charges the ring for the wait as well, and the first version of\n this did exactly that: it reported `promotion` for frames whose promote leg\n was 96 ms and whose wait was minus 52 ms — the same 52 ms counted twice, in\n the wrong place. Promotion is measured from whichever came later.\n */\n const index = rows.indexOf(target)\n const previous = index > 0 ? rows[index - 1]! : null\n /*\n Arrival is the gap since the previous picture went out, always.\n\n An earlier version used the overshoot past the due time instead whenever the\n frame arrived late, and that hid the thing actually worth seeing. Measured\n at the clip seam: the frame missed its due time by 2 ms and had been\n preceded by 133 ms in which nothing was acquired at all. Charging it 2 ms\n named the wrong interval by two orders of magnitude.\n */\n const arrivalUs =\n previous === null ? 0 : Math.round((target.acquiredMs - previous.submittedMs) * 1000)\n const promoteUs = target.waitUs < 0 ? target.promoteUs + target.waitUs : target.promoteUs\n const legs: [SubmissionTrace['spentOn'], number][] = [\n ['arrival', arrivalUs],\n ['due', Math.max(0, target.waitUs)],\n ['promotion', promoteUs],\n ['submission', target.submitUs],\n ]\n let spentOn: SubmissionTrace['spentOn'] = 'unknown'\n let worst = -1\n for (const [name, value] of legs) {\n if (value > worst) {\n worst = value\n spentOn = name\n }\n }\n return { frameId, rows, spentOn, spentUs: Math.max(0, worst), invalid: false, reason: '' }\n}\n\n/** @experimental */\nexport interface SeamStage {\n framePtsUs: number\n /** Offsets in ms from the window's first sample. */\n metadataMs: number\n availableMs: number\n consumedMs: number\n /** Renderer to queue, queue to latch, in us. -1 when a stamp is missing. */\n graphUs: number\n latchUs: number\n}\n\n/** @experimental */\nexport interface SeamTrace {\n /** The boundary this window is centred on, in project time. */\n atUs: number\n /** The largest gap between consecutive frames reaching the renderer, in us. */\n outputGapUs: number\n /** The same gap measured at acquisition. */\n acquireGapUs: number\n /**\n * Who owns the gap.\n *\n * `graph` when the renderer produced and the queue did not follow.\n * `consumer` when it was queued and not latched. `production` when nothing\n * reached the renderer's output callback at all.\n *\n * That last one is deliberately broader than \"the decoder was unprepared\":\n * this stamp fires when the renderer is about to hand a frame to the surface,\n * so its absence covers a decoder that produced nothing *and* a renderer\n * blocked waiting for the sink to accept a new input stream. Separating those\n * two needs a stamp inside Media3 that is not exposed here. What it does\n * establish is that the graph and this consumer are not responsible, which is\n * what the acquisition gap alone could not say.\n */\n owner: 'production' | 'graph' | 'consumer' | 'unknown'\n stages: SeamStage[]\n invalid: boolean\n reason: string\n}\n\n/**\n * Where output stops at a clip boundary.\n *\n * The acquisition gap says delivery stopped; it cannot say whether the decoder\n * produced nothing, the video graph held what it produced, or this consumer\n * failed to latch what was queued. Those are three owners and three different\n * fixes, and they are separated by one stamp: when the renderer said a frame\n * was about to go to the surface.\n *\n * Samples carrying no frame are skipped rather than treated as a gap — the\n * poller runs faster than the frame rate, so most samples repeat the last\n * frame, and counting those would report a stall on every tick.\n *\n * @experimental\n */\nexport function traceSeam(\n samples: readonly SpikeSample[],\n atUs: number,\n spanUs = 600_000,\n): SeamTrace {\n const near = samples\n .filter(\n (s) =>\n s.framePtsUs >= 0 &&\n s.framePtsUs >= atUs - spanUs &&\n s.framePtsUs <= atUs + spanUs &&\n s.frameConsumedNs > 0,\n )\n .sort((a, b) => a.frameConsumedNs - b.frameConsumedNs)\n\n // One row per distinct frame: the poller ticks faster than frames arrive.\n const unique: SpikeSample[] = []\n for (const s of near) {\n if (unique.length === 0 || unique[unique.length - 1]!.frameId !== s.frameId) unique.push(s)\n }\n if (unique.length < 3) {\n return {\n atUs,\n outputGapUs: -1,\n acquireGapUs: -1,\n owner: 'unknown',\n stages: [],\n invalid: true,\n reason: 'too few frames around the boundary',\n }\n }\n\n const base = unique[0]!.frameConsumedNs\n const ms = (ns: number) => (ns > 0 ? Math.round((ns - base) / 1e6) : -1)\n const stages: SeamStage[] = unique.map((s) => ({\n framePtsUs: s.framePtsUs,\n metadataMs: ms(s.frameMetadataNs),\n availableMs: ms(s.frameAvailableNs),\n consumedMs: ms(s.frameConsumedNs),\n graphUs:\n s.frameMetadataNs > 0 && s.frameAvailableNs > 0\n ? Math.round((s.frameAvailableNs - s.frameMetadataNs) / 1000)\n : -1,\n latchUs:\n s.frameAvailableNs > 0 && s.frameConsumedNs > 0\n ? Math.round((s.frameConsumedNs - s.frameAvailableNs) / 1000)\n : -1,\n }))\n\n let outputGapUs = 0\n let acquireGapUs = 0\n let at = -1\n for (let i = 1; i < unique.length; i += 1) {\n const acquire = Math.round((unique[i]!.frameConsumedNs - unique[i - 1]!.frameConsumedNs) / 1000)\n if (acquire > acquireGapUs) {\n acquireGapUs = acquire\n at = i\n }\n const a = unique[i]!.frameMetadataNs\n const b = unique[i - 1]!.frameMetadataNs\n if (a > 0 && b > 0) outputGapUs = Math.max(outputGapUs, Math.round((a - b) / 1000))\n }\n\n /*\n The owner is read off the widest frame's own stages, not off the gap.\n\n A gap is a distance between two frames; the question is where the later one\n was held, and that is inside its own row.\n */\n let owner: SeamTrace['owner'] = 'unknown'\n if (at > 0) {\n const s = stages[at]!\n if (s.graphUs < 0 || s.latchUs < 0) owner = 'unknown'\n else if (s.graphUs >= s.latchUs && s.graphUs > 16_000) owner = 'graph'\n else if (s.latchUs > 16_000) owner = 'consumer'\n else owner = 'production'\n }\n return { atUs, outputGapUs, acquireGapUs, owner, stages, invalid: false, reason: '' }\n}\n\n/** One frame, seen from both sides of the pairing. @experimental */\nexport interface StartupRow {\n frameId: number\n /** What Media3 said this frame's presentation time was. */\n renderedPtsUs: number\n /** What the submission record was written under. The same number, or a bug. */\n recordedPtsUs: number\n /** What the pixels say the frame is. Independent of both. */\n marker: number\n /** The source frame the marker's own index implies, at this clip's speed. */\n markerPtsUs: number\n metadataMs: number\n consumedMs: number\n submittedMs: number\n}\n\n/** @experimental */\nexport interface StartupTrace {\n rows: StartupRow[]\n /**\n * Frames the renderer never emitted, by source index.\n *\n * The discriminator. If the renderer emitted a presentation time and the\n * pixels under it belong to a different frame, the *association* is wrong. If\n * it never emitted that time at all, the frame was dropped or never produced.\n * Those are different defects and the pixels alone cannot tell them apart.\n */\n missingSourceFrames: number[]\n /** Frames whose pixels disagree with the time they were rendered under. */\n misassociated: number[]\n invalid: boolean\n reason: string\n}\n\n/**\n * The first frames of a clip, from both sides of the pairing at once.\n *\n * Media3's presentation time and the marker in the pixels are independent\n * claims about the same frame. Joined by frame id, they separate \"frames 2 to 6\n * were never produced\" from \"frame 7's pixels arrived labelled as frame 2\" —\n * which look identical in a cadence result and need opposite fixes.\n *\n * @experimental\n */\nexport function traceStartup(\n samples: readonly SpikeSample[],\n records: readonly SubmissionRecord[],\n options: { sourceFps: number; speed: number; count?: number },\n): StartupTrace {\n const count = options.count ?? 10\n if (options.sourceFps <= 0 || options.speed <= 0) {\n return { rows: [], missingSourceFrames: [], misassociated: [], invalid: true, reason: 'no clip rate' }\n }\n // One source frame in project time: the unit both sides are compared in.\n const stepUs = 1_000_000 / options.sourceFps / options.speed\n\n const byFrame = new Map<number, SpikeSample>()\n for (const s of samples) {\n if (s.frameId < 0 || s.frameConsumedNs <= 0) continue\n const seen = byFrame.get(s.frameId)\n if (seen === undefined || s.frameConsumedNs < seen.frameConsumedNs) byFrame.set(s.frameId, s)\n }\n const ordered = records\n .filter((r) => r.submittedNs > 0)\n .sort((a, b) => a.submittedNs - b.submittedNs)\n .slice(0, count)\n if (ordered.length === 0) {\n return { rows: [], missingSourceFrames: [], misassociated: [], invalid: true, reason: 'no submissions' }\n }\n\n const base = ordered[0]!.submittedNs\n const ms = (ns: number) => (ns > 0 ? Math.round((ns - base) / 1e6) : -1)\n const rows: StartupRow[] = ordered.map((r) => {\n const s = byFrame.get(r.frameId)\n return {\n frameId: r.frameId,\n renderedPtsUs: s === undefined ? -1 : s.framePtsUs,\n recordedPtsUs: r.ptsUs,\n marker: r.submittedMarker,\n markerPtsUs: r.submittedMarker >= 0 ? Math.round(r.submittedMarker * stepUs) : -1,\n metadataMs: s === undefined ? -1 : ms(s.frameMetadataNs),\n consumedMs: s === undefined ? -1 : ms(s.frameConsumedNs),\n submittedMs: ms(r.submittedNs),\n }\n })\n\n /*\n Misassociated when the pixels and the rendered time name different frames;\n missing when the rule's frame never appears as a rendered time at all.\n */\n const misassociated: number[] = []\n const rendered = new Set<number>()\n for (const row of rows) {\n if (row.renderedPtsUs >= 0) rendered.add(Math.round(row.renderedPtsUs / stepUs))\n if (row.marker < 0 || row.renderedPtsUs < 0) continue\n if (Math.abs(row.markerPtsUs - row.renderedPtsUs) > stepUs / 2) misassociated.push(row.frameId)\n }\n const last = rows[rows.length - 1]!\n const highest = Math.max(\n last.marker >= 0 ? last.marker : 0,\n last.renderedPtsUs >= 0 ? Math.round(last.renderedPtsUs / stepUs) : 0,\n )\n const missingSourceFrames: number[] = []\n for (let i = 0; i <= highest; i += 1) if (!rendered.has(i)) missingSourceFrames.push(i)\n\n return { rows, missingSourceFrames, misassociated, invalid: false, reason: '' }\n}\n","import {\n compileAudioMixProgram,\n type ExportSourceContext,\n type MediaSource,\n type VideoProject,\n} from '@pictor-sdk/video-core'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\n\nimport { publishState } from './previewAudioDiagnostics'\nimport { resolveMixSources } from './mixSources'\nimport { requireNative } from './player'\nimport {\n cancelPreviewMix,\n preparePreviewMix,\n previewMixKey,\n sweepPreviewMixes,\n} from './previewMix'\nimport {\n decidePreviewAudio,\n type PreviewAudioStatus,\n} from './previewAudioSession'\n\n/**\n * Preview audio, bound to a screen's lifetime.\n *\n * The decisions live in `previewAudioSession.ts` and the mixing lives in\n * native; this is the part that acts — starting jobs, loading the player,\n * muting the other one, and refusing to publish an answer that has been\n * superseded.\n *\n * ## Epochs\n *\n * Every load, play and seek carries the epoch of the mix it belongs to. A\n * callback or a correction arriving with an older epoch is from a mix that has\n * since been replaced, and acting on it would move the picture to match audio\n * that is no longer playing. The epoch is bumped on every publish, so a late\n * result cannot masquerade as the current one.\n *\n * @experimental\n */\n\n/** @experimental */\nexport interface UsePreviewAudioOptions {\n project: VideoProject\n /** The same resolver the exporter is given. */\n resolveSource: (source: MediaSource, context: ExportSourceContext) => string | null | undefined\n /** Off entirely — for a screen that never plays. */\n enabled?: boolean\n /** How far apart the players may drift before audio is moved. */\n toleranceUs?: number\n}\n\n/** @experimental */\nexport interface PreviewAudioBinding {\n status: PreviewAudioStatus\n /** A sentence for a person when preparation failed. */\n error: string | null\n /** Whether playback may run. False while preparing or after a failure. */\n canPlay: boolean\n /** True once a mix matching the document is loaded. */\n ready: boolean\n /** How long the last preparation took, in ms. Null when it was cached. */\n lastPrepareMs: number | null\n /** Where that time went. Null for a cached mix. */\n lastProfile: unknown\n /*\n `play`, `pause`, `seek`, `setRate` and `checkDrift` were here.\n\n They were the second half of a two-player preview: this hook played the\n prepared mix on its own player while the video player played the picture,\n and `checkDrift` compared them and corrected past a tolerance. The preview\n now plays both from one composition on one clock, so there is no second\n position to compare and nothing to correct — the correction was a seek, and\n a seek at a clip seam cost the decode it was trying to cover.\n\n What is left is preparation: compile the program, produce the file, and say\n whether it is ready. The composition opens it. @see useCompositionPlayback\n */\n /**\n * The prepared mix, for a composition that carries the sound itself.\n *\n * Null until one matching the document exists. The preview plays audio and\n * video from one composition on one clock, so what this hook contributes is\n * the prepared program — the file and how long it runs — rather than a second\n * player to keep in step with the first. `play`, `pause`, `seek`, `setRate`\n * and `checkDrift` are what is left of that second player and are no longer\n * part of the preview path.\n */\n mix: { uri: string; durationUs: number } | null\n /** Cancel, release the player and drop the mix. */\n release(): void\n}\n\n/** @experimental */\nexport function usePreviewAudio(options: UsePreviewAudioOptions): PreviewAudioBinding {\n const { project, enabled = true } = options\n const resolveSource = useRef(options.resolveSource)\n resolveSource.current = options.resolveSource\n\n const [status, setStatusState] = useState<PreviewAudioStatus>('source')\n const [error, setErrorState] = useState<string | null>(null)\n const [readyKey, setReadyKeyState] = useState<string | null>(null)\n const [lastPrepareMs, setLastPrepareMsState] = useState<number | null>(null)\n const [lastProfile, setLastProfileState] = useState<unknown>(null)\n const [mix, setMix] = useState<{ uri: string; durationUs: number } | null>(null)\n\n /*\n Every publication goes through one seam. Five setters scattered across\n three effects is exactly the shape that makes \"does rendering cause this\"\n unanswerable, and routing them here costs a call and makes it a switch.\n See `previewAudioDiagnostics`.\n */\n // Memoized so each publisher has one identity for the hook's whole life:\n // React's setters are stable, so these never rebuild, and the effects below\n // can depend on them without that dependency meaning anything at runtime.\n const setStatus = useMemo(() => publishState('status', setStatusState), [setStatusState])\n const setError = useMemo(() => publishState('error', setErrorState), [setErrorState])\n const setReadyKey = useMemo(\n () => publishState('readyKey', setReadyKeyState),\n [setReadyKeyState],\n )\n const setLastPrepareMs = useMemo(\n () => publishState('lastPrepareMs', setLastPrepareMsState),\n [setLastPrepareMsState],\n )\n const setLastProfile = useMemo(\n () => publishState('lastProfile', setLastProfileState),\n [setLastProfileState],\n )\n\n const preparingKey = useRef<string | null>(null)\n const failedKey = useRef<string | null>(null)\n const epoch = useRef(0)\n const mounted = useRef(true)\n\n /**\n * What this document wants, and what it needs to decode.\n *\n * Recomputed whenever the project changes and keyed by the compiled program,\n * so an edit that cannot change the sound produces the same key and starts\n * nothing.\n */\n const wanted = useMemo(() => {\n if (!enabled || project.audio.length === 0) {\n return {\n key: '',\n programJson: '',\n urisJson: '',\n durationUs: 0,\n unresolved: [] as string[],\n }\n }\n const program = compileAudioMixProgram(project)\n const resolved = resolveMixSources(project, program, (s, c) => resolveSource.current(s, c))\n const programJson = JSON.stringify(program)\n const urisJson = JSON.stringify(resolved.uris)\n return {\n key: resolved.unresolved.length > 0 ? '' : previewMixKey(programJson, urisJson),\n programJson,\n urisJson,\n durationUs: Number(program.durationUs),\n unresolved: resolved.unresolved,\n }\n }, [project, enabled])\n\n const decision = decidePreviewAudio({\n wantedKey: wanted.key,\n readyKey,\n preparingKey: preparingKey.current,\n failedKey: failedKey.current,\n })\n\n /* A source that cannot be resolved is a failure with a name, not a wait. */\n const unresolved = wanted.unresolved.length > 0 && enabled && project.audio.length > 0\n\n useEffect(() => {\n mounted.current = true\n return () => {\n mounted.current = false\n }\n }, [])\n\n /** Silence the other player exactly when the mix owns the sound. */\n useEffect(() => {\n if (!enabled) return\n try {\n requireNative().previewMuteSourceAudio(decision.muteSourceAudio || unresolved)\n } catch {\n // No adapter. The decision still stands for everything else.\n }\n }, [decision.muteSourceAudio, unresolved, enabled])\n\n useEffect(() => {\n if (unresolved) {\n setStatus('failed')\n setError(wanted.unresolved[0] ?? 'a sound could not be read')\n return\n }\n setStatus(decision.status)\n if (decision.status !== 'failed') setError(null)\n }, [decision.status, unresolved, wanted.unresolved, setError, setStatus])\n\n /** Start a job when the document wants a mix nobody is preparing. */\n useEffect(() => {\n if (!decision.shouldPrepare || unresolved) return\n const key = wanted.key\n preparingKey.current = key\n const jobId = `preview-${key.slice(0, 8)}-${epoch.current + 1}`\n const startedAt = Date.now()\n\n void preparePreviewMix(jobId, wanted.programJson, wanted.urisJson).then((result) => {\n if (!mounted.current) return\n /*\n Two independent staleness checks, and both are needed. The job may have\n been superseded inside native — it says so — and the document may have\n moved on while the promise was in flight, which native cannot know.\n */\n if (result.ok === false && result.superseded) return\n if (key !== preparingKey.current) return\n preparingKey.current = null\n\n if (result.ok === false) {\n failedKey.current = key\n setError(result.reason)\n setStatus('failed')\n return\n }\n\n failedKey.current = null\n epoch.current += 1\n setLastPrepareMs(result.cached ? null : Date.now() - startedAt)\n setLastProfile(result.cached ? null : (result.profile ?? null))\n try {\n requireNative().previewAudioLoad(result.uri, epoch.current)\n } catch {\n failedKey.current = key\n setError('the prepared mix could not be opened')\n setStatus('failed')\n return\n }\n /*\n Published for the composition, which is what plays it.\n\n The duration comes from the compiled program rather than from the file:\n it is the length the mix was trimmed to, and taking it from the compiler\n keeps it on the same side of the boundary as every other timing value.\n */\n setMix({ uri: result.uri, durationUs: Number(wanted.durationUs) })\n setReadyKey(result.key)\n // Bounded: keep the one being played and let the rest go.\n sweepPreviewMixes([result.key])\n })\n }, [\n decision.shouldPrepare,\n wanted.key,\n wanted.programJson,\n wanted.urisJson,\n unresolved,\n setError,\n setLastPrepareMs,\n setLastProfile,\n setReadyKey,\n setStatus,\n ])\n\n /*\n The second player's transport lived here: `play`, `pause`, `seek`,\n `setRate` and `checkDrift`.\n\n Deleted with the architecture that needed them. The preview plays the mix\n from the composition, on the same clock as the picture, so there is no\n second position to seek, rate or compare. `previewSyncStop` survives in\n `release` because a preparation in flight still has to be abandoned.\n */\n\n const release = useCallback(() => {\n cancelPreviewMix()\n try {\n requireNative().previewSyncStop('release')\n requireNative().previewAudioRelease()\n requireNative().previewMuteSourceAudio(false)\n } catch {\n // Teardown paths overlap on purpose; each must be safe after the others.\n }\n preparingKey.current = null\n setReadyKey(null)\n setMix(null)\n }, [setReadyKey])\n\n /* Backgrounding and unmount both stop work and give the player back. */\n useEffect(() => release, [release])\n\n return {\n status: unresolved ? 'failed' : status,\n error,\n canPlay: !unresolved && decision.canPlay,\n ready: decision.status === 'ready',\n lastPrepareMs,\n lastProfile,\n // Null until a mix matching the document is ready, so a composition built\n // from it is never built from a stale one.\n mix: decision.status === 'ready' ? mix : null,\n release,\n }\n}\n","/**\n * One isolation control for the preview-audio hook, and nothing else.\n *\n * ## What it is for\n *\n * The picture stops advancing when a mix is loaded, and the minimal failing\n * configuration loads a *cached* mix — no decode, no encode, no codecs at all.\n * What is left in common is this hook being active, and being active means two\n * separable things: the native calls it makes, and the React state it\n * publishes. Five `useState` values re-render the editor while a surface, a\n * texture consumer and a composition callback are all being re-established.\n *\n * Suppressing the publications while leaving every native call in place\n * separates those two. If the freeze survives suppression it belongs to the\n * native side; if it disappears, the trigger is a render-driven ownership\n * transition and the fix belongs in whatever that render disturbs.\n *\n * ## What it is not\n *\n * Not a fix, and not reachable from a production build. Suppressed, the hook\n * cannot report readiness, so audio never plays — the control is deliberately\n * useless for anything except attribution. Anyone tempted to leave it on to\n * \"reduce renders\" is turning a diagnostic into a silent feature regression.\n *\n * @experimental\n */\n\n/** @experimental */\nexport interface PreviewAudioDiagnostics {\n /** Drop the hook's React state publications, keeping every native call. */\n suppressPublications: boolean\n /** Log each publication with its value. Off by default; noisy under a mix. */\n logPublications: boolean\n /** Publications made, and publications suppressed. */\n published: number\n suppressed: number\n}\n\n/** @experimental */\nexport const previewAudioDiagnostics: PreviewAudioDiagnostics = {\n suppressPublications: false,\n logPublications: false,\n published: 0,\n suppressed: 0,\n}\n\n/**\n * Wrap a setter so it can be observed and, under the control, withheld.\n *\n * Takes the name rather than deriving one, because a minified build would\n * otherwise label every publication `t`.\n *\n * @experimental\n */\nexport function publishState<T>(name: string, set: (value: T) => void): (value: T) => void {\n return (value: T) => {\n if (previewAudioDiagnostics.suppressPublications) {\n previewAudioDiagnostics.suppressed += 1\n return\n }\n previewAudioDiagnostics.published += 1\n if (previewAudioDiagnostics.logPublications) {\n console.log(`AUDIOPUB ${name} ${JSON.stringify(value ?? null)}`)\n }\n set(value)\n }\n}\n","import { requireNative } from './player'\n\n/**\n * The AAC encoder's frozen behaviour, re-measurable on demand.\n *\n * The encode loop is 16% of a preview preparation and was 89% of it before it\n * was rewritten. That rewrite was checked against a baseline of natively\n * generated PCM fixtures — inputs that are formulas rather than files, so the\n * two halves of the comparison cannot drift apart — and the check was exact:\n * access-unit count, payload bytes, payload hash, CSD, timestamps, decoded\n * frame count, RMS and transient positions all matched.\n *\n * This is the door to that. Diagnostics-gated in native, so a release build\n * without them refuses.\n *\n * @experimental\n */\n\n/** @experimental */\nexport interface EncoderGateResult {\n ok: boolean\n fixture?: string\n /** Compressed shape. */\n units?: number\n payloadBytes?: number\n payloadHash?: number\n csd0?: number\n firstUs?: number\n lastUs?: number\n headTimestamps?: number[]\n /** Decoded result. */\n decodedFrames?: number\n decodedRms?: number\n peak?: number\n markerPositionsMs?: number[]\n expectedMarkersMs?: number[]\n /** What it cost. */\n encodeMs?: number\n inputFrames?: number\n fileBytes?: number\n error?: string\n}\n\n/** The fixtures the gate can run. @experimental */\nexport const ENCODER_GATE_FIXTURES = [\n 'short',\n 'acceptance',\n 'silence',\n 'saturated',\n 'markers',\n] as const\n\n/**\n * Encode one fixture and describe the result completely.\n *\n * Never throws: a gate that is unavailable is an answer.\n *\n * @experimental\n */\nexport function runEncoderGate(fixture: string, maxInputSize = 65536): EncoderGateResult {\n try {\n return JSON.parse(requireNative().encoderGateRun(fixture, maxInputSize)) as EncoderGateResult\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) }\n }\n}\n","import { requireNative } from './player'\n\n/**\n * The paired player trace.\n *\n * Both players sampled on one monotonic clock, buffered natively and drained\n * afterwards. It exists because the question — do the picture and the sound\n * agree — needs samples closer together than a bridge hop, and far closer than\n * `uiautomator` can time.\n *\n * Diagnostics-only in native: a build without them refuses, and even with them\n * it does nothing until armed.\n *\n * @experimental\n */\n\n/** One sample or event. @experimental */\nexport interface TraceEvent {\n seq: number\n /** `System.nanoTime()`, the clock both players publish on. */\n atNs: number\n type: string\n v: Record<string, unknown>\n}\n\n/** @experimental */\nexport interface TracePage {\n ok: boolean\n /**\n * Whether the *run* is trustworthy.\n *\n * False when the sampler threw. The events before the fault are real and the\n * silence after it is not evidence of anything — a summary computed over a\n * truncated run reads exactly like one computed over a complete one.\n */\n valid: boolean\n /** Why it stopped being trustworthy, or null. */\n fault: string | null\n events: TraceEvent[]\n nextSeq: number\n total: number\n /** Events overwritten before they could be read. Non-zero means a hole. */\n dropped: number\n}\n\nconst EMPTY: TracePage = {\n ok: false,\n valid: false,\n fault: 'trace unavailable',\n events: [],\n nextSeq: 0,\n total: 0,\n dropped: 0,\n}\n\n/** Arm the trace and start sampling. @experimental */\nexport function armPreviewTrace(on: boolean, intervalMs = 25): boolean {\n try {\n const value = JSON.parse(requireNative().previewTraceArm(on, intervalMs)) as { ok?: boolean }\n return value.ok === true\n } catch {\n return false\n }\n}\n\n/** Label the trace, so a harness can find its own cases. @experimental */\nexport function markPreviewTrace(label: string): void {\n try {\n requireNative().previewTraceMark(label)\n } catch {\n // Unarmed or unavailable; a mark that lands nowhere is not worth raising.\n }\n}\n\n/**\n * Drain a page of events.\n *\n * Paged rather than whole: a full trace is far past what a single log line\n * survives, and truncation in the middle is the failure this is meant to\n * detect rather than cause.\n *\n * @experimental\n */\nexport function drainPreviewTrace(fromSeq: number, limit = 200): TracePage {\n try {\n const value = JSON.parse(requireNative().previewTraceDrain(fromSeq, limit)) as TracePage\n return value.ok ? value : EMPTY\n } catch {\n return EMPTY\n }\n}\n\n/**\n * Every event, drained in pages until the buffer is exhausted.\n *\n * @experimental\n */\nexport function drainAllPreviewTrace(limit = 200): TracePage {\n const events: TraceEvent[] = []\n let seq = 0\n let dropped = 0\n let total = 0\n let valid = true\n let fault: string | null = null\n // Bounded: the ring is finite, so this cannot page forever even if the\n // sampler is still running while it drains.\n for (let guard = 0; guard < 200; guard += 1) {\n const page = drainPreviewTrace(seq, limit)\n // Carried from every page: a fault recorded mid-drain still invalidates\n // the run, and the last page is not special.\n if (page.fault) {\n valid = false\n fault = page.fault\n }\n if (!page.ok || page.events.length === 0) {\n total = page.total || total\n break\n }\n events.push(...page.events)\n dropped += page.dropped\n total = page.total\n if (page.nextSeq === seq) break\n seq = page.nextSeq\n }\n return { ok: true, valid, fault, events, nextSeq: seq, total, dropped }\n}\n\n/**\n * The offset between the players, summarised over the samples that mattered.\n *\n * Only samples where **both** players were playing are counted: a paused\n * player's position does not advance, so including it would average in a\n * divergence that is not drift.\n *\n * @experimental\n */\nexport function summariseOffsets(events: readonly TraceEvent[]): {\n samples: number\n playingSamples: number\n /** Samples whose presented frame was the same one as the tick before. */\n staleFrameSamples: number\n /**\n * Samples with no presented frame at all.\n *\n * `FrameClock.latest` is null until the renderer consumes something, and\n * again after a generation bump. Counting these as \"stale\" hid them: a run\n * reported 56 stale of 506 while every sampled point in the same run had no\n * record whatsoever, because an absent field is not a false one.\n */\n absentFrameSamples: number\n /** Samples with a frame that had advanced since the previous tick. */\n freshFrameSamples: number\n /** The longest run of consecutive stale samples. */\n longestStaleRun: number\n minUs: number\n maxUs: number\n meanUs: number\n worstAbsUs: number\n} {\n let n = 0\n let stale = 0\n let absent = 0\n let fresh = 0\n let staleRun = 0\n let longestStaleRun = 0\n let min = Number.POSITIVE_INFINITY\n let max = Number.NEGATIVE_INFINITY\n let sum = 0\n let samples = 0\n for (const e of events) {\n if (e.type !== 'sample') continue\n samples += 1\n /*\n Stale frames are counted, never filtered.\n\n A presented time that has not moved since the last tick describes a frame\n the renderer already showed. Dropping those samples and reporting the\n percentage that remain would be reporting a pass over the interval where\n the picture was least trustworthy.\n */\n if (e.v.pfFresh === undefined) {\n absent += 1\n staleRun += 1\n if (staleRun > longestStaleRun) longestStaleRun = staleRun\n } else if (e.v.pfFresh === false) {\n stale += 1\n staleRun += 1\n if (staleRun > longestStaleRun) longestStaleRun = staleRun\n } else {\n fresh += 1\n staleRun = 0\n }\n if (e.v.videoPlaying !== true || e.v.audioPlaying !== true) continue\n const offset = Number(e.v.offsetUs)\n if (!Number.isFinite(offset)) continue\n n += 1\n sum += offset\n if (offset < min) min = offset\n if (offset > max) max = offset\n }\n return {\n samples,\n playingSamples: n,\n staleFrameSamples: stale,\n absentFrameSamples: absent,\n freshFrameSamples: fresh,\n longestStaleRun,\n minUs: n > 0 ? min : 0,\n maxUs: n > 0 ? max : 0,\n meanUs: n > 0 ? Math.round(sum / n) : 0,\n worstAbsUs: n > 0 ? Math.max(Math.abs(min), Math.abs(max)) : 0,\n }\n}\n\n/**\n * What the stall coordinator has done.\n *\n * Holds, releases and realigning seeks. A gate asserts that seams produced\n * holds and that every hold resolved — a coordinator that never engaged and one\n * that engaged and never released look identical in an offset summary.\n *\n * @experimental\n */\nexport function previewSyncCounters(): Record<string, unknown> {\n try {\n return JSON.parse(requireNative().previewSyncCounters()) as Record<string, unknown>\n } catch {\n return { ok: false }\n }\n}\n\n/**\n * Stop playback for a host reason — background, picker, unmount.\n *\n * Overrides a resume the coordinator is holding, so playback cannot restart at\n * a moment nobody asked for.\n *\n * @experimental\n */\nexport function stopPreviewForHost(reason: string): void {\n try {\n requireNative().previewSyncStop(reason)\n } catch {\n // No adapter, or nothing playing. Neither is worth raising in teardown.\n }\n}\n\n/**\n * Frames offered, queued and drawn.\n *\n * The instrument-disruption check reads this with tracing off and again with it\n * on: if the drawn count advances at the same rate either way, the trace is not\n * changing what it measures. Three defects in this area were the measurement\n * rather than the product, so that check comes before the numbers do.\n *\n * @experimental\n */\nexport function previewFramePipeline(): Record<string, number> {\n try {\n return JSON.parse(requireNative().previewFramePipeline()) as Record<string, number>\n } catch {\n return {}\n }\n}\n\n/** Which parts of the music preview exist. All true in production. @experimental */\nexport interface PreviewParts {\n /** Whether the drift check may act. Off measures without correcting. */\n driftCorrection: boolean\n loadPlayer: boolean\n startMix: boolean\n positionPump: boolean\n coordinator: boolean\n}\n\n/**\n * Turn parts of the music preview off, to bisect a failure.\n *\n * Gate-only in native. Guessing between a second player, its position pump, the\n * stall coordinator and the mix actually playing is how the previous attempts\n * in this area became tuning; this is how one of them gets ruled out at a time.\n *\n * @experimental\n */\nexport function setPreviewParts(parts: Partial<PreviewParts>): PreviewParts | null {\n try {\n const v = JSON.parse(requireNative().previewConfigSet(JSON.stringify(parts))) as\n PreviewParts & { ok?: boolean }\n return v.ok ? v : null\n } catch {\n return null\n }\n}\n\n/** @experimental */\nexport function previewParts(): PreviewParts | null {\n try {\n return JSON.parse(requireNative().previewConfigGet()) as PreviewParts\n } catch {\n return null\n }\n}\n\n/**\n * Silence the video player's own audio.\n *\n * Exposed because a bisect control needs the *same* mute in every\n * configuration: without it, the video-only row would be the only one with\n * audible clip audio, and any difference against the others would be\n * confounded by a whole track the rest do not have.\n *\n * @experimental\n */\nexport function muteSourceAudio(muted: boolean): void {\n try {\n requireNative().previewMuteSourceAudio(muted)\n } catch {\n // No adapter. The caller's other work still stands.\n }\n}\n\n/**\n * Every codec mix preparation owns, and what became of it.\n *\n * A `release()` that was called is not a release that finished, so both ends\n * are recorded along with the exception path. The preview video decoder is not\n * here: it belongs to the player and outlives any job, and counting it would\n * make \"live after publication\" always true.\n *\n * @experimental\n */\nexport function previewCodecLedger(): {\n live: number\n peakLive: number\n total: number\n entries: Record<string, unknown>[]\n} {\n try {\n return JSON.parse(requireNative().previewCodecLedger()) as {\n live: number\n peakLive: number\n total: number\n entries: Record<string, unknown>[]\n }\n } catch {\n return { live: -1, peakLive: -1, total: 0, entries: [] }\n }\n}\n"],"mappings":"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","names":["useCallback","useEffect","useRef","useState","playlistOf","useRef","useState","playlistOf","useEffect","useCallback","playIntent","playlistOf","projectDuration","useCallback","useEffect","useMemo","useRef","useState","AppState","useState","useMemo","playlistOf","projectDuration","useRef","useEffect","AppState","useCallback","playIntent","DEFAULT_POLL_MS","uriOf","VideoExportCancelled","projectDuration","projectDuration","VideoExportCancelled","counter","call","compileAudioMixProgram","record","record","compileAudioMixProgram","useCallback","useEffect","useMemo","useRef","useState","useRef","useState","useMemo","compileAudioMixProgram","useEffect","useCallback","EMPTY"]}
|
|
1
|
+
{"version":3,"sources":["../src/memoryGuard.ts","../src/syncTrace.ts","../src/NativePictorVideo.ts","../src/player.ts","../src/media3.ts","../src/playlistSession.ts","../src/surfaceOwner.ts","../src/useVideoSurface.ts","../src/useCompositionPlayback.ts","../src/compositionSpike.ts","../src/playbackLifecycle.ts","../src/useMedia3Playback.ts","../src/surfaceRecovery.ts","../src/thumbnailProvider.ts","../src/convergence.ts","../src/exportJob.ts","../src/mixSources.ts","../src/exportDestination.ts","../src/exportProgress.ts","../src/exportProgressPump.ts","../src/exporter.ts","../src/mediaStore.ts","../src/overlayStaging.ts","../src/audioProbe.ts","../src/managedSources.ts","../src/previewMix.ts","../src/previewAudioSession.ts","../src/tracePhases.ts","../src/spikeExcursions.ts","../src/previewCadence.ts","../src/submissionScoring.ts","../src/usePreviewAudio.ts","../src/previewAudioDiagnostics.ts","../src/encoderGate.ts","../src/previewTrace.ts"],"sourcesContent":["/**\n * A debug safety cutoff on native memory.\n *\n * **Remove before production.** This exists because an earlier revision drove\n * the native heap to 1.5 GB in sixty seconds and the process was killed —\n * taking other applications on the device with it, since Android reclaims from\n * everything before it gives up on the foreground app. A diagnostic run must\n * not be able to do that again.\n *\n * Two independent triggers, because a leak can be fast or merely relentless:\n *\n * - **Ceiling.** An absolute limit, well under anything that would put the\n * device under pressure.\n * - **Sustained rise.** Memory that climbs across every sample of a window with\n * no plateau, which is what a slow leak looks like and what a ceiling alone\n * would take minutes to catch.\n *\n * Pure, so the rule is tested rather than trusted: a cutoff that misfires stops\n * a good run, and one that never fires is not a cutoff.\n *\n * @experimental\n */\n\nexport interface MemoryGuard {\n readonly ceilingBytes: number\n readonly windowSamples: number\n /** Minimum total climb across the window to count as a rise, in bytes. */\n readonly riseBytes: number\n samples: number[]\n tripped: null | 'ceiling' | 'sustained-rise'\n}\n\n/**\n * 250 MB. Healthy single-clip playback plateaus near 65 MB.\n *\n * Was 400 MB, which a 1 MB/s leak takes six minutes to reach — long enough for\n * a five-minute run to finish, look successful, and leave the process fat\n * enough that the next run pushed the device into reclaiming from other\n * applications.\n */\nexport const DEFAULT_CEILING_BYTES = 250 * 1024 * 1024\n\n/** Forty samples at 4 Hz — ten seconds of uninterrupted climbing. */\nexport const DEFAULT_WINDOW_SAMPLES = 40\n\n/**\n * 8 MB across the window: ~0.8 MB/s.\n *\n * Was 24 MB over three seconds — 8 MB/s, calibrated against a leak that hit\n * 1.5 GB in a minute. It was blind to the 1.07 MB/s leak that actually got the\n * process killed, so the cutoff never fired. A cutoff tuned only to the last\n * failure is not a cutoff.\n *\n * A threshold this small is safe only because the rise must also be *strictly*\n * increasing at every one of the forty samples. Real memory jitters; a leak\n * does not.\n */\nexport const DEFAULT_RISE_BYTES = 8 * 1024 * 1024\n\nexport function createMemoryGuard(\n ceilingBytes = DEFAULT_CEILING_BYTES,\n windowSamples = DEFAULT_WINDOW_SAMPLES,\n riseBytes = DEFAULT_RISE_BYTES,\n): MemoryGuard {\n return {\n ceilingBytes,\n windowSamples: Math.max(3, Math.floor(windowSamples)),\n riseBytes,\n samples: [],\n tripped: null,\n }\n}\n\n/**\n * Record a reading and decide whether to stop.\n *\n * Once tripped it stays tripped: the caller has already torn the session down,\n * and a guard that untrips would invite it to start again into the same leak.\n */\nexport function observeMemory(guard: MemoryGuard, bytes: number): MemoryGuard {\n if (guard.tripped) return guard\n if (!Number.isFinite(bytes) || bytes <= 0) return guard\n\n if (bytes >= guard.ceilingBytes) {\n return { ...guard, samples: [...guard.samples, bytes].slice(-guard.windowSamples), tripped: 'ceiling' }\n }\n\n const samples = [...guard.samples, bytes].slice(-guard.windowSamples)\n if (samples.length < guard.windowSamples) return { ...guard, samples }\n\n // **Strictly** increasing across the whole window, and a meaningful total\n // climb. Both conditions matter, and strictness is the subtle one: allowing\n // equal consecutive samples made a single large allocation followed by a flat\n // plateau — a decoder starting up — read as a leak. A leak climbs at every\n // sample; an allocation climbs once and then stops.\n let climbing = true\n for (let i = 1; i < samples.length; i++) {\n if (samples[i]! <= samples[i - 1]!) {\n climbing = false\n break\n }\n }\n const climb = samples[samples.length - 1]! - samples[0]!\n if (climbing && climb >= guard.riseBytes) {\n return { ...guard, samples, tripped: 'sustained-rise' }\n }\n return { ...guard, samples }\n}\n\n/** A sentence a human can act on, or null while nothing is wrong. */\nexport function describeTrip(guard: MemoryGuard): string | null {\n if (!guard.tripped) return null\n const mb = (b: number) => (b / (1024 * 1024)).toFixed(0)\n const latest = guard.samples[guard.samples.length - 1] ?? 0\n return guard.tripped === 'ceiling'\n ? `native heap reached ${mb(latest)} MB (ceiling ${mb(guard.ceilingBytes)} MB) — session released`\n : `native heap rose ${mb(latest - (guard.samples[0] ?? 0))} MB across ${guard.samples.length} samples without plateauing — session released`\n}\n","/**\n * Per-tick synchronisation forensics.\n *\n * A single offset number cannot say *why* video is late, and optimising against\n * it invites exactly the wrong fixes — presenting future frames, nudging the\n * clock, delaying audio. Each of those improves the number and worsens the\n * playback.\n *\n * So every tick is decomposed into causes that can be acted on separately:\n *\n * - **Frame quantisation** — video only changes every frame period, so the\n * picture is always somewhat behind the audio instant. Bounded by one frame\n * by definition, and not a defect.\n * - **Decoder lateness** — the frame that *should* be showing had not been\n * decoded yet. Fix by adjusting the bounded lead or queue depth.\n * - **Scheduler lateness** — the right frame *was* available and was not\n * selected. Fix the missed tick or the contention causing it.\n * - **Clock correction** — how far each raw `AudioTrack` report moves the\n * extrapolated position. Large jumps mean stale timestamps or a bad baseline.\n *\n * Pure, so the decomposition is tested rather than trusted.\n *\n * @experimental\n */\n\nexport interface TickSample {\n /** Authoritative extrapolated audio time. */\n audioUs: number\n /** Age of the newest raw `getTimestamp()` report at this tick. */\n rawAgeUs: number\n /** How far the newest raw report moved the extrapolated position. */\n correctionUs: number\n /** Newest decoded frame at or before `audioUs`, or null if none is eligible. */\n newestEligiblePtsUs: number | null\n /** What was actually presented, or null. */\n presentedPtsUs: number | null\n /** Interval since the previous tick. */\n tickIntervalUs: number\n queueDepth: number\n decoderLeadUs: number\n}\n\nexport interface SyncTrace {\n readonly frameUs: number\n quantisation: number[]\n decoderLate: number[]\n schedulerLate: number[]\n correction: number[]\n presentedOffset: number[]\n tickInterval: number[]\n ticks: number\n decoderLateTicks: number\n schedulerLateTicks: number\n}\n\nconst MAX_SAMPLES = 20_000\n\nexport function createSyncTrace(fps: number): SyncTrace {\n const rate = Number.isFinite(fps) && fps > 0 ? fps : 30\n return {\n frameUs: Math.round(1_000_000 / rate),\n quantisation: [],\n decoderLate: [],\n schedulerLate: [],\n correction: [],\n presentedOffset: [],\n tickInterval: [],\n ticks: 0,\n decoderLateTicks: 0,\n schedulerLateTicks: 0,\n }\n}\n\n/**\n * The frame that ought to be on screen at `audioUs`.\n *\n * Assumes a regular frame grid anchored at zero, which the controlled fixture\n * has. On a variable-rate source this is an approximation and the quantisation\n * figure should be read as indicative — which is why it is stated here rather\n * than left for a reader to infer from a clean-looking number.\n */\nexport function expectedPtsAt(audioUs: number, frameUs: number): number {\n if (audioUs <= 0 || frameUs <= 0) return 0\n return Math.floor(audioUs / frameUs) * frameUs\n}\n\nexport function record(trace: SyncTrace, sample: TickSample): SyncTrace {\n if (trace.ticks >= MAX_SAMPLES) return { ...trace, ticks: trace.ticks + 1 }\n\n const expected = expectedPtsAt(sample.audioUs, trace.frameUs)\n const next: SyncTrace = {\n ...trace,\n ticks: trace.ticks + 1,\n quantisation: [...trace.quantisation, sample.audioUs - expected],\n correction: [...trace.correction, Math.abs(sample.correctionUs)],\n tickInterval: [...trace.tickInterval, sample.tickIntervalUs],\n decoderLate: trace.decoderLate,\n schedulerLate: trace.schedulerLate,\n presentedOffset: trace.presentedOffset,\n decoderLateTicks: trace.decoderLateTicks,\n schedulerLateTicks: trace.schedulerLateTicks,\n }\n\n // Decoder lateness: the frame that should be showing was not available.\n // Measured as how far short of `expected` the newest eligible frame fell.\n const eligible = sample.newestEligiblePtsUs\n if (eligible === null || eligible < expected) {\n const shortfall = eligible === null ? sample.audioUs : expected - eligible\n next.decoderLate = [...trace.decoderLate, shortfall]\n next.decoderLateTicks = trace.decoderLateTicks + 1\n }\n\n // Scheduler lateness: an eligible frame existed and something older — or\n // nothing — was shown instead. This is the category that means a tick was\n // missed rather than a frame being absent.\n if (eligible !== null && (sample.presentedPtsUs === null || sample.presentedPtsUs < eligible)) {\n const behind = eligible - (sample.presentedPtsUs ?? 0)\n next.schedulerLate = [...trace.schedulerLate, behind]\n next.schedulerLateTicks = trace.schedulerLateTicks + 1\n }\n\n if (sample.presentedPtsUs !== null) {\n next.presentedOffset = [...trace.presentedOffset, sample.presentedPtsUs - sample.audioUs]\n }\n return next\n}\n\nexport interface Distribution {\n n: number\n p50: number\n p95: number\n p99: number\n max: number\n}\n\nexport function distribution(values: readonly number[]): Distribution {\n if (values.length === 0) return { n: 0, p50: 0, p95: 0, p99: 0, max: 0 }\n const abs = values.map(Math.abs).sort((a, b) => a - b)\n const at = (q: number) => abs[Math.min(abs.length - 1, Math.floor(q * abs.length))]!\n return { n: abs.length, p50: at(0.5), p95: at(0.95), p99: at(0.99), max: abs[abs.length - 1]! }\n}\n\nexport interface SyncReport {\n quantisation: Distribution\n decoderLate: Distribution\n schedulerLate: Distribution\n correction: Distribution\n presentedOffset: Distribution\n tickInterval: Distribution\n ticks: number\n decoderLateTicks: number\n schedulerLateTicks: number\n frameUs: number\n /** Which cause dominates, so the fix is chosen by evidence. */\n dominant: 'none' | 'decoder' | 'scheduler' | 'quantisation'\n}\n\nexport function syncReport(trace: SyncTrace): SyncReport {\n const decoderLate = distribution(trace.decoderLate)\n const schedulerLate = distribution(trace.schedulerLate)\n const quantisation = distribution(trace.quantisation)\n const presentedOffset = distribution(trace.presentedOffset)\n\n // Attribute by how often each fires and how badly, not by which p95 is\n // numerically larger: a rare huge decoder stall and a constant one-frame\n // scheduler lag need different fixes.\n const decoderShare = trace.ticks === 0 ? 0 : trace.decoderLateTicks / trace.ticks\n const schedulerShare = trace.ticks === 0 ? 0 : trace.schedulerLateTicks / trace.ticks\n let dominant: SyncReport['dominant'] = 'none'\n if (presentedOffset.p95 > trace.frameUs) {\n if (decoderShare >= schedulerShare && decoderShare > 0.01) dominant = 'decoder'\n else if (schedulerShare > 0.01) dominant = 'scheduler'\n else dominant = 'quantisation'\n }\n\n return {\n quantisation,\n decoderLate,\n schedulerLate,\n correction: distribution(trace.correction),\n presentedOffset,\n tickInterval: distribution(trace.tickInterval),\n ticks: trace.ticks,\n decoderLateTicks: trace.decoderLateTicks,\n schedulerLateTicks: trace.schedulerLateTicks,\n frameUs: trace.frameUs,\n dominant,\n }\n}\n","/* eslint-disable @typescript-eslint/no-wrapper-object-types --\n * React Native's codegen recognises the upper-cased `Object` as its\n * `UnsafeObject` type and rejects the primitive `object`. This file is a\n * codegen spec before it is TypeScript, so the generator's spelling wins.\n */\nimport { TurboModuleRegistry, type TurboModule } from 'react-native'\n\n/**\n * The native surface. **Internal** until the API stabilises.\n *\n * Numbers, strings and plain objects only. `ExoPlayer`, `MediaItem`, `Surface`\n * and every other Android or Media3 type stops at the Kotlin side of this\n * boundary and never appears in a Pictor signature.\n *\n * Nothing here is per frame. Frames reach the renderer through the\n * `__pictorVideoSurface` host object on the UI runtime, so the JS thread is not\n * in the picture's path at all; these calls are controls and a throttled\n * snapshot.\n */\nexport interface Spec extends TurboModule {\n /** Publishes `__pictorVideoSurface` on the JS runtime. */\n installVideoSurface(): boolean\n /**\n * Publishes `__pictorOverlayStaging` on the JS runtime.\n *\n * Native chooses the directory; JS never supplies a destination.\n */\n installOverlayStaging(): boolean\n\n /** Open a source — local `android.resource:`/`file:` or remote — and render it. */\n media3Prepare(uri: string): boolean\n /** Diagnostics: a player with no video output, for heap attribution. */\n media3PrepareNoSurface(uri: string): boolean\n /** Diagnostics: point the existing player at another source. */\n media3Rebind(uri: string): boolean\n /** Diagnostics: end the source, keep the player. */\n media3StopKeepingPlayer(): void\n /**\n * Replace the playlist. Two parallel arrays: sources, then four numbers per\n * entry — clipping start and end in microseconds, speed in milli-units, and\n * muted as 0 or 1.\n */\n media3SetPlaylist(\n uris: string[],\n bounds: number[],\n startWindow: number,\n startPositionUs: number,\n ): boolean\n /** Seek in the player's coordinates: window index plus offset. */\n media3SeekToWindow(windowIndex: number, positionUs: number): void\n /**\n * **V3.3 gate only.** Encode one source through Transformer, once.\n *\n * Not an exporter and not reachable from `VideoExporter`: it takes a single\n * URI rather than a project, writes to an adapter-private temporary, and\n * exists to answer whether Skia can draw inside Transformer's frame processor\n * without changing the picture. `withSkia` selects the effect chain — false is\n * the vanilla run that establishes what the codec alone costs.\n *\n * Returns a JSON string rather than a shaped object because the shape is a\n * gate's report, not an API, and freezing it here would be pretending\n * otherwise.\n *\n * @experimental\n */\n exportGateRun(uri: string, withSkia: boolean, timeoutSeconds: number): string\n /**\n * **V3.3 gate only.** Execute one serialized, versioned `VideoScene`.\n *\n * Native receives a scene, never a project: ids, indices and numbers that\n * `resolveVideoScene` already settled. It validates the schema version, the\n * command order and every identity before Transformer starts, and refuses\n * anything it cannot execute faithfully rather than skipping it.\n *\n * @experimental\n */\n exportGateRunScene(\n uri: string,\n sceneJson: string,\n timeoutSeconds: number,\n /** `-1` never fails; `0` fails after GL init before the first frame; `n` after n frames. */\n failAfterFrames: number,\n ): string\n /**\n * **V3.3 gate only.** Execute a compiled scene *program* over a sequence.\n *\n * One document for the whole export: sorted, contiguous, half-open segments\n * compiled from the project's clip placements and overlay spans. Native\n * validates coverage, then selects a segment per frame by binary search — a\n * lookup, not an interpretation.\n *\n * @experimental\n */\n exportGateRunProgram(uris: string[], programJson: string, timeoutSeconds: number): string\n /** **V3.3 gate only.** Delete every temporary the gate created. @experimental */\n /**\n * **V3.3 gate only.** A sequence whose clipping and speed TypeScript chose.\n *\n * `itemsJson` is `{ uri, startMs, endMs, speed, durationUs?, forceAudioTrack? }[]`.\n * Native hands these to Media3 and computes none of them: which part of a\n * source plays, and how fast, is timeline interpretation and belongs to the\n * compiler.\n *\n * `forceAudioTrack` is sequence-level and honoured if any item sets it. A\n * source with no audio track fails outright at any rate other than 1 without\n * it — Media3 drives its speed path through audio.\n *\n * @experimental\n */\n exportGateRunItems(itemsJson: string, programJson: string, timeoutSeconds: number): string\n exportGateReleaseTemporary(): number\n /**\n * **V3.3 gate only.** Compare two files in **display space**.\n *\n * Display space is the only space the question means anything in: a file\n * storing 1080×1920 with a rotation flag and one storing 1920×1080 with the\n * orientation baked in display identically, and comparing stored dimensions\n * would call that a difference.\n *\n * Returns a JSON report of per-timestamp statistics. It states no verdict —\n * the threshold is frozen by the caller, from the control, before the thing\n * being tested is looked at.\n *\n * @experimental\n */\n exportGateCompare(leftUri: string, rightUri: string, timestampsUs: number[]): string\n /**\n * **V3.3 gate only.** Mean colour of a central region, per timestamp.\n *\n * Reports numbers and draws no conclusion. Which source a colour implies, and\n * which frame of it, is arithmetic the TypeScript oracle owns.\n *\n * @experimental\n */\n exportGateSample(uri: string, timestampsUs: number[]): string\n /**\n * **V3.3 gate only.** Muxed sample timestamps and per-track durations.\n *\n * A frame count and a whole-file duration cannot tell a held tail frame from a\n * correctly-timed one, or a gap from a slow rate. The written timestamps can.\n * Reports them; judges nothing.\n *\n * @experimental\n */\n /**\n * **V3.4 gate only.** Mean and extremes over caller-chosen rectangles.\n *\n * `probesJson` is `{ timeUs, x, y, w, h }[]`. The fixed centre region\n * {@link exportGateSample} uses straddles a transformed clip's edge and\n * averages source pixels with cleared ones; the caller has the corner oracle\n * and passes rectangles it has confirmed are covered.\n *\n * @experimental\n */\n /**\n * **V3.5 gate only.** Decoded audio: loudness and spectrum per window.\n *\n * A pitch-shifting speed change and a pitch-preserving one produce files whose\n * duration and sample count are identical, so counting cannot tell them apart.\n * Also reports AAC encoder delay, padding and the first presentation time,\n * which a seam alignment check needs.\n *\n * @experimental\n */\n exportGateAudio(uri: string, windowsJson: string): string\n exportGateProbe(uri: string, probesJson: string): string\n exportGateTracks(uri: string): string\n /**\n * **V3.4 gate only.** One decoded frame as base64 PNG, in display space.\n *\n * @experimental\n */\n exportGateFramePng(uri: string, timeUs: number): string\n /**\n * **V3.4 gate only.** Encode a still through the treatment's encoder.\n *\n * A comparison between two renderers is only about the renderers if\n * everything after them is the same, so the reference is encoded and decoded\n * exactly as the treatment is.\n *\n * @experimental\n */\n exportGateEncodeStill(pngBase64: string, frames: number): string\n /**\n * **V3.4 gate only.** Geometry and interior colour, measured separately.\n *\n * A maximum channel delta is near-useless at a transformed edge: one row of\n * pixels on the far side of a bright/black boundary reads 255 while the\n * picture is right. Returns mask overlap, edge displacement and interior\n * colour error, and draws no conclusion about any of them.\n *\n * @experimental\n */\n exportGateGeometry(\n leftUri: string,\n leftUs: number,\n rightUri: string,\n rightUs: number,\n maskThreshold: number,\n ): string\n /**\n * **V3.3 gate only.** Per-sample checksums of the compressed video payload.\n *\n * Transmuxing must move encoded bytes and change only timestamps and container\n * metadata. Decoded pixels cannot show that — a re-encode that happens to look\n * identical would pass — so the payload itself is checksummed.\n *\n * @experimental\n */\n exportGateSampleHashes(uri: string): string\n /**\n * **V3.3 gate only.** One Transformer job per clip, then a transmux concat.\n *\n * `shardsJson` is `{ uri, startMs, endMs, speed, timelineOffsetUs,\n * forceAudioTrack, frameDropFps?, encoderRepeatUs?, emitsPerInput?,\n * outputPtsUs?, cadenceFailAfterEmits? }[]`. The cadence arrays come straight\n * from `resampleClip`; native walks them and computes no grid arithmetic. Native adds `timelineOffsetUs` to each local\n * presentation timestamp and does segment lookup; it computes no placement,\n * duration or rate. Intermediates are deleted on every path.\n *\n * @experimental\n */\n exportGateRunShards(\n shardsJson: string,\n programJson: string,\n timeoutSeconds: number,\n overlaysJson: string,\n ): string\n /**\n * **V3.4 gate only.** Write raw RGBA bytes to a sidecar and checksum them.\n *\n * Pixels never travel in scene JSON. This stages them beside it and returns\n * the path, length and CRC32 a descriptor must declare.\n *\n * @experimental\n */\n /**\n * **V3.5 gate only.** Video shards, an audio timeline, then a muxer copy.\n *\n * The two pipelines never meet until the last step, and that step decodes\n * nothing: `MediaMuxer` writes both sets of encoded samples unchanged, so\n * neither takes a second generation.\n *\n * @experimental\n */\n /**\n * Run an export off the bridge so it can be cancelled while it runs.\n *\n * Resolves with the same JSON outcome the blocking call returns. @experimental\n */\n exportRunProjectAsync(\n jobId: string,\n shardsJson: string,\n programJson: string,\n overlaysJson: string,\n mixProgramJson: string,\n mixUrisJson: string,\n timeoutSeconds: number,\n /** `\"\"` for a temporary, or `{kind:'file',path,overwrite}`. @experimental */\n destinationJson: string,\n ): Promise<string>\n /**\n * Save a finished export into the device's video collection.\n *\n * Inserts pending, streams, verifies, then publishes. Settles once: the\n * published `content:` uri, or an error with the pending row already\n * deleted. @experimental\n */\n mediaSaveAsync(jobId: string, sourceUri: string): Promise<string>\n /**\n * Offer a saved video to the system share sheet.\n *\n * The durable `content:` uri only, with a read grant and nothing else.\n * Returns what the intent carried, so a gate can inspect it. @experimental\n */\n mediaShare(uri: string): string\n /** Ask a running save to stop. Idempotent. @experimental */\n mediaSaveCancel(jobId: string): string\n /** Delete this app's own stale pending rows. Never a published item. @experimental */\n mediaSaveSweepPending(): string\n /** Confirm a saved item still opens. @experimental */\n mediaSaveReadable(uri: string): string\n /**\n * Release one temporary output this module produced. Idempotent.\n *\n * Refuses any path it did not create, so it cannot delete a caller's file.\n * @experimental\n */\n exportReleaseTemporaryOutput(uri: string): string\n /**\n * Remove `.pictor-staging-*.tmp` orphans a killed process left behind.\n *\n * Positively identified by prefix and suffix; never a wildcard. @experimental\n */\n exportSweepStagingOrphans(directory: string): string\n /** Counted work plus the beat total, for the progress pump. @experimental */\n exportProgressSnapshot(): string\n /**\n * What a picked audio file actually contains.\n *\n * Duration, audio-track presence, seekability and MIME, read by opening the\n * file. The host asks this before a sound may enter a project: the mix\n * compiler has no decoder and believes whatever the document declares, and a\n * document that claimed audio a file lacked cost a whole export. Returns\n * `{ok, durationUs, hasAudioTrack, seekable, mimeType}` or `{ok:false,\n * reason}` — never throws, because an unreadable pick is an answer.\n * @experimental\n */\n /**\n * Prepare the audio a preview plays, off the JS thread.\n *\n * The same mixer an export runs, over the same compiled program. Resolves\n * `{ok, uri, key, cached, byteLength}`, or `{ok:false, superseded}` when a\n * newer request replaced it — which a caller must never publish.\n * @experimental\n */\n previewMixPrepare(\n jobId: string,\n programJson: string,\n urisJson: string,\n ): Promise<string>\n /**\n * Load a prepared mix into the mixed-audio player, stamped with an epoch.\n *\n * The epoch is how a start or a seek proves it belongs to the mix currently\n * playing; one carrying an older epoch is dropped. @experimental\n */\n previewAudioLoad(uri: string, epoch: number): string\n /** Start, if the epoch still matches. @experimental */\n previewAudioPlay(epoch: number): string\n /** @experimental */\n previewAudioPause(): string\n /** Seek to a global timeline position, if the epoch still matches. @experimental */\n previewAudioSeek(positionUs: number, epoch: number): string\n /** Preview rate. Clip speed is already compiled into the mix. @experimental */\n previewAudioSetRate(rate: number): string\n /** Position and the wall clock it was sampled at, for drift. @experimental */\n previewAudioState(): string\n /**\n * Silence the video player's own audio while a mixed preview plays.\n *\n * The mix contains the clips' audio too, so both audible plays the timeline\n * twice. @experimental\n */\n previewMuteSourceAudio(muted: boolean): string\n /** @experimental */\n previewAudioRelease(): string\n /**\n * Gate-only: encode a fixed, natively generated PCM fixture and describe the\n * result — access units, timestamps, CSD, payload hashes and decoded metrics.\n *\n * The inputs are formulas rather than files, so a baseline taken before a\n * change to the encoder loop is comparable with one taken after it.\n * @experimental\n */\n encoderGateRun(fixture: string, maxInputSize: number): string\n /** Gate-only: where samples go, stage by stage, for one marker source. @experimental */\n primingAttribution(): string\n /** Gate-only: the file-measurable Music gates, in one run. @experimental */\n musicGates(): string\n\n /**\n * Gate-only: mix the marker fixture at a chosen timeline start, decode the\n * result and report where the markers actually landed. @experimental\n */\n previewAlignmentGate(startUs: number): string\n\n /** Gate-only: arm the paired player trace and sample every `intervalMs`. @experimental */\n previewTraceArm(on: boolean, intervalMs: number): string\n /** Gate-only: label the trace so a harness can find its own cases. @experimental */\n previewTraceMark(label: string): string\n /** Gate-only: drain trace events from a sequence number, paged. @experimental */\n previewTraceDrain(fromSeq: number, limit: number): string\n /**\n * Compare both players on one clock and correct the mix past a tolerance.\n *\n * Native because that is where both anchors are published; measuring across\n * the bridge would carry the hop's own jitter into the answer. @experimental\n */\n previewDriftCheck(toleranceUs: number): string\n /**\n * Stop playback for a host reason — background, picker, unmount.\n *\n * Overrides any automatic resume the stall coordinator is holding, so\n * playback cannot restart at a moment nobody asked for. @experimental\n */\n previewSyncStop(reason: string): string\n /** What the stall coordinator has done, for assertions. @experimental */\n previewSyncCounters(): string\n /**\n * Frames offered, queued and drawn.\n *\n * Read-only accounting, available in release, so the question \"is the trace\n * disturbing the picture?\" can be asked without the trace. @experimental\n */\n previewFramePipeline(): string\n /**\n * Gate-only: the shared-clock playback spike.\n *\n * `spikeStart` takes `{items:[{uri,startUs,endUs,speed}], mixUri, width,\n * height}` straight from `playlistOf()`, so the compiler decides the timing\n * and native maps it onto media items without interpreting the project.\n */\n spikeStart(specJson: string): string\n spikePlay(): string\n spikePause(): string\n spikeSeek(positionUs: number): string\n spikeClaim(): string\n spikeState(): string\n spikeDrain(fromSeq: number, limit: number): string\n spikeRelease(): string\n /**\n * Gate-only: turn parts of the music preview off, to bisect a failure.\n *\n * `{loadPlayer, startMix, positionPump, coordinator}`, all true in\n * production. @experimental\n */\n previewConfigSet(json: string): string\n /** What the configuration currently is. @experimental */\n previewConfigGet(): string\n /** Every codec mix preparation owns, and what became of it. @experimental */\n previewCodecLedger(): string\n /** Stop whatever is preparing. @experimental */\n previewMixCancel(): string\n /** The cache key for a program, without preparing it. @experimental */\n previewMixKey(programJson: string, urisJson: string): string\n /** Drop one prepared mix. @experimental */\n previewMixRelease(key: string): string\n /** Keep only these keys; remove the rest and every partial. @experimental */\n previewMixSweep(keepKeysJson: string): string\n /** What is prepared, for assertions. @experimental */\n previewMixLedger(): string\n /**\n * Copy a picked source into app-owned durable storage.\n *\n * A picker returns a reference, not a file: a `content://` grant dies with\n * the activity result and a cache copy dies whenever Android wants storage\n * back. The project persists what this returns, never what a picker did.\n * Content-addressed, so the same song added twice is stored once. Returns\n * `{ok, uri, token, byteLength, sha256, created}` or `{ok:false, reason}`.\n * @experimental\n */\n ingestManagedSource(uri: string): string\n /** Drop one managed source by token. @experimental */\n releaseManagedSource(token: string): string\n /**\n * Remove every managed source not named in `keepTokensJson`, and every\n * partial a killed process left. The only thing that deletes media.\n * @experimental\n */\n sweepManagedSources(keepTokensJson: string): string\n /**\n * Every filename in the managed directory, including partials and strays.\n *\n * Read-only accounting. `managedSourceLedger` reports only well-formed\n * sources, which cannot show what a sweep left behind. @experimental\n */\n managedSourceList(): string\n /** Gate-only: sweep with an unparsed keep list. @experimental */\n managedSourceGateSweepRaw(rawKeep: string): string\n /** Gate-only: seed a file with an exact name, to exercise the sweep. @experimental */\n managedSourceGateSeed(name: string, bytes: number): string\n /** Tokens and sizes of what is stored, for assertions. @experimental */\n managedSourceLedger(): string\n probeAudioSource(uri: string): string\n /** Confirm the destination is closed and parseable. @experimental */\n exportProbeOutput(uri: string): string\n /** Gate-only: hold inside the media copy, so a kill can land there. @experimental */\n exportGateHoldMediaCopy(enabled: boolean): string\n /** Gate-only: wedge the cadence program after N frames. -1 is off. @experimental */\n exportGateStallAfterFrames(frames: number): string\n /**\n * Gate-only: the smallest failing Media3 export, one stage at a time.\n *\n * Takes a JSON spec — uri, source span in microseconds, the configured speed\n * and the emission plan `resampleClip` compiled — and runs a plain\n * Transformer, then the same run with speed mapping, cadence and Skia added\n * individually. Starts in the background and returns immediately; each rung\n * and the whole ladder are reported to logcat under `PictorLadderRung` and\n * `PictorLadder`. @experimental\n */\n exportGateLadder(specJson: string): string\n /**\n * Arm the gate-only controls. Everything gate-only is inert until this is\n * called, and the production exporter never calls it. @experimental\n */\n exportGateEnableDiagnostics(enabled: boolean): string\n /** Gate-only: fail publication at a chosen point. @experimental */\n exportGatePublishFault(mode: string): string\n /** Gate-only: does this directory's filesystem have an atomic no-replace? @experimental */\n exportGateProbeNoReplace(directory: string): string\n /** Gate-only: size and CRC32, to prove a file did or did not change. @experimental */\n exportGateFileDigest(path: string): string\n /** Gate-only: which temporaries are still registered, by name. @experimental */\n exportGateLiveTemporaries(): string\n /** Gate-only: remove a file, so a case can start from a known state. @experimental */\n exportGateDeleteFile(path: string): string\n /** Gate-only: a writable directory that is not the gallery. @experimental */\n exportGateExternalFilesDir(): string\n /** Gate-only: the current heartbeat epoch. @experimental */\n exportGateBeatEpoch(): string\n /** Gate-only: offer a beat for `epoch`; reports acceptance. @experimental */\n exportGateTestBeat(source: string, epoch: number): string\n /** Gate-only: enable sample hashing for conformance checks. @experimental */\n exportGateProvenance(enabled: boolean): string\n /** Heartbeat counts and the maximum gap per source. @experimental */\n exportGateHeartbeats(): string\n /** Gate-only: started/completed/failed/released accounting. @experimental */\n exportGateAccounting(): string\n /** Stop `jobId` if it is the one running. Idempotent. @experimental */\n exportCancelJob(jobId: string): string\n exportGateRunProject(\n shardsJson: string,\n programJson: string,\n overlaysJson: string,\n mixProgramJson: string,\n mixUrisJson: string,\n timeoutSeconds: number,\n destinationJson: string,\n ): string\n /**\n * **V3.5 gate only.** Mix a compiled `AudioMixProgram` and encode it once.\n *\n * `urisJson` maps span id to a host-resolved URI. Native reads only the plan:\n * no audio kinds, no duration policy, no project.\n *\n * @experimental\n */\n exportGateMixAudio(programJson: string, urisJson: string): string\n /**\n * **V3.5 gate only.** Decode one window and report every stage.\n *\n * Running the identical window at 1x and another rate settles which stage is\n * responsible: matching cropped counts with differing stretched counts point\n * at the stretch, differing cropped counts at the decode.\n *\n * @experimental\n */\n exportGateDecodeProbe(uri: string, fromUs: number, toUs: number, speed: number): string\n /** Per-window dominant-frequency trajectory and harmonic band energies. @experimental */\n exportGateTrajectory(uri: string, spec: string): string\n /** Copy a gate output to external files, for the human gates. @experimental */\n exportGateStage(uri: string, name: string): string\n /** Measure this encoder configuration's priming from a burst at `at`. @experimental */\n exportGatePrimingProbe(rate: number, channels: number, bitrate: number, at: number): string\n /** Decode a window under a simulated decoder warm-up requirement. @experimental */\n exportGatePrerollProbe(uri: string, fromUs: number, toUs: number, warmupUs: number): string\n /**\n * **V3.5 gate only.** Run the time-stretch alone on synthetic PCM.\n *\n * @experimental\n */\n exportGateStretchProbe(rate: number, channels: number, frames: number, speed: number): string\n exportGateStageResource(base64: string, width: number, height: number): string\n media3Play(): void\n media3Pause(): void\n /** Microseconds, like every other time in Pictor. */\n media3Seek(positionUs: number): void\n media3SetRate(rate: number): void\n /** Media3's own recovery after a source failure: prepare again. */\n media3Retry(): void\n media3Release(): void\n /** A throttled snapshot: position, size, counters and any source error. */\n media3State(): Object\n\n /**\n * Start decoding one still frame. Returns a token to poll or cancel with.\n *\n * Not a Promise: a strip issues and abandons requests in bursts, and a\n * rejected Promise per abandoned request is noisy and hard to cancel. A\n * token the caller owns makes \"stop this one\" expressible.\n */\n thumbnailRequest(uri: string, sourceTimeUs: number, widthPx: number): number\n thumbnailCancel(token: number): void\n /** Base64 JPEG, or null while it is still decoding. Consumes the result. */\n thumbnailTake(token: number): string | null\n thumbnailError(token: number): string | null\n thumbnailRunning(token: number): boolean\n thumbnailReleaseSource(uri: string): void\n thumbnailReleaseAll(): void\n /** `{ decoded, cancelled, failed, running, retrievers }`. */\n thumbnailCounters(): Object\n}\n\nexport default TurboModuleRegistry.get<Spec>('PictorVideo')\n","/**\n * Reaching the native adapter.\n *\n * This file used to hold `AndroidVideoPlayer`: a full player over `MediaExtractor`,\n * `MediaCodec`, `AudioTrack` and an `ImageReader` pool of `AHardwareBuffer`s.\n * That path reached one-frame synchronisation and is gone. Media3's ExoPlayer\n * rendering into the persistent external-OES `SurfaceTexture` replaced it, for\n * the reasons in `docs/research/media3-mapping.md` — codec pacing, buffer\n * ownership, async-flush races and per-device workarounds are ExoPlayer's\n * problem to have already solved, not Pictor's to solve again.\n *\n * What remains is the module accessor and the one call that is not playback:\n * publishing the presentation surface.\n *\n * @experimental\n */\nimport Native from './NativePictorVideo'\n\n/** @experimental */\nexport const AndroidVideoPlayer = {\n /**\n * Publish the presentation surface host object on the JS runtime.\n *\n * It is captured into a worklet closure from there; the UI runtime has its\n * own global and does not see this one.\n */\n installVideoSurface(): boolean {\n return requireNative().installVideoSurface()\n },\n}\n\nexport function requireNative() {\n if (!Native) {\n throw new Error(\n '@pictor-sdk/video-android: the native module is not installed. It is Android-only and requires minSdkVersion 29.',\n )\n }\n return Native\n}\n","import { type PlaylistResult } from '@pictor-sdk/video-core'\nimport { requireNative } from './player'\n\n/**\n * Playback backed by Media3's ExoPlayer.\n *\n * Media3 owns decoding, buffering, audio focus, synchronisation, seeking, speed\n * and lifecycle. Pictor owns the document, the timeline and the Skia\n * composition. This is the whole of the seam between them.\n *\n * **No Media3 or Android type appears here.** Positions are microseconds\n * because that is Pictor's unit; the adapter converts.\n *\n * Video is rendered into the persistent external texture, so the renderer holds\n * one `SkImage` for a whole session and nothing is imported per frame.\n *\n * @experimental\n */\nexport interface Media3State {\n positionUs: number\n durationUs: number\n playing: boolean\n buffering: boolean\n ready: boolean\n ended: boolean\n rate: number\n width: number\n height: number\n released: boolean\n error: string\n /** Bumped on seek, source change and release; pairs frames with a clip. */\n generation: number\n /** Frame metadata awaiting a renderer. Bounded. */\n queueDepth: number\n /** Metadata dropped because the queue was full. Should stay zero. */\n overflowed: number\n /** Frames consumed with no sample to pair. Expected briefly after a seek. */\n unpaired: number\n /** Samples discarded because their generation was superseded. */\n stale: number\n queueHighWater: number\n /** Live threads in the process. Diagnostics for leak attribution. */\n threads: number\n /** Allocated native heap, for the safety cutoff. */\n nativeHeapBytes: number\n /** The anchor's own position, before extrapolation. Diagnostics. */\n anchorPositionUs: number\n /** Frames the buffer queue dropped because the renderer was behind. */\n coalesced: number\n /** How coarse `Player.currentPosition` is, measured rather than assumed. */\n anchorPublications: number\n anchorChanges: number\n anchorMaxStepUs: number\n anchorMeanStepUs: number\n anchorMaxStaleNs: number\n /** Which playlist window is playing, and how many there are. */\n windowIndex: number\n windowCount: number\n /** Offset inside the current window, in microseconds. */\n windowPositionUs: number\n /** Window transitions so far — seams crossed. Diagnostics. */\n transitions: number\n /**\n * Frames whose latch failed and was retried on the next tick.\n *\n * `updateTexImage` returns quietly when Skia's GL context is not current on\n * the calling thread, so this counts the retries rather than letting the\n * frames disappear. A handful around startup and playlist changes is normal;\n * a rate that tracks the frame rate is not.\n */\n missedLatches: number\n /**\n * What the single latch owner did, per frame.\n *\n * `skipped` counts ticks refused because the surface was retired or replaced\n * — the invalid call, prevented rather than caught. `failed` must stay\n * visible: a latch that quietly does nothing looks exactly like a paused\n * video, which is how an earlier defect hid for a whole session.\n */\n updatesAttempted: number\n updatesSucceeded: number\n updatesSkipped: number\n updatesFailed: number\n /**\n * Times a requested rate exceeded the range and had to be clamped.\n *\n * Should stay zero: the UI only offers rates that compose within range. A\n * non-zero value means the picture and the timeline are counting differently.\n */\n rateConstrained: number\n /** The rate actually in force: clip speed times preview rate, clamped. */\n effectiveRate: number\n /**\n * The rate asked for, before clamping.\n *\n * Beside `effectiveRate` this says whether a ceiling was *selected* or merely\n * reached: 10 requested and 10 effective is a legitimate extreme, while 25\n * requested and 10 effective is a refusal.\n */\n requestedRate: number\n /**\n * Window indices that had no speed. Should stay zero.\n *\n * Non-zero means the playlist and the window index were read at different\n * moments — the shape that left a cleared playlist running at the rate of the\n * playlist before it.\n */\n staleWindowSpeeds: number\n /** How many per-window speeds the player holds. Zero when idle. */\n windowSpeedCount: number\n /**\n * The two halves of frame delivery, counted separately.\n *\n * `framesOffered` is Media3 telling us a frame is about to be released;\n * `framesAvailable` is the SurfaceTexture saying one arrived. If the first\n * climbs while the second does not, the decoder is running but its output is\n * not reaching our surface — which is a completely different fault from the\n * renderer failing to pull.\n */\n framesOffered: number\n framesAvailable: number\n /**\n * Frames waiting to be latched, right now.\n *\n * A level rather than an edge, so it survives the consumer being disarmed.\n * Usually 0 or 1; larger after a background, and it must fall to 0 within a\n * tick of the consumer re-arming.\n */\n pendingDepth: number\n /** Availability announced for a generation already retired. Should stay 0. */\n framesRejected: number\n /**\n * The generation the pending counters are keeping for.\n *\n * Must equal the consumer's generation. A frame pending for a different one\n * is a frame nobody will ever take.\n */\n pendingGeneration: number\n /** Producer identity, for proving which object a consumer is holding. */\n controllerId: number\n pendingId: number\n orphaned: number\n /** `mime WxH rotN @fps` for the active item. Empty when there is no video. */\n videoFormat: string\n /** The decoder Media3 selected, e.g. `c2.exynos.h264.decoder`. */\n decoderName: string\n /**\n * What the decoder was configured for, and what it produced.\n *\n * Read from the player's own events rather than inferred from the device: a\n * mixed-resolution seam is the one moment input and output can disagree, and\n * naming the decoder from the model instead of asking is how an attribution\n * survives without ever being checked.\n */\n inputFormats: number\n inputWidth: number\n inputHeight: number\n inputRotation: number\n /** Media3's decoder-reuse verdict across the last format change; -1 if none. */\n decoderReuse: number\n outputSizes: number\n outputWidth: number\n outputHeight: number\n /** Increments on each decoder init — makes re-attachment visible. */\n surfaceGeneration: number\n /**\n * Seek bookkeeping for the cross-window freeze soak.\n *\n * `seeksRequested` counts calls; `seeksAccepted` counts those the player's\n * own thread acted on. A gap between them, or a `pendingSeek` that stays\n * true, is a seek that never reached the player — which is what the observed\n * freeze would look like from this side.\n */\n /**\n * Surface attachment bookkeeping, for diagnostics only.\n *\n * The adapter attaches automatically the instant a surface becomes live, so a\n * host never calls anything. These exist so the invariant can be *checked*:\n * `attachedSurfaceGeneration` trailing `liveSurfaceGeneration` means the\n * player is rendering into a surface nobody is watching.\n */\n surfaceAttachments: number\n attachedSurfaceGeneration: number\n liveSurfaceGeneration: number\n seeksRequested: number\n seeksAccepted: number\n pendingSeek: boolean\n /** Media3's own playback state constant. 1 idle, 2 buffering, 3 ready, 4 ended. */\n playbackState: number\n /** PTS of the last frame the renderer actually paired, or -1. */\n lastPairedPtsUs: number\n /**\n * Source failures reported by the player, not inferred from the state.\n *\n * A failed source otherwise looks exactly like an idle one — nothing playing,\n * nothing buffering, no duration — so the count is what distinguishes \"never\n * started\" from \"started and broke\".\n */\n errorCount: number\n /** Media3's error code, and its name plus message. Empty when healthy. */\n errorCode: number\n errorMessage: string\n /** Whether Media3 classifies the failure as worth retrying. */\n errorRecoverable: boolean\n}\n\nconst EMPTY: Media3State = {\n positionUs: 0,\n durationUs: 0,\n playing: false,\n buffering: false,\n ready: false,\n ended: false,\n rate: 1,\n width: 0,\n height: 0,\n released: true,\n error: '',\n generation: 0,\n queueDepth: 0,\n overflowed: 0,\n unpaired: 0,\n stale: 0,\n queueHighWater: 0,\n threads: 0,\n nativeHeapBytes: 0,\n anchorPositionUs: 0,\n coalesced: 0,\n anchorPublications: 0,\n anchorChanges: 0,\n anchorMaxStepUs: 0,\n anchorMeanStepUs: 0,\n anchorMaxStaleNs: 0,\n windowIndex: 0,\n windowCount: 0,\n windowPositionUs: 0,\n transitions: 0,\n missedLatches: 0,\n updatesAttempted: 0,\n updatesSucceeded: 0,\n updatesSkipped: 0,\n updatesFailed: 0,\n rateConstrained: 0,\n effectiveRate: 1,\n requestedRate: 1,\n staleWindowSpeeds: 0,\n windowSpeedCount: 0,\n framesOffered: 0,\n framesAvailable: 0,\n pendingDepth: 0,\n framesRejected: 0,\n pendingGeneration: 0,\n controllerId: 0,\n pendingId: 0,\n orphaned: 0,\n videoFormat: '',\n decoderName: '',\n inputFormats: 0,\n inputWidth: 0,\n inputHeight: 0,\n inputRotation: 0,\n decoderReuse: -1,\n outputSizes: 0,\n outputWidth: 0,\n outputHeight: 0,\n surfaceGeneration: 0,\n surfaceAttachments: 0,\n attachedSurfaceGeneration: 0,\n liveSurfaceGeneration: 0,\n seeksRequested: 0,\n seeksAccepted: 0,\n pendingSeek: false,\n playbackState: 0,\n lastPairedPtsUs: -1,\n errorCount: 0,\n errorCode: 0,\n errorMessage: '',\n errorRecoverable: false,\n}\n\n/** @experimental */\nexport const Media3 = {\n /**\n * Open a local clip and render it into the presentation surface.\n *\n * The surface must already exist: it is created on the UI runtime, where\n * Skia's GL context lives, and preparing without one is an error rather than\n * a silently black picture.\n */\n prepare(uri: string): boolean {\n return requireNative().media3Prepare(uri)\n },\n\n /** Diagnostics: a player with no video output, for heap attribution. */\n prepareNoSurface(uri: string): boolean {\n return requireNative().media3PrepareNoSurface(uri)\n },\n\n /** Diagnostics: reuse the existing player for another source. */\n rebind(uri: string): boolean {\n return requireNative().media3Rebind(uri)\n },\n\n /** Diagnostics: end the source, keep the player. */\n stopKeepingPlayer(): void {\n requireNative().media3StopKeepingPlayer()\n },\n\n /**\n * Replace the playlist from a project's playable windows.\n *\n * Takes the `PlaylistResult` straight from `@pictor-sdk/video-core` so the\n * project → window mapping has exactly one definition, tested there as a\n * round trip.\n * The sources must already be URI strings; this adapter does not interpret a\n * host's media descriptors, and a non-URI source is a host normalisation bug\n * rather than something to guess at.\n *\n * The OES texture and the `SkImage` survive this. Changing the playlist is\n * not a reason to rebuild what the renderer holds.\n */\n setPlaylist(\n playlist: PlaylistResult,\n start: { windowIndex: number; positionUs: number } = { windowIndex: 0, positionUs: 0 },\n options: { allowDivergence?: boolean } = {},\n ): boolean {\n // Refuse a playlist that would not play what the timeline shows.\n //\n // An unsupported clip keeps its span on the timeline while contributing\n // nothing to playback, so the two clocks diverge by exactly its duration\n // and every seam after it lands wrong. That is far worse than an error: it\n // looks like a working editor that is slightly out of sync. A host that\n // genuinely wants the playable remainder must ask for it.\n if (playlist.unsupported.length > 0 && options.allowDivergence !== true) {\n throw new Error(\n '@pictor-sdk/video-android: refusing to build a playlist that would diverge from the ' +\n `timeline. ${playlist.unsupported.length} clip(s) cannot play:\\n` +\n playlist.unsupported.map((u) => ` · ${u.message}`).join('\\n') +\n '\\nPass { allowDivergence: true } to play the remainder anyway.',\n )\n }\n const uris: string[] = []\n const bounds: number[] = []\n for (const item of playlist.items) {\n uris.push(uriOf(item.source))\n bounds.push(item.startUs, item.endUs, Math.round(item.speed * 1000), item.muted ? 1 : 0)\n }\n return requireNative().media3SetPlaylist(uris, bounds, start.windowIndex, start.positionUs)\n },\n\n /** Seek in the player's coordinates. Pair with `projectTimeToWindow`. */\n seekToWindow(windowIndex: number, positionUs: number): void {\n requireNative().media3SeekToWindow(windowIndex, positionUs)\n },\n\n play(): void {\n requireNative().media3Play()\n },\n\n pause(): void {\n requireNative().media3Pause()\n },\n\n /** Microseconds, like every other time in Pictor. */\n seek(positionUs: number): void {\n requireNative().media3Seek(positionUs)\n },\n\n setRate(rate: number): void {\n requireNative().media3SetRate(rate)\n },\n\n /**\n * Try the current source again after a failure.\n *\n * This is Media3's own recovery path: it re-resolves the source and resumes\n * from the current position. Worth calling when `errorRecoverable` is true —\n * a network source that went away and came back — and pointless otherwise.\n */\n retry(): void {\n requireNative().media3Retry()\n },\n\n release(): void {\n requireNative().media3Release()\n },\n\n /** A snapshot for throttled reads. Never call this per frame. */\n state(): Media3State {\n const raw = requireNative().media3State() as Partial<Media3State> | null\n return raw ? { ...EMPTY, ...raw } : EMPTY\n },\n}\n\n/**\n * A source as a URI string, or a readable failure.\n *\n * `MediaSource` is deliberately open — the timeline stores whatever descriptor\n * a host uses — so the adapter is where it has to become something Android can\n * open. Failing loudly here is better than handing `undefined` to `Uri.parse`\n * and getting a black picture with no explanation.\n */\nfunction uriOf(source: unknown): string {\n if (typeof source === 'string') return source\n if (source && typeof source === 'object' && 'uri' in source) {\n const uri = (source as { uri?: unknown }).uri\n if (typeof uri === 'string') return uri\n }\n throw new Error(\n `@pictor-sdk/video-android: a clip's source is not a URI. Normalise host media to { kind: 'uri', uri } before opening a project. Received: ${JSON.stringify(source)}`,\n )\n}\n","/**\n * Which playlist Media3 is holding, and against which surface.\n *\n * The bug this exists to prevent, in order:\n *\n * 1. The editor opens and builds surface A.\n * 2. The system picker opens. The app backgrounds; A is no longer usable.\n * 3. Two clips are added to the document while the picker is closing.\n * 4. The editor returns and builds surface B.\n * 5. Nothing prepares, because the only thing that ever triggered preparation\n * was the *playlist changing*, and it changed in step 3 — when there was no\n * surface to prepare against. `setPlaylist` was refused, the signature was\n * recorded as applied anyway, and the editor sat there with two clips on the\n * timeline and a player holding nothing.\n *\n * Two corrections follow from that. Readiness is **level-triggered**: the\n * question is \"does the playlist Media3 holds match the document, against the\n * surface that is live *now*\", asked whenever either side moves — not \"did the\n * playlist just change\". And a refusal is **not** recorded as applied, so it is\n * retried rather than remembered as done.\n *\n * A generation is what makes \"the surface that is live now\" expressible. Without\n * it a caller can only know that *a* surface exists, which cannot distinguish\n * the one it prepared against from the one that replaced it.\n *\n * Pure, so the sequence above is a test rather than a device run.\n *\n * @experimental\n */\n\n/** No surface. Distinct from every real generation, which start at 1. */\nexport const NO_SURFACE = 0\n\n/** @experimental */\nexport interface PlaylistSession {\n /** The playlist Media3 is holding, or null if it holds none. */\n appliedSignature: string | null\n /** The surface generation it was prepared against. */\n appliedGeneration: number\n /**\n * A preparation was attempted and refused.\n *\n * Kept so the retry is driven by this rather than by hoping the document\n * changes again. A user who has added their clips will not add them twice.\n */\n pending: boolean\n}\n\n/** @experimental */\nexport function idlePlaylistSession(): PlaylistSession {\n return { appliedSignature: null, appliedGeneration: NO_SURFACE, pending: false }\n}\n\n/** What the document and the device currently want. @experimental */\nexport interface PlaylistIntent {\n /** A stable description of the playlist the document implies. */\n signature: string\n /** The live surface generation, or {@link NO_SURFACE}. */\n generation: number\n}\n\n/**\n * Whether to call `setPlaylist` now.\n *\n * @experimental\n */\nexport function shouldPrepare(session: PlaylistSession, intent: PlaylistIntent): boolean {\n // Nothing to prepare against. Attempting it is the refusal that started all\n // of this, and it would poison the session by recording a signature.\n if (intent.generation === NO_SURFACE) return false\n if (session.pending) return true\n if (session.appliedSignature !== intent.signature) return true\n // The playlist is unchanged but the surface underneath it is not. A player\n // attached to a retired surface decodes, releases frames and reports no\n // error while the live texture receives nothing.\n return session.appliedGeneration !== intent.generation\n}\n\n/**\n * Record the outcome.\n *\n * A refusal leaves the session **unchanged except for `pending`**. Recording the\n * signature on failure is precisely the mistake that made the editor wait for an\n * edit that was never going to come.\n *\n * @experimental\n */\nexport function afterPrepare(\n session: PlaylistSession,\n intent: PlaylistIntent,\n ok: boolean,\n): PlaylistSession {\n if (!ok) return { ...session, pending: true }\n return {\n appliedSignature: intent.signature,\n appliedGeneration: intent.generation,\n pending: false,\n }\n}\n\n/**\n * The surface went away — backgrounded, replaced, released.\n *\n * The signature is kept: it still describes what Media3 was asked to hold, and\n * the comparison that matters next is against the *generation*, which is now\n * guaranteed to differ from any live one.\n *\n * @experimental\n */\nexport function retireSurface(session: PlaylistSession): PlaylistSession {\n return { ...session, appliedGeneration: NO_SURFACE, pending: true }\n}\n\n/**\n * Whether a callback carrying `generation` is still worth acting on.\n *\n * A screen's teardown routinely runs after the next screen's setup, and an\n * asynchronous surface creation can land after the surface it created has been\n * replaced. Both arrive looking exactly like a valid result.\n *\n * @experimental\n */\nexport function isCurrentGeneration(generation: number, live: number): boolean {\n return generation !== NO_SURFACE && generation === live\n}\n","import { NO_SURFACE } from './playlistSession'\n\n/**\n * Who is allowed to create a presentation surface, and when.\n *\n * There is one external texture in the process, so there is one of these. Not\n * one per hook instance: a hook is remounted by a navigation, a fast refresh or\n * a rotation, and each remount would bring its own lock and its own idea of\n * what is in flight. Two creates then overlap, each registers a generation, and\n * the second overwrites the native handles the first is still holding — the GL\n * texture and `SurfaceTexture` from the first are then unreachable and never\n * released. The symptom is a hook reporting one generation while the adapter\n * has moved on to another, with an abandoned surface in between.\n *\n * ## The generation is native's, and only native's\n *\n * `create()` returns the generation `VideoSurface.register` assigned. This\n * module publishes that number unchanged and keeps no counter of its own — a\n * second counter is a second answer to \"which surface is live\", and the whole\n * class of bug here is two answers to one question.\n *\n * ## A superseded attempt is retired, not ignored\n *\n * An attempt whose owner went away while it was on the UI runtime still\n * *succeeds*: the texture exists, the `Surface` is registered, Media3 could\n * attach to it. Ignoring its report leaves exactly that — a live, registered\n * surface nobody will ever draw or release. So it is released, on the runtime\n * that owns the GL context, before anything else is started.\n *\n * ## Serialised, and retirement does not unlock\n *\n * A request that arrives while an attempt is unresolved is queued, and\n * backgrounding or unmounting does **not** release the lock: the earlier\n * attempt is still out there on the UI runtime and will still come back. The\n * queue is one deep, because every waiting request wants the same thing.\n *\n * Effects are injected so the interleavings are deterministic in a test rather\n * than a matter of timing on a device.\n *\n * @experimental\n */\n\n/** What native `create()` reports. @experimental */\nexport interface SurfaceCreateResult {\n ok: boolean\n /** The generation `register` assigned, or 0 on failure. */\n generation: number\n textureId: number\n error: string\n}\n\n/** @experimental */\nexport interface SurfaceOwnerEffects {\n /**\n * Schedule a create on the UI runtime.\n *\n * Must eventually call {@link SurfaceOwner.complete}, whatever happens.\n */\n create(): void\n /**\n * Release a specific generation, on the runtime that owns the GL context.\n *\n * Takes a generation rather than \"the current surface\" so a late attempt\n * cannot release the surface that replaced it.\n */\n retire(generation: number): void\n}\n\n/** @experimental */\nexport interface SurfaceReadiness {\n /** The live generation, exactly as native reported it. 0 when there is none. */\n generation: number\n /** The last failure, or empty. */\n error: string\n}\n\n/** @experimental */\nexport interface SurfaceOwner {\n readonly readiness: SurfaceReadiness\n /** Whether an attempt is on the UI runtime right now. */\n readonly busy: boolean\n /** Generations created by a superseded attempt and released. Diagnostics. */\n readonly retired: number\n /**\n * Ask for a surface on behalf of `token`.\n *\n * Returns false when the request was queued or refused rather than started.\n */\n request(token: number): boolean\n /** That owner is gone. Does not unlock: an unresolved attempt is still out there. */\n retireOwner(token: number): void\n /** An attempt finished. */\n complete(result: SurfaceCreateResult): void\n /** Adopt a surface that already exists, without creating one. */\n adopt(generation: number, token: number): void\n subscribe(listener: () => void): () => void\n /** Point the owner at the newest caller's effects. */\n useEffects(next: SurfaceOwnerEffects): void\n}\n\n/** @experimental */\nexport function createSurfaceOwner(initial: SurfaceOwnerEffects): SurfaceOwner {\n // Mutable, because the singleton outlives every hook that uses it and each\n // one brings its own closure over the published handle and the texture size.\n // The newest is the right one: they all drive the same native object.\n let effects = initial\n let readiness: SurfaceReadiness = { generation: NO_SURFACE, error: '' }\n let busy = false\n let retired = 0\n /** The owner the in-flight attempt was started for. */\n let activeToken: number | null = null\n /**\n * Whether that owner went away while its attempt was on the UI runtime.\n *\n * A flag rather than a comparison against whoever is current now: a *second*\n * hook asking for a surface does not invalidate the one being built, because\n * they share it. Only the asking owner disappearing does. Comparing against\n * the current token retired a perfectly good surface every time a remount\n * overlapped a create, which is a leak dressed as caution.\n */\n let activeRetired = false\n /** The owner currently entitled to a surface. */\n let currentToken: number | null = null\n /** One waiting request. Every waiter wants the same thing. */\n let queuedToken: number | null = null\n const listeners = new Set<() => void>()\n\n const emit = () => {\n for (const listener of listeners) listener()\n }\n\n const publish = (next: SurfaceReadiness) => {\n if (next.generation === readiness.generation && next.error === readiness.error) return\n readiness = next\n emit()\n }\n\n const start = (token: number) => {\n busy = true\n activeToken = token\n activeRetired = false\n effects.create()\n }\n\n const owner: SurfaceOwner = {\n get readiness() {\n return readiness\n },\n get busy() {\n return busy\n },\n get retired() {\n return retired\n },\n\n request(token) {\n currentToken = token\n if (readiness.generation !== NO_SURFACE) return false\n if (busy) {\n queuedToken = token\n return false\n }\n start(token)\n return true\n },\n\n retireOwner(token) {\n if (activeToken === token) activeRetired = true\n if (currentToken === token) currentToken = null\n if (queuedToken === token) queuedToken = null\n // Deliberately not `busy = false`. The attempt is on the UI runtime and\n // will come back; unlocking here is what lets a second create overlap it.\n },\n\n adopt(generation, token) {\n currentToken = token\n if (generation === NO_SURFACE) return\n publish({ generation, error: '' })\n },\n\n complete(result) {\n const wasRetired = activeRetired\n const hasOwner = currentToken !== null\n busy = false\n activeToken = null\n activeRetired = false\n\n if (result.ok && result.generation > NO_SURFACE) {\n // Superseded means nobody is left who asked for this: the owner that\n // started it retired, or every owner has gone. Not \"somebody else asked\n // in the meantime\" — they want the same surface.\n const superseded = wasRetired || !hasOwner\n const stale = result.generation < readiness.generation\n if (superseded || stale) {\n // It exists and is registered, so it has to be released rather than\n // forgotten. Releasing by generation means a late attempt cannot take\n // down the surface that replaced it.\n retired += 1\n effects.retire(result.generation)\n } else {\n publish({ generation: result.generation, error: '' })\n }\n } else {\n publish({\n generation: readiness.generation,\n error: result.error || 'surface create failed',\n })\n }\n\n const next = queuedToken\n queuedToken = null\n // Only if there is still no surface: a queued request that has since been\n // satisfied by the attempt that just finished wants nothing more.\n if (next !== null && readiness.generation === NO_SURFACE && next === currentToken) {\n start(next)\n }\n },\n\n useEffects(next) {\n effects = next\n },\n\n subscribe(listener) {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n }\n\n return owner\n}\n\n/**\n * The process-wide owner, created on first use.\n *\n * A module-level singleton because the thing it guards — the one external\n * texture — is one per process. `resetSurfaceOwnerForTests` exists so a test can\n * start from a known state; nothing in production calls it.\n *\n * @experimental\n */\nlet singleton: SurfaceOwner | null = null\n\n/** @experimental */\nexport function surfaceOwner(effects: SurfaceOwnerEffects): SurfaceOwner {\n if (!singleton) singleton = createSurfaceOwner(effects)\n else singleton.useEffects(effects)\n return singleton\n}\n\n/** @experimental */\nexport function resetSurfaceOwnerForTests(): void {\n singleton = null\n}\n\n/** Fresh token for a hook instance. Only ever increases. @experimental */\nlet nextToken = 0\n\n/** @experimental */\nexport function nextOwnerToken(): number {\n nextToken += 1\n return nextToken\n}\n","import { useCallback, useEffect, useRef, useState } from 'react'\nimport { AppState, type AppStateStatus } from 'react-native'\nimport { runOnJS, runOnUI, useSharedValue } from 'react-native-reanimated'\nimport { NO_SURFACE } from './playlistSession'\nimport { AndroidVideoPlayer } from './player'\nimport { nextOwnerToken, surfaceOwner, type SurfaceCreateResult } from './surfaceOwner'\n\n/**\n * The presentation surface, and whether it is usable *right now*.\n *\n * This was host code, and every host would have got it wrong in the same way.\n * `create()` must run on the UI runtime, where Skia's GL context lives, and\n * `runOnUI` **schedules** that rather than performing it — so a host that sets\n * its state beside the call declares the surface ready a frame or two before the\n * texture exists. A playlist built in that window is refused outright, and the\n * editor shows clips on the timeline with a player holding nothing.\n *\n * Readiness is therefore reported from *inside* the worklet, after the texture\n * and the native `Surface` both exist, and it carries **the generation native\n * assigned**. This hook keeps no counter of its own: a second counter is a\n * second answer to \"which surface is live\", and the fault it replaced was\n * exactly two answers disagreeing.\n *\n * ## Level-triggered, not one-shot\n *\n * The surface is re-examined whenever the app returns to the foreground, and\n * marked unavailable when it leaves. Backgrounding is the ordinary case, not an\n * edge case: choosing media means going to another activity and coming back, and\n * the clips arrive in the document *while there is no surface to prepare\n * against*. Anything that only reacts to the document changing has already\n * missed its chance by the time it returns.\n *\n * ## Serialisation is process-wide\n *\n * Creation is arbitrated by {@link surfaceOwner}, a module-level singleton,\n * because the thing it guards — the one external texture — is one per process.\n * A lock held per hook instance is no lock at all: a remount brings a fresh one,\n * two creates overlap, and the second overwrites the native handles the first is\n * still holding. Nor does unmounting release it: the earlier attempt is still\n * out on the UI runtime and will still come back.\n *\n * A surface built by an attempt whose owner has since gone is **released**, not\n * ignored. It exists, it is registered, and Media3 could attach to it.\n *\n * @experimental\n */\n\n/** How often to retry creating a surface while there is none, in milliseconds. */\nconst CREATE_RETRY_MS = 250\n\n/**\n * The one stable slot, installed on first ask.\n *\n * Synchronous, and callable during a render, because the consumer has to be able\n * to capture it *before* any surface exists. An effect is too late: the frame\n * callback is built on the first render, and anything it captures then is what\n * it keeps.\n *\n * The object never changes. `create` fills its target in place and retirement\n * clears it, so every call through it resolves to whatever the slot points at\n * now. There is deliberately nothing replaceable to hold.\n *\n * @experimental\n */\nexport function videoSurfaceSlot(): VideoSurfaceHandle | undefined {\n const published = (globalThis as unknown as { __pictorVideoSurface?: VideoSurfaceHandle })\n .__pictorVideoSurface\n if (published) return published\n if (!AndroidVideoPlayer.installVideoSurface()) return undefined\n return (globalThis as unknown as { __pictorVideoSurface?: VideoSurfaceHandle })\n .__pictorVideoSurface\n}\n\n/** @experimental */\nexport interface VideoSurfaceOptions {\n /** Backing size of the texture. Frames are scaled into it. */\n width: number\n height: number\n enabled?: boolean\n}\n\n/**\n * The surface as the UI runtime sees it.\n *\n * Structural rather than an imported Android type: nothing platform-specific\n * reaches Pictor's public API.\n *\n * @experimental\n */\nexport interface VideoSurfaceHandle {\n create(\n width: number,\n height: number,\n ): { textureId: number; ok: boolean; generation: number; error?: string }\n generation(): number\n alive(): boolean\n /** Release a named generation. Does nothing if it is not the one held. */\n releaseGeneration(generation: number): boolean\n}\n\n/** @experimental */\nexport interface VideoSurface {\n /**\n * The stable slot.\n *\n * Available from the first render and never replaced, so a consumer captures\n * it once and reads through it. It answers `generation() === 0` while empty,\n * which is how a renderer knows there is nothing to draw — a null would have\n * to be captured, and a captured null is what it stays.\n */\n surface: unknown | undefined\n /**\n * The live generation, exactly as native reported it. 0 when there is none.\n *\n * Feed to `useMedia3Playback`. Every change is a reason to reconcile the\n * playlist, including the change back to 0.\n */\n generation: number\n /** Failure to install or create, for a host's error slot. Empty when healthy. */\n error: string\n}\n\n/** @experimental */\nexport function useVideoSurface(options: VideoSurfaceOptions): VideoSurface {\n const { width, height, enabled = true } = options\n const [readiness, setReadiness] = useState({ generation: NO_SURFACE, error: '' })\n // Resolved during render, not in an effect: a consumer building a frame\n // callback on its first render must have the slot to capture by then.\n const handleRef = useRef<VideoSurfaceHandle | undefined>(undefined)\n if (!handleRef.current) handleRef.current = videoSurfaceSlot()\n const textureId = useSharedValue(0)\n\n /** This hook instance's identity, so the owner can tell it from a remount. */\n const tokenRef = useRef(0)\n if (tokenRef.current === 0) tokenRef.current = nextOwnerToken()\n const token = tokenRef.current\n\n const [attempt, setAttempt] = useState(0)\n\n const ownerRef = useRef<ReturnType<typeof surfaceOwner> | undefined>(undefined)\n\n /**\n * Resolve an attempt on the process-wide owner.\n *\n * Reached through `runOnJS` from inside the worklet, which is the only place\n * that can honestly say the texture and the `Surface` exist.\n */\n const complete = useCallback((result: SurfaceCreateResult) => {\n ownerRef.current?.complete(result)\n }, [])\n\n const owner = surfaceOwner({\n create() {\n const published = handleRef.current\n if (!published) {\n complete({ ok: false, generation: 0, textureId: 0, error: '__pictorVideoSurface missing' })\n return\n }\n runOnUI(() => {\n 'worklet'\n const created = published.create(width, height)\n textureId.value = created.textureId\n if (created.ok && !(created.generation > 0)) {\n runOnJS(complete)({\n ok: false,\n generation: 0,\n textureId: created.textureId,\n error: 'adapter too old: create() reports no generation',\n })\n return\n }\n runOnJS(complete)({\n ok: created.ok && created.textureId > 0,\n generation: created.generation,\n textureId: created.textureId,\n // The native reason, not a summary of it. \"No EGL context current on\n // this thread\" and \"no JNIEnv\" send a reader to different places.\n error: created.ok ? '' : created.error || 'surface create failed',\n })\n })()\n },\n retire(generation: number) {\n const published = handleRef.current\n if (!published || typeof published.releaseGeneration !== 'function') return\n // On the UI runtime, because deleting a GL texture needs the context that\n // owns it to be current — which is true on exactly one thread.\n runOnUI(() => {\n 'worklet'\n published.releaseGeneration(generation)\n })()\n },\n })\n ownerRef.current = owner\n\n useEffect(() => {\n const unsubscribe = owner.subscribe(() => setReadiness(owner.readiness))\n setReadiness(owner.readiness)\n return unsubscribe\n }, [owner])\n\n const ensure = useCallback(() => {\n if (!enabled) return\n // Through the same accessor the first render used. Calling\n // `installVideoSurface` again here is what produced two host objects: the\n // consumer captured the one installed during render, `create` ran on the one\n // this effect installed over it, and the two reported different generations\n // forever with neither being wrong about itself.\n const published = videoSurfaceSlot()\n if (!published) {\n setReadiness((prev) => ({ ...prev, error: '__pictorVideoSurface missing' }))\n return\n }\n handleRef.current = published\n\n // Already live: adopt it rather than build a second. Guarded because an\n // adapter built before these accessors answers `undefined`, and calling that\n // throws inside an effect — which presents as a blank screen rather than as\n // the version mismatch it is.\n if (\n typeof published.alive === 'function' &&\n typeof published.generation === 'function' &&\n published.alive() &&\n published.generation() !== NO_SURFACE\n ) {\n owner.adopt(published.generation(), token)\n return\n }\n\n owner.request(token)\n }, [enabled, owner, token])\n\n useEffect(() => {\n ensure()\n }, [ensure, attempt])\n\n /**\n * Keep asking until there is a surface.\n *\n * `create` needs Skia's GL context current on the UI runtime, and that is only\n * true once a canvas has drawn — which has not happened when this hook's first\n * effect runs. Measured on a Pixel 7: the first attempt returns \"no EGL\n * context current on this thread\", and a later one succeeds.\n *\n * A retry rather than a wait on some readiness signal, because there is no\n * such signal: the context becoming current is a side effect of rendering, not\n * an event anything publishes. The owner refuses to overlap attempts, so\n * asking often is safe.\n */\n useEffect(() => {\n if (!enabled) return\n if (readiness.generation !== NO_SURFACE) return\n const id = setInterval(() => setAttempt((n) => n + 1), CREATE_RETRY_MS)\n return () => clearInterval(id)\n }, [enabled, readiness.generation])\n\n useEffect(() => {\n if (!enabled) return\n const onChange = (status: AppStateStatus) => {\n if (status === 'active') {\n // Level-triggered: ask again rather than assume the answer survived.\n ensure()\n return\n }\n // This owner is gone for now. The lock is deliberately not released — an\n // attempt on the UI runtime is still out there — and a surface it produces\n // will be retired rather than published to nobody.\n owner.retireOwner(token)\n }\n const subscription = AppState.addEventListener('change', onChange)\n return () => subscription.remove()\n }, [enabled, ensure, owner, token])\n\n useEffect(() => () => owner.retireOwner(token), [owner, token])\n\n return {\n // Always the slot, never undefined once installed. Gating this on readiness\n // is what put a null into the consumer's closure and kept it there.\n surface: handleRef.current,\n generation: readiness.generation,\n error: readiness.error,\n }\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { AppState } from 'react-native'\nimport {\n playIntent,\n playlistOf,\n projectDuration,\n type PlayIntent,\n type UnsupportedSource,\n type VideoProject,\n} from '@pictor-sdk/video-core'\nimport {\n compositionSpikeSpec,\n type CompositionSpikeState,\n spikePause,\n spikePlay,\n spikeRelease,\n spikeSeek,\n spikeStart,\n spikeState,\n} from './compositionSpike'\nimport { createPlaybackLifecycle, type PlaybackLifecycle } from './playbackLifecycle'\nimport type { Media3Playback } from './useMedia3Playback'\n\n/**\n * The preview, on one clock.\n *\n * ## What this replaces\n *\n * Two players kept in step: a video player and a separate player for the\n * prepared audio mix, with a drift check correcting between them. That\n * architecture was measured to its limit and could not hold synchronisation\n * across a clip seam — the correction is a seek, a seek costs a decode, and a\n * decode at the moment of a seam is what the correction was trying to cover.\n *\n * A `CompositionPlayer` has one clock, one playback thread and one audio graph.\n * The prepared mix becomes an audio-only sequence beside the video sequence, so\n * the two cannot drift: there is nothing to drift against.\n *\n * ## What it does not fix\n *\n * The clip seam still costs a picture. Media3 tears down the video decoder at\n * an item boundary and builds another, and for 119 to 168 ms nothing reaches\n * the renderer while the clock runs on — the audio is continuous and the\n * picture holds. That behaviour was watched on a device and accepted for v1;\n * see `docs/research/preview-acceptance.md` for what was accepted and what was\n * explicitly not.\n *\n * ## Why the same contract\n *\n * It returns {@link Media3Playback}, unchanged. The editor, the transport and\n * the gates were all written against it, and swapping the engine underneath is\n * a smaller and more reviewable change than rewriting every caller to a shape\n * that would end up equivalent.\n *\n * @experimental\n */\nexport interface UseCompositionPlaybackOptions {\n project: VideoProject\n /** Timeline time the host wants shown. */\n playheadUs: number\n /**\n * The prepared audio program, from `usePreviewAudio`.\n *\n * Null while it is being prepared, after it has failed, and — legitimately —\n * when the document has no audio at all.\n *\n * Those are different situations and the hook has to tell them apart. A\n * document *with* audio waits: building the composition without its sound\n * would play a silent preview and gain the music a second later, which is two\n * timelines to anyone watching. A document *without* audio has nothing to\n * wait for, and refusing to build would leave the picture dead.\n */\n mix: { uri: string; durationUs: number } | null\n /** Turns a document source into something the player can open. */\n resolveSource: (source: { kind: string; uri?: string }) => string | null\n /** @see UseMedia3PlaybackOptions.surfaceGeneration */\n surfaceGeneration?: number\n enabled?: boolean\n}\n\n/** How often the composition's state is read, in milliseconds. */\nconst POLL_MS = 100\n\n/**\n * What a composition knows about itself.\n *\n * Deliberately not `Media3State`. That type is the shape the video player's\n * native state comes back in — forty fields about queue depth, anchors, native\n * heap and window indices — and a composition has none of them. Synthesising\n * one would be inventing forty numbers to satisfy a type, and a reader would\n * have no way to tell the invented ones from the measured ones.\n *\n * @experimental\n */\nexport interface CompositionPlaybackState {\n playing: boolean\n ready: boolean\n buffering: boolean\n ended: boolean\n released: boolean\n /** Project time, which for a single composition is the only time there is. */\n positionUs: number\n durationUs: number\n /** The presentation size, or 0 while nothing has been measured. */\n width: number\n height: number\n error: string | null\n}\n\nfunction composedState(snapshot: CompositionSpikeState): CompositionPlaybackState {\n const playbackState = snapshot.playbackState ?? 1\n return {\n playing: snapshot.playing === true,\n ready: playbackState === 3,\n buffering: playbackState === 2,\n ended: playbackState === 4,\n // `released` counts releases; a positive count with nothing attached is the\n // released state this means.\n released: snapshot.attached !== true && (snapshot.released ?? 0) > 0,\n positionUs: snapshot.positionUs ?? 0,\n durationUs: snapshot.durationUs ?? 0,\n /*\n Zero, and honestly so.\n\n A composition renders into the presentation surface at the size the host\n gave it; it does not report a decoded size the way the video player did.\n The host already knows the size it asked for, and a fabricated number here\n would be worse than none.\n */\n width: 0,\n height: 0,\n error: snapshot.error ?? null,\n }\n}\n\n/** @experimental */\nexport type CompositionPlayback = Omit<Media3Playback, 'state'> & {\n state: CompositionPlaybackState | null\n}\n\n/** @experimental */\nexport function useCompositionPlayback(\n options: UseCompositionPlaybackOptions,\n): CompositionPlayback {\n const { project, mix, enabled = true } = options\n const surfaceGeneration = options.surfaceGeneration ?? 0\n const resolveSource = useRef(options.resolveSource)\n resolveSource.current = options.resolveSource\n\n const [state, setState] = useState<CompositionPlaybackState | null>(null)\n const [playing, setPlaying] = useState(false)\n const playheadRef = useRef(options.playheadUs)\n playheadRef.current = options.playheadUs\n\n const playlist = useMemo(() => playlistOf(project), [project])\n const items = playlist.items\n const durationUs = useMemo(() => projectDuration(project), [project])\n\n /*\n What the composition is made of, as a string.\n\n Every edit makes a new project object and most of them — a selection, a\n playhead move — change nothing the player should be playing. The mix is part\n of it: a rebuilt mix is a different composition even when the video is\n identical, and starting the old one would play the sound the user just\n changed.\n */\n // A document with audio has a mix to wait for; one without does not.\n const needsMix = project.audio.length > 0\n const signature = useMemo(() => {\n if (needsMix && mix === null) return ''\n const parts = items.map(\n (i) => `${JSON.stringify(i.source)}|${i.startUs}|${i.endUs}|${i.speed}|${i.muted}`,\n )\n const sound = mix === null ? 'silent' : `${mix.uri}:${mix.durationUs}`\n return `${parts.join(';')}#${playlist.unsupported.length}@${sound}`\n }, [items, playlist.unsupported.length, mix, needsMix])\n\n const [unsupported, setUnsupported] = useState<readonly UnsupportedSource[]>([])\n const lastSeekRef = useRef<Media3Playback['lastSeek']>(null)\n\n /*\n This screen's ownership, unchanged from the path it replaces.\n\n The player outlives any screen: leaving the editor while playing left the\n sound running, and backgrounding did the same. One lifecycle per hook\n instance, and a screen mounted later starts from nothing.\n */\n const lifecycleRef = useRef<PlaybackLifecycle | null>(null)\n if (lifecycleRef.current === null) {\n lifecycleRef.current = createPlaybackLifecycle({\n pause: () => void spikePause(),\n release: () => void spikeRelease(),\n })\n }\n const lifecycle = lifecycleRef.current\n const [lifecycleEpoch, setLifecycleEpoch] = useState(lifecycle.epoch)\n\n const loaded = useRef('')\n useEffect(() => {\n if (!enabled || signature === '') return\n if (surfaceGeneration === 0) return\n if (loaded.current === signature) return\n /*\n An empty URI is a composition with no audio sequence, which the adapter\n builds and plays. It is not a missing mix — that case never reaches here,\n because the signature is empty until one exists.\n */\n const { spec, unsupported: refused } = compositionSpikeSpec(\n project,\n mix ?? { uri: '', durationUs: 0 },\n (s) => resolveSource.current(s),\n )\n setUnsupported(\n refused.map((reason) => ({ clipId: reason.split(':')[0] ?? '', reason }) as UnsupportedSource),\n )\n /*\n A project with nothing in it is a reconciliation too.\n\n This used to return here, and returning is not nothing: the player is a\n process-wide singleton that goes on playing whatever it last loaded. So\n removing the only clip *while it was playing* left the removed clip\n audible until it reached its own end — measured on a Pixel 7 as roughly\n four more seconds of a six-second clip, three times out of three — with\n the picture already cleared and the clock already reading 0:00.000,\n because those follow the document and only the player does not.\n\n Pausing is the whole fix. The composition is deliberately **not**\n released: emptying a timeline is usually a step on the way to putting\n something else in it, and `loaded.current` is left alone so that undoing\n the removal reuses the composition already loaded instead of rebuilding\n one that has not changed. Releasing is what `PlaybackLifecycle` does when\n the *screen* goes, which is a different question from what the document\n currently holds.\n\n Nothing here touches the document. Playback stopping says nothing about\n where the playhead should be.\n */\n if (spec.items.length === 0) {\n spikePause()\n return\n }\n const started = spikeStart(spec) as { ok?: boolean }\n // A refusal is not a load. Recording it as one is what left an editor\n // waiting for an edit the person had already made.\n if (started.ok === true) loaded.current = signature\n }, [enabled, signature, surfaceGeneration, project, mix])\n\n /*\n Backgrounding pauses; returning re-arms.\n\n The same wiring `useMedia3Playback` has, and its absence here was the same\n bug `PlaybackLifecycle` was written to fix, reintroduced by a newer playback\n path that never got it. `useVideoSurface` already retires the presentation\n surface when the app leaves, so the picture stopped and the sound carried\n on — measured on a Pixel 7 as a composition still reporting `playing` with\n its position advancing from 1.3 s to 4.4 s across a press of Home.\n\n Suspend rather than teardown: the picker comes up over the editor as a\n background transition, and rebuilding a player the screen never lost would\n be a stall nobody asked for. The document is not touched either way.\n */\n useEffect(() => {\n const subscription = AppState.addEventListener('change', (next) => {\n if (next === 'active') {\n lifecycle.resume()\n setLifecycleEpoch(lifecycle.epoch)\n return\n }\n lifecycle.suspend('background')\n setLifecycleEpoch(lifecycle.epoch)\n setPlaying(false)\n })\n return () => subscription.remove()\n }, [lifecycle])\n\n useEffect(() => {\n return () => {\n lifecycle.teardown('unmount')\n }\n }, [lifecycle])\n\n useEffect(() => {\n if (!enabled) return\n // Captured now and checked on every tick: an interval is not cleared the\n // instant a screen goes, and a poll landing afterwards must not write into\n // one that is no longer there.\n const epoch = lifecycle.epoch\n const id = setInterval(() => {\n if (!lifecycle.accepts(epoch)) return\n const next = composedState(spikeState())\n /*\n Only a change of meaning re-renders.\n\n Replacing the state object every tick re-renders the editor ten times a\n second to move a line, and at ten hertz the line moves visibly in steps.\n A host that wants the position drives it from `positionUs()`.\n */\n setState((previous: CompositionPlaybackState | null) =>\n previous !== null &&\n previous.playing === next.playing &&\n previous.ready === next.ready &&\n previous.buffering === next.buffering &&\n previous.ended === next.ended &&\n previous.released === next.released &&\n previous.error === next.error\n ? previous\n : next,\n )\n setPlaying(next.playing)\n }, POLL_MS)\n return () => clearInterval(id)\n }, [enabled, lifecycle, lifecycleEpoch])\n\n const seek = useCallback((timeUs: number) => {\n const at = Math.max(0, Math.round(timeUs))\n lastSeekRef.current = { globalUs: at, windowIndex: 0, localUs: at }\n spikeSeek(at)\n }, [])\n\n const positionUs = useCallback((): number | null => {\n const snapshot = spikeState()\n if (!snapshot.attached) return null\n const at = snapshot.positionUs\n return typeof at === 'number' ? at : null\n }, [])\n\n const play = useCallback((): PlayIntent => {\n const snapshot = spikeState()\n const intent = playIntent({\n positionUs: typeof snapshot.positionUs === 'number' ? snapshot.positionUs : 0,\n durationUs,\n playbackState: typeof snapshot.playbackState === 'number' ? snapshot.playbackState : 1,\n ended: snapshot.playbackState === 4,\n })\n if (intent.seekToUs !== null) seek(intent.seekToUs)\n if (!intent.play) return intent\n // Refused once the screen has given the player back: a late press from a\n // control that has not unmounted yet must not start a released player.\n if (lifecycle.released) return intent\n spikePlay()\n setPlaying(true)\n return intent\n }, [durationUs, lifecycle, seek])\n\n useEffect(() => {\n setLifecycleEpoch(lifecycle.epoch)\n }, [lifecycle, surfaceGeneration])\n\n return {\n playing,\n play,\n get lastSeek() {\n return lastSeekRef.current\n },\n positionUs,\n effectiveRate: 1,\n pause: useCallback(() => {\n spikePause()\n setPlaying(false)\n }, []),\n /*\n Rate is the document's, not the transport's.\n\n Clip speed is compiled into the composition and into the mix, and a\n preview-wide rate on top of it would be a second speed nobody asked for.\n The contract keeps the method; this path has nothing to do with it.\n */\n setRate: useCallback(() => {}, []),\n seek,\n state,\n items,\n unsupported,\n divergenceUs: 0,\n lifecycle,\n // One player, so there is no half to stall: the recovery this reported was\n // the video player running while the audio player had stopped, or the\n // reverse. @see docs/research/preview-acceptance.md\n stall: null,\n }\n}\n","import { playlistOf, type VideoProject } from '@pictor-sdk/video-core'\nimport { requireNative } from './player'\n\n/**\n * The shared-clock playback spike, from the host's side.\n *\n * ## What it is testing\n *\n * Whether one playback-clock authority can drive the picture and a prepared,\n * project-wide mix at once. The two-player preview cannot meet the 60 ms gate\n * and the reasons are structural: anchors polled at 50 ms make a single\n * comparison uncertain by up to ±56 ms against a 60 ms tolerance, the\n * correction loop runs at 500 ms while each corrective seek costs 200-400 ms,\n * and at a seam the project clock divides by a speed the player applies about\n * 450 ms later.\n *\n * ## Why the spec is built here\n *\n * `playlistOf` is the shared compiler, and it already decides every number this\n * needs: the source bounds a clip plays between, its speed, and where the\n * window sits on the project timeline. Passing its output through verbatim is\n * what keeps the adapter free of a second interpretation of the document —\n * native maps items onto media items and nothing else.\n *\n * Diagnostics-only, and it says nothing about live PCM gain or fades: it plays\n * the same prepared mix file the current preview plays.\n *\n * @experimental\n */\n\n/** One clip, as the compiler described it. @experimental */\nexport interface CompositionSpikeItem {\n uri: string\n /** Clipping bounds in **source** time, which is where speed is applied. */\n startUs: number\n endUs: number\n /** Full source duration, before trimming or speed. Omit only for legacy callers. */\n sourceDurationUs?: number\n /** Exclusive project-time end from the playlist compiler, after trim and speed. */\n timelineEndUs?: number\n speed: number\n}\n\n/** @experimental */\nexport interface CompositionSpikeSpec {\n /**\n * Hold the shared clock at each clip boundary.\n *\n * Off, and measured off for a reason: with `playWhenReady` false\n * `CompositionPlayer` does not advance across an item boundary, so the\n * incoming frame the hold waits for is never produced and the hold runs to\n * its bound. Holding the clock and preparing the next stream are mutually\n * exclusive on this player.\n */\n seamHold?: boolean\n /**\n * Prepare the incoming item with an explicit seek while the clock is held.\n *\n * Only meaningful with `seamHold`. Pausing natural traversal and seeking to a\n * target are different operations: the seek path resets the renderers, which\n * is what makes a paused player produce a frame, and is the same path the\n * existing seek readiness already passes on.\n */\n seamSeek?: boolean\n items: CompositionSpikeItem[]\n /** The prepared mix. Empty plays the picture with no sound at all. */\n mixUri: string\n /**\n * The mix's duration.\n *\n * Required whenever `mixUri` is set: the player computes each sequence's\n * duration before playing, and an audio-only item cannot report one itself.\n */\n mixDurationUs: number\n width: number\n height: number\n /**\n * The request this attachment belongs to.\n *\n * Claimed before preparation starts. Native refuses an attachment whose id is\n * older than the newest claim, so a preparation that finishes late cannot put\n * a stale mix on screen for even one frame. Omit for runs with no race.\n */\n requestId?: number\n}\n\n/** @experimental */\nexport interface CompositionSpikeState {\n ok: boolean\n attached: boolean\n built: number\n released: number\n error: string | null\n /**\n * Readiness latency, reported apart from synchronisation.\n *\n * A preparation pause is a latency figure and an A/V offset is an error;\n * averaging one into the other is how a slow start reads as drift.\n */\n startupMs: number\n seekMs: number\n /** What the person asked for, independent of whether the transport is running. */\n intentPlaying?: boolean\n /** A hold waiting for the first frame of the current epoch. */\n releasePending?: boolean\n releases?: number\n readinessTimeouts?: number\n readinessFailure?: string | null\n /** How long the last hold lasted before playback was released. */\n holdMs?: number\n /**\n * Wall-clock milliseconds spent holding the clock at each clip boundary.\n *\n * Listed, never averaged. Each entry is a pause the person sees: the decoder\n * is torn down and rebuilt at a boundary and the clock waits rather than the\n * picture falling behind it. **This is not seamless playback**, which is why\n * it is reported apart from every synchronisation figure.\n */\n seamHoldMs?: number[]\n /** Holds that reached their bound without a correctly associated frame. */\n seamHoldTimeouts?: number\n seamHoldActive?: boolean\n atNs?: number\n positionUs?: number\n playing?: boolean\n playbackState?: number\n durationUs?: number\n presentedUs?: number | null\n /** Playlist window belonging to the latched frame, not the current playhead. */\n presentedWindowIndex?: number\n /** How old the presented frame was when it was read, so a stalled sample can be excluded. */\n presentedAgeNs?: number\n /** How long the paired read itself took. Bounds the pairing error. */\n readSpanNs?: number\n /** How stale the pairing was when it was read out. */\n pairingAgeNs?: number\n}\n\n/**\n * Build the spike's spec from a project and a prepared mix.\n *\n * Unsupported clips are not silently dropped: `playlistOf` reports them, and a\n * spec built from a playlist missing a window would put every later seam in the\n * wrong place — which is exactly the class of error this spike exists to\n * measure.\n *\n * @experimental\n */\nexport function compositionSpikeSpec(\n project: VideoProject,\n mix: { uri: string; durationUs: number },\n resolve: (source: { kind: string; uri?: string }) => string | null,\n): { spec: CompositionSpikeSpec; unsupported: string[] } {\n const playlist = playlistOf(project)\n const items: CompositionSpikeItem[] = []\n const unsupported = playlist.unsupported.map((u) => `${u.clipId}: ${u.reason}`)\n const clips = new Map(project.clips.map((clip) => [clip.id, clip]))\n for (const item of playlist.items) {\n const uri = resolve(item.source as unknown as { kind: string; uri?: string })\n if (uri === null) {\n unsupported.push(`${item.clipId}: unresolved`)\n continue\n }\n items.push({\n uri,\n startUs: Number(item.startUs),\n endUs: Number(item.endUs),\n sourceDurationUs: clips.get(item.clipId)?.metadata.duration,\n timelineEndUs: Number(item.timelineEndUs),\n speed: item.speed,\n })\n }\n return {\n spec: {\n items,\n mixUri: mix.uri,\n mixDurationUs: mix.durationUs,\n width: project.size.width,\n height: project.size.height,\n },\n unsupported,\n }\n}\n\nfunction call(fn: () => string, label: string): Record<string, unknown> {\n try {\n return JSON.parse(fn()) as Record<string, unknown>\n } catch (error) {\n return { ok: false, error: `${label}: ${String(error)}` }\n }\n}\n\n/** Claim the next request id, before preparing. @experimental */\nexport function spikeClaim(): number {\n const result = call(() => requireNative().spikeClaim(), 'spikeClaim')\n return typeof result.requestId === 'number' ? result.requestId : 0\n}\n\n/** @experimental */\nexport function spikeStart(spec: CompositionSpikeSpec): Record<string, unknown> {\n return call(() => requireNative().spikeStart(JSON.stringify(spec)), 'spikeStart')\n}\n\n/** @experimental */\nexport function spikePlay(): Record<string, unknown> {\n return call(() => requireNative().spikePlay(), 'spikePlay')\n}\n\n/** @experimental */\nexport function spikePause(): Record<string, unknown> {\n return call(() => requireNative().spikePause(), 'spikePause')\n}\n\n/** @experimental */\nexport function spikeSeek(positionUs: number): Record<string, unknown> {\n return call(() => requireNative().spikeSeek(positionUs), 'spikeSeek')\n}\n\n/** @experimental */\nexport function spikeState(): CompositionSpikeState {\n return call(() => requireNative().spikeState(), 'spikeState') as unknown as CompositionSpikeState\n}\n\n/**\n * One retained sample, unpacked.\n *\n * Packed across the boundary because a gate reads thousands of these, and named\n * here because a gate that indexes into an array of eighteen longs is one\n * transposition away from a confident wrong answer.\n *\n * @experimental\n */\nexport interface SpikeSample {\n seq: number\n atNs: number\n positionUs: number\n playing: boolean\n ready: boolean\n epoch: number\n sinceEpochNs: number\n frameId: number\n frameGeneration: number\n frameWindow: number\n framePtsUs: number\n /**\n * When Media3's renderer said this frame was about to go to the surface.\n *\n * The earliest boundary observable here, and the one that separates the\n * owners of a gap: nothing before this is the decoder or the item's\n * preparation, this to {@link SpikeSample.frameAvailableNs} is the video\n * graph, and that to {@link SpikeSample.frameConsumedNs} is this consumer.\n */\n frameMetadataNs: number\n /** The time Media3 *asked* the surface to present at. An intention. */\n frameReleaseNs: number\n /** The time the consumer latched it. The last boundary observable here. */\n frameConsumedNs: number\n frameAvailableNs: number\n coalesced: number\n offered: number\n available: number\n taken: number\n readSpanNs: number\n}\n\n/** @experimental */\nexport function spikeDrain(fromSeq: number, limit = 400): {\n ok: boolean\n rows: SpikeSample[]\n nextSeq: number\n total: number\n dropped: number\n} {\n const page = call(() => requireNative().spikeDrain(fromSeq, limit), 'spikeDrain') as {\n ok?: boolean\n rows?: number[][]\n nextSeq?: number\n total?: number\n dropped?: number\n }\n return {\n ok: page.ok === true,\n rows: (page.rows ?? []).map((r) => ({\n seq: r[0]!,\n atNs: r[1]!,\n positionUs: r[2]!,\n playing: (r[3]! & 1) === 1,\n ready: (r[3]! & 2) === 2,\n epoch: r[4]!,\n sinceEpochNs: r[5]!,\n frameId: r[6]!,\n frameGeneration: r[7]!,\n frameWindow: r[8]!,\n framePtsUs: r[9]!,\n frameReleaseNs: r[10]!,\n frameConsumedNs: r[11]!,\n frameAvailableNs: r[12]!,\n coalesced: r[13]!,\n offered: r[14]!,\n available: r[15]!,\n taken: r[16]!,\n readSpanNs: r[17]!,\n frameMetadataNs: r[18] ?? -1,\n })),\n nextSeq: page.nextSeq ?? fromSeq,\n total: page.total ?? 0,\n dropped: page.dropped ?? 0,\n }\n}\n\n/** @experimental */\nexport function spikeRelease(): Record<string, unknown> {\n return call(() => requireNative().spikeRelease(), 'spikeRelease')\n}\n","/**\n * Who owns playback, and until when.\n *\n * Media3's player is a **process-wide singleton**. Nothing about it ends when\n * the screen that started it goes away, so on a Pixel 7 leaving the editor while\n * playing left the audio running, and backgrounding the app did the same: the\n * picture stopped because its surface was retired, and the sound carried on\n * because nobody had told the player anything.\n *\n * A singleton with no owner is the whole problem, so this gives it one. A screen\n * takes ownership when it mounts and gives it back exactly once — and a screen\n * that mounts afterwards starts from nothing rather than inheriting whatever the\n * last one was doing.\n *\n * ## Two kinds of stopping, deliberately different\n *\n * **Suspend** is temporary: the app went to the background, or the picker came\n * up over the editor. Playback pauses and the player is kept, because the screen\n * is coming back and rebuilding a player it never lost would be a stall the user\n * did nothing to earn.\n *\n * **Teardown** is final: the screen unmounted, or navigation left it. The player\n * is paused *and then* released, in that order, so nothing is audible in the\n * gap. After a teardown this object does nothing at all — a late suspend from a\n * listener that has not been removed yet must not resurrect anything.\n *\n * Neither is a reason to touch the document. Playback stopping says nothing\n * about what the project is or where its playhead should be, and clearing either\n * would turn leaving a screen into an edit.\n *\n * ## Epochs\n *\n * Every stop bumps an epoch, and callbacks carry the epoch they were scheduled\n * under. A poll that fires after the screen backgrounded still runs — clearing\n * an interval is not instantaneous — and `accepts` is how it finds out that the\n * answer it is about to deliver is no longer wanted.\n *\n * @experimental\n */\n\n/** Why playback stopped. Recorded so a log can say which path ran. @experimental */\nexport type PlaybackStopReason = 'unmount' | 'background' | 'blur'\n\n/** @experimental */\nexport interface PlaybackLifecycleEffects {\n /** Stop the sound. Must be safe to call when already paused. */\n pause(): void\n /** Give up the native player, and with it audio focus. */\n release(): void\n}\n\n/** @experimental */\nexport interface PlaybackLifecycle {\n /** Whether this screen currently owns running playback. */\n readonly active: boolean\n /** Whether the player has been given back. Final. */\n readonly released: boolean\n /** Why it last stopped, or `null`. */\n readonly reason: PlaybackStopReason | null\n /** Bumped by every stop; callbacks carry the value they were scheduled under. */\n readonly epoch: number\n /** Whether work scheduled under `epoch` may still act. */\n accepts(epoch: number): boolean\n /** Pause and keep the player. Idempotent within one suspension. */\n suspend(reason: PlaybackStopReason): void\n /** Become active again. Idempotent; ignored after teardown. */\n resume(): void\n /** Pause, then release, exactly once, forever. */\n teardown(reason: PlaybackStopReason): void\n}\n\n/** @experimental */\nexport function createPlaybackLifecycle(\n effects: PlaybackLifecycleEffects,\n): PlaybackLifecycle {\n let active = true\n let released = false\n let reason: PlaybackStopReason | null = null\n let epoch = 1\n\n return {\n get active() {\n return active\n },\n get released() {\n return released\n },\n get reason() {\n return reason\n },\n get epoch() {\n return epoch\n },\n\n accepts(at) {\n // Never after teardown, whatever epoch is offered: the player is gone and\n // there is nothing a late callback could correctly do with it.\n if (released) return false\n return active && at === epoch\n },\n\n suspend(why) {\n if (released || !active) return\n active = false\n reason = why\n epoch += 1\n effects.pause()\n },\n\n resume() {\n if (released || active) return\n active = true\n reason = null\n epoch += 1\n },\n\n teardown(why) {\n if (released) return\n const wasActive = active\n released = true\n active = false\n reason = why\n epoch += 1\n /*\n Pause first, then release — and only if it was still running.\n\n Releasing a playing player is the one ordering that can be heard: the\n release is not instantaneous, and the gap between \"still playing\" and\n \"gone\" is exactly the audio that outlived the screen. A screen torn down\n while already suspended was paused on the way in, so pausing it again\n would be a native call that says nothing.\n */\n if (wasActive) effects.pause()\n effects.release()\n },\n }\n}\n","import {\n playIntent,\n playableDuration,\n playlistOf,\n projectDuration,\n projectTimeToWindow,\n windowToProjectTime,\n type PlayIntent,\n type PlaylistItem,\n type UnsupportedSource,\n type VideoProject,\n} from '@pictor-sdk/video-core'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { AppState } from 'react-native'\nimport { Media3, type Media3State } from './media3'\nimport { createPlaybackLifecycle, type PlaybackLifecycle } from './playbackLifecycle'\nimport {\n classifyStall,\n shouldRecover,\n type RecoverySample,\n type StallKind,\n} from './surfaceRecovery'\nimport {\n NO_SURFACE,\n afterPrepare,\n idlePlaylistSession,\n shouldPrepare,\n} from './playlistSession'\n\n/**\n * Keep a Media3 playlist in step with a project, and the playhead with playback.\n *\n * Two directions, deliberately separated:\n *\n * - **Project → player.** When the clips, their order or their trims change,\n * the playlist is replaced. The current *project time* is preserved across\n * that, not the window index — deleting the second of four clips must not\n * leave the playhead in whatever the second window now happens to be.\n * - **Player → playhead.** While playing, the player's position is polled at a\n * readable rate and mapped back to project time. This is a **throttled\n * semantic update**, not a per-frame one: the picture is composed on the UI\n * runtime from the external texture and never waits for React.\n *\n * The playlist is only replaced when it genuinely differs. A trim drag\n * dispatches a command per finger sample, and rebuilding the playlist on each\n * would restart decoding many times a second.\n *\n * @experimental\n */\n\n/** @experimental */\nexport interface UseMedia3PlaybackOptions {\n project: VideoProject\n /** Timeline time the host wants shown. */\n playheadUs: number\n /** Called with the project time as playback advances. Throttled. */\n onPositionUs?: (timeUs: number) => void\n /** How often to poll the player, in milliseconds. */\n pollMs?: number\n /**\n * The live presentation surface generation, from `useVideoSurface`.\n *\n * Not a boolean. A playlist prepared against a retired surface renders into\n * something nobody is watching — the player decodes, releases frames and\n * reports no error — so the question is *which* surface, not whether one\n * exists. Every change is a reason to reconcile, including the change to 0.\n */\n surfaceGeneration?: number\n enabled?: boolean\n}\n\n/** @experimental */\nexport interface Media3Playback {\n playing: boolean\n /**\n * Start or resume playback, replaying from zero when there is nothing left.\n *\n * Returns what it decided, so a host can show it or count it. See\n * `playIntent` in `@pictor-sdk/video-core` for why the end is a special case.\n */\n play(): PlayIntent\n pause(): void\n setRate(rate: number): void\n /** Seek in project time. Mapped to the player's own coordinates. */\n seek(timeUs: number): void\n state: Media3State | null\n /** The windows currently loaded, for diagnostics and gates. */\n items: readonly PlaylistItem[]\n /**\n * Clips the player cannot open. Non-empty means playback is **not** loaded.\n *\n * Surfaced rather than swallowed: the alternative is an editor that plays a\n * shorter timeline than it draws, which reads as a synchronisation bug.\n */\n unsupported: readonly UnsupportedSource[]\n /**\n * Which half of the pipeline stalled after a surface restoration, or `null`.\n *\n * Non-null means a black picture was detected and the handshake re-run — a\n * host should surface it rather than show a silent audio-only result.\n */\n stall: StallKind | null\n /**\n * This screen's ownership of the shared player, for diagnostics and gates.\n *\n * Exposed so a host can log which path stopped playback and assert that a\n * screen released exactly once.\n */\n lifecycle: PlaybackLifecycle\n /**\n * How far the timeline and the player disagree, in microseconds. Zero when\n * everything is playable, and asserted by the device gates.\n */\n divergenceUs: number\n /**\n * The player's position in project time, read now, without rendering.\n *\n * For a host driving a UI-thread playhead: it samples this a few times a\n * second and advances between samples itself. Returns `null` when the player\n * has no position to give — before a playlist is loaded, or after release.\n */\n positionUs(): number | null\n /**\n * The rate actually in force: clip speed and preview rate, composed and\n * clamped by the adapter. A host showing a rate should show this one.\n */\n effectiveRate: number\n /**\n * The last seek this hook actually issued.\n *\n * Global time in, and the `{windowIndex, localUs}` it was converted to. A\n * player reporting an unexpected position is either being sent the wrong\n * thing or mishandling the right thing, and those need different fixes — this\n * is what tells them apart instead of leaving it to arithmetic on a\n * screenshot.\n */\n lastSeek: { globalUs: number; windowIndex: number; localUs: number } | null\n}\n\nconst DEFAULT_POLL_MS = 100\n\n/** How often a refused preparation is retried, in milliseconds. */\nconst RETRY_INTERVAL_MS = 250\n\n/** @experimental */\nexport function useMedia3Playback(options: UseMedia3PlaybackOptions): Media3Playback {\n const { project, playheadUs, enabled = true } = options\n const surfaceGeneration = options.surfaceGeneration ?? NO_SURFACE\n const pollMs = options.pollMs ?? DEFAULT_POLL_MS\n const [state, setState] = useState<Media3State | null>(null)\n const [playing, setPlaying] = useState(false)\n\n const playlist = useMemo(() => playlistOf(project), [project])\n const items = playlist.items\n const divergenceUs = useMemo(\n () => projectDuration(project) - playableDuration(playlist),\n [project, playlist],\n )\n\n /**\n * A stable description of the playlist.\n *\n * Compared as a string rather than by reference: every edit produces a new\n * project object, but most edits — a selection, a playhead move, an opacity\n * change — do not change what the player should be playing.\n */\n const signature = useMemo(\n () =>\n items\n .map((i) => `${JSON.stringify(i.source)}|${i.startUs}|${i.endUs}|${i.speed}|${i.muted}`)\n .join(';') +\n // Unsupported clips belong in the signature even though they are not\n // windows. Adding a photo between two videos leaves the video windows\n // identical, so a signature built from items alone would not change and\n // the player would keep playing a timeline that had grown underneath it —\n // the exact divergence this is all meant to prevent.\n `!${playlist.unsupported.map((u) => `${u.clipId}:${u.reason}`).join(',')}`,\n [items, playlist.unsupported],\n )\n\n const playheadRef = useRef(playheadUs)\n playheadRef.current = playheadUs\n const itemsRef = useRef(items)\n itemsRef.current = items\n\n const session = useRef(idlePlaylistSession())\n /** Bumped by the retry timer, purely to run the reconciliation again. */\n const [retry, setRetry] = useState(0)\n\n /**\n * Reconcile what Media3 holds with what the document and the surface imply.\n *\n * Level-triggered. The old version fired on the playlist *changing*, which is\n * the one moment it must not rely on: clips are added while the system picker\n * is closing and there is no surface to prepare against, so the change has\n * already happened by the time preparation becomes possible. See\n * `playlistSession.ts`.\n */\n useEffect(() => {\n if (!enabled) return\n const intent = { signature, generation: surfaceGeneration }\n if (!shouldPrepare(session.current, intent)) return\n\n let ok: boolean\n try {\n if (playlist.unsupported.length > 0 || items.length === 0) {\n // Deliberately not loaded. The host is told why through `unsupported`,\n // and the timeline keeps drawing — what it must not do is play\n // something shorter than what it shows and call that working.\n ok = Media3.setPlaylist({ items: [], unsupported: [] }) !== false\n } else {\n // Preserve project time, not window index. The window that held the\n // playhead may not be the same window — or may not exist — after the\n // edit.\n const start = projectTimeToWindow(items, playheadRef.current) ?? {\n windowIndex: 0,\n positionUs: 0,\n }\n ok = Media3.setPlaylist(playlist, start) !== false\n }\n } catch {\n // A refusal, not a crash. Recording it as applied is what left an editor\n // waiting for an edit the user had already made.\n ok = false\n }\n session.current = afterPrepare(session.current, intent, ok)\n }, [signature, items, playlist, enabled, surfaceGeneration, retry])\n\n /**\n * Retry a refused preparation without waiting for anything else to move.\n *\n * A surface can arrive without its generation changing again, and a user who\n * has added their clips will not add them a second time to prod it.\n */\n useEffect(() => {\n if (!enabled) return\n if (!session.current.pending) return\n const id = setInterval(() => setRetry((n) => n + 1), RETRY_INTERVAL_MS)\n return () => clearInterval(id)\n }, [enabled, retry, surfaceGeneration, signature])\n\n /*\n This screen's ownership of the process-wide player.\n\n Media3's player outlives any screen, so leaving the editor while playing\n left the audio running and backgrounding the app did the same — the picture\n stopped because its surface was retired, and nothing had told the player.\n One lifecycle per hook instance: a screen mounted later starts from nothing\n rather than inheriting what the last one was doing.\n */\n const stallRef = useRef<StallKind | null>(null)\n const surfaceGenerationRef = useRef(surfaceGeneration ?? 0)\n surfaceGenerationRef.current = surfaceGeneration ?? 0\n const lifecycleRef = useRef<PlaybackLifecycle | null>(null)\n if (lifecycleRef.current === null) {\n lifecycleRef.current = createPlaybackLifecycle({\n pause: () => Media3.pause(),\n release: () => Media3.release(),\n })\n }\n const lifecycle = lifecycleRef.current\n /*\n The epoch, made visible to React.\n\n The polls capture an epoch when they start and compare on every tick, so\n resuming has to restart them under the new one — otherwise a screen that\n came back from the background would poll forever under an epoch that no\n longer matches and never report a position again.\n */\n const [lifecycleEpoch, setLifecycleEpoch] = useState(lifecycle.epoch)\n\n /*\n Backgrounding pauses; returning re-arms.\n\n Suspend rather than teardown: the picker comes up over the editor as a\n background transition, and rebuilding a player the screen never lost would\n be a stall nobody asked for. The document is not touched either way —\n playback stopping says nothing about where the playhead should be.\n */\n useEffect(() => {\n const subscription = AppState.addEventListener('change', (next) => {\n if (next === 'active') {\n lifecycle.resume()\n setLifecycleEpoch(lifecycle.epoch)\n return\n }\n lifecycle.suspend('background')\n setLifecycleEpoch(lifecycle.epoch)\n setPlaying(false)\n })\n return () => subscription.remove()\n }, [lifecycle])\n\n /*\n The screen going is the end of its playback, exactly once.\n\n Last of the effects declared here, so it is the first to be cleaned up: a\n teardown that ran after the polls were cleared would release a player the\n polls were still reading.\n */\n useEffect(() => {\n return () => {\n lifecycle.teardown('unmount')\n }\n }, [lifecycle])\n\n /*\n The watchdog for a surface that came back half-attached.\n\n The failure it exists for reports nothing: after a background or a picker\n the clock advances, audio plays, no error is raised and the picture is\n black. A sample is kept from the last time the pipeline looked healthy, and\n when the clock has moved for long enough without a frame being latched the\n handshake is run again. `classifyStall` names which half is broken, because\n re-preparing fixes a stale generation and does nothing for a consumer that\n has stopped pulling.\n */\n const healthy = useRef<RecoverySample | null>(null)\n const [stall, setStall] = useState<StallKind | null>(null)\n\n const lastMeaning = useRef('')\n\n useEffect(() => {\n if (!enabled) return\n // Captured now, checked on every tick: an interval is not cleared the\n // instant the screen goes, and a poll that lands afterwards must not write\n // a position into a screen that is no longer there.\n const epoch = lifecycle.epoch\n const id = setInterval(() => {\n if (!lifecycle.accepts(epoch)) return\n const snapshot = Media3.state()\n // Position is not part of this. Replacing the state object on every tick\n // re-renders the whole editor ten times a second to move a line, which is\n // both wasteful and — because it lands at ten hertz — visibly steppy. The\n // meaning of the state changes far less often than its numbers do, so\n // only a change in meaning is worth a render, and a host that wants the\n // position drives it from `positionUs()` on the UI thread.\n const meaning = [\n snapshot.playing,\n snapshot.ready,\n snapshot.buffering,\n snapshot.ended,\n snapshot.released,\n snapshot.width,\n snapshot.height,\n snapshot.windowIndex,\n snapshot.windowCount,\n snapshot.errorCount,\n snapshot.errorMessage,\n snapshot.rateConstrained,\n ].join('|')\n if (meaning !== lastMeaning.current) {\n lastMeaning.current = meaning\n setState(snapshot)\n setPlaying(snapshot.playing)\n }\n const sample: RecoverySample = {\n atMs: Date.now(),\n positionUs: snapshot.windowPositionUs,\n framesOffered: snapshot.framesOffered,\n framesAvailable: snapshot.framesAvailable,\n // Latched is not reported directly: what arrived, less what is still\n // waiting, is the count the consumer has actually taken.\n framesLatched: snapshot.framesAvailable - snapshot.pendingDepth,\n framesRejected: snapshot.framesRejected,\n pendingGeneration: snapshot.pendingGeneration,\n consumerGeneration: surfaceGenerationRef.current,\n }\n const since = healthy.current\n if (since === null) {\n healthy.current = sample\n } else if (classifyStall(since, sample) === null) {\n // Healthy, or simply not running: either way this is the new baseline.\n healthy.current = sample\n if (stallRef.current !== null) setStall(null)\n } else if (shouldRecover(since, sample)) {\n const kind = classifyStall(since, sample)\n healthy.current = sample\n stallRef.current = kind\n setStall(kind)\n // Re-run the surface/playlist handshake. Never silently: a plausible\n // audio-only result is the thing this refuses to leave alone.\n session.current = { ...session.current, pending: true }\n setRetry((n) => n + 1)\n }\n\n if (!snapshot.playing) return\n const projectUs = windowToProjectTime(\n itemsRef.current,\n snapshot.windowIndex,\n snapshot.windowPositionUs,\n )\n if (projectUs !== null) options.onPositionUs?.(projectUs)\n }, pollMs)\n return () => clearInterval(id)\n // `onPositionUs` is read through the closure on each tick; depending on it\n // would restart the interval whenever the host re-created the callback.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [enabled, pollMs, lifecycle, lifecycleEpoch])\n\n const lastSeekRef = useRef<Media3Playback['lastSeek']>(null)\n\n /**\n * Seek in **global** timeline time.\n *\n * The store's playhead is global and stays global; the conversion to a window\n * and a position inside it happens here, once, through `projectTimeToWindow` —\n * the same mapping `play` and the playlist load use. Handing a player a global\n * time as though it were window-local is silent: it clamps to the end of\n * whichever clip it is in and reports a perfectly plausible position.\n */\n const seek = useCallback((timeUs: number) => {\n const target = projectTimeToWindow(itemsRef.current, timeUs)\n if (!target) return\n lastSeekRef.current = {\n globalUs: Math.round(timeUs),\n windowIndex: target.windowIndex,\n localUs: target.positionUs,\n }\n Media3.seekToWindow(target.windowIndex, target.positionUs)\n }, [])\n\n const positionUs = useCallback((): number | null => {\n const snapshot = Media3.state()\n if (snapshot.released || itemsRef.current.length === 0) return null\n return windowToProjectTime(\n itemsRef.current,\n snapshot.windowIndex,\n snapshot.windowPositionUs,\n )\n }, [])\n\n const durationUsRef = useRef(0)\n durationUsRef.current = projectDuration(project)\n\n /**\n * Play, replaying from the start if there is nothing left to play.\n *\n * The decision is `playIntent` in `@pictor-sdk/video-core`, not an `if` here: it\n * is the same question a host's own transport has to answer, and two answers\n * would eventually disagree. Read fresh from the player rather than from\n * `state`, which only re-renders on a change of meaning and can be a poll\n * behind at the moment of the press.\n */\n const play = useCallback(() => {\n const snapshot = Media3.state()\n const intent = playIntent({\n positionUs:\n windowToProjectTime(\n itemsRef.current,\n snapshot.windowIndex,\n snapshot.windowPositionUs,\n ) ?? 0,\n durationUs: durationUsRef.current,\n playbackState: snapshot.playbackState,\n ended: snapshot.ended,\n })\n if (intent.seekToUs !== null) {\n const target = projectTimeToWindow(itemsRef.current, intent.seekToUs)\n if (target) Media3.seekToWindow(target.windowIndex, target.positionUs)\n }\n if (!intent.play) return intent\n // Refused once the screen has given the player back: a late press from a\n // control that has not unmounted yet must not start a released player.\n if (lifecycle.released) return intent\n Media3.play()\n setPlaying(true)\n return intent\n }, [lifecycle])\n\n return {\n playing,\n play,\n get lastSeek() {\n return lastSeekRef.current\n },\n positionUs,\n effectiveRate: state?.effectiveRate ?? 1,\n pause: useCallback(() => {\n Media3.pause()\n setPlaying(false)\n }, []),\n setRate: useCallback((rate: number) => Media3.setRate(rate), []),\n seek,\n state,\n items,\n unsupported: playlist.unsupported,\n divergenceUs,\n lifecycle,\n stall,\n }\n}\n","/**\n * Deciding that a restored surface is not actually showing anything.\n *\n * After a background, a picker, or any other trip away from the editor, the\n * player and the surface have to find each other again. When that handshake\n * half-succeeds the result is the worst kind of failure: audio plays, the clock\n * advances, no error is reported, and the picture is black. Nothing in the\n * player's own state says anything is wrong, because from its point of view\n * nothing is.\n *\n * So it is decided from the counters instead, and the counters are already\n * separated into the four things that can go wrong — see `Media3State`, where\n * `framesOffered` is the decoder saying a frame is coming and `framesAvailable`\n * is the surface saying one arrived.\n *\n * The point of naming the branch is that the repairs differ. Re-running the\n * handshake fixes a stale generation and does nothing at all for a consumer\n * that has stopped pulling, and \"retry everything\" would have made both look\n * like the same intermittent bug.\n *\n * @experimental\n */\n\n/** How long a stall must persist before it counts. @experimental */\nexport const RECOVERY_GRACE_MS = 1500\n\n/** What a sample of the pipeline looked like at one instant. @experimental */\nexport interface RecoverySample {\n atMs: number\n /** The player's own clock. Advancing means audio is being produced. */\n positionUs: number\n /** Media3 announcing frames. */\n framesOffered: number\n /** The SurfaceTexture receiving them. */\n framesAvailable: number\n /** Frames the consumer actually took. */\n framesLatched: number\n /** Announcements for a generation already retired. */\n framesRejected: number\n /** The generation the producer is filling. */\n pendingGeneration: number\n /** The generation the consumer is watching. */\n consumerGeneration: number\n}\n\n/**\n * Which half of the pipeline is broken, or `null` when it is healthy.\n *\n * @experimental\n */\nexport type StallKind =\n /** Frames arrive and nobody takes them: the consumer or its frame loop. */\n | 'consumer'\n /** Producer and consumer disagree about which surface: a stale handshake. */\n | 'handshake'\n /** The player is running but nothing reaches the surface: attachment. */\n | 'attachment'\n /** Frames are latched and the picture is still wrong: scene or image. */\n | 'resource'\n\n/**\n * Name the branch from two samples.\n *\n * `null` means nothing is wrong *that this can see* — including the ordinary\n * case of a player that is simply not advancing, which is what a paused editor\n * looks like and is not a fault.\n *\n * @experimental\n */\nexport function classifyStall(since: RecoverySample, now: RecoverySample): StallKind | null {\n // A clock that is not moving is not a stall. Nothing is expected of a player\n // that is paused, ended or buffering, and calling that a fault would make\n // every pause look like a broken surface.\n if (now.positionUs <= since.positionUs) return null\n\n // Checked before anything else: when the two sides disagree about the\n // generation, every other counter is describing a surface nobody is watching.\n if (\n now.pendingGeneration !== now.consumerGeneration ||\n now.framesRejected > since.framesRejected\n ) {\n return 'handshake'\n }\n\n const latched = now.framesLatched > since.framesLatched\n if (latched) {\n // The pipeline is delivering. Whatever is wrong is further along — the\n // scene, the image, the matrix — and re-running the handshake would only\n // interrupt something that is working.\n return 'resource'\n }\n\n const arrived = now.framesAvailable > since.framesAvailable\n if (arrived) return 'consumer'\n\n // The decoder may or may not be announcing; either way nothing reached the\n // surface, so the player is not attached to the one being watched.\n return 'attachment'\n}\n\n/**\n * Whether the stall has lasted long enough to act on.\n *\n * A grace period because none of this is instantaneous: a surface is recreated,\n * a playlist is reconciled and the first frame decoded, and a check that fired\n * inside that window would recover a pipeline that was about to work.\n *\n * @experimental\n */\nexport function shouldRecover(\n since: RecoverySample,\n now: RecoverySample,\n graceMs: number = RECOVERY_GRACE_MS,\n): boolean {\n if (now.atMs - since.atMs < graceMs) return false\n const kind = classifyStall(since, now)\n // `resource` is deliberately excluded: frames are being latched, so the\n // handshake this recovers is not what is wrong, and retrying it would blank a\n // working picture on a timer.\n return kind === 'consumer' || kind === 'handshake' || kind === 'attachment'\n}\n","import {\n createThumbnailPolicy,\n thumbnailKey,\n type MediaSource,\n type AbortLike,\n type Thumbnail,\n type ThumbnailProvider,\n type ThumbnailRequest,\n} from '@pictor-sdk/video-core'\nimport { requireNative } from './player'\n\n/**\n * `ThumbnailProvider` over Media3's retriever, with the core policy on top.\n *\n * The division is the point: `@pictor-sdk/video-core` owns deduplication, the\n * bounded cache and visible-range ordering, all of which are pure; this file\n * owns the part that needs a codec. A host that writes its own provider — for a\n * remote thumbnail service, say — reuses the policy and replaces only this.\n *\n * Results arrive as base64 JPEG rather than as a native image handle. A strip\n * thumbnail is a few kilobytes and it is drawn by React Native's own image\n * pipeline, so there is nothing here to leak and no ownership to transfer. The\n * `AHardwareBuffer` lesson applies to full-resolution frames at 30 per second,\n * not to a 64-pixel square that changes when the user scrolls.\n *\n * @experimental\n */\n\n/** @experimental */\nexport interface AndroidThumbnailOptions {\n /**\n * How many decoded thumbnails to keep.\n *\n * 240 covers several screens of a dense strip at 64 px — enough that\n * scrolling back does not re-decode, bounded enough that a long project\n * cannot grow without limit.\n */\n capacity?: number\n /** How often to check whether a decode has finished, in milliseconds. */\n pollMs?: number\n}\n\nconst DEFAULT_CAPACITY = 240\nconst DEFAULT_POLL_MS = 16\n\n/** @experimental */\nexport function createAndroidThumbnailProvider(\n options: AndroidThumbnailOptions = {},\n): ThumbnailProvider {\n const policy = createThumbnailPolicy<string>({\n capacity: options.capacity ?? DEFAULT_CAPACITY,\n })\n const pollMs = options.pollMs ?? DEFAULT_POLL_MS\n /** Tokens in flight, by request key, so a cancel can reach the native side. */\n const tokens = new Map<string, number>()\n\n function uriOf(source: MediaSource): string {\n if (typeof source === 'string') return source\n if (source && typeof source === 'object' && 'uri' in source) {\n const uri = (source as { uri?: unknown }).uri\n if (typeof uri === 'string') return uri\n }\n throw new Error(\n `@pictor-sdk/video-android: a thumbnail source is not a URI. Normalise host media to { kind: 'uri', uri } first. Received: ${JSON.stringify(source)}`,\n )\n }\n\n return {\n async request(request: ThumbnailRequest, signal?: AbortLike): Promise<Thumbnail | null> {\n const key = thumbnailKey(request)\n const cached = policy.get(request)\n if (cached !== undefined) {\n return { image: cached, sourceTimeUs: request.sourceTimeUs, size: { width: 0, height: 0 } }\n }\n if (signal?.aborted) return null\n\n const native = requireNative()\n policy.begin(request)\n const token = native.thumbnailRequest(\n uriOf(request.source),\n request.sourceTimeUs,\n request.widthPx,\n )\n tokens.set(key, token)\n\n const stop = () => {\n native.thumbnailCancel(token)\n tokens.delete(key)\n policy.cancel(request)\n }\n signal?.addEventListener('abort', stop, { once: true })\n\n try {\n // Poll rather than await a bridged Promise: the native side is a token\n // API precisely so a request can be abandoned, and a Promise that\n // cannot be cancelled would decode frames nobody will see.\n for (;;) {\n if (signal?.aborted) return null\n const data = native.thumbnailTake(token)\n if (data !== null && data !== undefined) {\n tokens.delete(key)\n const evicted = policy.store(request, `data:image/jpeg;base64,${data}`)\n // Base64 strings are garbage-collected; eviction is bookkeeping\n // rather than disposal, and the returned values are ignored on\n // purpose. A provider handing back native images would dispose here.\n void evicted\n return {\n image: policy.get(request)!,\n sourceTimeUs: request.sourceTimeUs,\n size: { width: request.widthPx, height: 0 },\n }\n }\n const error = native.thumbnailError(token)\n if (error !== null && error !== undefined) {\n tokens.delete(key)\n policy.cancel(request)\n return null\n }\n if (!native.thumbnailRunning(token)) {\n // Neither a result nor an error, and not running: the frame could\n // not be produced. A source can legitimately have no frame at a\n // time — past the end of a variable-rate clip, for instance.\n tokens.delete(key)\n policy.cancel(request)\n return null\n }\n await new Promise<void>((resolve) => setTimeout(() => resolve(), pollMs))\n }\n } finally {\n signal?.removeEventListener('abort', stop)\n }\n },\n\n releaseSource(source: MediaSource) {\n policy.evictSource(source)\n requireNative().thumbnailReleaseSource(uriOf(source))\n },\n\n release() {\n for (const token of tokens.values()) requireNative().thumbnailCancel(token)\n tokens.clear()\n policy.clear()\n requireNative().thumbnailReleaseAll()\n },\n }\n}\n\n/** Decode counters, for gates and diagnostics. @experimental */\nexport function thumbnailCounters(): {\n decoded: number\n cancelled: number\n failed: number\n running: number\n retrievers: number\n} {\n return requireNative().thumbnailCounters() as {\n decoded: number\n cancelled: number\n failed: number\n running: number\n retrievers: number\n }\n}\n","/**\n * Deciding when playback has settled, instead of assuming it after a timeout.\n *\n * The first attempt excluded a fixed 1.5 s after every transition. That is a\n * guess in both directions: too short for a seek, whose tail leaked into the\n * steady figures, and too long for a rate change that settles immediately. It\n * also hides the thing worth knowing — how long recovery actually took.\n *\n * So a transition opens a phase, and the phase closes when the offsets say so:\n * [CONVERGED_RUN] consecutive samples inside the expected bound. The recovery\n * duration and the worst offset seen on the way are kept, because a transient\n * that is excluded but not reported is a failure nobody can see.\n *\n * [MAX_RECOVERY_SAMPLES] is a safety net, not a policy. A phase that never\n * converges is closed anyway and marked, so a permanent failure surfaces as a\n * failure instead of an ever-growing exclusion window.\n *\n * Pure, so the rule is tested rather than trusted.\n *\n * @experimental\n */\n\n/** What kind of transition opened a phase. Steady is the absence of one. */\nexport type Phase =\n | 'steady'\n | 'startup'\n | 'seek'\n | 'source'\n | 'pauseResume'\n | 'rateChange'\n\n/**\n * Consecutive in-bound samples that end a transient.\n *\n * Eight at 30 fps is about a quarter second of agreement — long enough that a\n * single lucky frame cannot close a phase, short enough to measure recovery\n * rather than round it up.\n */\nexport const CONVERGED_RUN = 8\n\n/** A phase is closed after this many samples whatever they say. */\nexport const MAX_RECOVERY_SAMPLES = 600\n\nexport interface PhaseState {\n phase: Phase\n /** Offsets collected while this phase was open. */\n samples: number[]\n /** Consecutive in-bound samples so far. */\n run: number\n /** Worst absolute offset seen during the transient, in microseconds. */\n worstUs: number\n /** Samples taken before it converged. */\n recoverySamples: number\n /** True once converged; further samples belong to steady state. */\n converged: boolean\n /** True when the safety net closed it instead of convergence. */\n timedOut: boolean\n}\n\nexport function beginPhase(phase: Phase): PhaseState {\n return {\n phase,\n samples: [],\n run: 0,\n worstUs: 0,\n recoverySamples: 0,\n converged: phase === 'steady',\n timedOut: false,\n }\n}\n\n/**\n * Feed one offset. Returns the updated phase and where the sample belongs.\n *\n * A sample taken while a transient is open belongs to that transient, never to\n * steady state — merging them would report the transitions as if they were\n * playback.\n */\nexport function observe(\n state: PhaseState,\n offsetUs: number,\n boundUs: number,\n): { state: PhaseState; bucket: Phase } {\n if (state.converged) {\n return { state, bucket: 'steady' }\n }\n\n const magnitude = Math.abs(offsetUs)\n const next: PhaseState = {\n ...state,\n samples: [...state.samples, offsetUs],\n recoverySamples: state.recoverySamples + 1,\n worstUs: Math.max(state.worstUs, magnitude),\n run: magnitude <= boundUs ? state.run + 1 : 0,\n }\n\n if (next.run >= CONVERGED_RUN) {\n next.converged = true\n } else if (next.recoverySamples >= MAX_RECOVERY_SAMPLES) {\n // Closed by the safety net. Marked, so it reads as a failure to converge\n // rather than quietly becoming steady state.\n next.converged = true\n next.timedOut = true\n }\n\n return { state: next, bucket: state.phase }\n}\n\n/** How long recovery took, in frames, or null while still recovering. */\nexport function recoveryFrames(state: PhaseState): number | null {\n return state.converged ? state.recoverySamples : null\n}\n","import { VideoExportCancelled } from '@pictor-sdk/video-core'\n\n/**\n * One export at a time, settled exactly once, cancellable anywhere.\n *\n * The experimental exporter blocked the bridge for a whole encode and threw\n * from `cancel()`. That was honest — a silent no-op behind a cancel button is\n * worse than a refusal — but it is not something a host can ship, so this is the\n * orchestration that makes cancellation real.\n *\n * ## Why cancellation cannot just reject the promise\n *\n * A Transformer nobody is listening to still encodes, still writes and still\n * holds its codecs. Rejecting in JavaScript would hand the host a cancelled\n * state over an export that is still running, still filling the cache and still\n * about to produce a file nobody is waiting for. So `cancel` reaches native\n * first and the promise settles from the job's own checkpoints.\n *\n * ## Checkpoints\n *\n * Cancellation is cooperative. The work is a sequence of stages, each of which\n * calls {@link ExportJob.checkpoint} on the way past, and the first checkpoint\n * after a cancel throws. That is why the stages are named: a cancellation\n * carries where it stopped, which is the difference between a log that says\n * \"cancelled\" and one that says which resources were live at the time.\n *\n * ## Settling once\n *\n * Every path — success, failure, cancellation — clears the active job, and\n * nothing may be delivered afterwards. A progress callback that arrives after a\n * host has shown a result is a UI that goes backwards from \"done\".\n *\n * @experimental\n */\n\n/** The lifecycle points an export can be stopped at. @experimental */\nexport const EXPORT_STAGES = [\n 'resolving',\n 'overlays',\n 'shards',\n 'cadence',\n 'audio',\n 'concatenating',\n 'finalising',\n 'publishing',\n] as const\n\n/** @experimental */\nexport type ExportStage = (typeof EXPORT_STAGES)[number]\n\n/** @experimental */\nexport interface ExportJob {\n readonly id: string\n /**\n * Give cancellation a chance to take effect.\n *\n * Throws {@link VideoExportCancelled} if the job has been cancelled. Called\n * between stages and inside long loops, because a cancel that can only be\n * observed when the work finishes is not a cancel.\n */\n checkpoint(stage: ExportStage): void\n /** Deliver progress, unless the job has settled. */\n progress(value: number, deliver: (value: number) => void): void\n}\n\n/** @experimental */\nexport interface ExportJobsOptions {\n /**\n * Stop the native side of a job.\n *\n * Given the job id so a late cancel cannot stop a *different* export that\n * started in the meantime.\n */\n cancelNative(jobId: string): void\n /** Injected in tests. */\n now?: () => number\n}\n\n/**\n * The job domain's ledger.\n *\n * Kept apart from the native run counters on purpose. One export job drives\n * several Transformer runs, so adding a job total to a run total produces a\n * number that reconciles with nothing — and reads like a reconciliation, which\n * is worse than reporting neither.\n *\n * @experimental\n */\nexport interface ExportJobCounts {\n /** Jobs that began. Refused starts are not accepted jobs. */\n accepted: number\n succeeded: number\n cancelled: number\n failed: number\n /** Exactly 0 or 1: one job at a time. */\n active: number\n}\n\n/** @experimental */\nexport interface ExportJobs {\n /** The running job's id, or `null`. */\n readonly active: string | null\n /**\n * `accepted === succeeded + cancelled + failed + active`, always.\n *\n * Terminal outcomes are mutually exclusive: a cancelled job is not also a\n * failed one, however it unwound.\n */\n readonly counts: ExportJobCounts\n /** Run `body` as the one active job. */\n run<T>(body: (job: ExportJob) => Promise<T>): Promise<T>\n /** Ask the running job to stop. Idempotent; a no-op when none is running. */\n cancel(): void\n}\n\nlet counter = 0\n\n/** @experimental */\nexport function createExportJobs(options: ExportJobsOptions): ExportJobs {\n const now = options.now ?? (() => Date.now())\n let active: string | null = null\n let cancelledAt: ExportStage | null = null\n let cancelRequested = false\n let settled = false\n let accepted = 0\n let succeeded = 0\n let cancelled = 0\n let failed = 0\n\n const clear = () => {\n active = null\n cancelRequested = false\n cancelledAt = null\n settled = true\n }\n\n return {\n get active() {\n return active\n },\n\n get counts() {\n return {\n accepted,\n succeeded,\n cancelled,\n failed,\n active: active === null ? 0 : 1,\n }\n },\n\n async run<T>(body: (job: ExportJob) => Promise<T>): Promise<T> {\n if (active !== null) {\n /*\n Refused, not queued.\n\n The native module runs one export per process. Interleaving two would\n have them share a Transformer, a muxer and a cache directory, and the\n first symptom would be a corrupt file rather than an error.\n */\n throw new Error(\n `createMedia3VideoExporter: an export is already running (${active}); ` +\n 'cancel it before starting another',\n )\n }\n\n counter += 1\n accepted += 1\n const id = `export_${now().toString(36)}_${counter.toString(36)}`\n active = id\n cancelRequested = false\n cancelledAt = null\n settled = false\n\n const job: ExportJob = {\n id,\n checkpoint(stage) {\n if (cancelRequested) {\n cancelledAt = stage\n throw new VideoExportCancelled(id, stage)\n }\n },\n progress(value, deliver) {\n if (settled || cancelRequested) return\n deliver(value)\n },\n }\n\n try {\n const result = await body(job)\n /*\n A cancel that arrived while the last stage was finishing still wins.\n\n The alternative is resolving with a file the user asked us not to\n make, which is the outcome a cancel button exists to prevent.\n */\n if (cancelRequested) {\n throw new VideoExportCancelled(id, cancelledAt ?? 'finalising')\n }\n clear()\n succeeded += 1\n return result\n } catch (error) {\n const wasCancelled = cancelRequested\n const stage = cancelledAt ?? 'finalising'\n clear()\n if (wasCancelled) cancelled += 1\n else failed += 1\n if (wasCancelled) {\n /*\n The cancellation is the outcome, whatever the body threw.\n\n Tearing down a Transformer mid-encode raises its own errors, and\n reporting one of those would tell a host the export failed when the\n user had simply stopped it.\n */\n if (error instanceof VideoExportCancelled) throw error\n throw new VideoExportCancelled(id, stage)\n }\n throw error\n }\n },\n\n cancel() {\n // No job, nothing to stop, nothing to say. Repeated presses reach native\n // once, because a second stop for the same job is not a second request.\n if (active === null || cancelRequested) return\n cancelRequested = true\n options.cancelNative(active)\n },\n }\n}\n","import type {\n AudioMixProgram,\n ExportSourceContext,\n MediaSource,\n VideoProject,\n} from '@pictor-sdk/video-core'\n\n/**\n * Turning a compiled mix program's spans into files to decode.\n *\n * Shared by the exporter and by preview preparation, deliberately. The two must\n * agree about which file a span means, or a preview would be mixed from one set\n * of sources and the export from another — and the difference would only be\n * audible in the finished video.\n *\n * @experimental\n */\n\n/**\n * What a span is, so a host can resolve it as what it actually is.\n *\n * A span carved from a clip's own audio resolves as that clip — the same\n * context the video pass used, so the same media resolves the same way. A span\n * from a music item resolves as that item. The id alone cannot say which, and a\n * host that guessed would resolve the first thing that matched.\n */\nexport function mixSpanContext(\n project: VideoProject,\n spanId: string,\n): ExportSourceContext | null {\n const isClip = project.clips.some((clip) => clip.id === spanId)\n const isAudio = project.audio.some((item) => item.id === spanId)\n // Ambiguous or absent are the same answer: there is no one owner to resolve\n // against, and picking either would be a guess.\n if (isClip === isAudio) return null\n return isClip ? { kind: 'clip', clipId: spanId } : { kind: 'audio', audioId: spanId }\n}\n\n/** @experimental */\nexport interface ResolvedMixSources {\n /** Span id to readable URI, for every span that needs decoding. */\n uris: Record<string, string>\n /** One sentence per span that could not be resolved. Empty means usable. */\n unresolved: string[]\n}\n\n/**\n * Resolve every span that needs decoding.\n *\n * Silent spans are skipped: they occupy their time and contribute no samples,\n * so asking a host to resolve one would be asking for a file that is not\n * needed — and a host that returned nothing would look like a failure.\n *\n * @experimental\n */\nexport function resolveMixSources(\n project: VideoProject,\n program: AudioMixProgram,\n resolveSource: (source: MediaSource, context: ExportSourceContext) => string | null | undefined,\n): ResolvedMixSources {\n const uris: Record<string, string> = {}\n const unresolved: string[] = []\n\n for (const span of program.spans) {\n if (span.silent || span.source === null) continue\n const context = mixSpanContext(project, span.id)\n if (context === null) {\n unresolved.push(\n `audio span '${span.id}' matches no clip or audio item, so it has no owner to resolve against`,\n )\n continue\n }\n const uri = resolveSource(span.source, context)\n if (typeof uri !== 'string' || uri.length === 0) {\n unresolved.push(`audio span '${span.id}' could not be resolved to a readable source`)\n continue\n }\n uris[span.id] = uri\n }\n\n return { uris, unresolved }\n}\n","import type { VideoExportDestination } from '@pictor-sdk/video-core'\n\n/**\n * Where the file goes, and what is refused before anything is encoded.\n *\n * ## Refusal, never fallback\n *\n * A destination that cannot be honoured fails the export naming the reason. The\n * tempting alternative — write to the cache and return that instead — produces\n * an export that reports success while the file is nowhere the caller asked for,\n * and nothing downstream can tell that apart from having worked. A host that\n * wants a temporary can ask for one.\n *\n * ## `file:` only, for now\n *\n * A `content:` uri is not a path. A document provider need not be a filesystem,\n * so there is no directory to stage in and no rename to be atomic about;\n * publishing would have to be a copy into an opaque stream, with no way to leave\n * the previous contents intact if it failed halfway. That is a different design,\n * not a missing branch, so it is refused by name.\n *\n * ## Paths do not appear in errors\n *\n * A message that quotes `/data/user/0/com.host.app/files/private/...` puts an\n * app-private path into a log, a crash report and whatever the host displays.\n * Every reason here names the file and nothing above it.\n *\n * @experimental\n */\n\n/** A destination this adapter can honour. @experimental */\nexport type ResolvedDestination =\n | { kind: 'temporary' }\n | { kind: 'file'; path: string; overwrite: boolean }\n\n/** @experimental */\nexport interface DestinationDecision {\n destination: ResolvedDestination | null\n /** Empty when `destination` is set. Never contains a full path. */\n reasons: string[]\n}\n\n/**\n * The last segment, for a message that has to name something.\n *\n * Directories are the private part. A filename the caller chose is already\n * theirs, and without it a refusal cannot say which destination it refused.\n */\nexport function describeTarget(path: string): string {\n const trimmed = path.replace(/\\/+$/, '')\n const name = trimmed.slice(trimmed.lastIndexOf('/') + 1)\n return name.length > 0 ? name : '(unnamed)'\n}\n\nconst KNOWN_SCHEMES: Record<string, string> = {\n 'content:': 'a content uri is not a filesystem path, so it cannot be published atomically',\n 'http:': 'a remote destination cannot be written to directly',\n 'https:': 'a remote destination cannot be written to directly',\n 'android.resource:': 'app resources are read-only',\n 'asset:': 'bundled assets are read-only',\n}\n\n/** @experimental */\nexport function resolveDestination(destination?: VideoExportDestination): DestinationDecision {\n if (destination === undefined) return { destination: { kind: 'temporary' }, reasons: [] }\n if (destination.kind === 'temporary') return { destination: { kind: 'temporary' }, reasons: [] }\n if (destination.kind !== 'file') {\n return {\n destination: null,\n reasons: [`destination kind '${String((destination as { kind: string }).kind)}' is not supported`],\n }\n }\n\n const uri = destination.uri\n if (typeof uri !== 'string' || uri.trim().length === 0) {\n return { destination: null, reasons: ['the file destination has no uri'] }\n }\n\n const scheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.exec(uri)?.[0]?.toLowerCase()\n if (scheme !== undefined && scheme !== 'file:') {\n const known = KNOWN_SCHEMES[scheme]\n return {\n destination: null,\n reasons: [\n known !== undefined\n ? `destination scheme '${scheme}' is not supported: ${known}`\n : `destination scheme '${scheme}' is not supported; only file: is`,\n ],\n }\n }\n if (scheme === undefined) {\n // A bare path is ambiguous — relative to what? — and accepting it would\n // silently resolve against a working directory nobody set.\n return { destination: null, reasons: ['the file destination must be a file: uri'] }\n }\n\n let path: string\n try {\n // `file:///a/b` and `file:/a/b` both name an absolute path; `file://host/a`\n // names another machine's, which is not something to guess at.\n const withoutScheme = uri.slice('file:'.length)\n const rest = withoutScheme.startsWith('//') ? withoutScheme.slice(2) : withoutScheme\n if (withoutScheme.startsWith('//') && !rest.startsWith('/')) {\n return {\n destination: null,\n reasons: ['the file destination names a remote host, which is not supported'],\n }\n }\n path = decodeURIComponent(rest)\n } catch {\n return { destination: null, reasons: ['the file destination uri is malformed'] }\n }\n\n if (!path.startsWith('/')) {\n return { destination: null, reasons: ['the file destination must be an absolute path'] }\n }\n if (path.endsWith('/')) {\n return {\n destination: null,\n reasons: ['the file destination is a directory; it must name the file to write'],\n }\n }\n if (path.includes('\\0')) {\n return { destination: null, reasons: ['the file destination uri is malformed'] }\n }\n /*\n `..` is refused rather than normalised.\n\n Resolving it would mean a destination that names one directory and writes to\n another, and the staging file — which must live beside the target for the\n publication to be a same-filesystem rename — would be placed by arithmetic\n the caller did not do.\n */\n if (path.split('/').some((segment) => segment === '..')) {\n return { destination: null, reasons: ['the file destination must not contain \"..\"'] }\n }\n const name = describeTarget(path)\n if (name === '.' || name === '(unnamed)') {\n return {\n destination: null,\n reasons: ['the file destination is a directory; it must name the file to write'],\n }\n }\n\n return {\n destination: { kind: 'file', path, overwrite: destination.overwrite === true },\n reasons: [],\n }\n}\n","import type { VideoExportProgress } from '@pictor-sdk/video-core'\nimport type { ExportStage } from './exportJob'\n\n/**\n * How far an export has got, and how to tell slow from stuck.\n *\n * ## Weighting is derived, not decided\n *\n * Equal percentages per stage would be a lie the bar tells: a 0.1× shard writes\n * three hundred frames while its neighbour writes sixty, and splitting them\n * evenly makes the first appear stuck and the second appear instant. The weights\n * come from the work the compiler already planned — output frames per shard,\n * audio samples, the mux — so the bar moves at the rate the encoder does.\n *\n * Output frames, specifically. Cadence changes how many frames a shard *writes*\n * for a given source, and writing is what the progress is measuring the passage\n * of; counting source frames would make slow motion look fast and fast motion\n * look stuck.\n *\n * ## What 1 means\n *\n * One is not \"the encoder finished\". It is \"the destination is complete and\n * readable\", which is a later moment: there is still a mux, and after that a\n * publication. Reporting 1 at the end of encoding produces a \"done\" that cannot\n * yet be opened, which is worse than a bar that sits at 97 for a moment.\n *\n * ## Progress and heartbeat are different signals\n *\n * **Progress** is completed work shown to a person, throttled to a handful of\n * updates a second because nobody can read more than that and every update\n * costs a bridge crossing.\n *\n * **Heartbeat** is evidence that work is still happening. It is not throttled,\n * it does not have to move the visible percentage, and it is counted natively:\n * a single muxed sample batch or a block of copied destination bytes is a\n * heartbeat even when the bar does not visibly change for a second.\n *\n * Conflating them is what makes a watchdog wrong in both directions. Keyed on\n * visible progress, a long mux or a large destination copy looks stalled\n * because its percentage barely moves; keyed on elapsed time per stage, a 0.1×\n * shard that legitimately runs for minutes looks stalled too. What actually\n * marks a wedge is **silence** — no heartbeat from any source at all.\n *\n * @experimental\n */\n\n/** What a shard will write. @experimental */\nexport interface ShardPlan {\n /** Output frames, after cadence. */\n frames: number\n}\n\n/** @experimental */\nexport interface ProgressPlanInput {\n shards: readonly ShardPlan[]\n /** Mixed audio samples the timeline will produce. Zero when silent. */\n audioSamples: number\n durationUs: number\n}\n\n/** Fractions of the whole export, summing to 1. @experimental */\nexport interface ProgressPlan {\n shardWeights: number[]\n audioWeight: number\n /** Concatenation and mux: real work, and the last thing before publication. */\n muxWeight: number\n /**\n * Publication: writing the destination and making it readable.\n *\n * Held back deliberately so the encoded-and-muxed state cannot reach 1. A bar\n * that fills before the file exists produces a \"done\" that cannot be opened,\n * which is a worse lie than sitting at 97 for a moment.\n */\n publishWeight: number\n durationUs: number\n /** The totals the fractions are measured against. */\n shardFrames: number[]\n audioSamples: number\n}\n\n/**\n * How long without a heartbeat counts as wedged.\n *\n * Derived from the longest silence real work produces, not from how long the\n * platform takes to notice a wedge. The first version had that backwards — it\n * justified twelve seconds by sitting *past* Media3's observed 10.5 s stall\n * detection, which makes a watchdog decorative: it can only ever be the second\n * thing to notice.\n *\n * The maximum legitimate heartbeat gap, measured across **three passes of seven\n * cases** on a Pixel 7 — short and long 1×, 0.1× and 10× cadence, mixed-speed\n * multi-clip, muted, and project audio — is **2046 ms**, in the 0.1× case. Seven\n * seconds is 3.4× that, and 3.5 s ahead of the platform's own 10.5 s stall\n * detection. Both margins are the point: too close to the first and it fires on\n * healthy work, too close to the second and it is not worth having.\n *\n * Three passes rather than one because a single pass is not a maximum. The same\n * 0.1× case measured 1668, 1426 and 2046 ms in three different sessions, and a\n * threshold frozen from the lowest of those would have been derived from a run\n * that happened to go well.\n *\n * ## Two corrections this number carries\n *\n * The first series measured 1668 ms with gate provenance hashing running — every\n * sample of both intermediates and the result read and digested, beating as it\n * went. A threshold calibrated against work the user never pays for is\n * calibrated against the wrong export, so the hashing moved behind a conformance\n * flag. The second series missed the *per-shard* hashing, which was still\n * running. With all of it gated the worst source moved from `muxBatch` to\n * `audioBlock`: the mux beats on its own sample batches, and once the hashing\n * that was reading them stopped, the audio mix became the quietest stage again.\n *\n * ## The headroom here is small, and that is the finding\n *\n * 3.4× the worst gap and 3.5 s before the platform leaves roughly one doubling\n * of the worst legitimate silence before the two constraints meet. On a slower\n * encoder there may be no gap between them at all, which is why\n * `Media3ExporterOptions.stallGraceMs` exists.\n *\n * Any of this was only measurable after the silence was instrumented rather\n * than tolerated. Before, the maximum gap was 5.4 s and no useful threshold\n * existed between it and the platform's timeout; the gap turned out to be the\n * Sonic time-stretch and AAC encode inside the audio mix, which at 0.1× turn one\n * second of source into ten with nothing beating.\n *\n * @experimental\n */\nexport const STALL_GRACE_MS = 7_000\n\n/** The measured maximum, kept beside the threshold it justifies. @experimental */\nexport const MAX_OBSERVED_HEARTBEAT_GAP_MS = 2_046\n\n/** A unit of work, in the same currency as the weights. */\nconst AUDIO_UNIT = 1 / 512\nconst MUX_UNITS = 24\nconst PUBLISH_UNITS = 8\n\n/** @experimental */\nexport function planProgressWeights(input: ProgressPlanInput): ProgressPlan {\n const shardUnits = input.shards.map((s) => Math.max(0, s.frames))\n const audioUnits = input.audioSamples > 0 ? input.audioSamples * AUDIO_UNIT : 0\n const total =\n shardUnits.reduce((a, b) => a + b, 0) + audioUnits + MUX_UNITS + PUBLISH_UNITS\n // The fixed mux and publish units keep this above zero, so an empty timeline\n // weighs its publication rather than dividing by nothing.\n return {\n shardWeights: shardUnits.map((u) => u / total),\n audioWeight: audioUnits / total,\n muxWeight: MUX_UNITS / total,\n publishWeight: PUBLISH_UNITS / total,\n durationUs: Math.max(0, input.durationUs),\n shardFrames: shardUnits,\n audioSamples: Math.max(0, input.audioSamples),\n }\n}\n\n/** @experimental */\nexport interface ProgressReporterOptions {\n plan: ProgressPlan\n durationUs: number\n onProgress?: (progress: VideoExportProgress) => void\n /** Bridge crossings are bounded to roughly this spacing. */\n throttleMs?: number\n now?: () => number\n}\n\n/** @experimental */\nexport interface ProgressReporter {\n /** Emit 0, once, when work begins. */\n begin(): void\n /** Report a shard's completed output frames. */\n shard(index: number, framesDone: number): void\n /** Report mixed audio samples. */\n audio(samplesDone: number): void\n /** Concatenation and mux finished. Not 1. */\n muxed(): void\n /** The destination is complete and readable. Emits 1, once. */\n complete(): void\n /**\n * Apply several updates and emit at most once.\n *\n * A poll reads every shard, the audio and the mux together, and without this\n * the first of those spends the throttle slot and the rest are dropped until\n * the next tick — so the last shard's frames and the whole audio mix could\n * arrive a tick late or, at the end of an export, never. One read of native\n * state is one update to the bar.\n */\n batch(body: () => void): void\n /** Deliver nothing further. */\n stop(): void\n}\n\n/** @experimental */\nexport function createProgressReporter(options: ProgressReporterOptions): ProgressReporter {\n const now = options.now ?? (() => Date.now())\n const throttleMs = options.throttleMs ?? 120\n const plan = options.plan\n const shardDone = plan.shardWeights.map(() => 0)\n let audioDone = 0\n let muxDone = 0\n let publishDone = 0\n let highest = 0\n let began = false\n let finished = false\n let stopped = false\n let lastEmitMs = Number.NEGATIVE_INFINITY\n let batching = 0\n let batchForce = false\n\n const fraction = () => {\n let sum = 0\n for (let i = 0; i < shardDone.length; i += 1) {\n sum += plan.shardWeights[i]! * shardDone[i]!\n }\n return sum + plan.audioWeight * audioDone + plan.muxWeight * muxDone + plan.publishWeight * publishDone\n }\n\n const emit = (value: number, force: boolean) => {\n /*\n Nothing crosses before the export is declared started.\n\n Everything that can refuse an export runs before `begin`, and a bar that\n appears and then vanishes because a photo clip was rejected claims work\n that never happened.\n */\n if (stopped || !began) return\n if (batching > 0) {\n // Held until the batch closes. `force` survives, because a completion\n // inside a batch is still a completion.\n batchForce = batchForce || force\n return\n }\n /*\n Monotonic by construction.\n\n A native counter that restarts — a retried shard, a stage re-entered —\n must not drag the bar backwards, so what is reported is the high-water\n mark rather than the latest computation.\n */\n const next = Math.max(highest, Math.min(1, value))\n const at = now()\n if (!force && at - lastEmitMs < throttleMs) return\n /*\n A repeat is not an update.\n\n Polling reports the same fraction every tick while a shard encodes, and\n delivering it is a bridge crossing that tells a host nothing — measured on\n device, a one-second export spent three of its ten callbacks re-sending 0.\n Liveness is the heartbeat's job, and it does not go through here.\n */\n if (!force && began && next === highest) return\n highest = next\n lastEmitMs = at\n /*\n A host's callback is not allowed to end the export.\n\n It runs on the caller's side and can throw for reasons that have nothing\n to do with encoding — a render error, a disposed component. Aborting a\n working encode because a progress indicator failed would be the wrong\n trade in every case.\n */\n try {\n options.onProgress?.({\n progress: highest,\n time: Math.min(plan.durationUs, Math.round(plan.durationUs * highest)),\n })\n } catch {\n // Deliberately swallowed; see above.\n }\n }\n\n return {\n begin() {\n if (began || stopped) return\n began = true\n emit(0, true)\n },\n shard(index, framesDone) {\n if (finished || stopped) return\n if (index < 0 || index >= shardDone.length) return\n const total = Math.max(1, plan.shardFrames[index] ?? 1)\n shardDone[index] = Math.max(shardDone[index]!, Math.min(1, framesDone / total))\n emit(fraction(), false)\n },\n audio(samplesDone) {\n if (finished || stopped) return\n const total = Math.max(1, plan.audioSamples)\n audioDone = Math.max(audioDone, Math.min(1, samplesDone / total))\n emit(fraction(), false)\n },\n muxed() {\n if (finished || stopped) return\n muxDone = 1\n emit(fraction(), true)\n },\n complete() {\n if (finished || stopped) return\n finished = true\n publishDone = 1\n emit(1, true)\n },\n batch(body) {\n batching += 1\n try {\n body()\n } finally {\n batching -= 1\n }\n const force = batchForce\n batchForce = false\n emit(finished ? 1 : fraction(), force)\n },\n stop() {\n stopped = true\n },\n }\n}\n\n/** What a wedged export looked like when it was noticed. @experimental */\nexport interface StallVerdict {\n stage: ExportStage | string\n units: number\n idleMs: number\n}\n\n/** @experimental */\nexport interface StallWatchdogOptions {\n thresholdMs?: number\n}\n\n/** Where a heartbeat came from, so a gap can be attributed. @experimental */\nexport const HEARTBEAT_SOURCES = [\n 'videoFrame',\n 'cadence',\n 'audioBlock',\n 'muxBatch',\n 'publishBytes',\n] as const\n\n/** @experimental */\nexport type HeartbeatSource = (typeof HEARTBEAT_SOURCES)[number]\n\n/** @experimental */\nexport interface StallWatchdog {\n /**\n * Record a heartbeat. Any beat counts, whether or not it moved the bar.\n *\n * `units` is carried for the report only — a repeated count is still a\n * heartbeat, because the pipeline spoke.\n */\n advanced(stage: ExportStage | string, units: number, atMs: number): void\n /** The verdict if nothing has reported for the threshold, else `null`. */\n stalledFor(atMs: number): StallVerdict | null\n}\n\n/**\n * Slow is not stuck.\n *\n * Keyed on *any* report rather than on elapsed time per stage: a 0.1× export\n * spends minutes inside one shard and is perfectly healthy while doing it, and\n * a stage timeout would either fire on that or be too long to catch anything.\n * What distinguishes a wedge is silence.\n *\n * Fires once. A wedged export is one event, and the cleanup it triggers is the\n * same path cancellation already proved.\n *\n * @experimental\n */\nexport function createStallWatchdog(options: StallWatchdogOptions = {}): StallWatchdog {\n const thresholdMs = options.thresholdMs ?? STALL_GRACE_MS\n let lastStage: ExportStage | string | null = null\n let lastUnits = 0\n let lastAtMs = 0\n let fired = false\n\n return {\n advanced(stage, units, atMs) {\n lastStage = stage\n lastUnits = units\n lastAtMs = atMs\n },\n stalledFor(atMs) {\n // Nothing has started, so nothing is stalled.\n if (fired || lastStage === null) return null\n const idleMs = atMs - lastAtMs\n if (idleMs < thresholdMs) return null\n fired = true\n return { stage: lastStage, units: lastUnits, idleMs }\n },\n }\n}\n","import type { VideoExportProgress } from '@pictor-sdk/video-core'\nimport type { ExportStage } from './exportJob'\nimport {\n STALL_GRACE_MS,\n createProgressReporter,\n createStallWatchdog,\n type ProgressPlan,\n type StallVerdict,\n} from './exportProgress'\n\n/**\n * Reading native counters, and turning them into a progress stream.\n *\n * The native run is one call that resolves when the whole export is done, so\n * there is nothing to subscribe to: progress has to be *read*. That is the whole\n * reason this exists as a pump rather than an event stream, and it is also why\n * the two signals stay separate on either side of the boundary.\n *\n * **Natively, nothing is throttled.** Every emitted frame, every block of mixed\n * audio and every muxed sample batch increments a counter or beats, at whatever\n * rate the encoder runs. Throttling there would be throttling the evidence.\n *\n * **Across the bridge, everything is throttled.** This polls at 8 Hz and the\n * reporter emits at most that often, because nobody can read a progress bar\n * faster and every update costs a crossing. A 0.1× export that beats a thousand\n * times a second still produces eight callbacks.\n *\n * The watchdog reads the *beat total* rather than the progress fraction. A long\n * mux moves the bar by almost nothing while beating steadily, and treating that\n * as a stall would kill a healthy export — which is the failure mode the\n * separation exists to prevent.\n *\n * @experimental\n */\n\n/** What native reports about work completed so far. @experimental */\nexport interface ProgressSnapshot {\n /** Output frames written, per shard index. */\n shards: readonly number[]\n /** Mixed audio samples handed to the encoder. */\n audioSamples: number\n /** What `audioSamples` counts up to. Zero before the mix starts. */\n audioPlanned: number\n /** Concatenation and mux finished. */\n muxed: boolean\n /**\n * Every heartbeat from every source, summed.\n *\n * Liveness only. It is deliberately not a work total — a mux batch and an\n * output frame are not the same amount of anything — so it feeds the watchdog\n * and never the bar.\n */\n beats: number\n}\n\n/** @experimental */\nexport interface ProgressPumpOptions {\n plan: ProgressPlan\n durationUs: number\n /** Read the native counters. Called on every tick. */\n read(): ProgressSnapshot\n onProgress?: (progress: VideoExportProgress) => void\n /** Called once, when nothing has reported for the threshold. */\n onStall(verdict: StallVerdict): void\n /** Bridge delivery rate. 125 ms is 8 Hz. */\n pollMs?: number\n thresholdMs?: number\n now?: () => number\n}\n\n/** @experimental */\nexport interface ProgressPump {\n /** Emit `{ progress: 0, time: 0 }`, once. */\n begin(): void\n /** Read native once and report what changed. */\n tick(): void\n /**\n * The destination is closed and verified readable. Emits 1, once.\n *\n * Separate from the mux on purpose: a bar that fills when the encoder stops\n * produces a \"done\" over a file that cannot yet be opened.\n */\n complete(): void\n /** Deliver nothing further, whatever arrives. */\n stop(): void\n /** How often {@link tick} expects to be called. */\n readonly pollMs: number\n}\n\n/**\n * Where the export was when it went quiet.\n *\n * Derived from what has finished rather than from a native stage string, so a\n * stall report cannot claim a stage the counters contradict.\n */\nfunction stageOf(snapshot: ProgressSnapshot, plan: ProgressPlan): ExportStage {\n if (snapshot.muxed) return 'publishing'\n const shardsDone = plan.shardFrames.every(\n (total, index) => (snapshot.shards[index] ?? 0) >= total,\n )\n if (!shardsDone) return 'shards'\n if (snapshot.audioPlanned > 0 && snapshot.audioSamples < snapshot.audioPlanned) return 'audio'\n return 'concatenating'\n}\n\n/** @experimental */\nexport function createProgressPump(options: ProgressPumpOptions): ProgressPump {\n const now = options.now ?? (() => Date.now())\n const pollMs = options.pollMs ?? 125\n const plan = options.plan\n const reporter = createProgressReporter({\n plan,\n durationUs: options.durationUs,\n onProgress: options.onProgress,\n // The reporter's own throttle is the poll interval: a tick that finds\n // nothing new should not be able to spend a crossing saying so.\n throttleMs: pollMs,\n now,\n })\n const watchdog = createStallWatchdog({ thresholdMs: options.thresholdMs ?? STALL_GRACE_MS })\n let lastBeats = -1\n let stopped = false\n let stalled = false\n\n return {\n pollMs,\n\n begin() {\n reporter.begin()\n // Seeded here rather than on the first tick. Without it the threshold\n // would be measured from whenever polling happened to start, which for a\n // slow first shard is a different moment from when work began.\n watchdog.advanced('resolving', 0, now())\n },\n\n tick() {\n if (stopped || stalled) return\n const snapshot = options.read()\n /*\n One read of native state is one update to the bar.\n\n Without the batch the first shard spends the throttle slot and the rest\n — including the whole audio mix — are dropped until the next tick, so\n the last thing to change before an export ends could never be delivered\n at all.\n */\n reporter.batch(() => {\n for (let i = 0; i < plan.shardFrames.length; i += 1) {\n reporter.shard(i, snapshot.shards[i] ?? 0)\n }\n if (snapshot.audioPlanned > 0 && plan.audioSamples > 0) {\n /*\n Rescaled rather than counted twice.\n\n Native counts interleaved samples at its own rate and channel count;\n the plan counts what the compiler predicted. Passing native's number\n straight through would weight audio by whichever of the two happened\n to be larger, so what crosses is the fraction.\n */\n const fraction = Math.min(1, snapshot.audioSamples / snapshot.audioPlanned)\n reporter.audio(plan.audioSamples * fraction)\n }\n if (snapshot.muxed) reporter.muxed()\n })\n\n const at = now()\n if (snapshot.beats !== lastBeats) {\n lastBeats = snapshot.beats\n watchdog.advanced(stageOf(snapshot, plan), snapshot.beats, at)\n }\n const verdict = watchdog.stalledFor(at)\n if (verdict) {\n // Once. The export is torn down from here and there is nothing further\n // to observe; continuing to poll a dead run would report it again.\n stalled = true\n options.onStall(verdict)\n }\n },\n\n complete() {\n if (stopped) return\n reporter.complete()\n },\n\n stop() {\n stopped = true\n reporter.stop()\n },\n }\n}\n","import { createExportJobs } from './exportJob'\nimport { resolveMixSources } from './mixSources'\nimport { describeTarget, resolveDestination } from './exportDestination'\nimport { planProgressWeights } from './exportProgress'\nimport { createProgressPump } from './exportProgressPump'\nimport {\n VideoExportCancelled,\n VideoExportFailed,\n VideoExportStalled,\n compileAudioMixProgram,\n compileVideoSceneProgram,\n isPlayableWindow,\n layout,\n planClipShards,\n projectDuration,\n validateAudioMixProgram,\n validateProject,\n VIDEO_SCENE_PROGRAM_VERSION,\n type ExportSourceContext,\n type MediaSource,\n type ResampleRequest,\n type VideoExportProgress,\n type VideoExportSpec,\n type VideoExporter,\n type VideoProject,\n} from '@pictor-sdk/video-core'\nimport NativePictorVideoModule from './NativePictorVideo'\n\n/** The turbo module, or a failure that says so rather than a null dereference. */\nfunction nativeModule(): NonNullable<typeof NativePictorVideoModule> {\n if (!NativePictorVideoModule) {\n throw new VideoExportRejected(['the Pictor video native module is not installed'])\n }\n return NativePictorVideoModule\n}\n\n/**\n * The Media3 exporter.\n *\n * Honours `VideoExporter` in full: asynchronous, cancellable while it runs,\n * monotonic progress, temporary and file destinations, one export at a time,\n * typed refusal, cancellation, stall and failure, and deterministic cleanup on\n * every one of those paths.\n *\n * Nothing constructs it for you. It is not on `VideoRuntime`, not behind the\n * editor's Next button, and not reachable from `@pictor-sdk/video-ui` — a host\n * builds it explicitly, which is what keeps the surface reviewable. Wiring it\n * to a button is Slice 5's job, not this file's.\n *\n * ## The gate boundary\n *\n * Nothing here reaches a `exportGate*` method. Provenance hashing, fault\n * injection, artificial holds and test beats live behind\n * `exportGateEnableDiagnostics`, which this never calls, and which leaves them\n * inert even when a payload asks for them.\n *\n * ## The whole project is interpreted here, in TypeScript\n *\n * `planClipShards` and `compileVideoSceneProgram` run once, sources are resolved\n * once, overlays are rasterised once, and what crosses to native is a list of\n * shards, a compiled program and a resource manifest — all JSON and file paths.\n * Native never sees a `VideoProject`, a `VideoClip`, a trim, a speed or a\n * transform field. It looks up segments by timestamp and draws what they say.\n *\n * ## It refuses rather than approximating\n *\n * Every clip kind, spec field and overlay state this slice does not implement is\n * rejected by name before anything is encoded. A photo clip silently skipped, or\n * a bitrate silently ignored, produces a file that is plausible and wrong — and\n * a plausible wrong file is worse than no file, because nothing downstream can\n * tell it apart from a correct one.\n *\n * @experimental\n */\n\n/**\n * An overlay raster, described structurally.\n *\n * Deliberately not imported from `@pictor-sdk/video-skia`. The adapter has no\n * opinion about how an overlay is rendered — that is the whole point of the\n * boundary — and importing the renderer would drag Skia into a package whose\n * job is to talk to Media3. What it needs is the shape of a descriptor, which\n * is JSON.\n *\n * @experimental\n */\nexport interface OverlayResourceDescriptor {\n overlayId: string\n resourceId: string\n size: { width: number; height: number }\n rasterBounds: { x: number; y: number; width: number; height: number }\n rasterScale: number\n rasterWidth: number\n rasterHeight: number\n format: string\n premultiplied: boolean\n colorSpace: string\n file: string\n byteLength: number\n checksum: number\n}\n\n/** What preparing a program's overlays produced. @experimental */\nexport interface OverlayPreparationResult {\n resources: readonly OverlayResourceDescriptor[]\n /** Still resolving. An export must not proceed and omit them. */\n pending: readonly string[]\n /** Cannot be exported at all. */\n invalid: readonly { overlayId: string; kind: string; reason: string }[]\n}\n\n/** Everything the host must supply. @experimental */\nexport interface Media3ExporterOptions {\n /**\n * Turn an opaque `MediaSource` into something Android can open.\n *\n * The exporter never inspects a descriptor: `MediaSource` is `JsonValue` and\n * only the host knows whether it is an asset id, a content URI or a row in a\n * database. Returning `null` fails the export naming the clip or the span.\n *\n * `context` says which part of the project is being resolved. It used to be\n * the clip, which left the audio path with nothing honest to pass: every\n * span, project music included, was resolved with the *first video clip* as\n * context. A resolver that keys on the clip returned that clip's media for\n * the whole timeline's audio — a plausible file with the wrong sound in it.\n */\n resolveSource(source: MediaSource, context: ExportSourceContext): string | null\n /**\n * Rasterise every overlay the compiled program names.\n *\n * The host wires its registry, its prepared resources and its raster sink\n * behind this — typically `prepareProgramOverlayExports` from\n * `@pictor-sdk/video-skia`. Injecting the whole step rather than its three inputs\n * is what keeps this package free of a renderer dependency, and it is the same\n * reason `resolveSource` is injected rather than a media library being.\n *\n * Omitted means \"no overlays are exportable\": a project that has any then\n * fails naming each one, rather than exporting without them.\n */\n prepareOverlays?(\n program: { segments: readonly { commands: readonly unknown[] }[] },\n projectSize: { width: number; height: number },\n rasterScale: number,\n ): OverlayPreparationResult\n /**\n * Called after an export finishes, successfully or not.\n *\n * The sidecars belong to the host's raster sink, so the host removes them.\n * Passing the list rather than the sink keeps this from having to know how\n * they were stored.\n */\n releaseResources?(files: readonly string[]): void\n /**\n * How long without any heartbeat counts as wedged.\n *\n * Defaults to {@link STALL_GRACE_MS}, which is **Pixel 7 evidence**: the worst\n * legitimate silence measured across seven production-path cases on that\n * device, with margin. It is not a claim about Android. A slower encoder, a\n * thermally throttled phone or a codec that batches differently can produce a\n * longer honest gap, and the number to change is this one — raising it costs\n * only how late a genuine wedge is noticed, while leaving it too low kills\n * healthy exports.\n */\n stallGraceMs?: number\n /**\n * How often native counters are read, and progress delivered.\n *\n * Defaults to 125 ms — 8 Hz, inside the 5–10 Hz a person can actually read.\n * Native counting is unthrottled regardless; this is only the bridge rate.\n */\n progressPollMs?: number\n /**\n * Which mechanism re-times a clip's **video** when its speed is not 1.\n *\n * `'cadence'` (the default, and what has shipped) compiles an exact emit plan\n * in `resampleClip` and a GL effect drops or repeats frames to match it. The\n * plan's length is the clip's length by construction, so project time is\n * exact — and above 1× the decoder still reads every source frame, which is\n * what produces a burst of output followed immediately by end-of-stream.\n *\n * `'media3'` sends no plan, so no cadence effect is built and\n * `EditedMediaItem.setSpeed` carries the video timing on its own.\n *\n * Exposed because the burst is implicated in a vendor encoder that drops its\n * output end-of-stream at high rates — see\n * `docs/research/export-speed-stall.md`. It is a comparison switch, not a\n * recommendation: the two paths select different frames, and which one is\n * correct is what the comparison is for.\n *\n * @experimental\n */\n videoSpeedVia?: 'cadence' | 'media3'\n}\n\n/** Why an export will not start. @experimental */\nexport class VideoExportRejected extends Error {\n readonly reasons: readonly string[]\n constructor(reasons: readonly string[]) {\n super(`export rejected: ${reasons.join('; ')}`)\n this.name = 'VideoExportRejected'\n this.reasons = reasons\n }\n}\n\n/** The frame rate an export runs at when the spec does not say. */\nconst DEFAULT_FPS = 30\n\n\n/**\n * What this slice cannot encode, stated rather than skipped.\n *\n * Each of these has a milestone. Until then the export fails naming the clip or\n * the field, because the alternative is a file that looks finished.\n */\nfunction describeUnsupported(project: VideoProject, spec: VideoExportSpec): string[] {\n const reasons: string[] = []\n\n for (const clip of project.clips) {\n if (clip.kind === 'photo') {\n reasons.push(`clip '${clip.id}' is a photo, which this exporter does not encode yet`)\n continue\n }\n }\n\n if (spec.format !== undefined && spec.format !== 'mp4') {\n reasons.push(`format '${spec.format}' is not supported; this exporter writes mp4`)\n }\n if (spec.scale !== undefined && spec.scale !== 1) {\n // Honoured or rejected, never quietly ignored.\n reasons.push(`scale ${spec.scale} is not supported yet; only 1 is`)\n }\n if (spec.fps !== undefined && spec.fps !== DEFAULT_FPS) {\n reasons.push(`fps ${spec.fps} is not supported yet; only ${DEFAULT_FPS} is`)\n }\n if (spec.bitrate !== undefined) {\n reasons.push('bitrate is not supported yet; the encoder default is used')\n }\n return reasons\n}\n\n/**\n * Build an exporter.\n *\n * @experimental\n */\nexport function createMedia3VideoExporter(options: Media3ExporterOptions): VideoExporter {\n /*\n One job at a time, cancellable at every stage.\n\n The registry owns identity, exclusivity and settling exactly once; the body\n below only has to call `job.checkpoint` where stopping is safe. See\n `exportJob.ts` for why cancellation cannot simply reject the promise.\n */\n const jobs = createExportJobs({\n cancelNative: (jobId) => {\n nativeModule().exportCancelJob(jobId)\n },\n })\n\n return {\n async export(\n project: VideoProject,\n spec: VideoExportSpec,\n onProgress?: (progress: VideoExportProgress) => void,\n ) {\n return jobs.run(async (job) => {\n // First thing, so a cancel pressed before any work is observed before\n // the compile, the overlay pass and the source resolution rather than\n // after them. Measured: without it a pre-native cancel took 4.7 s to\n // settle, because the first checkpoint was past all of that.\n job.checkpoint('resolving')\n const issues = validateProject(project)\n if (issues.length > 0) {\n throw new VideoExportRejected(issues.map((issue) => issue.message))\n }\n const unsupported = describeUnsupported(project, spec)\n if (unsupported.length > 0) throw new VideoExportRejected(unsupported)\n\n /*\n Refused, never redirected.\n\n Falling back to a temporary would produce an export that reports success\n while the file is nowhere the caller asked for — and nothing downstream\n can tell that apart from having worked.\n */\n const decision = resolveDestination(spec.destination)\n if (decision.destination === null) throw new VideoExportRejected(decision.reasons)\n const destination = decision.destination\n\n const playable = project.clips.filter((clip) => isPlayableWindow(clip))\n if (playable.length === 0) throw new VideoExportRejected(['the project has no playable clips'])\n if (!(projectDuration(project) > 0)) {\n throw new VideoExportRejected(['the project has no duration'])\n }\n\n // Resolved without inspecting the descriptor: only the host knows what a\n // `MediaSource` is, and this must not learn.\n const uris: string[] = []\n const unresolved: string[] = []\n for (const clip of playable) {\n const uri = options.resolveSource(clip.source, { kind: 'clip', clipId: clip.id })\n if (typeof uri !== 'string' || uri.length === 0) {\n unresolved.push(`clip '${clip.id}' could not be resolved to a readable source`)\n continue\n }\n uris.push(uri)\n }\n if (unresolved.length > 0) throw new VideoExportRejected(unresolved)\n\n const fps = DEFAULT_FPS\n const placements = layout(project)\n const requests: ResampleRequest[] = playable.map((clip) => ({\n sourceStartUs: clip.sourceIn,\n sourceEndUs: clip.sourceOut,\n speed: clip.speed,\n sourceFps: clip.metadata.fps ?? fps,\n targetFps: fps,\n }))\n const plan = planClipShards(requests, fps)\n const program = compileVideoSceneProgram(project)\n if (program.segments.length === 0) {\n throw new VideoExportRejected(['the compiled program has no segments'])\n }\n void placements\n\n /*\n Overlays are rasterised and checked **before** any native work.\n\n A resource discovered missing once encoding has started leaves a partial\n file, and the frames already written are silently missing something the\n timeline said was on screen.\n */\n const named = new Set<string>()\n for (const segment of program.segments) {\n for (const command of segment.commands) {\n if (command.op === 'overlay') named.add(command.overlayId)\n }\n }\n const overlayPlan: OverlayPreparationResult = options.prepareOverlays\n ? options.prepareOverlays(program, project.size, 1)\n : {\n resources: [],\n pending: [],\n invalid: [...named].map((overlayId) => ({\n overlayId,\n kind: 'unknown',\n reason: 'no prepareOverlays was supplied, so this overlay has no portable form',\n })),\n }\n // Before the plan is judged: a cancel raised from inside the host's\n // preparation must be reported as `overlays`, not as whatever the\n // validation below happens to say first.\n job.checkpoint('overlays')\n\n /*\n Anything already staged is released on every failure path below.\n\n A rejection after rasterising leaves sidecars nothing else will come back\n for, and the run that failed is exactly the one nobody cleans up after.\n */\n const stagedNow = overlayPlan.resources.map((resource) => resource.file)\n if (overlayPlan.pending.length > 0) {\n options.releaseResources?.(stagedNow)\n throw new VideoExportRejected(\n overlayPlan.pending.map(\n (id: string) => `overlay '${id}' is still preparing; export would omit it`,\n ),\n )\n }\n if (overlayPlan.invalid.length > 0) {\n options.releaseResources?.(stagedNow)\n throw new VideoExportRejected(\n overlayPlan.invalid.map(\n (bad: { overlayId: string; kind: string; reason: string }) =>\n `overlay '${bad.overlayId}' (${bad.kind}): ${bad.reason}`,\n ),\n )\n }\n const resources = overlayPlan.resources\n\n /*\n Whether a re-timed clip gets an emit plan at all.\n\n Omitting it is how the cadence effect is left out: native builds one\n only when both arrays arrive, so there is no second switch to keep in\n step. A clip at 1× has nothing to re-time either way.\n */\n const viaMedia3 = options.videoSpeedVia === 'media3'\n const shards = plan.map((shard, index) => {\n const clip = playable[index]!\n const cadence =\n viaMedia3 && clip.speed !== 1 ? null : cadenceFor(shard.plan.frames)\n return {\n uri: uris[index]!,\n startMs: Math.round(clip.sourceIn / 1000),\n endMs: Math.round(clip.sourceOut / 1000),\n speed: clip.speed,\n timelineOffsetUs: shard.timelineOffsetUs,\n /*\n The shard's own audio is dropped and replaced with generated silence.\n\n Not because anything wants that silence — the final mux takes only\n the video track from this pipeline — but because Media3 drives its\n speed path through audio, and a video-only item at any rate other\n than 1 stalls with \"no output sample written\". So the track exists to\n satisfy the re-timing and is discarded.\n\n What matters is that the *source's* audio never enters this pipeline:\n a per-shard audio track has to be re-encoded to concatenate, which is\n what put a second generation on everything the user hears.\n\n `ExportGate` notes that `setRemoveAudio` beside a speed change is a\n combination Media3 warns about, and that was tried as a candidate\n fix for the high-rate export stall: giving the re-timing the real\n stream instead of silence. It made no difference to the fault — the\n encoder stopped at exactly the same place — so the change was\n reverted rather than kept for a benefit that could not be shown.\n See `docs/research/export-speed-stall.md`.\n */\n forceAudioTrack: true,\n removeAudio: true,\n emitsPerInput: cadence?.emitsPerInput,\n outputPtsUs: cadence?.outputPtsUs,\n spanUs: Math.round((shard.plan.frameCount * 1_000_000) / fps),\n }\n })\n\n /*\n The whole timeline's audio, compiled once and resolved once.\n\n `compileAudioMixProgram` settles placements, trims, gains and fades; the\n backend mixes and encodes exactly what it says. Sources are resolved by\n span id through the same host resolver the video pipeline uses, and a\n silent span needs none.\n */\n const mixProgram = compileAudioMixProgram(project)\n const mixProblems = validateAudioMixProgram(mixProgram)\n if (mixProblems.length > 0) throw new VideoExportRejected(mixProblems)\n /*\n Resolved as what each span actually is, not as whichever clip came\n first — and by the same function preview preparation calls, so the two\n cannot disagree about which file a span means. A disagreement there\n would be audible only in the finished video.\n */\n const resolved = resolveMixSources(project, mixProgram, options.resolveSource)\n const mixUris = resolved.uris\n if (resolved.unresolved.length > 0) throw new VideoExportRejected(resolved.unresolved)\n job.checkpoint('resolving')\n\n job.checkpoint('overlays')\n const staged = resources.map((resource) => resource.file)\n\n /*\n Progress starts here, after everything that can refuse.\n\n A bar that appears and then vanishes because a photo clip was rejected\n is worse than one that never appeared: the export never began, and\n showing 0% claims otherwise. Everything above this line either throws or\n establishes the plan the weights are computed from.\n */\n const durationUs = projectDuration(project)\n const progressPlan = planProgressWeights({\n shards: plan.map((shard) => ({ frames: shard.plan.frameCount })),\n audioSamples: mixProgram.spans.some((span) => !span.silent)\n ? Math.max(0, mixProgram.durationUs)\n : 0,\n durationUs,\n })\n // A box rather than a `let`: the assignment happens in a callback, and\n // TypeScript narrows a closed-over `let` to `never` at the read.\n const stalled: { at: { stage: string; idleMs: number } | null } = { at: null }\n const pump = createProgressPump({\n plan: progressPlan,\n durationUs,\n read: () => JSON.parse(nativeModule().exportProgressSnapshot()) as never,\n // Delivered through the job, so nothing arrives after settlement: a\n // host that has already shown a result must not be told 90%.\n onProgress: onProgress\n ? (value) => { job.progress(value.progress, () => onProgress(value)) }\n : undefined,\n onStall: (verdict) => {\n stalled.at = { stage: String(verdict.stage), idleMs: verdict.idleMs }\n /*\n Torn down through the path cancellation already proved.\n\n A wedged export holds a Transformer, a muxer and its codecs, and the\n only mechanism in this process that reliably lets go of all three is\n the native cancel. What differs is what the caller is told: nobody\n asked to stop, so this settles as `VideoExportStalled`.\n */\n nativeModule().exportCancelJob(job.id)\n },\n thresholdMs: options.stallGraceMs,\n pollMs: options.progressPollMs,\n })\n pump.begin()\n const ticker = setInterval(() => { pump.tick() }, pump.pollMs)\n\n let outcome: { ok: boolean; uri: string; error: string }\n try {\n outcome = JSON.parse(\n await nativeModule().exportRunProjectAsync(\n job.id,\n JSON.stringify(shards),\n JSON.stringify({\n version: VIDEO_SCENE_PROGRAM_VERSION,\n projectSize: { width: project.size.width, height: project.size.height },\n durationUs: program.durationUs,\n segments: program.segments,\n }),\n JSON.stringify(resources),\n JSON.stringify(mixProgram),\n JSON.stringify(mixUris),\n /* timeoutSeconds= */ 600,\n destination.kind === 'file'\n ? JSON.stringify({\n kind: 'file',\n path: destination.path,\n overwrite: destination.overwrite,\n })\n : '',\n ),\n ) as { ok: boolean; uri: string; error: string }\n } finally {\n clearInterval(ticker)\n // Every path, including the one that threw: the sidecars are this\n // export's and nothing else will come back for them.\n options.releaseResources?.(staged)\n }\n\n /*\n A stall outranks whatever native said on the way out.\n\n Tearing down a wedged Transformer reports itself as a cancellation,\n because a cancellation is how it was torn down. Reporting that would\n tell a host the user pressed stop when nothing of the sort happened.\n */\n const wedged = stalled.at\n if (wedged) {\n pump.stop()\n throw new VideoExportStalled(job.id, wedged.stage, wedged.idleMs)\n }\n\n /*\n A native cancellation is a cancellation, with the stage native recorded.\n\n Without this the job registry falls back to its own default and every\n cancellation reports the same stage, which is worse than no stage at\n all: it looks like information.\n */\n const cancelledDuring = /^cancelled during (\\w+)/.exec(outcome.error ?? '')\n if (!outcome.ok && cancelledDuring) {\n pump.stop()\n throw new VideoExportCancelled(job.id, cancelledDuring[1]!)\n }\n\n if (!outcome.ok) {\n /*\n No uri either way.\n\n A partial file is worse than none — it plays, it is the wrong length,\n and a caller that took the uri would have no way to know. Native\n deletes what it created on every one of its own failure paths; nothing\n is swept here, because a sweep would also take the temporary a\n *previous* export handed the caller.\n */\n pump.stop()\n const error = outcome.error || 'the export failed'\n if (isRefusal(error)) throw new VideoExportRejected([error])\n throw new VideoExportFailed(job.id, 'encoding', error)\n }\n job.checkpoint('publishing')\n\n /*\n One is the destination being readable, not the encoder stopping.\n\n The container is closed after the last sample is written, and a file\n that is closed is not necessarily one that parses. Probing it costs a\n `MediaExtractor` open and turns \"the encode finished\" into \"there is a\n file you can play\", which is the only claim a bar reaching 1 should be\n making.\n */\n const probe = JSON.parse(nativeModule().exportProbeOutput(outcome.uri)) as {\n readable: boolean\n error?: string\n tracks?: number\n }\n if (!probe.readable) {\n pump.stop()\n // Written and unopenable: the work happened and the platform produced\n // something unusable, which is a failure rather than a refusal.\n throw new VideoExportFailed(\n job.id,\n 'publishing',\n `${describeTarget(outcome.uri)} could not be read back: ${probe.error ?? 'no video track'}`,\n )\n }\n pump.complete()\n pump.stop()\n\n /*\n Ownership, stated in the type.\n\n A temporary is the adapter's until the caller says otherwise, so it\n carries the one call that disposes of it. A file destination is the\n caller's — they chose the path — so that branch has no `release` at all,\n rather than one that quietly does nothing. An optional cleanup method on\n a single shape would put \"delete the user's video\" one typo away from\n every success path.\n */\n if (destination.kind === 'file') return { kind: 'file', uri: outcome.uri }\n const temporaryUri = outcome.uri\n return {\n kind: 'temporary',\n uri: temporaryUri,\n release: async () => {\n // Idempotent, and scoped to this file: native refuses any path it did\n // not create, so a second call — or one after the cache was cleared —\n // is not an error.\n await Promise.resolve()\n nativeModule().exportReleaseTemporaryOutput(temporaryUri)\n },\n }\n })\n },\n\n /**\n * Ask the running export to stop.\n *\n * Idempotent, and a no-op when nothing is running. Reaches the native job\n * before the promise settles: a Transformer nobody is listening to still\n * encodes, still writes and still holds its codecs, so rejecting in\n * JavaScript alone would show a cancelled state over a running export.\n */\n cancel(): void {\n jobs.cancel()\n },\n }\n}\n\n/**\n * Refusal or failure — the distinction a caller acts on.\n *\n * A refusal means the project, the destination or a resource was not something\n * this adapter could encode, decided before or instead of doing work: change\n * the input. A failure means the work started and the platform gave up: retry,\n * check storage, or report a bug. Reporting one as the other sends a caller to\n * the wrong place, which is worse than an unhelpful message.\n *\n * Matched on the prefixes native produces rather than on a code, because the\n * native side reports strings — and the list is exhaustive over the reasons\n * `runProject` can refuse, so anything unrecognised is a failure. That default\n * is the safe one: a new encoder error misreported as \"unsupported\" tells a user\n * to edit a project that was fine.\n */\nconst REFUSAL_PATTERNS = [\n /^program rejected/,\n /^overlay resources? /,\n /^overlay resource /,\n /^no shards/,\n /already exists; pass overwrite/,\n /^the directory for /,\n /^the destination /,\n /cannot be written without overwrite/,\n /^the staged file could not be read back/,\n]\n\nfunction isRefusal(error: string): boolean {\n return REFUSAL_PATTERNS.some((pattern) => pattern.test(error))\n}\n\n\n/**\n * The cadence transport: copies owed per arriving input, and their timestamps.\n *\n * Straight from `resampleClip`'s plan. Native walks these arrays and computes no\n * grid arithmetic of its own.\n */\nfunction cadenceFor(\n frames: readonly { outputPtsUs: number; sourceFrameIndex: number }[],\n): { emitsPerInput: number[]; outputPtsUs: number[] } {\n const first = frames[0]!.sourceFrameIndex\n const last = frames[frames.length - 1]!.sourceFrameIndex\n const emitsPerInput: number[] = []\n for (let i = 0; i <= last - first; i += 1) emitsPerInput.push(0)\n const outputPtsUs: number[] = []\n for (const frame of frames) {\n const slot = frame.sourceFrameIndex - first\n emitsPerInput[slot] = (emitsPerInput[slot] ?? 0) + 1\n outputPtsUs.push(frame.outputPtsUs)\n }\n return { emitsPerInput, outputPtsUs }\n}\n","import type { MediaSaver, SavedMedia } from '@pictor-sdk/video-core'\nimport NativePictorVideoModule from './NativePictorVideo'\n\n/**\n * Saving a finished export into Android's video collection.\n *\n * A thin boundary. The insert-pending / stream / verify / publish sequence is\n * native, because it is where the file descriptors and the cancellation live;\n * this turns one JSON result into either a {@link SavedMedia} or a typed error,\n * and gives every save an id so a cancel reaches the right one.\n *\n * ## What a failure guarantees\n *\n * Nothing partial. Every unsuccessful native path — a short write, a full disk,\n * a cancellation, an unreadable result — deletes the pending row before\n * returning, and a pending row was never visible to the gallery in the first\n * place. So a failed save leaves the collection exactly as it was.\n *\n * ## What it never does\n *\n * Delete a published item. Releasing the export's temporary is a separate act\n * on a separate file, and a saved video belongs to the user from the moment it\n * is published.\n *\n * @experimental\n */\n\n/** Why a save did not happen. @experimental */\nexport class MediaSaveFailed extends Error {\n /** Safe to display: never a path, a stack, or a platform message. */\n readonly reason: string\n constructor(reason: string) {\n super(`save failed: ${reason}`)\n this.name = 'MediaSaveFailed'\n this.reason = reason\n }\n}\n\n/** A save the user asked to stop. @experimental */\nexport class MediaSaveCancelled extends Error {\n constructor() {\n super('save cancelled')\n this.name = 'MediaSaveCancelled'\n }\n}\n\n/**\n * Offer a saved video to the system share sheet.\n *\n * Only after a successful save, and only ever the durable `content:` uri. The\n * cache path the export produced is app-private: a receiver could not open it,\n * and handing one out is the mistake `FileUriExposedException` exists to catch.\n *\n * Sharing changes nothing. The same row is offered every time — no copy, no\n * rename, no re-publish, and nothing released.\n *\n * Returns `false` when the device has nothing that accepts a video, which is an\n * outcome rather than a failure: the saved video is untouched either way.\n *\n * @experimental\n */\nexport function shareSavedMedia(uri: string): { ok: boolean; noTargets: boolean; reason: string } {\n if (!NativePictorVideoModule) {\n return { ok: false, noTargets: false, reason: 'sharing is not available' }\n }\n if (!uri.startsWith('content://')) {\n /*\n Refused rather than attempted.\n\n The only uri worth sharing is the published one. A cache path reaching\n here would mean the save had not happened, and offering it would either\n throw on a modern target or hand a receiver something it cannot read.\n */\n return { ok: false, noTargets: false, reason: 'only a saved video can be shared' }\n }\n const result = JSON.parse(NativePictorVideoModule.mediaShare(uri)) as {\n ok?: boolean\n noTargets?: boolean\n error?: string\n }\n return {\n ok: result.ok === true,\n noTargets: result.noTargets === true,\n reason: result.error ?? '',\n }\n}\n\nlet counter = 0\n\n/** @experimental */\nexport function createMediaStoreSaver(): MediaSaver {\n /*\n The job whose cancel is live.\n\n Held so a cancel arriving after one save settled cannot stop the next: the\n id is checked natively, and clearing it here means a stray press reaches\n nothing at all.\n */\n let active: string | null = null\n\n const native = () => {\n if (!NativePictorVideoModule) {\n throw new MediaSaveFailed('the Pictor video native module is not installed')\n }\n return NativePictorVideoModule\n }\n\n return {\n async save(sourceUri: string): Promise<SavedMedia> {\n if (active !== null) {\n // Refused rather than queued: two saves of the same export would insert\n // two rows, and the user asked for one video.\n throw new MediaSaveFailed('a save is already running')\n }\n counter += 1\n const id = `save_${counter.toString(36)}`\n active = id\n let outcome: {\n ok: boolean\n uri?: string\n displayName?: string\n location?: string\n error?: string\n cancelled?: boolean\n }\n try {\n outcome = JSON.parse(await native().mediaSaveAsync(id, sourceUri))\n } finally {\n active = null\n }\n if (outcome.cancelled === true) throw new MediaSaveCancelled()\n if (!outcome.ok || typeof outcome.uri !== 'string') {\n throw new MediaSaveFailed(outcome.error ?? 'the file could not be saved')\n }\n return {\n uri: outcome.uri,\n displayName: outcome.displayName ?? '',\n location: outcome.location ?? '',\n }\n },\n\n cancel(): void {\n if (active === null) return\n native().mediaSaveCancel(active)\n },\n }\n}\n\n/**\n * Delete pending rows a killed process left behind.\n *\n * Worth calling once at startup. A pending row is invisible to the gallery and\n * still occupies storage, so nothing will ever prompt a user to clean one up.\n *\n * Scoped to this app's own still-pending rows under its own folder: a published\n * item is never a candidate, and neither is anything another app wrote.\n *\n * @experimental\n */\nexport function sweepPendingMediaSaves(): {\n swept: number\n inspected: number\n /** Pending rows seen and deliberately left alone. */\n skipped: number\n} {\n if (!NativePictorVideoModule) return { swept: 0, inspected: 0, skipped: 0 }\n const result = JSON.parse(NativePictorVideoModule.mediaSaveSweepPending()) as {\n swept?: number\n inspected?: number\n skipped?: number\n }\n return {\n swept: result.swept ?? 0,\n inspected: result.inspected ?? 0,\n skipped: result.skipped ?? 0,\n }\n}\n","import { requireNative } from './player'\n\n/**\n * Where an export's overlay rasters are written.\n *\n * ## Why the adapter owns this\n *\n * `OverlayResources.kt`, in this package, is what reads these files back and\n * checks their length and CRC. The writer belongs beside the reader: a host\n * that produced the bytes *and* chose where they lived would need a filesystem\n * dependency and a path convention, and the two ends of one format would be\n * maintained apart.\n *\n * It also keeps the promise the rest of the library makes. `@pictor-sdk/video-core`,\n * `@pictor-sdk/video-text` and the example app acquire no filesystem package for\n * this; they receive a structural sink and never learn a path.\n *\n * ## Why it is synchronous\n *\n * `OverlayRasterSink.stage` must return a descriptor before the renderer can\n * continue building its preparation, and React Native has no synchronous\n * filesystem. The bytes are written by a JSI host function, which runs on the\n * calling thread and returns a value like any other function — no bridge round\n * trip, no second event loop, no waiting on a promise that cannot be awaited.\n *\n * That is a real cost: the JS thread is blocked for the write. Measured on a\n * Pixel 7, release build, at roughly 5 ms per megabyte:\n *\n * | raster | bytes | stall |\n * |---------------|--------|----------|\n * | 671 x 114 | 299 KB | 2 ms |\n * | 1080 x 400 | 1.7 MB | 7 ms |\n * | 1080 x 1920 | 8.1 MB | 36–48 ms |\n * | 3240 x 5760 | 71 MB | 357 ms |\n *\n * The first three are the operating point: a raster covers the overlay's own\n * bounds at export scale, not the project, and the two overlays in the Text\n * acceptance document staged 426 KB and 306 KB. A full-bleed overlay in a\n * 1080x1920 export is the realistic worst case at ~40 ms. The last row is the\n * pessimistic bound — a project-sized raster at 3x — and it is what the note\n * below is about.\n *\n * All of it is paid once per overlay during preparation, before the export\n * begins and before there is a progress bar to stutter. It is not a per-frame\n * cost. So the synchronous contract stands at these sizes; see\n * `measureStagingStall` for when it should not.\n *\n * @experimental\n */\n\n/** What native returns for one staged raster. */\ninterface StagedDescriptor {\n /** Opaque ownership handle. The only thing that may be released. */\n token: string\n /** Where it went. Read by the export reader; not shown to anyone. */\n file: string\n byteLength: number\n checksum: number\n}\n\ninterface StagingHost {\n stage(bytes: ArrayBuffer, width: number, height: number): StagedDescriptor\n release(token: string): boolean\n releaseAll(): number\n sweep(): number\n ledger(): { created: number; released: number; live: number }\n}\n\ndeclare const global: { __pictorOverlayStaging?: StagingHost }\n\nlet installed = false\n\n/**\n * Install the staging primitive, once.\n *\n * Idempotent, and safe to call before an export rather than at module load: a\n * host that never exports an overlay never pays for it.\n */\nexport function installOverlayStaging(): boolean {\n if (installed && global.__pictorOverlayStaging !== undefined) return true\n installed = requireNative().installOverlayStaging()\n return installed && global.__pictorOverlayStaging !== undefined\n}\n\nfunction host(): StagingHost {\n const h = global.__pictorOverlayStaging\n if (h === undefined) {\n throw new Error('@pictor-sdk/video-android: overlay staging is not installed')\n }\n return h\n}\n\n/** What a sink hands back, plus the token that owns it. @experimental */\nexport interface StagedRaster {\n file: string\n byteLength: number\n checksum: number\n}\n\n/**\n * A sink, and the tokens it is holding.\n *\n * The token list is the whole point of the wrapper: the structural\n * `OverlayRasterSink` contract identifies a staged raster by its file, and a\n * caller that released by path would be naming something it was never given\n * authority over. Every release here goes through the token native issued.\n *\n * @experimental\n */\nexport interface OverlayStagingSink {\n stage(\n bytes: Uint8Array,\n meta: { overlayId: string; resourceId: string; rasterWidth: number; rasterHeight: number },\n ): StagedRaster\n /** Discard one staged raster, by the file the descriptor carried. */\n remove(file: string): void\n /** Discard everything this sink still holds. Idempotent. */\n releaseAll(): number\n /** For assertions: created, released and still live. */\n ledger(): { created: number; released: number; live: number }\n}\n\n/**\n * Remove sidecars a previous process left behind.\n *\n * Scoped by native to this module's own directory and file suffix, so it cannot\n * take a cache file belonging to anything else. Returns how many it removed.\n *\n * @experimental\n */\nexport function sweepOverlayStaging(): number {\n if (!installOverlayStaging()) return 0\n return host().sweep()\n}\n\n/** @experimental */\nexport function createOverlayStagingSink(): OverlayStagingSink {\n if (!installOverlayStaging()) {\n throw new Error('@pictor-sdk/video-android: overlay staging could not be installed')\n }\n /** file → token, so a structural `remove(file)` can release exactly. */\n const tokens = new Map<string, string>()\n\n return {\n stage(bytes, meta) {\n /*\n A view over a larger buffer is copied rather than passed whole.\n\n `Uint8Array` may be a window onto a bigger `ArrayBuffer`, and handing\n native the underlying buffer would stage the whole thing — the right\n length by accident when the view happens to cover it, and silently wrong\n when it does not.\n */\n const exact =\n bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength\n ? (bytes.buffer as ArrayBuffer)\n : (bytes.slice().buffer as ArrayBuffer)\n\n const staged = host().stage(exact, meta.rasterWidth, meta.rasterHeight)\n tokens.set(staged.file, staged.token)\n return {\n file: staged.file,\n byteLength: staged.byteLength,\n checksum: staged.checksum,\n }\n },\n\n remove(file) {\n const token = tokens.get(file)\n // Releasing something never staged, or already released, is not an error:\n // the caller's cleanup paths overlap on purpose, and each must be safe to\n // run after the others.\n if (token === undefined) return\n tokens.delete(file)\n host().release(token)\n },\n\n releaseAll() {\n tokens.clear()\n return host().releaseAll()\n },\n\n ledger() {\n return host().ledger()\n },\n }\n}\n\n/**\n * How long a synchronous stage blocks the JS thread, for a raster this size.\n *\n * Exists because the synchronous contract is only defensible if the number is\n * small. If a maximum-size raster stalls long enough to drop frames in the\n * export sheet, the answer is to make `OverlayRasterSink.stage` asynchronous —\n * not to keep a blocking write behind a synchronous signature because it is\n * convenient.\n *\n * @experimental\n */\nexport function measureStagingStall(\n sink: OverlayStagingSink,\n width: number,\n height: number,\n): { ms: number; bytes: number } {\n const bytes = new Uint8Array(width * height * 4)\n const at = Date.now()\n const staged = sink.stage(bytes, {\n overlayId: 'measure',\n resourceId: 'measure',\n rasterWidth: width,\n rasterHeight: height,\n })\n const ms = Date.now() - at\n sink.remove(staged.file)\n return { ms, bytes: bytes.byteLength }\n}\n","import type { AudioProbe } from '@pictor-sdk/video-core'\nimport { requireNative } from './player'\n\n/**\n * Reading what a picked audio file actually contains.\n *\n * The structural half of {@link AudioProbe} lives in `@pictor-sdk/video-core`,\n * which has no decoder and never will; this is the adapter that can answer it.\n * A host calls this once, before adding, and passes the result to\n * `acceptAudioSource`.\n *\n * Nothing here identifies the file. The display name is the host's to carry —\n * a picker URI embeds an id and sometimes a user's own folder names.\n *\n * @experimental\n */\n\n/** A probe that failed, with a reason fit for a log rather than a person. @experimental */\nexport interface AudioProbeFailure {\n ok: false\n /** Why the file could not be read. Diagnostic, not a user-facing sentence. */\n reason: string\n}\n\n/** What native learned, minus the display name only the host knows. @experimental */\nexport type AudioProbeResult = ({ ok: true } & Omit<AudioProbe, 'displayName'>) | AudioProbeFailure\n\n/**\n * Open `uri` and report its audio.\n *\n * Never throws for an unreadable file: a pick that cannot be read is an answer,\n * and a host that had to wrap this in a `try` would be handling a normal\n * outcome as an exception. It throws only if the adapter is not installed,\n * which is a wiring mistake rather than a file.\n *\n * @experimental\n */\nexport function probeAudioSource(uri: string): AudioProbeResult {\n let raw: string\n try {\n raw = requireNative().probeAudioSource(uri)\n } catch (error) {\n return { ok: false, reason: error instanceof Error ? error.message : String(error) }\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return { ok: false, reason: 'probe returned unparseable JSON' }\n }\n\n const value = parsed as Partial<AudioProbe> & { ok?: boolean; reason?: string }\n if (value.ok !== true) {\n return { ok: false, reason: typeof value.reason === 'string' ? value.reason : 'probe failed' }\n }\n\n /*\n Normalised here, not trusted as-is.\n\n Everything downstream treats these as decided facts — `hasAudioTrack` ends\n up in `metadata.hasAudio`, which is what the encoder is told. A missing or\n malformed field must not arrive as `undefined` and read as permission.\n */\n return {\n ok: true,\n durationUs: Number.isFinite(value.durationUs) ? Number(value.durationUs) : 0,\n hasAudioTrack: value.hasAudioTrack === true,\n seekable: value.seekable === true,\n mimeType: typeof value.mimeType === 'string' ? value.mimeType : '',\n }\n}\n","import { requireNative } from './player'\n\n/**\n * Media the app owns, copied out of wherever it was picked from.\n *\n * A picker returns a reference, not a file. A `content://` grant is scoped to\n * the activity result that produced it; `copyToCacheDirectory` improves on that\n * by writing into **cache**, which Android reclaims whenever it wants storage\n * back. Either way a project persisted with a picker URI reopens with media\n * that has quietly gone, and nothing says so until an export fails.\n *\n * So ingestion copies into `filesDir` and the project stores that. See\n * `ManagedSources.kt` for why the name is the content hash and why nothing is\n * ever deleted implicitly.\n *\n * @experimental\n */\n\n/** Where a managed source lives, and what it is. @experimental */\nexport interface ManagedSource {\n /** A `file://` URI in app-owned durable storage. Safe to persist. */\n uri: string\n /** The content hash. Names the file; never derived from the user's filename. */\n token: string\n byteLength: number\n sha256: string\n /**\n * Whether this call brought the file into existence.\n *\n * False means the bytes were already stored and something else may be\n * pointing at them — a caller unwinding its own work must not release a\n * source it did not create.\n */\n created: boolean\n}\n\n/** @experimental */\nexport interface ManagedSourceFailure {\n ok: false\n /** Diagnostic. Never shown to a person: it may name a path. */\n reason: string\n}\n\n/** @experimental */\nexport type IngestResult = ({ ok: true } & ManagedSource) | ManagedSourceFailure\n\n/**\n * Copy a picked source into durable, app-owned storage.\n *\n * Never throws for an unreadable source: a pick that cannot be copied is an\n * answer. It throws only if the adapter is not installed.\n *\n * @experimental\n */\nexport function ingestManagedSource(uri: string): IngestResult {\n return parse<IngestResult>(\n () => requireNative().ingestManagedSource(uri),\n (value) => ({\n ok: true,\n uri: String(value.uri ?? ''),\n token: String(value.token ?? ''),\n byteLength: Number(value.byteLength ?? 0),\n sha256: String(value.sha256 ?? ''),\n created: value.created === true,\n }),\n )\n}\n\n/**\n * Drop one managed source.\n *\n * For unwinding an ingestion whose item was never created. Not for Undo: an\n * Undo that deleted the source would leave Redo restoring an item that points\n * at nothing.\n *\n * @experimental\n */\nexport function releaseManagedSource(token: string): boolean {\n try {\n const value = JSON.parse(requireNative().releaseManagedSource(token)) as { removed?: boolean }\n return value.removed === true\n } catch {\n return false\n }\n}\n\n/**\n * Remove every managed source not named in `keepTokens`.\n *\n * The only thing that deletes media, and it is always given the full set of\n * what the project still references. Also removes partials a killed process\n * left behind.\n *\n * @experimental\n */\nexport function sweepManagedSources(keepTokens: readonly string[]): {\n removed: number\n kept: number\n partials: number\n /** Files that are not ours, counted so the accounting matches a listing. */\n skipped: number\n} {\n try {\n const value = JSON.parse(\n requireNative().sweepManagedSources(JSON.stringify([...keepTokens])),\n ) as { ok?: boolean; removed?: number; kept?: number; partials?: number; skipped?: number }\n if (value.ok !== true) return { removed: 0, kept: 0, partials: 0, skipped: 0 }\n return {\n removed: Number(value.removed ?? 0),\n kept: Number(value.kept ?? 0),\n partials: Number(value.partials ?? 0),\n skipped: Number(value.skipped ?? 0),\n }\n } catch {\n // A sweep that cannot be understood removes nothing. The opposite default\n // would delete a project's media on a parse error.\n return { removed: 0, kept: 0, partials: 0, skipped: 0 }\n }\n}\n\n/**\n * Every filename in the managed directory, including partials and strays.\n *\n * Read-only accounting, so it is available in release for the same reason the\n * ledgers are: it is how a sweep is proven rather than asserted.\n * {@link managedSourceLedger} reports only well-formed sources, which cannot\n * show what a sweep left behind.\n *\n * @experimental\n */\nexport function managedSourceList(): { name: string; byteLength: number }[] {\n try {\n const value = JSON.parse(requireNative().managedSourceList()) as {\n items?: { name: string; byteLength: number }[]\n }\n return value.items ?? []\n } catch {\n return []\n }\n}\n\n/**\n * Gate-only: put a file in the managed directory with an exact name.\n *\n * Ingestion only ever produces well-formed, referenced sources, so the cases a\n * sweep exists to handle — an orphan, a stale partial, a file that is not ours\n * — cannot be reached through the normal API. Refused in a release build built\n * without diagnostics.\n *\n * @experimental\n */\nexport function seedManagedSourceForGate(name: string, bytes: number): boolean {\n try {\n const value = JSON.parse(requireNative().managedSourceGateSeed(name, bytes)) as { ok?: boolean }\n return value.ok === true\n } catch {\n return false\n }\n}\n\n/**\n * Gate-only: sweep with a keep list handed over exactly as written.\n *\n * The production path serialises its own list, so an unparseable one can never\n * reach native — which makes \"an unreadable keep list deletes nothing\"\n * unprovable through it. Returns what native reported.\n *\n * @experimental\n */\nexport function sweepManagedSourcesRawForGate(rawKeep: string): {\n ok: boolean\n removed: number\n kept: number\n partials: number\n skipped: number\n} {\n try {\n const value = JSON.parse(requireNative().managedSourceGateSweepRaw(rawKeep)) as {\n ok?: boolean\n removed?: number\n kept?: number\n partials?: number\n skipped?: number\n }\n return {\n ok: value.ok === true,\n removed: Number(value.removed ?? 0),\n kept: Number(value.kept ?? 0),\n partials: Number(value.partials ?? 0),\n skipped: Number(value.skipped ?? 0),\n }\n } catch {\n return { ok: false, removed: 0, kept: 0, partials: 0, skipped: 0 }\n }\n}\n\n/** What is stored, for assertions. Tokens and sizes, never names. @experimental */\nexport function managedSourceLedger(): {\n count: number\n bytes: number\n items: { token: string; byteLength: number }[]\n} {\n try {\n const value = JSON.parse(requireNative().managedSourceLedger()) as {\n count?: number\n bytes?: number\n items?: { token: string; byteLength: number }[]\n }\n return {\n count: Number(value.count ?? 0),\n bytes: Number(value.bytes ?? 0),\n items: value.items ?? [],\n }\n } catch {\n return { count: 0, bytes: 0, items: [] }\n }\n}\n\n/**\n * The token a managed URI names, or null if it is not one of ours.\n *\n * Lets a host collect the keep list for {@link sweepManagedSources} straight\n * from a project's sources without storing a second index that could drift.\n *\n * @experimental\n */\nexport function managedSourceToken(uri: string): string | null {\n const match = /\\/([0-9a-f]{64})\\.bin$/.exec(uri)\n return match ? match[1]! : null\n}\n\nfunction parse<T>(\n call: () => string,\n build: (value: Record<string, unknown>) => T,\n): T | ManagedSourceFailure {\n let raw: string\n try {\n raw = call()\n } catch (error) {\n return { ok: false, reason: error instanceof Error ? error.message : String(error) }\n }\n let value: Record<string, unknown>\n try {\n value = JSON.parse(raw) as Record<string, unknown>\n } catch {\n return { ok: false, reason: 'native returned unparseable JSON' }\n }\n if (value.ok !== true) {\n return { ok: false, reason: typeof value.reason === 'string' ? value.reason : 'failed' }\n }\n return build(value)\n}\n","import { requireNative } from './player'\n\n/**\n * The audio a preview plays.\n *\n * ## Preparation, not playback\n *\n * A preview mix is produced by the same code an export runs: every contributing\n * source is decoded, re-timed by its clip's speed, ramped, summed and saturated\n * once, then encoded. That takes real time proportional to the project, and no\n * amount of naming it \"preview\" makes it realtime. A caller has to show that it\n * is preparing and must not pretend otherwise.\n *\n * ## Why not mix in the player\n *\n * Because then there would be two definitions of what a project sounds like,\n * and the one a person hears while editing would be the one that was never\n * tested against an export. Gain, fades, trims, speed and overlap are decided\n * by `compileAudioMixProgram` and executed by `AudioMixer` — here and in the\n * exporter, from the same plan.\n *\n * ## Superseding\n *\n * One job at a time. A newer request cancels the one in flight, which unwinds\n * at its next checkpoint, deletes its partial and reports `superseded`. A\n * superseded result is not a failure and must never be published: it is the\n * answer to a question nobody is asking any more.\n *\n * @experimental\n */\n\n/** A prepared mix. @experimental */\nexport interface PreparedMix {\n ok: true\n /** A `file://` URI in app-private storage. */\n uri: string\n /** The content key: mix program, sources and mixer configuration. */\n key: string\n /** True when nothing had to be encoded — the file was already there. */\n cached: boolean\n byteLength: number\n /**\n * Where the time went, when it was actually prepared.\n *\n * Milliseconds per stage and frames per source. Absent for a cached mix,\n * which did no work to attribute.\n */\n profile?: {\n stageMs: Record<string, number>\n counts: Record<string, number>\n sources: Record<string, Record<string, number>>\n }\n}\n\n/** @experimental */\nexport type PreviewMixResult =\n | PreparedMix\n /** A newer request replaced this one. Not a failure; never publish it. */\n | { ok: false; superseded: true; key: string }\n /** It ran and failed. The reason is diagnostic and may name a path. */\n | { ok: false; superseded?: false; key?: string; reason: string }\n\n/**\n * Prepare the mix for a compiled program, or hand back the one already\n * prepared.\n *\n * `urisJson` maps span id to a resolved, durable source URI — the same\n * resolution an export performs, so the two agree about which file a span\n * means.\n *\n * Never rejects: a mix that cannot be produced is an outcome.\n *\n * @experimental\n */\nexport async function preparePreviewMix(\n jobId: string,\n programJson: string,\n urisJson: string,\n): Promise<PreviewMixResult> {\n let raw: string\n try {\n raw = await requireNative().previewMixPrepare(jobId, programJson, urisJson)\n } catch (error) {\n return { ok: false, reason: error instanceof Error ? error.message : String(error) }\n }\n\n let value: Record<string, unknown>\n try {\n value = JSON.parse(raw) as Record<string, unknown>\n } catch {\n return { ok: false, reason: 'native returned unparseable JSON' }\n }\n\n if (value.ok === true) {\n return {\n ok: true,\n uri: String(value.uri ?? ''),\n key: String(value.key ?? ''),\n cached: value.cached === true,\n byteLength: Number(value.byteLength ?? 0),\n ...(value.profile ? { profile: value.profile as PreparedMix['profile'] } : {}),\n }\n }\n if (value.superseded === true) {\n return { ok: false, superseded: true, key: String(value.key ?? '') }\n }\n return {\n ok: false,\n ...(typeof value.key === 'string' ? { key: value.key } : {}),\n reason: typeof value.error === 'string' ? value.error : 'the mix could not be prepared',\n }\n}\n\n/**\n * Stop whatever is preparing.\n *\n * Safe to call when nothing is: cancelling twice, or cancelling after a job\n * already finished, is how a caller's teardown paths overlap on purpose.\n *\n * @experimental\n */\nexport function cancelPreviewMix(): void {\n try {\n requireNative().previewMixCancel()\n } catch {\n // Nothing to stop, or no adapter. Neither is worth raising during teardown.\n }\n}\n\n/**\n * The key a program would produce, without preparing it.\n *\n * Lets a caller ask \"is this the mix I am already playing?\" before starting a\n * job — which is what stops a selection change, or any edit that does not\n * change the sound, from rebuilding anything.\n *\n * @experimental\n */\nexport function previewMixKey(programJson: string, urisJson: string): string {\n try {\n return requireNative().previewMixKey(programJson, urisJson)\n } catch {\n return ''\n }\n}\n\n/** Drop one prepared mix. @experimental */\nexport function releasePreviewMix(key: string): boolean {\n try {\n const value = JSON.parse(requireNative().previewMixRelease(key)) as { removed?: boolean }\n return value.removed === true\n } catch {\n return false\n }\n}\n\n/**\n * Keep only these mixes; remove the rest and every partial.\n *\n * The bound on the cache. A sweep that cannot read its own result removes\n * nothing, rather than assuming the directory should be empty.\n *\n * @experimental\n */\nexport function sweepPreviewMixes(keepKeys: readonly string[]): {\n removed: number\n kept: number\n partials: number\n skipped: number\n} {\n const none = { removed: 0, kept: 0, partials: 0, skipped: 0 }\n try {\n const value = JSON.parse(requireNative().previewMixSweep(JSON.stringify([...keepKeys]))) as {\n ok?: boolean\n removed?: number\n kept?: number\n partials?: number\n skipped?: number\n }\n if (value.ok !== true) return none\n return {\n removed: Number(value.removed ?? 0),\n kept: Number(value.kept ?? 0),\n partials: Number(value.partials ?? 0),\n skipped: Number(value.skipped ?? 0),\n }\n } catch {\n return none\n }\n}\n\n/** What is prepared, for assertions. @experimental */\nexport function previewMixLedger(): {\n count: number\n bytes: number\n items: { name: string; byteLength: number }[]\n} {\n try {\n const value = JSON.parse(requireNative().previewMixLedger()) as {\n count?: number\n bytes?: number\n items?: { name: string; byteLength: number }[]\n }\n return {\n count: Number(value.count ?? 0),\n bytes: Number(value.bytes ?? 0),\n items: value.items ?? [],\n }\n } catch {\n return { count: 0, bytes: 0, items: [] }\n }\n}\n","import { compileAudioMixProgram, type VideoProject } from '@pictor-sdk/video-core'\n\n/**\n * Deciding what a preview should be doing about audio.\n *\n * Pure, and separate from the hook that acts on it, because every rule worth\n * getting right here is a rule about state rather than about React: whether a\n * project needs a mix at all, whether the mix on hand is the one the document\n * describes, whether a result that just arrived is still wanted, and whether\n * two players have drifted far enough to be worth correcting.\n *\n * ## Two sources of sound, never both\n *\n * Without music, the video player's own audio is the preview and nothing here\n * applies. With music, the mix covers the *whole* timeline — including the\n * audio of the clips themselves — so playing the clips' audio as well would\n * play everything twice. There is no state in which both are audible, and the\n * transition between them is the thing this file exists to sequence.\n *\n * @experimental\n */\n\n/** What a preview's audio is doing. @experimental */\nexport type PreviewAudioStatus =\n /** No music in the project. The video player's own audio is the preview. */\n | 'source'\n /** Music exists and the mix for *this* document is not ready. */\n | 'preparing'\n /** The mix on hand matches the document. Source audio is muted. */\n | 'ready'\n /** The mix could not be prepared. Nothing is played rather than the wrong thing. */\n | 'failed'\n\n/** @experimental */\nexport interface PreviewAudioDecision {\n status: PreviewAudioStatus\n /** The key the document wants. Empty when there is no music. */\n wantedKey: string\n /** Whether a job should be started for `wantedKey`. */\n shouldPrepare: boolean\n /** Whether the video player's own audio must be silenced. */\n muteSourceAudio: boolean\n /** Whether playback may run right now. */\n canPlay: boolean\n}\n\n/** @experimental */\nexport interface PreviewAudioInputs {\n /** The key the current document compiles to, or '' when it has no music. */\n wantedKey: string\n /** The key of the mix already prepared and loaded, if any. */\n readyKey: string | null\n /** The key a job is currently preparing, if any. */\n preparingKey: string | null\n /** The key whose preparation failed, if any. */\n failedKey: string | null\n}\n\n/**\n * Whether a project needs a mixed preview at all.\n *\n * True when anything in the compiled program is audible *and comes from an\n * audio item* — a project whose only sound is clip audio does not need a mix,\n * because the video player already plays exactly that and does it in realtime.\n *\n * @experimental\n */\nexport function previewAudioNeeded(project: VideoProject): boolean {\n return project.audio.length > 0\n}\n\n/**\n * What to do, given what is wanted and what is on hand.\n *\n * @experimental\n */\nexport function decidePreviewAudio(inputs: PreviewAudioInputs): PreviewAudioDecision {\n const { wantedKey, readyKey, preparingKey, failedKey } = inputs\n\n if (wantedKey === '') {\n // No music. The video player's audio is the preview, exactly as it was\n // before any of this existed.\n return {\n status: 'source',\n wantedKey,\n shouldPrepare: false,\n muteSourceAudio: false,\n canPlay: true,\n }\n }\n\n if (readyKey === wantedKey) {\n return {\n status: 'ready',\n wantedKey,\n shouldPrepare: false,\n // The mix contains the clips' audio too. Leaving the source audible\n // would play the whole timeline twice.\n muteSourceAudio: true,\n canPlay: true,\n }\n }\n\n if (failedKey === wantedKey) {\n /*\n Refusing rather than falling back.\n\n The tempting fallback is \"play the clip audio instead\", and it is wrong:\n the person would hear a preview missing its music, with no reliable way to\n tell that from a project whose music is silent. A named failure is worse\n to look at and better to act on.\n */\n return {\n status: 'failed',\n wantedKey,\n shouldPrepare: false,\n muteSourceAudio: true,\n canPlay: false,\n }\n }\n\n /*\n Preparing. A mix for an older document may still be loaded, and it must not\n play: it is the previous edit's sound, which is the most convincing kind of\n wrong. Source audio stays muted throughout, so the transition is silence\n rather than a burst of un-mixed clip audio.\n */\n return {\n status: 'preparing',\n wantedKey,\n shouldPrepare: preparingKey !== wantedKey,\n muteSourceAudio: true,\n canPlay: false,\n }\n}\n\n/** The key a project's audio compiles to, given resolved sources. @experimental */\nexport function previewProgramJson(project: VideoProject): string {\n return JSON.stringify(compileAudioMixProgram(project))\n}\n\n/*\n The drift comparison itself lives in native, not here.\n\n It used to be a function in this file, and it was never wired to a caller —\n measured, a clip seam left the mix 240 ms ahead of the picture and it stayed\n there. Reimplementing it here now would be worse than that: both anchors are\n published on one monotonic clock in native, and a JavaScript twin would have\n no way to read them at the same instant, so the two would disagree about what\n \"at the same time\" meant. See `PreviewTrace.driftCheck`.\n*/\n\n/**\n * The default tolerance.\n *\n * The drift this used to chase is prevented rather than corrected: `PreviewSync`\n * holds both transports through a stall and releases them aligned, which is\n * where all of it came from. A correcting seek during playback costs 200-340 ms\n * on a Pixel 7 — the same size as the error it was chasing, so it oscillated\n * between +264 ms and -198 ms — and realignment now happens while everything is\n * held, where a seek has nothing to chase.\n *\n * So this is back to the tolerance the gates were written against. It is a\n * backstop for something nobody has seen, not the mechanism.\n *\n * @experimental\n */\nexport const PREVIEW_DRIFT_TOLERANCE_US = 60_000\n","import type { TraceEvent } from './previewTrace'\n\n/**\n * The offset, segmented by the marks a gate put in the run.\n *\n * ## Why a whole-run summary is not enough\n *\n * `summariseOffsets` answers \"how bad did it get\", and a gate needs \"where\".\n * Measured on a Pixel 7, a run whose worst excursion was 266 ms spent every\n * moment between its transitions inside ±40 ms: the whole-run worst described\n * three settling transients and nothing else, and a single number cannot tell\n * that from a steady 266 ms error. They are not the same defect and they do not\n * have the same fix.\n *\n * So a run is cut at its marks, and each segment is summarised on its own. The\n * gate verdict is still the run's worst — this localises a failure, it does not\n * soften one.\n *\n * ## Fresh and stale are reported, never traded\n *\n * A stale presented frame is what a person actually sees, so its offset is real\n * and is counted in `worstAbsUs`. But a stale frame's offset grows purely\n * because audio advanced while the picture did not, which is a different fault\n * from two clocks disagreeing — so `freshWorstAbsUs` is reported beside it\n * rather than instead of it. Neither is allowed to stand alone: quoting only\n * the fresh figure is how a run with a nine-second frozen picture passes a\n * synchronisation gate.\n *\n * @experimental\n */\nexport interface PhaseSummary {\n /** The mark that opened this segment, or `'start'` before the first one. */\n label: string\n index: number\n /** Samples in the segment, whatever their state. */\n samples: number\n /** Samples with both players playing — the only ones an offset means anything for. */\n playingSamples: number\n /** Of those, the ones whose presented frame had advanced. */\n freshPlayingSamples: number\n stalePlayingSamples: number\n /** The longest unbroken run of stale samples, in samples. */\n longestStaleRun: number\n minUs: number\n maxUs: number\n meanUs: number\n /** Worst absolute offset over every playing sample, stale included. */\n worstAbsUs: number\n /** Worst over fresh samples only. Reported beside `worstAbsUs`, never instead. */\n freshWorstAbsUs: number\n /** Playing samples outside the tolerance. */\n overToleranceSamples: number\n /**\n * How long the segment took to come inside tolerance and stay there.\n *\n * `-1` when it never did, which is the difference between a transient and a\n * failure that happens to start at a transition. Measured from the segment's\n * first playing sample, so it is a settling time rather than a wall clock.\n */\n settledAfterUs: number\n}\n\n/** @experimental */\nexport interface PhaseReport {\n toleranceUs: number\n phases: PhaseSummary[]\n /** The run's worst, over every phase. This is the gate. */\n worstAbsUs: number\n freshWorstAbsUs: number\n overToleranceSamples: number\n /** Whether every playing sample in the run was inside tolerance. */\n pass: boolean\n}\n\n/**\n * The prefix a gate's own marks carry.\n *\n * Every mark is recorded as a `mark` event carrying its label, and the trace is\n * also full of system events — holds, releases, drift corrections, transport\n * transitions. Cutting on \"not a sample\" would start a new phase at every one\n * of those, and cutting on every mark would let an ad-hoc label reshape a gate's\n * phases. So only marks whose label carries this prefix cut the run.\n *\n * @experimental\n */\nexport const PHASE_MARK_PREFIX = 'phase:'\n\nfunction num(v: unknown): number {\n return typeof v === 'number' ? v : Number(v)\n}\n\nfunction empty(label: string, index: number): PhaseSummary {\n return {\n label,\n index,\n samples: 0,\n playingSamples: 0,\n freshPlayingSamples: 0,\n stalePlayingSamples: 0,\n longestStaleRun: 0,\n minUs: 0,\n maxUs: 0,\n meanUs: 0,\n worstAbsUs: 0,\n freshWorstAbsUs: 0,\n overToleranceSamples: 0,\n settledAfterUs: 0,\n }\n}\n\n/**\n * Summarise a drained run, cut at its mark events.\n *\n * @experimental\n */\nexport function summarisePhases(\n events: readonly TraceEvent[],\n toleranceUs: number,\n): PhaseReport {\n const phases: PhaseSummary[] = []\n let current = empty('start', 0)\n // Accumulators kept outside the summary so the published shape stays plain\n // data — a gate serialises this straight into a log line.\n let sum = 0\n let staleRun = 0\n let firstPlayingNs = 0\n let lastOverNs = 0\n let sawPlaying = false\n let stillOver = false\n\n const close = () => {\n current.meanUs = current.playingSamples > 0 ? Math.round(sum / current.playingSamples) : 0\n /*\n Settling is measured to the *last* excursion, not the first return.\n\n A transient that goes out, comes back and goes out again has not settled\n at the first return, and reporting that would turn the worst case into the\n best-looking number available. -1 for a phase still outside tolerance at\n its final sample: that is not a transient at all.\n */\n current.settledAfterUs =\n current.overToleranceSamples === 0\n ? 0\n : stillOver\n ? -1\n : Math.round((lastOverNs - firstPlayingNs) / 1000)\n phases.push(current)\n sum = 0\n staleRun = 0\n firstPlayingNs = 0\n lastOverNs = 0\n sawPlaying = false\n stillOver = false\n }\n\n for (const e of events) {\n if (e.type !== 'sample') {\n const label = e.type === 'mark' ? String(e.v.label ?? '') : ''\n if (label.startsWith(PHASE_MARK_PREFIX)) {\n close()\n current = empty(label.slice(PHASE_MARK_PREFIX.length), phases.length)\n }\n continue\n }\n current.samples += 1\n if (e.v.videoPlaying !== true || e.v.audioPlaying !== true) {\n staleRun = 0\n continue\n }\n const offset = num(e.v.offsetUs)\n if (!Number.isFinite(offset)) continue\n current.playingSamples += 1\n if (!sawPlaying) {\n sawPlaying = true\n firstPlayingNs = num(e.atNs)\n }\n sum += offset\n if (current.playingSamples === 1) {\n current.minUs = offset\n current.maxUs = offset\n } else {\n current.minUs = Math.min(current.minUs, offset)\n current.maxUs = Math.max(current.maxUs, offset)\n }\n const abs = Math.abs(offset)\n if (abs > current.worstAbsUs) current.worstAbsUs = abs\n if (abs > toleranceUs) {\n current.overToleranceSamples += 1\n lastOverNs = num(e.atNs)\n stillOver = true\n } else {\n stillOver = false\n }\n if (e.v.pfFresh === true) {\n current.freshPlayingSamples += 1\n if (abs > current.freshWorstAbsUs) current.freshWorstAbsUs = abs\n staleRun = 0\n } else {\n current.stalePlayingSamples += 1\n staleRun += 1\n if (staleRun > current.longestStaleRun) current.longestStaleRun = staleRun\n }\n }\n close()\n\n const worstAbsUs = phases.reduce((w, p) => Math.max(w, p.worstAbsUs), 0)\n const freshWorstAbsUs = phases.reduce((w, p) => Math.max(w, p.freshWorstAbsUs), 0)\n const overToleranceSamples = phases.reduce((n, p) => n + p.overToleranceSamples, 0)\n return {\n toleranceUs,\n phases,\n worstAbsUs,\n freshWorstAbsUs,\n overToleranceSamples,\n pass: overToleranceSamples === 0,\n }\n}\n","import type { SpikeSample } from './compositionSpike'\n\n/**\n * Where a shared-clock run left the tolerance, and why.\n *\n * ## What is being compared\n *\n * The audio clock's position and the presented frame's timestamp, brought to\n * one instant. The frame was latched at `frameConsumedNs`; the position was\n * read at `atNs`. Rolling the position back to the latch is what puts them on\n * the same monotonic instant — the composition runs at project rate 1x, so the\n * roll-back is the elapsed time itself.\n *\n * ## Which boundary this observes\n *\n * The **latch**, not the display. `frameConsumedNs` is when the consumer called\n * `updateTexImage`; `frameReleaseNs` is the time Media3 *asked* the surface to\n * present at, which is an intention and not an acknowledgement. Actual scanout\n * happens one compositor pass later and is not observed anywhere in this path.\n * So an offset here is \"audio against latched picture\", and the display adds a\n * further unmeasured delay common to every sample.\n *\n * ## Uncertainty is stated, never subtracted\n *\n * Comparing a 30 fps frame's timestamp against a continuous clock carries an\n * irreducible ±16.7 ms, and the paired read adds a few more. That is reported\n * beside the verdict. It is not deducted from an excursion to bring it under\n * the line: a 95 ms excursion is not a 78 ms excursion because the instrument\n * is imperfect.\n *\n * @experimental\n */\n\n/** @experimental */\nexport const SPIKE_TOLERANCE_US = 60_000\n\n/**\n * Why one sample left the tolerance.\n *\n * Ordered by precedence, because more than one can be true and the first is the\n * one that explains the sample: a frame from before a seek is cross-epoch\n * whatever else is also true of it.\n *\n * @experimental\n */\nexport type ExcursionCause =\n /**\n * The latched frame predates the moment playback was released.\n *\n * Not a filter and not a drift. The readiness gate releases on a frame whose\n * *position* matches the epoch, and that frame may have been latched long\n * before — measured at 13.9 s, the whole interval between attaching the\n * composition and the person pressing play. The picture was right; the\n * frame's wall-clock age simply includes a pause, and charging that to\n * synchronisation measures the pause.\n *\n * Reported rather than excluded, because the size of it is the size of the\n * interval in which the first sample after a release is not yet a\n * synchronisation reading.\n */\n | 'pre-release-frame'\n /** The frame belongs to a different transition epoch or frame generation. */\n | 'cross-epoch'\n /** The frame was latched long enough ago that it is not a current reading. */\n | 'stale-pairing'\n /** The first frames after a seek or after readiness, before the picture caught up. */\n | 'first-frame-after-seek'\n /** The frame was latched well after the time it was asked to be presented at. */\n | 'delayed-presentation'\n /** The audio clock moved discontinuously between adjacent samples. */\n | 'audio-discontinuity'\n /** The mapped project time disagrees with the frame's own window and PTS. */\n | 'mapping'\n /** None of the above fitted. Deliberately not folded into a neighbour. */\n | 'unexplained'\n\n/** @experimental */\nexport interface Excursion {\n seq: number\n /** Against the latch: observed, and earlier than display. */\n offsetUs: number\n /** Against the time the frame was asked to appear. An intention. */\n offsetAtIntendedUs: number\n cause: ExcursionCause\n epoch: number\n sinceEpochMs: number\n frameWindow: number\n framePtsUs: number\n /** Latched minus asked-for. Large means the picture arrived late. */\n latchLagUs: number\n /** How old the latched frame was when the position was read. */\n frameAgeUs: number\n /** The audio clock's step from the previous sample, in microseconds. */\n positionStepUs: number\n /** The wall-clock step from the previous sample. */\n wallStepUs: number\n}\n\n/** @experimental */\nexport interface ExcursionReport {\n toleranceUs: number\n /** Samples where playback was declared active. Only these are judged. */\n activeSamples: number\n /** Samples before readiness, exposed rather than dropped. */\n preparingSamples: number\n excursions: Excursion[]\n byCause: Record<string, number>\n /**\n * Samples still showing a frame latched before the release.\n *\n * Reported apart, and *retained* in the acceptance set: they are real\n * playback, and if the picture never catches up they are the only thing that\n * says so.\n */\n preReleaseSamples: number\n preReleaseWorstUs: number\n worstAbsUs: number\n /** The same run judged at the intended-presentation boundary. */\n worstAtIntendedUs: number\n overAtIntended: number\n passAtIntended: boolean\n /** Mean latch-minus-intended over the excursions. Negative means early. */\n meanLatchLagUs: number\n pass: boolean\n uncertainty: {\n /** Half a frame interval at the measured rate. Irreducible. */\n frameQuantumUs: number\n /** The widest paired read in the run. */\n readSpanUs: number\n positionQuantumUs: number\n }\n}\n\n/**\n * The offset at each observable boundary.\n *\n * They are not the same instant and the difference is not noise. Measured on a\n * Pixel 7, the consumer latches a buffer about 34 ms *before* the time Media3\n * asked the surface to present it — `releaseOutputBuffer(index, releaseTimeNs)`\n * queues immediately with a desired present time in the future, and the\n * SurfaceTexture consumer takes it as soon as it is queued.\n *\n * So `atLatch` compares the audio clock against a frame that is in the texture\n * but has not been displayed, and is systematically early by that gap;\n * `atIntended` compares it against when that frame was *meant* to appear, which\n * is the best available estimate of what a viewer experiences and is still an\n * intention rather than an acknowledgement. The true display instant lies at or\n * after `atIntended`, and nothing in this process observes it.\n *\n * Both are returned because choosing one silently is how a boundary decision\n * becomes a verdict.\n *\n * @experimental\n */\nexport function offsetAt(\n sample: SpikeSample,\n /**\n * When the current playing interval was released, if known.\n *\n * A frame latched while paused carries the pause in its age. Charging that to\n * synchronisation measured a 13.9 second \"error\" for a picture that was\n * showing exactly the right position. Clamping the reference to the release\n * instant charges only the time playback has actually been running — so at\n * release the offset is (audio position - displayed PTS), and it grows from\n * there if the audio advances while the picture stays put.\n *\n * That growth is the point: this corrects the baseline without removing\n * coverage. A frame that is never replaced still accrues the offset it\n * deserves.\n */\n releaseAtNs = 0,\n): { atLatch: number; atIntended: number } {\n /*\n A frame already on screen when playback began has no age to subtract.\n\n For a frame presented during playback, the sound being ahead of it by the\n time since it appeared is not error — the frame is a sample of the recent\n past. Subtracting that age is what makes the measure a synchronisation\n error rather than a restatement of the frame rate.\n\n A pre-release frame is different in kind. It was displayed before the clock\n started, so there is no elapsed-since-display to discount: at the moment of\n release the comparison is simply \"what is on screen\" against \"where the\n sound is\", which is near zero when the readiness gate did its job. And if\n the picture then stays put while the sound runs on, the gap is exactly the\n staleness a viewer sees — so it is counted, not cancelled. Subtracting the\n age here would have the two grow together and report zero forever.\n */\n if (releaseAtNs > 0 && sample.frameConsumedNs < releaseAtNs) {\n const raw = sample.positionUs - sample.framePtsUs\n return { atLatch: raw, atIntended: raw }\n }\n return {\n atLatch: sample.positionUs - (sample.atNs - sample.frameConsumedNs) / 1000 - sample.framePtsUs,\n atIntended:\n sample.positionUs - (sample.atNs - sample.frameReleaseNs) / 1000 - sample.framePtsUs,\n }\n}\n\n/**\n * Classify a run's over-tolerance samples.\n *\n * `frameIntervalUs` is the source's nominal frame interval; half of it is the\n * irreducible quantisation of comparing a discrete frame against a continuous\n * clock.\n *\n * @experimental\n */\nexport function classifyExcursions(\n samples: readonly SpikeSample[],\n options: { toleranceUs?: number; frameIntervalUs?: number } = {},\n): ExcursionReport {\n const toleranceUs = options.toleranceUs ?? SPIKE_TOLERANCE_US\n const frameIntervalUs = options.frameIntervalUs ?? 33_333\n\n /*\n Judged only once playback is active, and the rest is exposed rather than\n dropped. A position that has not started moving is not synchronised or\n unsynchronised; counting it either way is a statement about nothing.\n */\n /*\n A frame from before this epoch is not a reading about this epoch.\n\n After a start or a seek the clock moves immediately while the first frame\n for the new position is still being decoded, so the newest latched frame is\n the one from *before* the transition. Pairing it against the moving clock\n produces an excursion the size of the seek — measured here at up to 13.8 s\n — and no classification of that number is about synchronisation.\n\n So the interval is exposed as preparing rather than judged, which is also\n what makes \"include every sample once playback is active\" mean something:\n active begins when the picture for this epoch has actually arrived.\n */\n /*\n And the clock has to be moving.\n\n `isPlaying` and `STATE_READY` both go true before the composition's clock\n starts: measured, the player reported both for fourteen seconds with its\n position at zero and exactly one frame ever latched. Judging that interval\n produced a 13.9 second \"excursion\" that is not a synchronisation reading at\n all — it is the preparation the brief asks to be exposed rather than scored.\n\n Observed rather than assumed: the position must have changed somewhere in\n the preceding window, so the test is \"this clock is running\" and not \"this\n player says it is\".\n */\n const ADVANCE_WINDOW_NS = 200_000_000\n const advancing = samples.map((s, i) => {\n for (let j = i - 1; j >= 0; j -= 1) {\n if (s.atNs - samples[j]!.atNs > ADVANCE_WINDOW_NS) break\n if (samples[j]!.positionUs !== s.positionUs) return true\n }\n return false\n })\n\n /*\n When the current playing interval began.\n\n A frame latched before it carries the pause in its age, so the first\n samples after a release are not yet synchronisation readings. Tracked so\n they can be *named* rather than dropped.\n */\n const releaseAtNs: number[] = []\n let intervalStart = 0\n for (let i = 0; i < samples.length; i += 1) {\n if (!advancing[i]) intervalStart = 0\n else if (intervalStart === 0) intervalStart = samples[i]!.atNs\n releaseAtNs.push(intervalStart)\n }\n\n const isActive = (s: SpikeSample, i: number) =>\n s.playing &&\n s.ready &&\n s.framePtsUs >= 0 &&\n s.frameConsumedNs > 0 &&\n // The picture for this epoch has arrived.\n s.frameConsumedNs >= s.atNs - s.sinceEpochNs &&\n advancing[i] === true\n const active = samples.filter(isActive)\n const preparing = samples.length - active.length\n\n const excursions: Excursion[] = []\n const byCause: Record<string, number> = {}\n let worstAbsUs = 0\n let worstAtIntendedUs = 0\n let overAtIntended = 0\n let readSpanUs = 0\n let latchLagSum = 0\n let preReleaseSamples = 0\n let preReleaseWorstUs = 0\n\n // Indexed by the sample's own position in the full run, because the release\n // boundary was computed there and `active` has holes.\n const releaseFor = new Map<number, number>()\n samples.forEach((s, i) => releaseFor.set(s.seq, releaseAtNs[i]!))\n\n for (let i = 0; i < active.length; i += 1) {\n const s = active[i]!\n const previous = i > 0 ? active[i - 1]! : null\n readSpanUs = Math.max(readSpanUs, s.readSpanNs / 1000)\n const releasedAt = releaseFor.get(s.seq) ?? 0\n const both = offsetAt(s, releasedAt)\n const offsetUs = both.atLatch\n const abs = Math.abs(offsetUs)\n const absIntended = Math.abs(both.atIntended)\n if (releasedAt > 0 && s.frameConsumedNs < releasedAt) {\n preReleaseSamples += 1\n if (abs > preReleaseWorstUs) preReleaseWorstUs = abs\n }\n if (abs > worstAbsUs) worstAbsUs = abs\n if (absIntended > worstAtIntendedUs) worstAtIntendedUs = absIntended\n if (absIntended > toleranceUs) overAtIntended += 1\n if (abs <= toleranceUs) continue\n\n const frameAgeUs = (s.atNs - s.frameConsumedNs) / 1000\n const latchLagUs = (s.frameConsumedNs - s.frameReleaseNs) / 1000\n latchLagSum += latchLagUs\n const positionStepUs = previous ? s.positionUs - previous.positionUs : 0\n const wallStepUs = previous ? (s.atNs - previous.atNs) / 1000 : 0\n\n /*\n First match wins, and the order is the point.\n\n A frame carried across a seek is cross-epoch whether or not it is also\n old and also late; calling it \"delayed presentation\" would send the fix\n to the renderer instead of to the epoch check.\n */\n let cause: ExcursionCause\n if (releasedAt > 0 && s.frameConsumedNs < releasedAt) {\n cause = 'pre-release-frame'\n } else if (s.frameConsumedNs > 0 && s.frameConsumedNs < s.atNs - s.sinceEpochNs) {\n cause = 'cross-epoch'\n } else if (previous && s.frameGeneration !== previous.frameGeneration) {\n cause = 'cross-epoch'\n } else if (frameAgeUs > 2 * frameIntervalUs) {\n cause = 'stale-pairing'\n } else if (s.sinceEpochNs / 1000 < 500_000) {\n cause = 'first-frame-after-seek'\n } else if (latchLagUs > frameIntervalUs) {\n cause = 'delayed-presentation'\n } else if (previous && Math.abs(positionStepUs - wallStepUs) > toleranceUs) {\n cause = 'audio-discontinuity'\n } else if (previous && s.frameWindow !== previous.frameWindow) {\n cause = 'mapping'\n } else {\n cause = 'unexplained'\n }\n\n byCause[cause] = (byCause[cause] ?? 0) + 1\n excursions.push({\n seq: s.seq,\n offsetUs: Math.round(offsetUs),\n offsetAtIntendedUs: Math.round(both.atIntended),\n cause,\n epoch: s.epoch,\n sinceEpochMs: Math.round(s.sinceEpochNs / 1_000_000),\n frameWindow: s.frameWindow,\n framePtsUs: s.framePtsUs,\n latchLagUs: Math.round(latchLagUs),\n frameAgeUs: Math.round(frameAgeUs),\n positionStepUs: Math.round(positionStepUs),\n wallStepUs: Math.round(wallStepUs),\n })\n }\n\n return {\n toleranceUs,\n activeSamples: active.length,\n preparingSamples: preparing,\n excursions,\n byCause,\n preReleaseSamples,\n preReleaseWorstUs: Math.round(preReleaseWorstUs),\n worstAbsUs: Math.round(worstAbsUs),\n worstAtIntendedUs: Math.round(worstAtIntendedUs),\n overAtIntended,\n passAtIntended: active.length > 0 && overAtIntended === 0,\n meanLatchLagUs: excursions.length\n ? Math.round(latchLagSum / excursions.length)\n : 0,\n // No samples is not a pass. A run that never played has not been judged.\n pass: active.length > 0 && excursions.length === 0,\n uncertainty: {\n frameQuantumUs: Math.round(frameIntervalUs / 2),\n readSpanUs: Math.round(readSpanUs),\n positionQuantumUs: 1000,\n },\n }\n}\n\n/**\n * The samples around one excursion, for looking at rather than inferring from.\n *\n * @experimental\n */\nexport function windowAround(\n samples: readonly SpikeSample[],\n seq: number,\n spanUs = 250_000,\n): SpikeSample[] {\n const centre = samples.find((s) => s.seq === seq)\n if (!centre) return []\n const spanNs = spanUs * 1000\n return samples.filter((s) => Math.abs(s.atNs - centre.atNs) <= spanNs)\n}\n","import { resampleClip, type ResampleRequest } from '@pictor-sdk/video-core'\nimport type { SpikeSample } from './compositionSpike'\nimport type { SubmissionRecord } from './submissionScoring'\n\n/**\n * Did the preview show the right frame at each output instant?\n *\n * ## The oracle this replaces\n *\n * The first version counted *source* frames and expected every one to appear.\n * That reported a 2x clip — 6 seconds of source occupying 3 seconds — as\n * delivering 98 of 180 frames with 58 missed intervals. The 180 was wrong: a\n * preview has its own output rate, and at 30 fps three seconds of timeline has\n * about 90 output instants, at each of which the resampling rule *skips* every\n * second source frame by design. Requiring all 180 asked the preview to run at\n * 60 fps and then scored it for refusing.\n *\n * So the oracle is the shared rule itself. `resampleClip` already decides which\n * source frame belongs at each output instant, at a stated target rate, and it\n * is the same function the exporter's contract is written against. Using it\n * here means the preview and the export cannot disagree about what \"correct\"\n * means.\n *\n * ## What is checked\n *\n * At each expected output instant, whether the picture on screen is the frame\n * the rule names. Not whether every source frame appeared — above 1x most of\n * them must not.\n *\n * @experimental\n */\n\n/** One clip, as the playlist compiled it. @experimental */\nexport interface CadenceClip {\n name: string\n /** Where the window sits on the project timeline. */\n timelineStartUs: number\n timelineEndUs: number\n /** Clipping bounds in source time. */\n sourceStartUs: number\n sourceEndUs: number\n speed: number\n sourceFps: number\n}\n\n/**\n * How far the preview may sit from the export's frame selection.\n *\n * The tolerance the rest of the video path is held to. A preview picture is\n * temporally accurate when the frame it shows is within this of the frame the\n * rule names, converted to project time — not when it is that exact frame.\n *\n * @experimental\n */\nexport const PREVIEW_TOLERANCE_US = 60_000\n\n/**\n * How far a correctly rendered picture may be from its own frame's content.\n *\n * The level tolerance absorbs the codec: a flat grey through H.264 at 4:2:0 and\n * back through a scaler does not return the exact value it went in as. The bar\n * tolerance is a little over one source row at the fixture's height, which is\n * what an interpolating scaler can move an edge by. Neither absorbs a flip, an\n * offset or a wrong scale, which move the bar by whole per cents.\n */\nconst PARITY_LEVEL_TOLERANCE = 12\nconst PARITY_BAR_TOLERANCE = 4 / 960\n\n/**\n * What a run established, as three outcomes rather than two.\n *\n * `invalid` is not a soft failure — it is the absence of a measurement, and it\n * must never be reachable by a run that simply had the probe switched off. A\n * boolean cannot say that: `false` reads as \"measured and wrong\", and three\n * device runs once reported `wrong=0, worstOffset=0.0 ms` because a shifted\n * button meant nothing was scorable at all. That was a clean pass by absence of\n * evidence and one paste from being reported as a fix working.\n *\n * @experimental\n */\nexport type Verdict = 'pass' | 'fail' | 'invalid'\n\n/** Rendering correctness, per clip. @experimental */\nexport interface CadenceParity {\n /** Submissions that carried a parity measurement. */\n measured: number\n /** Worst background-level error, 0..255. */\n worstLevelDelta: number\n /** Worst bar-position error, in 0..1 of height. */\n worstBarDelta: number\n /** Submissions whose rendering was outside tolerance. */\n failures: number\n pass: boolean\n}\n\n/** @experimental */\nexport interface CadenceResult {\n clip: string\n /** Output instants the rule names for the whole clip. */\n nominalInstants: number\n /** Output instants the rule expects across the span actually covered. */\n expectedInstants: number\n /**\n * Instants that could not be placed on the wall clock.\n *\n * No bracketing pair of player-clock samples inside one segment — the\n * transport was paused or seeking across them. Counted and excluded from\n * scoring rather than placed by extrapolation.\n */\n unplaceableInstants: number\n /**\n * How far the frame-derived clock sits from the player's, in microseconds.\n *\n * A diagnostic, never the anchor. Positive means pictures are handed over\n * later than the project time they carry — the pipeline's systematic\n * presentation delay.\n *\n * Anchoring on the frame-derived value would subtract this from every\n * measurement and call a uniformly late pipeline punctual, which is why\n * instants are placed from the player's clock and this is only reported.\n */\n frameDerivedOffsetUs: number\n /**\n * Instants before the first submission, and after the last.\n *\n * Named rather than absorbed. Scoring a clip over its nominal length would\n * count every instant a run never reached as a missed deadline; excluding\n * them silently would let a picture that arrived late or stopped early\n * disappear from coverage instead. They are excluded from the identity check\n * and reported here, and `leadingExcluded` in particular is the interval\n * where the previous clip was still on screen — which is the seam, and is\n * scored by the submission metric rather than by this one.\n */\n leadingExcluded: number\n trailingExcluded: number\n /** Instants where some frame was on screen. */\n coveredInstants: number\n /**\n * Instants showing a different source frame from the one the rule names.\n *\n * The real failure: a missed deadline is the picture being wrong at a moment\n * somebody was looking, not a source frame going unused.\n */\n wrongFrameInstants: number\n /** Instants with no frame at all — the picture had not caught up. */\n uncoveredInstants: number\n /** Unique pictures submitted inside the span. */\n submittedUnique: number\n /**\n * Source frames the rule says to skip.\n *\n * Above 1x this is most of them and is correct. Reported so a count of\n * \"missing\" frames cannot be read as loss.\n */\n ruleSkippedSourceFrames: number\n /** The interval between output instants, from the declared preview rate. */\n outputIntervalUs: number\n medianHoldUs: number\n maxHoldUs: number\n /**\n * How long one picture should stay up, from the rule.\n *\n * At 2x a new source frame is due at every output instant; at 0.5x the rule\n * repeats each one, so a hold of two instants is correct rather than a stall.\n */\n expectedHoldUs: number\n /**\n * Pictures held past their turn.\n *\n * Counted from the holds rather than from the covered span, because a stall\n * at the end of a run produces no further submissions and would otherwise\n * simply shorten the span it was scored over.\n */\n missedDeadlines: number\n /**\n * How far off the shown frame was, counted in source frames.\n *\n * Keyed by `shown - expected`, over every scored instant whose identity was\n * established — the correct ones included, so the shape of the whole run is\n * visible rather than only its tail. This is what separates a picture that is\n * one source frame early from one that is a third of a second stale, and at\n * 2x those are 8.3 ms and 330 ms of project time: the same count of \"wrong\n * instants\" and completely different defects.\n */\n offsets: Record<string, number>\n /**\n * The largest offset, in project time rather than in source frames.\n *\n * The conversion that makes the count mean something. One source frame at\n * speed `s` is `1 / sourceFps / s` of project time — 16.7 ms at 2x, 66.7 ms\n * at 0.5x — so the same offset of three frames is 50 ms in one clip and 200\n * ms in another. Judged against the synchronisation tolerance, not against\n * zero: a preview consumes a live decoder at its own display rate and cannot\n * seek to an exact source frame per instant the way an export does.\n */\n worstOffsetUs: number\n /**\n * Rendering correctness, against the frame each picture's own band names.\n *\n * Separate from everything above it. The rest of this result is about *which*\n * frame was shown; this is about whether that frame was drawn correctly —\n * colour, orientation and scale. A picture one source frame early and\n * rendered perfectly scores zero here.\n */\n parity: CadenceParity\n /**\n * Instants that showed exactly the frame the export rule names.\n *\n * Reported, never required. It is the export contract's measure, kept here so\n * the preview's tolerance is visible as a decision rather than as the absence\n * of a check.\n */\n exactInstants: number\n /**\n * What this run established. @see Verdict\n *\n * `pass` only when identity was read from pixels, parity was measured, and\n * both were clean. Anything unmeasured is `invalid`.\n */\n verdict: Verdict\n /** Why, when the verdict is not a pass. */\n verdictReason: string\n /** Whether the acquired-identity evidence was usable at all. */\n identity: CadenceIdentity\n /**\n * Wall-clock gaps between this clip's submissions.\n *\n * Per clip because the aggregate mixes rates: a 0.5x clip legitimately holds\n * each picture for two output intervals, so a global p95 at 66.7 ms is the\n * 0.5x clip behaving correctly and says nothing about whether the 2x clip\n * stalled. Reported separately so a delay cannot hide inside a hold.\n */\n frameTimeUs: { p50: number; p95: number; p99: number; max: number; samples: number }\n /**\n * Every instant whose picture was not the one the rule names.\n *\n * Including the boundary instants the scored span excludes: those are the\n * seam, and leaving them out of the identity check is how a wrong picture at\n * a clip change disappears from both metrics at once.\n */\n wrong: WrongInstant[]\n pass: boolean\n}\n\n/**\n * Why an instant showed the wrong picture.\n *\n * The first three are decided in pixels and name one place to look. The fourth\n * is what the metadata alone can support, which is less than it appears: on\n * this device `SurfaceTexture` reports a system-clock value, so an instant\n * classified without the marker cannot separate a frame acquired wrongly from a\n * frame chosen wrongly, and says so rather than picking one.\n *\n * @experimental\n */\nexport type WrongInstantCause =\n /**\n * The expected frame's pixels were not in the ring when the instant needed\n * them.\n *\n * Nothing downstream could have shown it. The fault is decode, acquisition or\n * the copy into the ring — and the test is by that instant, not by the end of\n * the clip, because every frame arrives eventually and a clip-global check\n * would charge a frame that had simply not arrived yet to selection.\n */\n | 'not-delivered'\n /**\n * The expected frame's pixels were in the ring, and a different frame was\n * submitted.\n *\n * The fault is the choice, not the supply.\n */\n | 'wrong-selection'\n /**\n * The submitted picture no longer held the frame its slot was filled with.\n *\n * A wrapped texture aliases storage the ring may redraw. This is that storage\n * changing under a picture composition still holds, and it is the one cause\n * here that is a correctness defect in the ring's own contract.\n */\n | 'storage-aliased'\n /**\n * The wrong picture, and the evidence cannot say whether it was acquired or\n * chosen wrongly.\n *\n * What the metadata supports with the probe off. Not a third fault: a\n * placeholder for an attribution not yet made.\n */\n | 'wrong-submitted-identity'\n /**\n * The right frame, submitted after the instant it belonged to.\n *\n * Only reachable without pixel identity. When the marker is readable, a frame\n * matching the rule makes the instant correct however late it was submitted,\n * so lateness stops being an identity fault and becomes a frame-time one.\n * Its evidence — the ring's own submission time and project timestamp — is\n * independent of the disputed pairing, which is why it survives there.\n */\n | 'late-delivery'\n /** No submission covered the instant at all. */\n | 'uncovered'\n\n/** @experimental */\nexport interface WrongInstant {\n projectUs: number\n /** From the shared rule: the source frame that belongs at this instant. */\n expectedSourceFrame: number\n expectedSourcePtsUs: number\n /** What was on screen, mapped back through the same speed. */\n shownSourceFrame: number\n shownPtsUs: number\n /** What the texture said it held when that frame was acquired. */\n acquiredTexturePtsUs: number\n /** The frame index in the pixels of the slot, and of the submitted picture. */\n acquiredMarker: number\n submittedMarker: number\n slot: number\n frameId: number\n submittedNs: number\n cause: WrongInstantCause\n /** True when the instant lies outside the scored span. */\n boundary: boolean\n}\n\n/** What the texture timestamp turned out to be. @experimental */\nexport type TimestampDomain =\n /** Comparable with the clip's media time. */\n | 'media'\n /** A system-clock value: `System.nanoTime()`, as measured on this device. */\n | 'system'\n /** Nothing was recorded, so there is nothing to establish. */\n | 'unknown'\n\n/** @experimental */\nexport interface CadenceIdentity {\n /**\n * The domain the texture timestamps were established to be in.\n *\n * Recorded rather than assumed, and recorded even when it is the one that\n * makes the check weaker. It was `media` by assumption for one whole run and\n * the assumption was wrong by forty-eight hours.\n */\n timestampDomain: TimestampDomain\n medianTextureSkewUs: number\n /**\n * Whether identity came from the pixels.\n *\n * When false, the fallback compares submitted timestamps — which on a\n * `system` domain cannot uniquely associate a buffer with a frame. Every\n * instant is then counted `identityUnverified` and the clip cannot pass. A\n * fallback that quietly declared those instants correct would be restoring\n * confidence the evidence does not support.\n */\n pixelIdentity: boolean\n /** Instants whose identity could not be established either way. */\n identityUnverified: number\n /** Distinct frame indices read out of the ring for this clip. */\n deliveredFrames: number\n /** Bands that were read, and bands that failed. */\n markersRead: number\n markersFailed: number\n}\n\n/**\n * The preview's own output rate.\n *\n * Stated rather than inferred from what arrived: an oracle that takes its\n * expectation from the measurement cannot fail.\n *\n * @experimental\n */\nexport const PREVIEW_OUTPUT_FPS = 30\n\n/**\n * Which source frame was on screen at `projectUs`, from what was submitted.\n *\n * The last submission at or before the instant — the same \"last one at or\n * before\" rule `resampleClip` samples the source with, because a nearest-match\n * would credit a frame that had not been shown yet.\n */\n/**\n * When an instant of project time happened, on the monotonic clock.\n *\n * Built from the player's own reported position paired with the monotonic time\n * it was read at — two values the pump records together and neither derived\n * from a frame. That independence is the point: an offset computed from\n * submission records folds any systematic presentation delay into the anchor,\n * so a pipeline uniformly late looks punctual to itself.\n *\n * Segmented by epoch and by whether the transport was running. A pause or a\n * seek breaks the correspondence between project time and the wall clock\n * entirely, and interpolating across one would place an instant at a moment\n * that never occurred. Instants with no bracketing pair inside a single segment\n * return null rather than a guess.\n */\nfunction wallClockAt(\n samples: readonly SpikeSample[],\n projectUs: number,\n intervalUs: number,\n): number | null {\n let before: SpikeSample | null = null\n let after: SpikeSample | null = null\n for (const s of samples) {\n if (!s.playing || s.positionUs < 0 || s.atNs <= 0) continue\n if (s.positionUs <= projectUs && (before === null || s.positionUs > before.positionUs)) before = s\n if (s.positionUs >= projectUs && (after === null || s.positionUs < after.positionUs)) after = s\n }\n /*\n One interval past the last sample, and no further.\n\n The final instant of a clip has nothing after it to bracket with, and\n refusing it would make every real run end in an unplaceable instant and\n report `invalid`. Extrapolating one interval at the project rate is exact\n enough for a span that short; beyond it the transport may have stopped and\n there is no evidence either way.\n */\n if (after === null) {\n if (before === null || projectUs - before.positionUs > intervalUs) return null\n return before.atNs + (projectUs - before.positionUs) * 1000\n }\n /*\n And one interval before the first sample, symmetrically.\n\n An instant can land just ahead of the earliest reading — the output grid and\n the poller are not aligned, and the grid's rounding puts one instant of a\n clip on the wrong side of it. The same bound applies: inside an interval the\n project rate places it exactly, beyond it there is no evidence.\n */\n if (before === null) {\n if (after.positionUs - projectUs > intervalUs) return null\n return after.atNs - (after.positionUs - projectUs) * 1000\n }\n // Different segments: a pause or a seek sits between them.\n if (before.epoch !== after.epoch) return null\n if (after.positionUs === before.positionUs) return before.atNs\n const t = (projectUs - before.positionUs) / (after.positionUs - before.positionUs)\n return before.atNs + t * (after.atNs - before.atNs)\n}\n\n/**\n * The picture on screen at an instant — as of that instant.\n *\n * Selected by when it was submitted, not only by what time it claims. Choosing\n * the greatest `ptsUs` at or before the instant let a frame submitted long\n * afterwards be credited to it: the screen showed something older at the time,\n * and the late arrival made the instant correct retroactively. A frame that had\n * not been handed to composition yet was not on screen, whatever its timestamp\n * says.\n *\n * The instant's wall-clock time is derived from the clip's first submission and\n * the project rate, which is 1x for the composition — the same assumption the\n * audio pairing documents and relies on.\n */\nfunction displayedAt(\n submissions: readonly SubmissionRecord[],\n projectUs: number,\n atNs: number | null,\n intervalNs: number,\n): SubmissionRecord | null {\n /*\n One output interval of slack, because an instant is an interval.\n\n An output instant is not a point in time — it is the span during which its\n picture should be on screen. A frame handed over anywhere inside that span\n was shown for part of it, and excluding it would charge the grid's own\n resolution to the pipeline. A frame arriving after the span has closed was\n never on screen for it, which is the case this rule exists to exclude.\n */\n if (atNs === null) return null\n const deadlineNs = atNs + intervalNs\n let best: SubmissionRecord | null = null\n for (const s of submissions) {\n if (s.ptsUs > projectUs) continue\n // Not yet submitted when the instant passed: it was not the picture.\n if (s.submittedNs > deadlineNs) continue\n if (best === null || s.submittedNs > best.submittedNs) best = s\n }\n return best\n}\n\n/** @experimental */\nexport function checkCadence(\n clip: CadenceClip,\n submissions: readonly SubmissionRecord[],\n outputFps = PREVIEW_OUTPUT_FPS,\n /**\n * How far the shown frame may be from the named one, in project time.\n *\n * Zero scores the export contract — exact selection, no tolerance — and the\n * default scores the preview's. @see PREVIEW_TOLERANCE_US\n */\n toleranceUs = PREVIEW_TOLERANCE_US,\n /**\n * The player's own clock, paired with monotonic time.\n *\n * Where instants are placed on the wall clock. Empty means no instant can be\n * placed, and the result is `invalid` rather than scored against a guess.\n */\n samples: readonly SpikeSample[] = [],\n): CadenceResult {\n const request: ResampleRequest = {\n sourceStartUs: clip.sourceStartUs as never,\n sourceEndUs: clip.sourceEndUs as never,\n speed: clip.speed,\n sourceFps: clip.sourceFps,\n targetFps: outputFps,\n }\n const plan = resampleClip(request)\n const outputIntervalUs = Math.round(1_000_000 / outputFps)\n\n const inClip = submissions\n .filter((s) => s.ptsUs >= clip.timelineStartUs && s.ptsUs < clip.timelineEndUs)\n .sort((a, b) => a.ptsUs - b.ptsUs)\n\n /*\n Is the texture's timestamp even in media time?\n\n On this device it is not: `SurfaceTexture.getTimestamp` returns a\n `System.nanoTime`-based value, about forty-nine hours of uptime, in the\n domain of the release time rather than of the presentation timestamp.\n Treating it as media time called every instant a wrong acquisition — a\n broken check reporting a broken pipeline.\n\n So the domain is tested rather than assumed, and when it does not hold the\n identity check falls back to the paired metadata and says it could not\n separate a wrong acquisition from a wrong promotion.\n */\n const skews = inClip\n .filter((s) => s.texturePtsUs >= 0)\n .map((s) => Math.abs(s.texturePtsUs - s.ptsUs))\n .sort((a, b) => a - b)\n const medianSkewUs = skews.length > 0 ? skews[Math.floor(skews.length / 2)]! : -1\n const timestampDomain: TimestampDomain =\n skews.length === 0 ? 'unknown' : medianSkewUs < 1_000_000 ? 'media' : 'system'\n const unique = new Set(inClip.map((s) => s.ptsUs))\n\n /*\n What the pixels say arrived, and what they say was shown.\n\n `delivered` is every distinct frame index the ring actually held during this\n clip. It is what separates a frame that was never supplied from a frame that\n was supplied and not chosen — the distinction the timestamps could not make,\n and the reason for the band in the first place.\n */\n const delivered = new Set<number>()\n let markersRead = 0\n let markersFailed = 0\n for (const record of inClip) {\n if (record.acquiredMarker >= 0) delivered.add(record.acquiredMarker)\n if (record.submittedMarker >= 0) markersRead += 1\n else if (record.markerFailure !== '' && record.markerFailure !== 'unread') markersFailed += 1\n }\n const pixelIdentity = markersRead > 0\n\n /*\n Scored across the span actually covered, not the clip's nominal length.\n\n A run that stopped early, or a seek that landed mid-clip, would otherwise\n report every instant it never reached as a missed deadline.\n */\n const first = inClip.length > 0 ? inClip[0]!.ptsUs : 0\n const last = inClip.length > 0 ? inClip[inClip.length - 1]!.ptsUs : 0\n /*\n The frame-derived offset, kept only as a cross-check.\n\n It was the anchor once, and it should not have been: an offset computed from\n submission records folds any systematic presentation delay into itself, so a\n pipeline uniformly late measures as punctual. Instants are placed from the\n player's own clock instead, and this is reported beside it — a growing\n difference between the two *is* the presentation delay, which is worth\n seeing rather than absorbing.\n */\n const clockOffsets = inClip\n .filter((r) => r.submittedNs > 0)\n .map((r) => r.submittedNs - r.ptsUs * 1000)\n .sort((a, b) => a - b)\n const playerOffsets = samples\n .filter((s) => s.playing && s.atNs > 0 && s.positionUs >= 0)\n .map((s) => s.atNs - s.positionUs * 1000)\n .sort((a, b) => a - b)\n /*\n The difference between the two clocks, not either one's absolute value.\n\n Both are monotonic-minus-project offsets and both are dominated by uptime —\n reported raw, one read as 203,687 seconds, which says nothing. What is worth\n seeing is how far apart they sit: the player says when a moment of project\n time occurred, the frames say when a picture for it was handed over, and the\n gap is the pipeline's systematic presentation delay. Anchoring on the frames\n would subtract exactly that and call the result punctual.\n */\n const frameDerivedOffsetNs =\n clockOffsets.length > 0 && playerOffsets.length > 0\n ? clockOffsets[Math.floor(clockOffsets.length / 2)]! -\n playerOffsets[Math.floor(playerOffsets.length / 2)]!\n : 0\n\n let expectedInstants = 0\n let leadingExcluded = 0\n let trailingExcluded = 0\n let covered = 0\n let wrong = 0\n let unverified = 0\n let unplaceable = 0\n let exact = 0\n const offsets: Record<string, number> = {}\n // One source frame, in project time: 16.7 ms at 2x, 66.7 ms at 0.5x. The\n // conversion that makes an offset comparable with the tolerance.\n const sourceIntervalUs = clip.sourceFps > 0 ? 1_000_000 / clip.sourceFps / clip.speed : 0\n const wrongInstants: WrongInstant[] = []\n for (const frame of plan.frames) {\n const projectUs = clip.timelineStartUs + Number(frame.outputPtsUs)\n if (inClip.length === 0) continue\n const boundary = projectUs < first || projectUs > last\n if (projectUs < first) leadingExcluded += 1\n else if (projectUs > last) trailingExcluded += 1\n else expectedInstants += 1\n\n const instantNs = wallClockAt(samples, projectUs, outputIntervalUs)\n if (instantNs === null && !boundary) unplaceable += 1\n const shown =\n instantNs === null ? null : displayedAt(inClip, projectUs, instantNs, outputIntervalUs * 1000)\n if (shown === null) {\n /*\n The boundary instants are checked too.\n\n They are excluded from the *coverage* arithmetic because the run never\n reached them, but their identity still matters: a leading instant is the\n seam, where the previous clip is still on screen, and dropping it from\n both metrics is how a wrong picture at a clip change goes unseen.\n */\n if (boundary) {\n wrongInstants.push({\n projectUs,\n expectedSourceFrame: frame.sourceFrameIndex,\n expectedSourcePtsUs: Number(frame.sourceTimeUs),\n shownSourceFrame: -1,\n shownPtsUs: -1,\n acquiredTexturePtsUs: -1,\n acquiredMarker: -1,\n submittedMarker: -1,\n slot: -1,\n frameId: -1,\n submittedNs: 0,\n cause: 'uncovered',\n boundary,\n })\n }\n continue\n }\n if (!boundary) covered += 1\n\n /*\n Identity by source frame, not by timestamp.\n\n The submitted PTS is project time and the rule names a source frame, so\n the comparison maps the shown picture back through the same speed the\n compiler applied. A tolerance of one source frame absorbs the rounding\n between the two directions.\n */\n /*\n What the texture holds, not what the record claims it holds.\n\n The two are usually the same and the case where they are not is exactly a\n wrong acquisition: the picture on screen is the texture's content, and\n scoring the record's timestamp instead would call that instant correct.\n */\n const shownProjectUs = timestampDomain === 'media' ? shown.texturePtsUs : shown.ptsUs\n const shownSourceUs = clip.sourceStartUs + (shownProjectUs - clip.timelineStartUs) * clip.speed\n /*\n The marker when there is one, the timestamp only when there is not.\n\n A read band names the frame outright; the derived index is an inference\n from the very association under test. When neither is available the\n instant is unverified rather than assumed correct.\n */\n const derivedIndex = Math.floor((shownSourceUs * clip.sourceFps) / 1_000_000)\n const shownIndex = shown.submittedMarker >= 0 ? shown.submittedMarker : derivedIndex\n const verified = shown.submittedMarker >= 0 || timestampDomain === 'media'\n if (!verified) {\n if (!boundary) unverified += 1\n continue\n }\n /*\n Counted before it is judged, and counted whether or not it passes.\n\n A pixel oracle can be exact, so it is: the rule names one source frame and\n the band says which one arrived. But \"wrong\" spans a frame that is 8.3 ms\n early and a frame that is a third of a second stale, and a count alone\n cannot tell them apart.\n */\n const offset = shownIndex - frame.sourceFrameIndex\n /*\n Over the scored span only.\n\n A boundary instant is one the run never reached, and what is on screen\n there is the previous clip or the last picture of this one — an offset of\n a few hundred frames that is not jitter and would swamp the distribution.\n Those instants are still checked; they appear in `wrong` flagged\n `boundary`, which is where they belong.\n */\n if (!boundary) {\n const key = String(offset)\n offsets[key] = (offsets[key] ?? 0) + 1\n }\n\n /*\n Two contracts, and this is the preview's.\n\n An export selects the exact frame `resampleClip` names: it drives the\n decoder to a source time and gets that frame. A preview consumes a live\n decoder at its own display rate, and above 1x the frame the rule names is\n usually never produced at the instant it is wanted — measured at 2x, 31 to\n 37 of about 90 instants wanted a frame no slot ever held, against 0 to 1\n the ring genuinely mis-chose. Requiring exact identity would be asking the\n preview to seek per frame.\n\n So the preview is judged temporally: the picture must be within\n `PREVIEW_TOLERANCE_US` of project time from the frame the rule names, and\n dropping frames during accelerated playback is permitted. This is an\n acceptance decision, not a discovery that the identity check passed —\n `exactInstants` and the offset distribution are reported either way, and\n an export must still be scored against exact selection.\n */\n const offsetUs = Math.abs(offset) * sourceIntervalUs\n // The derived index still needs its rounding slack; a band is exact.\n const slack = shown.submittedMarker >= 0 ? 0 : sourceIntervalUs\n if (offset === 0) exact += 1\n if (offsetUs <= Math.max(toleranceUs, slack)) continue\n if (!boundary) wrong += 1\n\n /*\n Three different faults, and they send the fix to three different places.\n\n The texture's own timestamp is what separates them: if it disagrees with\n the metadata the frame was recorded under, the pairing was wrong before\n the ring saw it. If it agrees and a different frame is on screen, the ring\n chose wrongly. If the right frame is on screen but arrived after its\n instant, nothing chose wrongly and it was simply late.\n */\n const acquired = shown.texturePtsUs\n /*\n Late delivery first, because its evidence stands on its own.\n\n Both terms are the ring's own records — when it submitted, and what\n project time it recorded the frame under — and neither comes from the\n texture. It survives the timestamp-domain problem intact, which is why it\n stays a separate cause rather than folding into the unresolved one.\n */\n const late = shown.submittedNs > 0 && shown.ptsUs < projectUs - outputIntervalUs\n /*\n Available *by this instant*, not somewhere in the clip.\n\n A clip-global check answers the wrong question: every frame arrives\n eventually, so nothing is ever \"not delivered\" and a picture that simply\n had not arrived yet is charged to selection. What decides the fault is\n whether the named frame was in the ring when the instant needed it.\n */\n const hadItByThen = inClip.some(\n (r) =>\n r.acquiredMarker === frame.sourceFrameIndex &&\n r.acquiredNs > 0 &&\n r.acquiredNs <= shown.submittedNs,\n )\n /*\n Three outcomes in pixels, and only three.\n\n With identity read from the picture, \"the right frame submitted late\"\n cannot produce a wrong instant: if the marker matches the rule's frame the\n instant is correct, whenever it was submitted. So the pixel path asks only\n whether the storage changed, whether the frame was there to show, and\n whether something else was shown instead. `late-delivery` belongs to the\n timestamp path, where identity is inferred from when a frame was recorded\n rather than from what it contains.\n */\n let cause: WrongInstantCause\n if (shown.submittedMarker >= 0) {\n if (shown.acquiredMarker >= 0 && shown.submittedMarker !== shown.acquiredMarker) {\n cause = 'storage-aliased'\n } else if (!hadItByThen) cause = 'not-delivered'\n else cause = 'wrong-selection'\n } else if (late) cause = 'late-delivery'\n else cause = 'wrong-submitted-identity'\n\n wrongInstants.push({\n projectUs,\n expectedSourceFrame: frame.sourceFrameIndex,\n expectedSourcePtsUs: Number(frame.sourceTimeUs),\n shownSourceFrame: shownIndex,\n shownPtsUs: shownProjectUs,\n acquiredTexturePtsUs: acquired,\n acquiredMarker: shown.acquiredMarker,\n submittedMarker: shown.submittedMarker,\n slot: shown.slot,\n frameId: shown.frameId,\n submittedNs: shown.submittedNs,\n cause,\n boundary,\n })\n }\n\n const walls: number[] = []\n for (let i = 1; i < inClip.length; i += 1) {\n const gap = (inClip[i]!.submittedNs - inClip[i - 1]!.submittedNs) / 1000\n if (gap > 0) walls.push(gap)\n }\n walls.sort((a, b) => a - b)\n const tail = (fraction: number) =>\n walls.length === 0\n ? 0\n : Math.round(walls[Math.min(walls.length - 1, Math.floor(walls.length * fraction))]!)\n const frameTimes = {\n p50: tail(0.5),\n p95: tail(0.95),\n p99: tail(0.99),\n max: walls.length > 0 ? Math.round(walls[walls.length - 1]!) : 0,\n samples: walls.length,\n }\n\n /*\n Parity over the submissions, not over the instants.\n\n Rendering is a property of a picture, and the same picture can cover several\n instants; scoring it once per instant would weight a held frame more heavily\n than a fleeting one for no reason.\n */\n let parityMeasured = 0\n let worstLevelDelta = 0\n let worstBarDelta = 0\n let parityFailures = 0\n for (const record of inClip) {\n if (record.levelDelta < 0 || record.barDelta < 0) continue\n parityMeasured += 1\n if (record.levelDelta > worstLevelDelta) worstLevelDelta = record.levelDelta\n if (record.barDelta > worstBarDelta) worstBarDelta = record.barDelta\n if (record.levelDelta > PARITY_LEVEL_TOLERANCE || record.barDelta > PARITY_BAR_TOLERANCE) {\n parityFailures += 1\n }\n }\n\n const stamps = [...unique].sort((a, b) => a - b)\n const holds: number[] = []\n for (let i = 1; i < stamps.length; i += 1) holds.push(stamps[i]! - stamps[i - 1]!)\n const repeats = Math.max(1, Math.round(1 / clip.speed))\n const expectedHoldUs = outputIntervalUs * repeats\n const missedDeadlines = holds.filter((h) => h > expectedHoldUs * 1.5).length\n holds.sort((a, b) => a - b)\n\n /*\n Nothing measured is not a failing measurement.\n\n Checked before anything is judged: a run with no submissions, no readable\n band, or no parity reading has established nothing about this clip, and\n saying `fail` would be as wrong as saying `pass`.\n */\n let verdict: Verdict = 'pass'\n let verdictReason = ''\n if (expectedInstants === 0) {\n verdict = 'invalid'\n verdictReason = 'no instants covered'\n } else if (!pixelIdentity) {\n verdict = 'invalid'\n verdictReason = 'identity not read from pixels'\n } else if (parityMeasured === 0) {\n verdict = 'invalid'\n verdictReason = 'rendering parity not measured'\n } else if (unplaceable > 0) {\n verdict = 'invalid'\n verdictReason = `${unplaceable} instants could not be placed on the clock`\n } else if (unverified > 0) {\n verdict = 'invalid'\n verdictReason = `${unverified} instants unverified`\n } else if (wrong > 0) {\n verdict = 'fail'\n verdictReason = `${wrong} wrong instants`\n } else if (covered !== expectedInstants) {\n verdict = 'fail'\n verdictReason = `${expectedInstants - covered} instants uncovered`\n } else if (missedDeadlines > 0) {\n verdict = 'fail'\n verdictReason = `${missedDeadlines} missed deadlines`\n } else if (parityFailures > 0) {\n verdict = 'fail'\n verdictReason = `${parityFailures} parity failures`\n }\n\n return {\n clip: clip.name,\n nominalInstants: plan.frames.length,\n leadingExcluded,\n trailingExcluded,\n expectedInstants,\n coveredInstants: covered,\n wrongFrameInstants: wrong,\n uncoveredInstants: expectedInstants - covered,\n submittedUnique: unique.size,\n ruleSkippedSourceFrames: plan.droppedSourceFrameCount,\n outputIntervalUs,\n medianHoldUs: holds.length > 0 ? holds[Math.floor(holds.length / 2)]! : 0,\n maxHoldUs: holds.length > 0 ? holds[holds.length - 1]! : 0,\n expectedHoldUs,\n missedDeadlines,\n parity: {\n measured: parityMeasured,\n worstLevelDelta,\n worstBarDelta: Number(worstBarDelta.toFixed(5)),\n failures: parityFailures,\n // No measurement is not a pass: a run that never looked has not looked.\n pass: parityMeasured > 0 && parityFailures === 0,\n },\n offsets,\n unplaceableInstants: unplaceable,\n frameDerivedOffsetUs: Math.round(frameDerivedOffsetNs / 1000),\n exactInstants: exact,\n worstOffsetUs: Math.round(\n Object.keys(offsets).reduce((worst, k) => Math.max(worst, Math.abs(Number(k))), 0) *\n (clip.sourceFps > 0 ? 1_000_000 / clip.sourceFps / clip.speed : 0),\n ),\n identity: {\n timestampDomain,\n medianTextureSkewUs: medianSkewUs,\n pixelIdentity,\n identityUnverified: unverified,\n deliveredFrames: delivered.size,\n markersRead,\n markersFailed,\n },\n frameTimeUs: frameTimes,\n wrong: wrongInstants,\n // Every expected instant covered, by the frame the shared rule names, and\n // no picture held past its turn.\n /*\n An unverified instant is not a passing one.\n\n With the probe off and the timestamps in the system domain there is no\n evidence about which frame was shown, and a check that reported that\n absence as success would be the same mistake as the one the band was\n built to correct.\n */\n /*\n The preview contract: temporally accurate, correctly rendered, covered.\n\n Exact export-frame identity is deliberately absent. `exactInstants` and\n `offsets` record it, and `@see PREVIEW_TOLERANCE_US` for why requiring it\n here would be requiring a per-frame seek.\n */\n pass: verdict === 'pass',\n verdict,\n verdictReason,\n }\n}\n","import type { SpikeSample } from './compositionSpike'\n\n/**\n * Score the picture where it is handed to composition.\n *\n * ## The boundary, stated\n *\n * **Submission**, not scanout. A frame is scored at the instant composition\n * begins consuming it; the compositor presents it some time later and nothing\n * in this process observes that. The delay is common to every frame, so it\n * shifts the whole series rather than spreading it — but it is not measured\n * here and is not subtracted from anything.\n *\n * ## Why an interval rather than a point\n *\n * A frame is not a sample of one instant; it is the correct picture for a span\n * of project time. Comparing a continuous audio clock against a single\n * timestamp charges the frame's own duration to synchronisation, which is why\n * the 0.5x clip — whose frames are correct for 66.7 ms each — looked worse than\n * the 1x clip at identical quality.\n *\n * So a submitted frame carries `[ptsUs, ptsUs + intervalUs)`, and the error is\n * how far outside that span the audio clock sits. Zero inside, and the distance\n * to the nearer edge outside. The interval comes from the measured cadence,\n * which for a clip of speed `s` and source interval `i` is `i / s` — 33.3 ms at\n * 1x, 66.7 ms at 0.5x.\n *\n * @experimental\n */\n\n/** One submission, as the consumer recorded it. @experimental */\nexport interface SubmissionRecord {\n frameId: number\n epoch: number\n ptsUs: number\n intervalUs: number\n submittedNs: number\n /** What the texture reported holding at acquisition. */\n texturePtsUs: number\n /** Which ring slot carried it. */\n slot: number\n occupancy: number\n /**\n * The frame index read out of the slot's pixels when it was acquired.\n *\n * -1 when the probe was off or the band could not be read; `markerFailure`\n * says which.\n */\n acquiredMarker: number\n /** The frame index read out of the picture actually handed to composition. */\n submittedMarker: number\n /** Empty when the band was read; otherwise why it was not. */\n markerFailure: string\n /**\n * The picture's whole life, so one row can be attributed.\n *\n * When it was acquired, when it was due for presentation, when it was\n * promoted. Without these, a late submission cannot be separated into a late\n * arrival, a long wait for its due time, and a slow promotion.\n */\n acquiredNs: number\n dueNs: number\n promotedNs: number\n /**\n * Rendering error against the frame the submitted band names: background\n * level in 0..255, bar position in 0..1 of height. -1 when not measured.\n */\n levelDelta: number\n barDelta: number\n}\n\n/** @experimental */\nexport interface ScoredSubmission {\n frameId: number\n ptsUs: number\n intervalUs: number\n submittedNs: number\n /** The audio clock at the submission instant, on the same monotonic timeline. */\n audioUs: number\n /** 0 inside the interval; otherwise the distance to the nearer edge. */\n errorUs: number\n /** Positive when the sound is past the frame's span — the picture is behind. */\n signedUs: number\n occupancy: number\n}\n\n/** @experimental */\nexport interface SubmissionReport {\n toleranceUs: number\n scored: number\n /** Submissions with no audio sample close enough to pair with. */\n unpaired: number\n excursions: ScoredSubmission[]\n worstErrorUs: number\n pass: boolean\n /**\n * What this run established, as three outcomes.\n *\n * A score computed from nothing is not a pass. Zero scored submissions means\n * the run did not observe playback, and that has to be distinguishable from\n * a run that observed it and found it clean.\n */\n verdict: 'pass' | 'fail' | 'invalid'\n verdictReason: string\n /** Gaps between consecutive submitted timestamps: the delivered cadence. */\n cadenceUs: { median: number; min: number; max: number; samples: number }\n /**\n * Wall-clock gaps between consecutive submissions.\n *\n * The tail is the point: a median at the output interval says nothing about\n * the frame that took four times as long, and that frame is the one somebody\n * sees. Taken from `submittedNs`, which is the instant composition began\n * consuming each picture.\n */\n frameTimeUs: { p50: number; p95: number; p99: number; max: number; samples: number }\n peakOccupancy: number\n}\n\n/**\n * The audio clock at an arbitrary instant, from the paired samples around it.\n *\n * The composition runs at project rate 1x, so the clock is linear between\n * samples and interpolation is exact rather than an approximation. Returns null\n * when there is no sample near enough to interpolate from, which is reported\n * rather than guessed.\n */\nfunction audioAt(\n samples: readonly SpikeSample[],\n atNs: number,\n windowNs: number,\n): number | null {\n let before: SpikeSample | null = null\n let after: SpikeSample | null = null\n for (const s of samples) {\n if (!s.playing) continue\n if (s.atNs <= atNs && (before === null || s.atNs > before.atNs)) before = s\n if (s.atNs >= atNs && (after === null || s.atNs < after.atNs)) after = s\n }\n if (before !== null && after !== null && after.atNs > before.atNs) {\n const spanUs = (after.atNs - before.atNs) / 1000\n const movedUs = after.positionUs - before.positionUs\n /*\n Never interpolate across a seek.\n\n Between two samples the clock advances with the wall clock, because the\n composition runs at project rate 1x. When it does not, it was *moved* —\n and interpolating across that produced a 2.7 second \"error\" for a frame\n that was correct when it was handed over: the picture was at 23.9 s and\n the position it was paired with had already jumped back to 21.2 s.\n\n Detected from the clock itself rather than from an epoch number, because\n the consumer's frame generation and the player's transition counter are\n different counters and equal values between them mean nothing.\n */\n if (Math.abs(movedUs - spanUs) <= DISCONTINUITY_US) {\n const t = (atNs - before.atNs) / (after.atNs - before.atNs)\n return before.positionUs + t * movedUs\n }\n // A jump: use the sample on the submission's own side of it.\n return before.positionUs + (atNs - before.atNs) / 1000\n }\n const nearest = before ?? after\n if (nearest === null) return null\n if (Math.abs(nearest.atNs - atNs) > windowNs) return null\n // Rate 1x, so extrapolating the short distance to the edge sample is exact.\n return nearest.positionUs + (atNs - nearest.atNs) / 1000\n}\n\n/**\n * How far the clock may diverge from the wall clock and still be running.\n *\n * Beyond this it was moved rather than advanced, which is a seek.\n */\nconst DISCONTINUITY_US = 100_000\n\n/** @experimental */\nexport function scoreSubmissions(\n submissions: readonly SubmissionRecord[],\n samples: readonly SpikeSample[],\n options: { toleranceUs?: number; pairWindowNs?: number } = {},\n): SubmissionReport {\n const toleranceUs = options.toleranceUs ?? 60_000\n const pairWindowNs = options.pairWindowNs ?? 50_000_000\n\n const scored: ScoredSubmission[] = []\n const excursions: ScoredSubmission[] = []\n let unpaired = 0\n let worstErrorUs = 0\n let peakOccupancy = 0\n\n for (const record of submissions) {\n if (record.ptsUs < 0 || record.submittedNs <= 0) continue\n if (record.occupancy > peakOccupancy) peakOccupancy = record.occupancy\n const audioUs = audioAt(samples, record.submittedNs, pairWindowNs)\n if (audioUs === null) {\n unpaired += 1\n continue\n }\n /*\n Inside the span is not an error of any size.\n\n The frame is the correct picture for the whole interval, so a clock\n anywhere within it is synchronised. Scoring the distance to `ptsUs`\n instead would report the frame's own duration as drift.\n */\n const start = record.ptsUs\n const end = record.ptsUs + Math.max(record.intervalUs, 0)\n const signedUs = audioUs < start ? audioUs - start : audioUs > end ? audioUs - end : 0\n const errorUs = Math.abs(signedUs)\n const entry: ScoredSubmission = {\n frameId: record.frameId,\n ptsUs: record.ptsUs,\n intervalUs: record.intervalUs,\n submittedNs: record.submittedNs,\n audioUs: Math.round(audioUs),\n errorUs: Math.round(errorUs),\n signedUs: Math.round(signedUs),\n occupancy: record.occupancy,\n }\n scored.push(entry)\n if (errorUs > worstErrorUs) worstErrorUs = errorUs\n if (errorUs > toleranceUs) excursions.push(entry)\n }\n\n const gaps: number[] = []\n const frameTimes: number[] = []\n for (let i = 1; i < scored.length; i += 1) {\n const gap = scored[i]!.ptsUs - scored[i - 1]!.ptsUs\n if (gap > 0) gaps.push(gap)\n const wall = (scored[i]!.submittedNs - scored[i - 1]!.submittedNs) / 1000\n if (wall > 0) frameTimes.push(wall)\n }\n gaps.sort((a, b) => a - b)\n frameTimes.sort((a, b) => a - b)\n const at = (fraction: number) =>\n frameTimes.length === 0\n ? 0\n : Math.round(frameTimes[Math.min(frameTimes.length - 1, Math.floor(frameTimes.length * fraction))]!)\n\n return {\n toleranceUs,\n scored: scored.length,\n unpaired,\n excursions,\n worstErrorUs: Math.round(worstErrorUs),\n // No submissions is not a pass: a run that showed nothing was not judged.\n pass: scored.length > 0 && excursions.length === 0,\n verdict: scored.length === 0 ? 'invalid' : excursions.length === 0 ? 'pass' : 'fail',\n verdictReason:\n scored.length === 0\n ? 'no submissions scored'\n : excursions.length === 0\n ? ''\n : `${excursions.length} excursions, worst ${Math.round(worstErrorUs / 1000)} ms`,\n cadenceUs: {\n median: gaps.length > 0 ? gaps[Math.floor(gaps.length / 2)]! : 0,\n min: gaps.length > 0 ? gaps[0]! : 0,\n max: gaps.length > 0 ? gaps[gaps.length - 1]! : 0,\n samples: gaps.length,\n },\n frameTimeUs: {\n p50: at(0.5),\n p95: at(0.95),\n p99: at(0.99),\n max: frameTimes.length > 0 ? Math.round(frameTimes[frameTimes.length - 1]!) : 0,\n samples: frameTimes.length,\n },\n peakOccupancy,\n }\n}\n\n/** One submission, with its whole life and the clock it was scored against. @experimental */\nexport interface TraceRow {\n frameId: number\n /** Offsets in ms from the window's first row, so the numbers are readable. */\n acquiredMs: number\n dueMs: number\n promotedMs: number\n submittedMs: number\n /**\n * Acquisition to due, due to promotion, promotion to submission, in us.\n *\n * A negative `waitUs` means the presentation time had already passed when the\n * frame was acquired — it was late before the ring saw it.\n */\n waitUs: number\n promoteUs: number\n submitUs: number\n ptsUs: number\n audioUs: number\n errorUs: number\n slot: number\n occupancy: number\n marker: number\n}\n\n/** @experimental */\nexport interface SubmissionTrace {\n /** The frame the window is centred on. */\n frameId: number\n rows: TraceRow[]\n /**\n * Where the centre frame's budget went, as the largest single interval.\n *\n * `arrival` when it was acquired late, `due` when it sat waiting for its\n * presentation time, `promotion` when it was due and not chosen, `submission`\n * when promotion and submission diverged. Empty when the row is incomplete.\n */\n spentOn: 'arrival' | 'due' | 'promotion' | 'submission' | 'unknown'\n /** The dominant leg's size, so a label cannot imply a magnitude it lacks. */\n spentUs: number\n /** True when the window could not be built from complete records. */\n invalid: boolean\n reason: string\n}\n\n/**\n * The life of one submission and its neighbours.\n *\n * Built for attribution, not for scoring: a submission excursion says a picture\n * was wrong at an instant, and this says where its time went. Acquisition, due\n * time, promotion and submission are four stamps on one frame, and the largest\n * gap between consecutive ones is the interval to investigate.\n *\n * Incomplete rows are reported rather than dropped. A ring buffer that wrapped,\n * a record written before the ring carried these stamps, or a frame that never\n * reached submission all produce a window that cannot be attributed, and a\n * silently shortened window would read as a clean one.\n *\n * @experimental\n */\nexport function traceSubmission(\n records: readonly SubmissionRecord[],\n scored: readonly ScoredSubmission[],\n frameId: number,\n span = 8,\n): SubmissionTrace {\n const ordered = records.filter((r) => r.submittedNs > 0).sort((a, b) => a.submittedNs - b.submittedNs)\n const centre = ordered.findIndex((r) => r.frameId === frameId)\n if (centre < 0) {\n return {\n frameId,\n rows: [],\n spentOn: 'unknown',\n spentUs: 0,\n invalid: true,\n reason: 'frame not retained',\n }\n }\n const from = Math.max(0, centre - span)\n const window = ordered.slice(from, centre + span + 1)\n const base = window[0]!.submittedNs\n const ms = (ns: number) => (ns > 0 ? Math.round((ns - base) / 1e6) : -1)\n\n const rows: TraceRow[] = window.map((r) => {\n const match = scored.find((s) => s.frameId === r.frameId)\n return {\n frameId: r.frameId,\n acquiredMs: ms(r.acquiredNs),\n dueMs: ms(r.dueNs),\n promotedMs: ms(r.promotedNs),\n submittedMs: ms(r.submittedNs),\n waitUs: r.acquiredNs > 0 && r.dueNs > 0 ? Math.round((r.dueNs - r.acquiredNs) / 1000) : -1,\n promoteUs: r.dueNs > 0 && r.promotedNs > 0 ? Math.round((r.promotedNs - r.dueNs) / 1000) : -1,\n submitUs:\n r.promotedNs > 0 && r.submittedNs > 0 ? Math.round((r.submittedNs - r.promotedNs) / 1000) : -1,\n ptsUs: r.ptsUs,\n audioUs: match === undefined ? -1 : match.audioUs,\n errorUs: match === undefined ? -1 : match.errorUs,\n slot: r.slot,\n occupancy: r.occupancy,\n marker: r.submittedMarker,\n }\n })\n\n const target = rows.find((r) => r.frameId === frameId)!\n /*\n Presence is checked on the raw stamps, not on the offsets.\n\n An offset is relative to the window's first submission, so a frame acquired\n before that submission has a negative one — which is ordinary, and which an\n earlier version of this check read as \"not recorded\" and refused to\n attribute. Zero nanoseconds means never stamped; a negative offset means\n earlier than the window.\n */\n const raw = window.find((r) => r.frameId === frameId)!\n if (raw.acquiredNs <= 0 || raw.dueNs <= 0 || raw.promotedNs <= 0) {\n return {\n frameId,\n rows,\n spentOn: 'unknown',\n spentUs: 0,\n invalid: true,\n reason: 'incomplete stamps',\n }\n }\n /*\n A frame acquired after its own due time was late before the ring saw it.\n\n `waitUs` is due minus acquired, so a negative value means the presentation\n time had already passed on arrival. Measuring promotion from that stale due\n time then charges the ring for the wait as well, and the first version of\n this did exactly that: it reported `promotion` for frames whose promote leg\n was 96 ms and whose wait was minus 52 ms — the same 52 ms counted twice, in\n the wrong place. Promotion is measured from whichever came later.\n */\n const index = rows.indexOf(target)\n const previous = index > 0 ? rows[index - 1]! : null\n /*\n Arrival is the gap since the previous picture went out, always.\n\n An earlier version used the overshoot past the due time instead whenever the\n frame arrived late, and that hid the thing actually worth seeing. Measured\n at the clip seam: the frame missed its due time by 2 ms and had been\n preceded by 133 ms in which nothing was acquired at all. Charging it 2 ms\n named the wrong interval by two orders of magnitude.\n */\n const arrivalUs =\n previous === null ? 0 : Math.round((target.acquiredMs - previous.submittedMs) * 1000)\n const promoteUs = target.waitUs < 0 ? target.promoteUs + target.waitUs : target.promoteUs\n const legs: [SubmissionTrace['spentOn'], number][] = [\n ['arrival', arrivalUs],\n ['due', Math.max(0, target.waitUs)],\n ['promotion', promoteUs],\n ['submission', target.submitUs],\n ]\n let spentOn: SubmissionTrace['spentOn'] = 'unknown'\n let worst = -1\n for (const [name, value] of legs) {\n if (value > worst) {\n worst = value\n spentOn = name\n }\n }\n return { frameId, rows, spentOn, spentUs: Math.max(0, worst), invalid: false, reason: '' }\n}\n\n/** @experimental */\nexport interface SeamStage {\n framePtsUs: number\n /** Offsets in ms from the window's first sample. */\n metadataMs: number\n availableMs: number\n consumedMs: number\n /** Renderer to queue, queue to latch, in us. -1 when a stamp is missing. */\n graphUs: number\n latchUs: number\n}\n\n/** @experimental */\nexport interface SeamTrace {\n /** The boundary this window is centred on, in project time. */\n atUs: number\n /** The largest gap between consecutive frames reaching the renderer, in us. */\n outputGapUs: number\n /** The same gap measured at acquisition. */\n acquireGapUs: number\n /**\n * Who owns the gap.\n *\n * `graph` when the renderer produced and the queue did not follow.\n * `consumer` when it was queued and not latched. `production` when nothing\n * reached the renderer's output callback at all.\n *\n * That last one is deliberately broader than \"the decoder was unprepared\":\n * this stamp fires when the renderer is about to hand a frame to the surface,\n * so its absence covers a decoder that produced nothing *and* a renderer\n * blocked waiting for the sink to accept a new input stream. Separating those\n * two needs a stamp inside Media3 that is not exposed here. What it does\n * establish is that the graph and this consumer are not responsible, which is\n * what the acquisition gap alone could not say.\n */\n owner: 'production' | 'graph' | 'consumer' | 'unknown'\n stages: SeamStage[]\n invalid: boolean\n reason: string\n}\n\n/**\n * Where output stops at a clip boundary.\n *\n * The acquisition gap says delivery stopped; it cannot say whether the decoder\n * produced nothing, the video graph held what it produced, or this consumer\n * failed to latch what was queued. Those are three owners and three different\n * fixes, and they are separated by one stamp: when the renderer said a frame\n * was about to go to the surface.\n *\n * Samples carrying no frame are skipped rather than treated as a gap — the\n * poller runs faster than the frame rate, so most samples repeat the last\n * frame, and counting those would report a stall on every tick.\n *\n * @experimental\n */\nexport function traceSeam(\n samples: readonly SpikeSample[],\n atUs: number,\n spanUs = 600_000,\n): SeamTrace {\n const near = samples\n .filter(\n (s) =>\n s.framePtsUs >= 0 &&\n s.framePtsUs >= atUs - spanUs &&\n s.framePtsUs <= atUs + spanUs &&\n s.frameConsumedNs > 0,\n )\n .sort((a, b) => a.frameConsumedNs - b.frameConsumedNs)\n\n // One row per distinct frame: the poller ticks faster than frames arrive.\n const unique: SpikeSample[] = []\n for (const s of near) {\n if (unique.length === 0 || unique[unique.length - 1]!.frameId !== s.frameId) unique.push(s)\n }\n if (unique.length < 3) {\n return {\n atUs,\n outputGapUs: -1,\n acquireGapUs: -1,\n owner: 'unknown',\n stages: [],\n invalid: true,\n reason: 'too few frames around the boundary',\n }\n }\n\n const base = unique[0]!.frameConsumedNs\n const ms = (ns: number) => (ns > 0 ? Math.round((ns - base) / 1e6) : -1)\n const stages: SeamStage[] = unique.map((s) => ({\n framePtsUs: s.framePtsUs,\n metadataMs: ms(s.frameMetadataNs),\n availableMs: ms(s.frameAvailableNs),\n consumedMs: ms(s.frameConsumedNs),\n graphUs:\n s.frameMetadataNs > 0 && s.frameAvailableNs > 0\n ? Math.round((s.frameAvailableNs - s.frameMetadataNs) / 1000)\n : -1,\n latchUs:\n s.frameAvailableNs > 0 && s.frameConsumedNs > 0\n ? Math.round((s.frameConsumedNs - s.frameAvailableNs) / 1000)\n : -1,\n }))\n\n let outputGapUs = 0\n let acquireGapUs = 0\n let at = -1\n for (let i = 1; i < unique.length; i += 1) {\n const acquire = Math.round((unique[i]!.frameConsumedNs - unique[i - 1]!.frameConsumedNs) / 1000)\n if (acquire > acquireGapUs) {\n acquireGapUs = acquire\n at = i\n }\n const a = unique[i]!.frameMetadataNs\n const b = unique[i - 1]!.frameMetadataNs\n if (a > 0 && b > 0) outputGapUs = Math.max(outputGapUs, Math.round((a - b) / 1000))\n }\n\n /*\n The owner is read off the widest frame's own stages, not off the gap.\n\n A gap is a distance between two frames; the question is where the later one\n was held, and that is inside its own row.\n */\n let owner: SeamTrace['owner'] = 'unknown'\n if (at > 0) {\n const s = stages[at]!\n if (s.graphUs < 0 || s.latchUs < 0) owner = 'unknown'\n else if (s.graphUs >= s.latchUs && s.graphUs > 16_000) owner = 'graph'\n else if (s.latchUs > 16_000) owner = 'consumer'\n else owner = 'production'\n }\n return { atUs, outputGapUs, acquireGapUs, owner, stages, invalid: false, reason: '' }\n}\n\n/** One frame, seen from both sides of the pairing. @experimental */\nexport interface StartupRow {\n frameId: number\n /** What Media3 said this frame's presentation time was. */\n renderedPtsUs: number\n /** What the submission record was written under. The same number, or a bug. */\n recordedPtsUs: number\n /** What the pixels say the frame is. Independent of both. */\n marker: number\n /** The source frame the marker's own index implies, at this clip's speed. */\n markerPtsUs: number\n metadataMs: number\n consumedMs: number\n submittedMs: number\n}\n\n/** @experimental */\nexport interface StartupTrace {\n rows: StartupRow[]\n /**\n * Frames the renderer never emitted, by source index.\n *\n * The discriminator. If the renderer emitted a presentation time and the\n * pixels under it belong to a different frame, the *association* is wrong. If\n * it never emitted that time at all, the frame was dropped or never produced.\n * Those are different defects and the pixels alone cannot tell them apart.\n */\n missingSourceFrames: number[]\n /** Frames whose pixels disagree with the time they were rendered under. */\n misassociated: number[]\n invalid: boolean\n reason: string\n}\n\n/**\n * The first frames of a clip, from both sides of the pairing at once.\n *\n * Media3's presentation time and the marker in the pixels are independent\n * claims about the same frame. Joined by frame id, they separate \"frames 2 to 6\n * were never produced\" from \"frame 7's pixels arrived labelled as frame 2\" —\n * which look identical in a cadence result and need opposite fixes.\n *\n * @experimental\n */\nexport function traceStartup(\n samples: readonly SpikeSample[],\n records: readonly SubmissionRecord[],\n options: { sourceFps: number; speed: number; count?: number },\n): StartupTrace {\n const count = options.count ?? 10\n if (options.sourceFps <= 0 || options.speed <= 0) {\n return { rows: [], missingSourceFrames: [], misassociated: [], invalid: true, reason: 'no clip rate' }\n }\n // One source frame in project time: the unit both sides are compared in.\n const stepUs = 1_000_000 / options.sourceFps / options.speed\n\n const byFrame = new Map<number, SpikeSample>()\n for (const s of samples) {\n if (s.frameId < 0 || s.frameConsumedNs <= 0) continue\n const seen = byFrame.get(s.frameId)\n if (seen === undefined || s.frameConsumedNs < seen.frameConsumedNs) byFrame.set(s.frameId, s)\n }\n const ordered = records\n .filter((r) => r.submittedNs > 0)\n .sort((a, b) => a.submittedNs - b.submittedNs)\n .slice(0, count)\n if (ordered.length === 0) {\n return { rows: [], missingSourceFrames: [], misassociated: [], invalid: true, reason: 'no submissions' }\n }\n\n const base = ordered[0]!.submittedNs\n const ms = (ns: number) => (ns > 0 ? Math.round((ns - base) / 1e6) : -1)\n const rows: StartupRow[] = ordered.map((r) => {\n const s = byFrame.get(r.frameId)\n return {\n frameId: r.frameId,\n renderedPtsUs: s === undefined ? -1 : s.framePtsUs,\n recordedPtsUs: r.ptsUs,\n marker: r.submittedMarker,\n markerPtsUs: r.submittedMarker >= 0 ? Math.round(r.submittedMarker * stepUs) : -1,\n metadataMs: s === undefined ? -1 : ms(s.frameMetadataNs),\n consumedMs: s === undefined ? -1 : ms(s.frameConsumedNs),\n submittedMs: ms(r.submittedNs),\n }\n })\n\n /*\n Misassociated when the pixels and the rendered time name different frames;\n missing when the rule's frame never appears as a rendered time at all.\n */\n const misassociated: number[] = []\n const rendered = new Set<number>()\n for (const row of rows) {\n if (row.renderedPtsUs >= 0) rendered.add(Math.round(row.renderedPtsUs / stepUs))\n if (row.marker < 0 || row.renderedPtsUs < 0) continue\n if (Math.abs(row.markerPtsUs - row.renderedPtsUs) > stepUs / 2) misassociated.push(row.frameId)\n }\n const last = rows[rows.length - 1]!\n const highest = Math.max(\n last.marker >= 0 ? last.marker : 0,\n last.renderedPtsUs >= 0 ? Math.round(last.renderedPtsUs / stepUs) : 0,\n )\n const missingSourceFrames: number[] = []\n for (let i = 0; i <= highest; i += 1) if (!rendered.has(i)) missingSourceFrames.push(i)\n\n return { rows, missingSourceFrames, misassociated, invalid: false, reason: '' }\n}\n","import {\n compileAudioMixProgram,\n type ExportSourceContext,\n type MediaSource,\n type VideoProject,\n} from '@pictor-sdk/video-core'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\n\nimport { publishState } from './previewAudioDiagnostics'\nimport { resolveMixSources } from './mixSources'\nimport { requireNative } from './player'\nimport {\n cancelPreviewMix,\n preparePreviewMix,\n previewMixKey,\n sweepPreviewMixes,\n} from './previewMix'\nimport {\n decidePreviewAudio,\n type PreviewAudioStatus,\n} from './previewAudioSession'\n\n/**\n * Preview audio, bound to a screen's lifetime.\n *\n * The decisions live in `previewAudioSession.ts` and the mixing lives in\n * native; this is the part that acts — starting jobs, loading the player,\n * muting the other one, and refusing to publish an answer that has been\n * superseded.\n *\n * ## Epochs\n *\n * Every load, play and seek carries the epoch of the mix it belongs to. A\n * callback or a correction arriving with an older epoch is from a mix that has\n * since been replaced, and acting on it would move the picture to match audio\n * that is no longer playing. The epoch is bumped on every publish, so a late\n * result cannot masquerade as the current one.\n *\n * @experimental\n */\n\n/** @experimental */\nexport interface UsePreviewAudioOptions {\n project: VideoProject\n /** The same resolver the exporter is given. */\n resolveSource: (source: MediaSource, context: ExportSourceContext) => string | null | undefined\n /** Off entirely — for a screen that never plays. */\n enabled?: boolean\n /** How far apart the players may drift before audio is moved. */\n toleranceUs?: number\n}\n\n/** @experimental */\nexport interface PreviewAudioBinding {\n status: PreviewAudioStatus\n /** A sentence for a person when preparation failed. */\n error: string | null\n /** Whether playback may run. False while preparing or after a failure. */\n canPlay: boolean\n /** True once a mix matching the document is loaded. */\n ready: boolean\n /** How long the last preparation took, in ms. Null when it was cached. */\n lastPrepareMs: number | null\n /** Where that time went. Null for a cached mix. */\n lastProfile: unknown\n /*\n `play`, `pause`, `seek`, `setRate` and `checkDrift` were here.\n\n They were the second half of a two-player preview: this hook played the\n prepared mix on its own player while the video player played the picture,\n and `checkDrift` compared them and corrected past a tolerance. The preview\n now plays both from one composition on one clock, so there is no second\n position to compare and nothing to correct — the correction was a seek, and\n a seek at a clip seam cost the decode it was trying to cover.\n\n What is left is preparation: compile the program, produce the file, and say\n whether it is ready. The composition opens it. @see useCompositionPlayback\n */\n /**\n * The prepared mix, for a composition that carries the sound itself.\n *\n * Null until one matching the document exists. The preview plays audio and\n * video from one composition on one clock, so what this hook contributes is\n * the prepared program — the file and how long it runs — rather than a second\n * player to keep in step with the first. `play`, `pause`, `seek`, `setRate`\n * and `checkDrift` are what is left of that second player and are no longer\n * part of the preview path.\n */\n mix: { uri: string; durationUs: number } | null\n /** Cancel, release the player and drop the mix. */\n release(): void\n}\n\n/** @experimental */\nexport function usePreviewAudio(options: UsePreviewAudioOptions): PreviewAudioBinding {\n const { project, enabled = true } = options\n const resolveSource = useRef(options.resolveSource)\n resolveSource.current = options.resolveSource\n\n const [status, setStatusState] = useState<PreviewAudioStatus>('source')\n const [error, setErrorState] = useState<string | null>(null)\n const [readyKey, setReadyKeyState] = useState<string | null>(null)\n const [lastPrepareMs, setLastPrepareMsState] = useState<number | null>(null)\n const [lastProfile, setLastProfileState] = useState<unknown>(null)\n const [mix, setMix] = useState<{ uri: string; durationUs: number } | null>(null)\n\n /*\n Every publication goes through one seam. Five setters scattered across\n three effects is exactly the shape that makes \"does rendering cause this\"\n unanswerable, and routing them here costs a call and makes it a switch.\n See `previewAudioDiagnostics`.\n */\n // Memoized so each publisher has one identity for the hook's whole life:\n // React's setters are stable, so these never rebuild, and the effects below\n // can depend on them without that dependency meaning anything at runtime.\n const setStatus = useMemo(() => publishState('status', setStatusState), [setStatusState])\n const setError = useMemo(() => publishState('error', setErrorState), [setErrorState])\n const setReadyKey = useMemo(\n () => publishState('readyKey', setReadyKeyState),\n [setReadyKeyState],\n )\n const setLastPrepareMs = useMemo(\n () => publishState('lastPrepareMs', setLastPrepareMsState),\n [setLastPrepareMsState],\n )\n const setLastProfile = useMemo(\n () => publishState('lastProfile', setLastProfileState),\n [setLastProfileState],\n )\n\n const preparingKey = useRef<string | null>(null)\n const failedKey = useRef<string | null>(null)\n const epoch = useRef(0)\n const mounted = useRef(true)\n\n /**\n * What this document wants, and what it needs to decode.\n *\n * Recomputed whenever the project changes and keyed by the compiled program,\n * so an edit that cannot change the sound produces the same key and starts\n * nothing.\n */\n const wanted = useMemo(() => {\n if (!enabled || project.audio.length === 0) {\n return {\n key: '',\n programJson: '',\n urisJson: '',\n durationUs: 0,\n unresolved: [] as string[],\n }\n }\n const program = compileAudioMixProgram(project)\n const resolved = resolveMixSources(project, program, (s, c) => resolveSource.current(s, c))\n const programJson = JSON.stringify(program)\n const urisJson = JSON.stringify(resolved.uris)\n return {\n key: resolved.unresolved.length > 0 ? '' : previewMixKey(programJson, urisJson),\n programJson,\n urisJson,\n durationUs: Number(program.durationUs),\n unresolved: resolved.unresolved,\n }\n }, [project, enabled])\n\n const decision = decidePreviewAudio({\n wantedKey: wanted.key,\n readyKey,\n preparingKey: preparingKey.current,\n failedKey: failedKey.current,\n })\n\n /* A source that cannot be resolved is a failure with a name, not a wait. */\n const unresolved = wanted.unresolved.length > 0 && enabled && project.audio.length > 0\n\n useEffect(() => {\n mounted.current = true\n return () => {\n mounted.current = false\n }\n }, [])\n\n /** Silence the other player exactly when the mix owns the sound. */\n useEffect(() => {\n if (!enabled) return\n try {\n requireNative().previewMuteSourceAudio(decision.muteSourceAudio || unresolved)\n } catch {\n // No adapter. The decision still stands for everything else.\n }\n }, [decision.muteSourceAudio, unresolved, enabled])\n\n useEffect(() => {\n if (unresolved) {\n setStatus('failed')\n setError(wanted.unresolved[0] ?? 'a sound could not be read')\n return\n }\n setStatus(decision.status)\n if (decision.status !== 'failed') setError(null)\n }, [decision.status, unresolved, wanted.unresolved, setError, setStatus])\n\n /** Start a job when the document wants a mix nobody is preparing. */\n useEffect(() => {\n if (!decision.shouldPrepare || unresolved) return\n const key = wanted.key\n preparingKey.current = key\n const jobId = `preview-${key.slice(0, 8)}-${epoch.current + 1}`\n const startedAt = Date.now()\n\n void preparePreviewMix(jobId, wanted.programJson, wanted.urisJson).then((result) => {\n if (!mounted.current) return\n /*\n Two independent staleness checks, and both are needed. The job may have\n been superseded inside native — it says so — and the document may have\n moved on while the promise was in flight, which native cannot know.\n */\n if (result.ok === false && result.superseded) return\n if (key !== preparingKey.current) return\n preparingKey.current = null\n\n if (result.ok === false) {\n failedKey.current = key\n setError(result.reason)\n setStatus('failed')\n return\n }\n\n failedKey.current = null\n epoch.current += 1\n setLastPrepareMs(result.cached ? null : Date.now() - startedAt)\n setLastProfile(result.cached ? null : (result.profile ?? null))\n try {\n requireNative().previewAudioLoad(result.uri, epoch.current)\n } catch {\n failedKey.current = key\n setError('the prepared mix could not be opened')\n setStatus('failed')\n return\n }\n /*\n Published for the composition, which is what plays it.\n\n The duration comes from the compiled program rather than from the file:\n it is the length the mix was trimmed to, and taking it from the compiler\n keeps it on the same side of the boundary as every other timing value.\n */\n setMix({ uri: result.uri, durationUs: Number(wanted.durationUs) })\n setReadyKey(result.key)\n // Bounded: keep the one being played and let the rest go.\n sweepPreviewMixes([result.key])\n })\n }, [\n decision.shouldPrepare,\n wanted.key,\n wanted.programJson,\n wanted.urisJson,\n unresolved,\n setError,\n setLastPrepareMs,\n setLastProfile,\n setReadyKey,\n setStatus,\n ])\n\n /*\n The second player's transport lived here: `play`, `pause`, `seek`,\n `setRate` and `checkDrift`.\n\n Deleted with the architecture that needed them. The preview plays the mix\n from the composition, on the same clock as the picture, so there is no\n second position to seek, rate or compare. `previewSyncStop` survives in\n `release` because a preparation in flight still has to be abandoned.\n */\n\n const release = useCallback(() => {\n cancelPreviewMix()\n try {\n requireNative().previewSyncStop('release')\n requireNative().previewAudioRelease()\n requireNative().previewMuteSourceAudio(false)\n } catch {\n // Teardown paths overlap on purpose; each must be safe after the others.\n }\n preparingKey.current = null\n setReadyKey(null)\n setMix(null)\n }, [setReadyKey])\n\n /* Backgrounding and unmount both stop work and give the player back. */\n useEffect(() => release, [release])\n\n return {\n status: unresolved ? 'failed' : status,\n error,\n canPlay: !unresolved && decision.canPlay,\n ready: decision.status === 'ready',\n lastPrepareMs,\n lastProfile,\n // Null until a mix matching the document is ready, so a composition built\n // from it is never built from a stale one.\n mix: decision.status === 'ready' ? mix : null,\n release,\n }\n}\n","/**\n * One isolation control for the preview-audio hook, and nothing else.\n *\n * ## What it is for\n *\n * The picture stops advancing when a mix is loaded, and the minimal failing\n * configuration loads a *cached* mix — no decode, no encode, no codecs at all.\n * What is left in common is this hook being active, and being active means two\n * separable things: the native calls it makes, and the React state it\n * publishes. Five `useState` values re-render the editor while a surface, a\n * texture consumer and a composition callback are all being re-established.\n *\n * Suppressing the publications while leaving every native call in place\n * separates those two. If the freeze survives suppression it belongs to the\n * native side; if it disappears, the trigger is a render-driven ownership\n * transition and the fix belongs in whatever that render disturbs.\n *\n * ## What it is not\n *\n * Not a fix, and not reachable from a production build. Suppressed, the hook\n * cannot report readiness, so audio never plays — the control is deliberately\n * useless for anything except attribution. Anyone tempted to leave it on to\n * \"reduce renders\" is turning a diagnostic into a silent feature regression.\n *\n * @experimental\n */\n\n/** @experimental */\nexport interface PreviewAudioDiagnostics {\n /** Drop the hook's React state publications, keeping every native call. */\n suppressPublications: boolean\n /** Log each publication with its value. Off by default; noisy under a mix. */\n logPublications: boolean\n /** Publications made, and publications suppressed. */\n published: number\n suppressed: number\n}\n\n/** @experimental */\nexport const previewAudioDiagnostics: PreviewAudioDiagnostics = {\n suppressPublications: false,\n logPublications: false,\n published: 0,\n suppressed: 0,\n}\n\n/**\n * Wrap a setter so it can be observed and, under the control, withheld.\n *\n * Takes the name rather than deriving one, because a minified build would\n * otherwise label every publication `t`.\n *\n * @experimental\n */\nexport function publishState<T>(name: string, set: (value: T) => void): (value: T) => void {\n return (value: T) => {\n if (previewAudioDiagnostics.suppressPublications) {\n previewAudioDiagnostics.suppressed += 1\n return\n }\n previewAudioDiagnostics.published += 1\n if (previewAudioDiagnostics.logPublications) {\n console.log(`AUDIOPUB ${name} ${JSON.stringify(value ?? null)}`)\n }\n set(value)\n }\n}\n","import { requireNative } from './player'\n\n/**\n * The AAC encoder's frozen behaviour, re-measurable on demand.\n *\n * The encode loop is 16% of a preview preparation and was 89% of it before it\n * was rewritten. That rewrite was checked against a baseline of natively\n * generated PCM fixtures — inputs that are formulas rather than files, so the\n * two halves of the comparison cannot drift apart — and the check was exact:\n * access-unit count, payload bytes, payload hash, CSD, timestamps, decoded\n * frame count, RMS and transient positions all matched.\n *\n * This is the door to that. Diagnostics-gated in native, so a release build\n * without them refuses.\n *\n * @experimental\n */\n\n/** @experimental */\nexport interface EncoderGateResult {\n ok: boolean\n fixture?: string\n /** Compressed shape. */\n units?: number\n payloadBytes?: number\n payloadHash?: number\n csd0?: number\n firstUs?: number\n lastUs?: number\n headTimestamps?: number[]\n /** Decoded result. */\n decodedFrames?: number\n decodedRms?: number\n peak?: number\n markerPositionsMs?: number[]\n expectedMarkersMs?: number[]\n /** What it cost. */\n encodeMs?: number\n inputFrames?: number\n fileBytes?: number\n error?: string\n}\n\n/** The fixtures the gate can run. @experimental */\nexport const ENCODER_GATE_FIXTURES = [\n 'short',\n 'acceptance',\n 'silence',\n 'saturated',\n 'markers',\n] as const\n\n/**\n * Encode one fixture and describe the result completely.\n *\n * Never throws: a gate that is unavailable is an answer.\n *\n * @experimental\n */\nexport function runEncoderGate(fixture: string, maxInputSize = 65536): EncoderGateResult {\n try {\n return JSON.parse(requireNative().encoderGateRun(fixture, maxInputSize)) as EncoderGateResult\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) }\n }\n}\n","import { requireNative } from './player'\n\n/**\n * The paired player trace.\n *\n * Both players sampled on one monotonic clock, buffered natively and drained\n * afterwards. It exists because the question — do the picture and the sound\n * agree — needs samples closer together than a bridge hop, and far closer than\n * `uiautomator` can time.\n *\n * Diagnostics-only in native: a build without them refuses, and even with them\n * it does nothing until armed.\n *\n * @experimental\n */\n\n/** One sample or event. @experimental */\nexport interface TraceEvent {\n seq: number\n /** `System.nanoTime()`, the clock both players publish on. */\n atNs: number\n type: string\n v: Record<string, unknown>\n}\n\n/** @experimental */\nexport interface TracePage {\n ok: boolean\n /**\n * Whether the *run* is trustworthy.\n *\n * False when the sampler threw. The events before the fault are real and the\n * silence after it is not evidence of anything — a summary computed over a\n * truncated run reads exactly like one computed over a complete one.\n */\n valid: boolean\n /** Why it stopped being trustworthy, or null. */\n fault: string | null\n events: TraceEvent[]\n nextSeq: number\n total: number\n /** Events overwritten before they could be read. Non-zero means a hole. */\n dropped: number\n}\n\nconst EMPTY: TracePage = {\n ok: false,\n valid: false,\n fault: 'trace unavailable',\n events: [],\n nextSeq: 0,\n total: 0,\n dropped: 0,\n}\n\n/** Arm the trace and start sampling. @experimental */\nexport function armPreviewTrace(on: boolean, intervalMs = 25): boolean {\n try {\n const value = JSON.parse(requireNative().previewTraceArm(on, intervalMs)) as { ok?: boolean }\n return value.ok === true\n } catch {\n return false\n }\n}\n\n/** Label the trace, so a harness can find its own cases. @experimental */\nexport function markPreviewTrace(label: string): void {\n try {\n requireNative().previewTraceMark(label)\n } catch {\n // Unarmed or unavailable; a mark that lands nowhere is not worth raising.\n }\n}\n\n/**\n * Drain a page of events.\n *\n * Paged rather than whole: a full trace is far past what a single log line\n * survives, and truncation in the middle is the failure this is meant to\n * detect rather than cause.\n *\n * @experimental\n */\nexport function drainPreviewTrace(fromSeq: number, limit = 200): TracePage {\n try {\n const value = JSON.parse(requireNative().previewTraceDrain(fromSeq, limit)) as TracePage\n return value.ok ? value : EMPTY\n } catch {\n return EMPTY\n }\n}\n\n/**\n * Every event, drained in pages until the buffer is exhausted.\n *\n * @experimental\n */\nexport function drainAllPreviewTrace(limit = 200): TracePage {\n const events: TraceEvent[] = []\n let seq = 0\n let dropped = 0\n let total = 0\n let valid = true\n let fault: string | null = null\n // Bounded: the ring is finite, so this cannot page forever even if the\n // sampler is still running while it drains.\n for (let guard = 0; guard < 200; guard += 1) {\n const page = drainPreviewTrace(seq, limit)\n // Carried from every page: a fault recorded mid-drain still invalidates\n // the run, and the last page is not special.\n if (page.fault) {\n valid = false\n fault = page.fault\n }\n if (!page.ok || page.events.length === 0) {\n total = page.total || total\n break\n }\n events.push(...page.events)\n dropped += page.dropped\n total = page.total\n if (page.nextSeq === seq) break\n seq = page.nextSeq\n }\n return { ok: true, valid, fault, events, nextSeq: seq, total, dropped }\n}\n\n/**\n * The offset between the players, summarised over the samples that mattered.\n *\n * Only samples where **both** players were playing are counted: a paused\n * player's position does not advance, so including it would average in a\n * divergence that is not drift.\n *\n * @experimental\n */\nexport function summariseOffsets(events: readonly TraceEvent[]): {\n samples: number\n playingSamples: number\n /** Samples whose presented frame was the same one as the tick before. */\n staleFrameSamples: number\n /**\n * Samples with no presented frame at all.\n *\n * `FrameClock.latest` is null until the renderer consumes something, and\n * again after a generation bump. Counting these as \"stale\" hid them: a run\n * reported 56 stale of 506 while every sampled point in the same run had no\n * record whatsoever, because an absent field is not a false one.\n */\n absentFrameSamples: number\n /** Samples with a frame that had advanced since the previous tick. */\n freshFrameSamples: number\n /** The longest run of consecutive stale samples. */\n longestStaleRun: number\n minUs: number\n maxUs: number\n meanUs: number\n worstAbsUs: number\n} {\n let n = 0\n let stale = 0\n let absent = 0\n let fresh = 0\n let staleRun = 0\n let longestStaleRun = 0\n let min = Number.POSITIVE_INFINITY\n let max = Number.NEGATIVE_INFINITY\n let sum = 0\n let samples = 0\n for (const e of events) {\n if (e.type !== 'sample') continue\n samples += 1\n /*\n Stale frames are counted, never filtered.\n\n A presented time that has not moved since the last tick describes a frame\n the renderer already showed. Dropping those samples and reporting the\n percentage that remain would be reporting a pass over the interval where\n the picture was least trustworthy.\n */\n if (e.v.pfFresh === undefined) {\n absent += 1\n staleRun += 1\n if (staleRun > longestStaleRun) longestStaleRun = staleRun\n } else if (e.v.pfFresh === false) {\n stale += 1\n staleRun += 1\n if (staleRun > longestStaleRun) longestStaleRun = staleRun\n } else {\n fresh += 1\n staleRun = 0\n }\n if (e.v.videoPlaying !== true || e.v.audioPlaying !== true) continue\n const offset = Number(e.v.offsetUs)\n if (!Number.isFinite(offset)) continue\n n += 1\n sum += offset\n if (offset < min) min = offset\n if (offset > max) max = offset\n }\n return {\n samples,\n playingSamples: n,\n staleFrameSamples: stale,\n absentFrameSamples: absent,\n freshFrameSamples: fresh,\n longestStaleRun,\n minUs: n > 0 ? min : 0,\n maxUs: n > 0 ? max : 0,\n meanUs: n > 0 ? Math.round(sum / n) : 0,\n worstAbsUs: n > 0 ? Math.max(Math.abs(min), Math.abs(max)) : 0,\n }\n}\n\n/**\n * What the stall coordinator has done.\n *\n * Holds, releases and realigning seeks. A gate asserts that seams produced\n * holds and that every hold resolved — a coordinator that never engaged and one\n * that engaged and never released look identical in an offset summary.\n *\n * @experimental\n */\nexport function previewSyncCounters(): Record<string, unknown> {\n try {\n return JSON.parse(requireNative().previewSyncCounters()) as Record<string, unknown>\n } catch {\n return { ok: false }\n }\n}\n\n/**\n * Stop playback for a host reason — background, picker, unmount.\n *\n * Overrides a resume the coordinator is holding, so playback cannot restart at\n * a moment nobody asked for.\n *\n * @experimental\n */\nexport function stopPreviewForHost(reason: string): void {\n try {\n requireNative().previewSyncStop(reason)\n } catch {\n // No adapter, or nothing playing. Neither is worth raising in teardown.\n }\n}\n\n/**\n * Frames offered, queued and drawn.\n *\n * The instrument-disruption check reads this with tracing off and again with it\n * on: if the drawn count advances at the same rate either way, the trace is not\n * changing what it measures. Three defects in this area were the measurement\n * rather than the product, so that check comes before the numbers do.\n *\n * @experimental\n */\nexport function previewFramePipeline(): Record<string, number> {\n try {\n return JSON.parse(requireNative().previewFramePipeline()) as Record<string, number>\n } catch {\n return {}\n }\n}\n\n/** Which parts of the music preview exist. All true in production. @experimental */\nexport interface PreviewParts {\n /** Whether the drift check may act. Off measures without correcting. */\n driftCorrection: boolean\n loadPlayer: boolean\n startMix: boolean\n positionPump: boolean\n coordinator: boolean\n}\n\n/**\n * Turn parts of the music preview off, to bisect a failure.\n *\n * Gate-only in native. Guessing between a second player, its position pump, the\n * stall coordinator and the mix actually playing is how the previous attempts\n * in this area became tuning; this is how one of them gets ruled out at a time.\n *\n * @experimental\n */\nexport function setPreviewParts(parts: Partial<PreviewParts>): PreviewParts | null {\n try {\n const v = JSON.parse(requireNative().previewConfigSet(JSON.stringify(parts))) as\n PreviewParts & { ok?: boolean }\n return v.ok ? v : null\n } catch {\n return null\n }\n}\n\n/** @experimental */\nexport function previewParts(): PreviewParts | null {\n try {\n return JSON.parse(requireNative().previewConfigGet()) as PreviewParts\n } catch {\n return null\n }\n}\n\n/**\n * Silence the video player's own audio.\n *\n * Exposed because a bisect control needs the *same* mute in every\n * configuration: without it, the video-only row would be the only one with\n * audible clip audio, and any difference against the others would be\n * confounded by a whole track the rest do not have.\n *\n * @experimental\n */\nexport function muteSourceAudio(muted: boolean): void {\n try {\n requireNative().previewMuteSourceAudio(muted)\n } catch {\n // No adapter. The caller's other work still stands.\n }\n}\n\n/**\n * Every codec mix preparation owns, and what became of it.\n *\n * A `release()` that was called is not a release that finished, so both ends\n * are recorded along with the exception path. The preview video decoder is not\n * here: it belongs to the player and outlives any job, and counting it would\n * make \"live after publication\" always true.\n *\n * @experimental\n */\nexport function previewCodecLedger(): {\n live: number\n peakLive: number\n total: number\n entries: Record<string, unknown>[]\n} {\n try {\n return JSON.parse(requireNative().previewCodecLedger()) as {\n live: number\n peakLive: number\n total: number\n entries: Record<string, unknown>[]\n }\n } catch {\n return { live: -1, peakLive: -1, total: 0, entries: [] }\n }\n}\n"],"mappings":"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","names":["useCallback","useEffect","useRef","useState","AppState","playlistOf","useRef","useState","playlistOf","useEffect","AppState","useCallback","playIntent","playlistOf","projectDuration","useCallback","useEffect","useMemo","useRef","useState","AppState","useState","useMemo","playlistOf","projectDuration","useRef","useEffect","AppState","useCallback","playIntent","DEFAULT_POLL_MS","uriOf","VideoExportCancelled","projectDuration","projectDuration","VideoExportCancelled","counter","call","compileAudioMixProgram","record","record","compileAudioMixProgram","useCallback","useEffect","useMemo","useRef","useState","useRef","useState","useMemo","compileAudioMixProgram","useEffect","useCallback","EMPTY"]}
|