@pictor-sdk/video-android 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -0
  3. package/android/CMakeLists.txt +30 -0
  4. package/android/build.gradle +119 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/cpp/pictor_overlaystage.cpp +372 -0
  7. package/android/src/main/cpp/pictor_publish.cpp +87 -0
  8. package/android/src/main/cpp/pictor_videosurface.cpp +735 -0
  9. package/android/src/main/java/dev/pictor/video/AacPriming.kt +261 -0
  10. package/android/src/main/java/dev/pictor/video/AudioMixer.kt +1112 -0
  11. package/android/src/main/java/dev/pictor/video/AudioProbe.kt +162 -0
  12. package/android/src/main/java/dev/pictor/video/CadenceShaderProgram.kt +337 -0
  13. package/android/src/main/java/dev/pictor/video/CodecLedger.kt +126 -0
  14. package/android/src/main/java/dev/pictor/video/CompositionSpike.kt +978 -0
  15. package/android/src/main/java/dev/pictor/video/Diagnostics.kt +54 -0
  16. package/android/src/main/java/dev/pictor/video/EncoderGate.kt +476 -0
  17. package/android/src/main/java/dev/pictor/video/ExportGate.kt +1974 -0
  18. package/android/src/main/java/dev/pictor/video/ExportHeartbeat.kt +72 -0
  19. package/android/src/main/java/dev/pictor/video/ExportProgress.kt +103 -0
  20. package/android/src/main/java/dev/pictor/video/FrameClock.kt +591 -0
  21. package/android/src/main/java/dev/pictor/video/FramePending.kt +181 -0
  22. package/android/src/main/java/dev/pictor/video/GateAudio.kt +456 -0
  23. package/android/src/main/java/dev/pictor/video/GateDestination.kt +445 -0
  24. package/android/src/main/java/dev/pictor/video/GateFrames.kt +662 -0
  25. package/android/src/main/java/dev/pictor/video/GateMux.kt +185 -0
  26. package/android/src/main/java/dev/pictor/video/ManagedSources.kt +333 -0
  27. package/android/src/main/java/dev/pictor/video/Media3Player.kt +934 -0
  28. package/android/src/main/java/dev/pictor/video/MediaStoreSave.kt +419 -0
  29. package/android/src/main/java/dev/pictor/video/OverlayResources.kt +169 -0
  30. package/android/src/main/java/dev/pictor/video/PictorVideoModule.kt +1612 -0
  31. package/android/src/main/java/dev/pictor/video/PictorVideoPackage.kt +26 -0
  32. package/android/src/main/java/dev/pictor/video/PlaybackAnchor.kt +161 -0
  33. package/android/src/main/java/dev/pictor/video/PreviewAudioPlayer.kt +291 -0
  34. package/android/src/main/java/dev/pictor/video/PreviewConfig.kt +96 -0
  35. package/android/src/main/java/dev/pictor/video/PreviewMix.kt +396 -0
  36. package/android/src/main/java/dev/pictor/video/PreviewSync.kt +374 -0
  37. package/android/src/main/java/dev/pictor/video/PreviewTrace.kt +478 -0
  38. package/android/src/main/java/dev/pictor/video/PrimingTrim.kt +337 -0
  39. package/android/src/main/java/dev/pictor/video/RepeatingEncoderFactory.kt +97 -0
  40. package/android/src/main/java/dev/pictor/video/SceneProgram.kt +169 -0
  41. package/android/src/main/java/dev/pictor/video/SceneProgramNative.kt +299 -0
  42. package/android/src/main/java/dev/pictor/video/SkiaPassThroughEffect.kt +695 -0
  43. package/android/src/main/java/dev/pictor/video/SurfaceAttachment.kt +110 -0
  44. package/android/src/main/java/dev/pictor/video/ThumbnailDecoder.kt +171 -0
  45. package/android/src/main/java/dev/pictor/video/VideoSurface.kt +394 -0
  46. package/app.plugin.js +43 -0
  47. package/dist/index.d.mts +3622 -0
  48. package/dist/index.d.ts +3622 -0
  49. package/dist/index.js +3737 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/index.mjs +3625 -0
  52. package/dist/index.mjs.map +1 -0
  53. package/package.json +82 -0
  54. package/plugin/gradleProperties.d.ts +19 -0
  55. package/plugin/gradleProperties.js +64 -0
  56. package/react-native.config.js +13 -0
  57. package/src/NativePictorVideo.ts +574 -0
  58. package/src/audioProbe.ts +72 -0
  59. package/src/compositionSpike.ts +305 -0
  60. package/src/convergence.ts +112 -0
  61. package/src/encoderGate.ts +66 -0
  62. package/src/exportDestination.ts +149 -0
  63. package/src/exportJob.ts +232 -0
  64. package/src/exportProgress.ts +391 -0
  65. package/src/exportProgressPump.ts +190 -0
  66. package/src/exporter.ts +655 -0
  67. package/src/index.ts +276 -0
  68. package/src/managedSources.ts +252 -0
  69. package/src/media3.ts +412 -0
  70. package/src/mediaStore.ts +177 -0
  71. package/src/memoryGuard.ts +118 -0
  72. package/src/mixSources.ts +82 -0
  73. package/src/overlayStaging.ts +216 -0
  74. package/src/playbackLifecycle.ts +137 -0
  75. package/src/player.ts +39 -0
  76. package/src/playlistSession.ts +125 -0
  77. package/src/previewAudioDiagnostics.ts +67 -0
  78. package/src/previewAudioSession.ts +168 -0
  79. package/src/previewCadence.ts +950 -0
  80. package/src/previewMix.ts +212 -0
  81. package/src/previewTrace.ts +348 -0
  82. package/src/spikeExcursions.ts +405 -0
  83. package/src/submissionScoring.ts +680 -0
  84. package/src/surfaceOwner.ts +264 -0
  85. package/src/surfaceRecovery.ts +121 -0
  86. package/src/syncTrace.ts +189 -0
  87. package/src/thumbnailProvider.ts +163 -0
  88. package/src/tracePhases.ts +217 -0
  89. package/src/useCompositionPlayback.ts +327 -0
  90. package/src/useMedia3Playback.ts +492 -0
  91. package/src/usePreviewAudio.ts +305 -0
  92. package/src/useVideoSurface.ts +282 -0
package/dist/index.js ADDED
@@ -0,0 +1,3737 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AndroidVideoPlayer: () => AndroidVideoPlayer,
24
+ CONVERGED_RUN: () => CONVERGED_RUN,
25
+ DEFAULT_CEILING_BYTES: () => DEFAULT_CEILING_BYTES,
26
+ DEFAULT_RISE_BYTES: () => DEFAULT_RISE_BYTES,
27
+ DEFAULT_WINDOW_SAMPLES: () => DEFAULT_WINDOW_SAMPLES,
28
+ ENCODER_GATE_FIXTURES: () => ENCODER_GATE_FIXTURES,
29
+ EXPORT_STAGES: () => EXPORT_STAGES,
30
+ MAX_OBSERVED_HEARTBEAT_GAP_MS: () => MAX_OBSERVED_HEARTBEAT_GAP_MS,
31
+ MAX_RECOVERY_SAMPLES: () => MAX_RECOVERY_SAMPLES,
32
+ Media3: () => Media3,
33
+ MediaSaveCancelled: () => MediaSaveCancelled,
34
+ MediaSaveFailed: () => MediaSaveFailed,
35
+ NO_SURFACE: () => NO_SURFACE,
36
+ PHASE_MARK_PREFIX: () => PHASE_MARK_PREFIX,
37
+ PREVIEW_DRIFT_TOLERANCE_US: () => PREVIEW_DRIFT_TOLERANCE_US,
38
+ PREVIEW_OUTPUT_FPS: () => PREVIEW_OUTPUT_FPS,
39
+ PREVIEW_TOLERANCE_US: () => PREVIEW_TOLERANCE_US,
40
+ RECOVERY_GRACE_MS: () => RECOVERY_GRACE_MS,
41
+ SPIKE_TOLERANCE_US: () => SPIKE_TOLERANCE_US,
42
+ STALL_GRACE_MS: () => STALL_GRACE_MS,
43
+ VideoExportRejected: () => VideoExportRejected,
44
+ afterPrepare: () => afterPrepare,
45
+ armPreviewTrace: () => armPreviewTrace,
46
+ beginPhase: () => beginPhase,
47
+ cancelPreviewMix: () => cancelPreviewMix,
48
+ checkCadence: () => checkCadence,
49
+ classifyExcursions: () => classifyExcursions,
50
+ classifyStall: () => classifyStall,
51
+ compositionSpikeSpec: () => compositionSpikeSpec,
52
+ createAndroidThumbnailProvider: () => createAndroidThumbnailProvider,
53
+ createExportJobs: () => createExportJobs,
54
+ createMedia3VideoExporter: () => createMedia3VideoExporter,
55
+ createMediaStoreSaver: () => createMediaStoreSaver,
56
+ createMemoryGuard: () => createMemoryGuard,
57
+ createOverlayStagingSink: () => createOverlayStagingSink,
58
+ createPlaybackLifecycle: () => createPlaybackLifecycle,
59
+ createProgressReporter: () => createProgressReporter,
60
+ createStallWatchdog: () => createStallWatchdog,
61
+ createSurfaceOwner: () => createSurfaceOwner,
62
+ createSyncTrace: () => createSyncTrace,
63
+ decidePreviewAudio: () => decidePreviewAudio,
64
+ describeTrip: () => describeTrip,
65
+ distribution: () => distribution,
66
+ drainAllPreviewTrace: () => drainAllPreviewTrace,
67
+ drainPreviewTrace: () => drainPreviewTrace,
68
+ expectedPtsAt: () => expectedPtsAt,
69
+ idlePlaylistSession: () => idlePlaylistSession,
70
+ ingestManagedSource: () => ingestManagedSource,
71
+ installOverlayStaging: () => installOverlayStaging,
72
+ isCurrentGeneration: () => isCurrentGeneration,
73
+ managedSourceLedger: () => managedSourceLedger,
74
+ managedSourceList: () => managedSourceList,
75
+ managedSourceToken: () => managedSourceToken,
76
+ markPreviewTrace: () => markPreviewTrace,
77
+ measureStagingStall: () => measureStagingStall,
78
+ mixSpanContext: () => mixSpanContext,
79
+ muteSourceAudio: () => muteSourceAudio,
80
+ nextOwnerToken: () => nextOwnerToken,
81
+ observe: () => observe,
82
+ observeMemory: () => observeMemory,
83
+ offsetAt: () => offsetAt,
84
+ planProgressWeights: () => planProgressWeights,
85
+ preparePreviewMix: () => preparePreviewMix,
86
+ previewAudioDiagnostics: () => previewAudioDiagnostics,
87
+ previewAudioNeeded: () => previewAudioNeeded,
88
+ previewCodecLedger: () => previewCodecLedger,
89
+ previewFramePipeline: () => previewFramePipeline,
90
+ previewMixKey: () => previewMixKey,
91
+ previewMixLedger: () => previewMixLedger,
92
+ previewParts: () => previewParts,
93
+ previewProgramJson: () => previewProgramJson,
94
+ previewSyncCounters: () => previewSyncCounters,
95
+ probeAudioSource: () => probeAudioSource,
96
+ recordTick: () => record,
97
+ recoveryFrames: () => recoveryFrames,
98
+ releaseManagedSource: () => releaseManagedSource,
99
+ releasePreviewMix: () => releasePreviewMix,
100
+ resetSurfaceOwnerForTests: () => resetSurfaceOwnerForTests,
101
+ resolveMixSources: () => resolveMixSources,
102
+ retireSurface: () => retireSurface,
103
+ runEncoderGate: () => runEncoderGate,
104
+ scoreSubmissions: () => scoreSubmissions,
105
+ seedManagedSourceForGate: () => seedManagedSourceForGate,
106
+ setPreviewParts: () => setPreviewParts,
107
+ shareSavedMedia: () => shareSavedMedia,
108
+ shouldPrepare: () => shouldPrepare,
109
+ shouldRecover: () => shouldRecover,
110
+ spikeClaim: () => spikeClaim,
111
+ spikeDrain: () => spikeDrain,
112
+ spikePause: () => spikePause,
113
+ spikePlay: () => spikePlay,
114
+ spikeRelease: () => spikeRelease,
115
+ spikeSeek: () => spikeSeek,
116
+ spikeStart: () => spikeStart,
117
+ spikeState: () => spikeState,
118
+ stopPreviewForHost: () => stopPreviewForHost,
119
+ summariseOffsets: () => summariseOffsets,
120
+ summarisePhases: () => summarisePhases,
121
+ surfaceOwner: () => surfaceOwner,
122
+ sweepManagedSources: () => sweepManagedSources,
123
+ sweepManagedSourcesRawForGate: () => sweepManagedSourcesRawForGate,
124
+ sweepOverlayStaging: () => sweepOverlayStaging,
125
+ sweepPendingMediaSaves: () => sweepPendingMediaSaves,
126
+ sweepPreviewMixes: () => sweepPreviewMixes,
127
+ syncReport: () => syncReport,
128
+ thumbnailCounters: () => thumbnailCounters,
129
+ traceSeam: () => traceSeam,
130
+ traceStartup: () => traceStartup,
131
+ traceSubmission: () => traceSubmission,
132
+ useCompositionPlayback: () => useCompositionPlayback,
133
+ useMedia3Playback: () => useMedia3Playback,
134
+ usePreviewAudio: () => usePreviewAudio,
135
+ useVideoSurface: () => useVideoSurface,
136
+ videoSurfaceSlot: () => videoSurfaceSlot,
137
+ windowAround: () => windowAround
138
+ });
139
+ module.exports = __toCommonJS(index_exports);
140
+
141
+ // src/memoryGuard.ts
142
+ var DEFAULT_CEILING_BYTES = 250 * 1024 * 1024;
143
+ var DEFAULT_WINDOW_SAMPLES = 40;
144
+ var DEFAULT_RISE_BYTES = 8 * 1024 * 1024;
145
+ function createMemoryGuard(ceilingBytes = DEFAULT_CEILING_BYTES, windowSamples = DEFAULT_WINDOW_SAMPLES, riseBytes = DEFAULT_RISE_BYTES) {
146
+ return {
147
+ ceilingBytes,
148
+ windowSamples: Math.max(3, Math.floor(windowSamples)),
149
+ riseBytes,
150
+ samples: [],
151
+ tripped: null
152
+ };
153
+ }
154
+ function observeMemory(guard, bytes) {
155
+ if (guard.tripped) return guard;
156
+ if (!Number.isFinite(bytes) || bytes <= 0) return guard;
157
+ if (bytes >= guard.ceilingBytes) {
158
+ return { ...guard, samples: [...guard.samples, bytes].slice(-guard.windowSamples), tripped: "ceiling" };
159
+ }
160
+ const samples = [...guard.samples, bytes].slice(-guard.windowSamples);
161
+ if (samples.length < guard.windowSamples) return { ...guard, samples };
162
+ let climbing = true;
163
+ for (let i = 1; i < samples.length; i++) {
164
+ if (samples[i] <= samples[i - 1]) {
165
+ climbing = false;
166
+ break;
167
+ }
168
+ }
169
+ const climb = samples[samples.length - 1] - samples[0];
170
+ if (climbing && climb >= guard.riseBytes) {
171
+ return { ...guard, samples, tripped: "sustained-rise" };
172
+ }
173
+ return { ...guard, samples };
174
+ }
175
+ function describeTrip(guard) {
176
+ if (!guard.tripped) return null;
177
+ const mb = (b) => (b / (1024 * 1024)).toFixed(0);
178
+ const latest = guard.samples[guard.samples.length - 1] ?? 0;
179
+ return guard.tripped === "ceiling" ? `native heap reached ${mb(latest)} MB (ceiling ${mb(guard.ceilingBytes)} MB) \u2014 session released` : `native heap rose ${mb(latest - (guard.samples[0] ?? 0))} MB across ${guard.samples.length} samples without plateauing \u2014 session released`;
180
+ }
181
+
182
+ // src/syncTrace.ts
183
+ var MAX_SAMPLES = 2e4;
184
+ function createSyncTrace(fps) {
185
+ const rate = Number.isFinite(fps) && fps > 0 ? fps : 30;
186
+ return {
187
+ frameUs: Math.round(1e6 / rate),
188
+ quantisation: [],
189
+ decoderLate: [],
190
+ schedulerLate: [],
191
+ correction: [],
192
+ presentedOffset: [],
193
+ tickInterval: [],
194
+ ticks: 0,
195
+ decoderLateTicks: 0,
196
+ schedulerLateTicks: 0
197
+ };
198
+ }
199
+ function expectedPtsAt(audioUs, frameUs) {
200
+ if (audioUs <= 0 || frameUs <= 0) return 0;
201
+ return Math.floor(audioUs / frameUs) * frameUs;
202
+ }
203
+ function record(trace, sample) {
204
+ if (trace.ticks >= MAX_SAMPLES) return { ...trace, ticks: trace.ticks + 1 };
205
+ const expected = expectedPtsAt(sample.audioUs, trace.frameUs);
206
+ const next = {
207
+ ...trace,
208
+ ticks: trace.ticks + 1,
209
+ quantisation: [...trace.quantisation, sample.audioUs - expected],
210
+ correction: [...trace.correction, Math.abs(sample.correctionUs)],
211
+ tickInterval: [...trace.tickInterval, sample.tickIntervalUs],
212
+ decoderLate: trace.decoderLate,
213
+ schedulerLate: trace.schedulerLate,
214
+ presentedOffset: trace.presentedOffset,
215
+ decoderLateTicks: trace.decoderLateTicks,
216
+ schedulerLateTicks: trace.schedulerLateTicks
217
+ };
218
+ const eligible = sample.newestEligiblePtsUs;
219
+ if (eligible === null || eligible < expected) {
220
+ const shortfall = eligible === null ? sample.audioUs : expected - eligible;
221
+ next.decoderLate = [...trace.decoderLate, shortfall];
222
+ next.decoderLateTicks = trace.decoderLateTicks + 1;
223
+ }
224
+ if (eligible !== null && (sample.presentedPtsUs === null || sample.presentedPtsUs < eligible)) {
225
+ const behind = eligible - (sample.presentedPtsUs ?? 0);
226
+ next.schedulerLate = [...trace.schedulerLate, behind];
227
+ next.schedulerLateTicks = trace.schedulerLateTicks + 1;
228
+ }
229
+ if (sample.presentedPtsUs !== null) {
230
+ next.presentedOffset = [...trace.presentedOffset, sample.presentedPtsUs - sample.audioUs];
231
+ }
232
+ return next;
233
+ }
234
+ function distribution(values) {
235
+ if (values.length === 0) return { n: 0, p50: 0, p95: 0, p99: 0, max: 0 };
236
+ const abs = values.map(Math.abs).sort((a, b) => a - b);
237
+ const at = (q) => abs[Math.min(abs.length - 1, Math.floor(q * abs.length))];
238
+ return { n: abs.length, p50: at(0.5), p95: at(0.95), p99: at(0.99), max: abs[abs.length - 1] };
239
+ }
240
+ function syncReport(trace) {
241
+ const decoderLate = distribution(trace.decoderLate);
242
+ const schedulerLate = distribution(trace.schedulerLate);
243
+ const quantisation = distribution(trace.quantisation);
244
+ const presentedOffset = distribution(trace.presentedOffset);
245
+ const decoderShare = trace.ticks === 0 ? 0 : trace.decoderLateTicks / trace.ticks;
246
+ const schedulerShare = trace.ticks === 0 ? 0 : trace.schedulerLateTicks / trace.ticks;
247
+ let dominant = "none";
248
+ if (presentedOffset.p95 > trace.frameUs) {
249
+ if (decoderShare >= schedulerShare && decoderShare > 0.01) dominant = "decoder";
250
+ else if (schedulerShare > 0.01) dominant = "scheduler";
251
+ else dominant = "quantisation";
252
+ }
253
+ return {
254
+ quantisation,
255
+ decoderLate,
256
+ schedulerLate,
257
+ correction: distribution(trace.correction),
258
+ presentedOffset,
259
+ tickInterval: distribution(trace.tickInterval),
260
+ ticks: trace.ticks,
261
+ decoderLateTicks: trace.decoderLateTicks,
262
+ schedulerLateTicks: trace.schedulerLateTicks,
263
+ frameUs: trace.frameUs,
264
+ dominant
265
+ };
266
+ }
267
+
268
+ // src/NativePictorVideo.ts
269
+ var import_react_native = require("react-native");
270
+ var NativePictorVideo_default = import_react_native.TurboModuleRegistry.get("PictorVideo");
271
+
272
+ // src/player.ts
273
+ var AndroidVideoPlayer = {
274
+ /**
275
+ * Publish the presentation surface host object on the JS runtime.
276
+ *
277
+ * It is captured into a worklet closure from there; the UI runtime has its
278
+ * own global and does not see this one.
279
+ */
280
+ installVideoSurface() {
281
+ return requireNative().installVideoSurface();
282
+ }
283
+ };
284
+ function requireNative() {
285
+ if (!NativePictorVideo_default) {
286
+ throw new Error(
287
+ "@pictor-sdk/video-android: the native module is not installed. It is Android-only and requires minSdkVersion 29."
288
+ );
289
+ }
290
+ return NativePictorVideo_default;
291
+ }
292
+
293
+ // src/media3.ts
294
+ var EMPTY = {
295
+ positionUs: 0,
296
+ durationUs: 0,
297
+ playing: false,
298
+ buffering: false,
299
+ ready: false,
300
+ ended: false,
301
+ rate: 1,
302
+ width: 0,
303
+ height: 0,
304
+ released: true,
305
+ error: "",
306
+ generation: 0,
307
+ queueDepth: 0,
308
+ overflowed: 0,
309
+ unpaired: 0,
310
+ stale: 0,
311
+ queueHighWater: 0,
312
+ threads: 0,
313
+ nativeHeapBytes: 0,
314
+ anchorPositionUs: 0,
315
+ coalesced: 0,
316
+ anchorPublications: 0,
317
+ anchorChanges: 0,
318
+ anchorMaxStepUs: 0,
319
+ anchorMeanStepUs: 0,
320
+ anchorMaxStaleNs: 0,
321
+ windowIndex: 0,
322
+ windowCount: 0,
323
+ windowPositionUs: 0,
324
+ transitions: 0,
325
+ missedLatches: 0,
326
+ updatesAttempted: 0,
327
+ updatesSucceeded: 0,
328
+ updatesSkipped: 0,
329
+ updatesFailed: 0,
330
+ rateConstrained: 0,
331
+ effectiveRate: 1,
332
+ requestedRate: 1,
333
+ staleWindowSpeeds: 0,
334
+ windowSpeedCount: 0,
335
+ framesOffered: 0,
336
+ framesAvailable: 0,
337
+ pendingDepth: 0,
338
+ framesRejected: 0,
339
+ pendingGeneration: 0,
340
+ controllerId: 0,
341
+ pendingId: 0,
342
+ orphaned: 0,
343
+ videoFormat: "",
344
+ decoderName: "",
345
+ inputFormats: 0,
346
+ inputWidth: 0,
347
+ inputHeight: 0,
348
+ inputRotation: 0,
349
+ decoderReuse: -1,
350
+ outputSizes: 0,
351
+ outputWidth: 0,
352
+ outputHeight: 0,
353
+ surfaceGeneration: 0,
354
+ surfaceAttachments: 0,
355
+ attachedSurfaceGeneration: 0,
356
+ liveSurfaceGeneration: 0,
357
+ seeksRequested: 0,
358
+ seeksAccepted: 0,
359
+ pendingSeek: false,
360
+ playbackState: 0,
361
+ lastPairedPtsUs: -1,
362
+ errorCount: 0,
363
+ errorCode: 0,
364
+ errorMessage: "",
365
+ errorRecoverable: false
366
+ };
367
+ var Media3 = {
368
+ /**
369
+ * Open a local clip and render it into the presentation surface.
370
+ *
371
+ * The surface must already exist: it is created on the UI runtime, where
372
+ * Skia's GL context lives, and preparing without one is an error rather than
373
+ * a silently black picture.
374
+ */
375
+ prepare(uri) {
376
+ return requireNative().media3Prepare(uri);
377
+ },
378
+ /** Diagnostics: a player with no video output, for heap attribution. */
379
+ prepareNoSurface(uri) {
380
+ return requireNative().media3PrepareNoSurface(uri);
381
+ },
382
+ /** Diagnostics: reuse the existing player for another source. */
383
+ rebind(uri) {
384
+ return requireNative().media3Rebind(uri);
385
+ },
386
+ /** Diagnostics: end the source, keep the player. */
387
+ stopKeepingPlayer() {
388
+ requireNative().media3StopKeepingPlayer();
389
+ },
390
+ /**
391
+ * Replace the playlist from a project's playable windows.
392
+ *
393
+ * Takes the `PlaylistResult` straight from `@pictor-sdk/video-core` so the
394
+ * project → window mapping has exactly one definition, tested there as a
395
+ * round trip.
396
+ * The sources must already be URI strings; this adapter does not interpret a
397
+ * host's media descriptors, and a non-URI source is a host normalisation bug
398
+ * rather than something to guess at.
399
+ *
400
+ * The OES texture and the `SkImage` survive this. Changing the playlist is
401
+ * not a reason to rebuild what the renderer holds.
402
+ */
403
+ setPlaylist(playlist, start = { windowIndex: 0, positionUs: 0 }, options = {}) {
404
+ if (playlist.unsupported.length > 0 && options.allowDivergence !== true) {
405
+ throw new Error(
406
+ `@pictor-sdk/video-android: refusing to build a playlist that would diverge from the timeline. ${playlist.unsupported.length} clip(s) cannot play:
407
+ ` + playlist.unsupported.map((u) => ` \xB7 ${u.message}`).join("\n") + "\nPass { allowDivergence: true } to play the remainder anyway."
408
+ );
409
+ }
410
+ const uris = [];
411
+ const bounds = [];
412
+ for (const item of playlist.items) {
413
+ uris.push(uriOf(item.source));
414
+ bounds.push(item.startUs, item.endUs, Math.round(item.speed * 1e3), item.muted ? 1 : 0);
415
+ }
416
+ return requireNative().media3SetPlaylist(uris, bounds, start.windowIndex, start.positionUs);
417
+ },
418
+ /** Seek in the player's coordinates. Pair with `projectTimeToWindow`. */
419
+ seekToWindow(windowIndex, positionUs) {
420
+ requireNative().media3SeekToWindow(windowIndex, positionUs);
421
+ },
422
+ play() {
423
+ requireNative().media3Play();
424
+ },
425
+ pause() {
426
+ requireNative().media3Pause();
427
+ },
428
+ /** Microseconds, like every other time in Pictor. */
429
+ seek(positionUs) {
430
+ requireNative().media3Seek(positionUs);
431
+ },
432
+ setRate(rate) {
433
+ requireNative().media3SetRate(rate);
434
+ },
435
+ /**
436
+ * Try the current source again after a failure.
437
+ *
438
+ * This is Media3's own recovery path: it re-resolves the source and resumes
439
+ * from the current position. Worth calling when `errorRecoverable` is true —
440
+ * a network source that went away and came back — and pointless otherwise.
441
+ */
442
+ retry() {
443
+ requireNative().media3Retry();
444
+ },
445
+ release() {
446
+ requireNative().media3Release();
447
+ },
448
+ /** A snapshot for throttled reads. Never call this per frame. */
449
+ state() {
450
+ const raw = requireNative().media3State();
451
+ return raw ? { ...EMPTY, ...raw } : EMPTY;
452
+ }
453
+ };
454
+ function uriOf(source) {
455
+ if (typeof source === "string") return source;
456
+ if (source && typeof source === "object" && "uri" in source) {
457
+ const uri = source.uri;
458
+ if (typeof uri === "string") return uri;
459
+ }
460
+ throw new Error(
461
+ `@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)}`
462
+ );
463
+ }
464
+
465
+ // src/playlistSession.ts
466
+ var NO_SURFACE = 0;
467
+ function idlePlaylistSession() {
468
+ return { appliedSignature: null, appliedGeneration: NO_SURFACE, pending: false };
469
+ }
470
+ function shouldPrepare(session, intent) {
471
+ if (intent.generation === NO_SURFACE) return false;
472
+ if (session.pending) return true;
473
+ if (session.appliedSignature !== intent.signature) return true;
474
+ return session.appliedGeneration !== intent.generation;
475
+ }
476
+ function afterPrepare(session, intent, ok) {
477
+ if (!ok) return { ...session, pending: true };
478
+ return {
479
+ appliedSignature: intent.signature,
480
+ appliedGeneration: intent.generation,
481
+ pending: false
482
+ };
483
+ }
484
+ function retireSurface(session) {
485
+ return { ...session, appliedGeneration: NO_SURFACE, pending: true };
486
+ }
487
+ function isCurrentGeneration(generation, live) {
488
+ return generation !== NO_SURFACE && generation === live;
489
+ }
490
+
491
+ // src/surfaceOwner.ts
492
+ function createSurfaceOwner(initial) {
493
+ let effects = initial;
494
+ let readiness = { generation: NO_SURFACE, error: "" };
495
+ let busy = false;
496
+ let retired = 0;
497
+ let activeToken = null;
498
+ let activeRetired = false;
499
+ let currentToken = null;
500
+ let queuedToken = null;
501
+ const listeners = /* @__PURE__ */ new Set();
502
+ const emit = () => {
503
+ for (const listener of listeners) listener();
504
+ };
505
+ const publish = (next) => {
506
+ if (next.generation === readiness.generation && next.error === readiness.error) return;
507
+ readiness = next;
508
+ emit();
509
+ };
510
+ const start = (token) => {
511
+ busy = true;
512
+ activeToken = token;
513
+ activeRetired = false;
514
+ effects.create();
515
+ };
516
+ const owner = {
517
+ get readiness() {
518
+ return readiness;
519
+ },
520
+ get busy() {
521
+ return busy;
522
+ },
523
+ get retired() {
524
+ return retired;
525
+ },
526
+ request(token) {
527
+ currentToken = token;
528
+ if (readiness.generation !== NO_SURFACE) return false;
529
+ if (busy) {
530
+ queuedToken = token;
531
+ return false;
532
+ }
533
+ start(token);
534
+ return true;
535
+ },
536
+ retireOwner(token) {
537
+ if (activeToken === token) activeRetired = true;
538
+ if (currentToken === token) currentToken = null;
539
+ if (queuedToken === token) queuedToken = null;
540
+ },
541
+ adopt(generation, token) {
542
+ currentToken = token;
543
+ if (generation === NO_SURFACE) return;
544
+ publish({ generation, error: "" });
545
+ },
546
+ complete(result) {
547
+ const wasRetired = activeRetired;
548
+ const hasOwner = currentToken !== null;
549
+ busy = false;
550
+ activeToken = null;
551
+ activeRetired = false;
552
+ if (result.ok && result.generation > NO_SURFACE) {
553
+ const superseded = wasRetired || !hasOwner;
554
+ const stale = result.generation < readiness.generation;
555
+ if (superseded || stale) {
556
+ retired += 1;
557
+ effects.retire(result.generation);
558
+ } else {
559
+ publish({ generation: result.generation, error: "" });
560
+ }
561
+ } else {
562
+ publish({
563
+ generation: readiness.generation,
564
+ error: result.error || "surface create failed"
565
+ });
566
+ }
567
+ const next = queuedToken;
568
+ queuedToken = null;
569
+ if (next !== null && readiness.generation === NO_SURFACE && next === currentToken) {
570
+ start(next);
571
+ }
572
+ },
573
+ useEffects(next) {
574
+ effects = next;
575
+ },
576
+ subscribe(listener) {
577
+ listeners.add(listener);
578
+ return () => {
579
+ listeners.delete(listener);
580
+ };
581
+ }
582
+ };
583
+ return owner;
584
+ }
585
+ var singleton = null;
586
+ function surfaceOwner(effects) {
587
+ if (!singleton) singleton = createSurfaceOwner(effects);
588
+ else singleton.useEffects(effects);
589
+ return singleton;
590
+ }
591
+ function resetSurfaceOwnerForTests() {
592
+ singleton = null;
593
+ }
594
+ var nextToken = 0;
595
+ function nextOwnerToken() {
596
+ nextToken += 1;
597
+ return nextToken;
598
+ }
599
+
600
+ // src/useVideoSurface.ts
601
+ var import_react = require("react");
602
+ var import_react_native2 = require("react-native");
603
+ var import_react_native_reanimated = require("react-native-reanimated");
604
+ var CREATE_RETRY_MS = 250;
605
+ function videoSurfaceSlot() {
606
+ const published = globalThis.__pictorVideoSurface;
607
+ if (published) return published;
608
+ if (!AndroidVideoPlayer.installVideoSurface()) return void 0;
609
+ return globalThis.__pictorVideoSurface;
610
+ }
611
+ function useVideoSurface(options) {
612
+ const { width, height, enabled = true } = options;
613
+ const [readiness, setReadiness] = (0, import_react.useState)({ generation: NO_SURFACE, error: "" });
614
+ const handleRef = (0, import_react.useRef)(void 0);
615
+ if (!handleRef.current) handleRef.current = videoSurfaceSlot();
616
+ const textureId = (0, import_react_native_reanimated.useSharedValue)(0);
617
+ const tokenRef = (0, import_react.useRef)(0);
618
+ if (tokenRef.current === 0) tokenRef.current = nextOwnerToken();
619
+ const token = tokenRef.current;
620
+ const [attempt, setAttempt] = (0, import_react.useState)(0);
621
+ const ownerRef = (0, import_react.useRef)(void 0);
622
+ const complete = (0, import_react.useCallback)((result) => {
623
+ ownerRef.current?.complete(result);
624
+ }, []);
625
+ const owner = surfaceOwner({
626
+ create() {
627
+ const published = handleRef.current;
628
+ if (!published) {
629
+ complete({ ok: false, generation: 0, textureId: 0, error: "__pictorVideoSurface missing" });
630
+ return;
631
+ }
632
+ (0, import_react_native_reanimated.runOnUI)(() => {
633
+ "worklet";
634
+ const created = published.create(width, height);
635
+ textureId.value = created.textureId;
636
+ if (created.ok && !(created.generation > 0)) {
637
+ (0, import_react_native_reanimated.runOnJS)(complete)({
638
+ ok: false,
639
+ generation: 0,
640
+ textureId: created.textureId,
641
+ error: "adapter too old: create() reports no generation"
642
+ });
643
+ return;
644
+ }
645
+ (0, import_react_native_reanimated.runOnJS)(complete)({
646
+ ok: created.ok && created.textureId > 0,
647
+ generation: created.generation,
648
+ textureId: created.textureId,
649
+ // The native reason, not a summary of it. "No EGL context current on
650
+ // this thread" and "no JNIEnv" send a reader to different places.
651
+ error: created.ok ? "" : created.error || "surface create failed"
652
+ });
653
+ })();
654
+ },
655
+ retire(generation) {
656
+ const published = handleRef.current;
657
+ if (!published || typeof published.releaseGeneration !== "function") return;
658
+ (0, import_react_native_reanimated.runOnUI)(() => {
659
+ "worklet";
660
+ published.releaseGeneration(generation);
661
+ })();
662
+ }
663
+ });
664
+ ownerRef.current = owner;
665
+ (0, import_react.useEffect)(() => {
666
+ const unsubscribe = owner.subscribe(() => setReadiness(owner.readiness));
667
+ setReadiness(owner.readiness);
668
+ return unsubscribe;
669
+ }, [owner]);
670
+ const ensure = (0, import_react.useCallback)(() => {
671
+ if (!enabled) return;
672
+ const published = videoSurfaceSlot();
673
+ if (!published) {
674
+ setReadiness((prev) => ({ ...prev, error: "__pictorVideoSurface missing" }));
675
+ return;
676
+ }
677
+ handleRef.current = published;
678
+ if (typeof published.alive === "function" && typeof published.generation === "function" && published.alive() && published.generation() !== NO_SURFACE) {
679
+ owner.adopt(published.generation(), token);
680
+ return;
681
+ }
682
+ owner.request(token);
683
+ }, [enabled, owner, token]);
684
+ (0, import_react.useEffect)(() => {
685
+ ensure();
686
+ }, [ensure, attempt]);
687
+ (0, import_react.useEffect)(() => {
688
+ if (!enabled) return;
689
+ if (readiness.generation !== NO_SURFACE) return;
690
+ const id = setInterval(() => setAttempt((n) => n + 1), CREATE_RETRY_MS);
691
+ return () => clearInterval(id);
692
+ }, [enabled, readiness.generation]);
693
+ (0, import_react.useEffect)(() => {
694
+ if (!enabled) return;
695
+ const onChange = (status) => {
696
+ if (status === "active") {
697
+ ensure();
698
+ return;
699
+ }
700
+ owner.retireOwner(token);
701
+ };
702
+ const subscription = import_react_native2.AppState.addEventListener("change", onChange);
703
+ return () => subscription.remove();
704
+ }, [enabled, ensure, owner, token]);
705
+ (0, import_react.useEffect)(() => () => owner.retireOwner(token), [owner, token]);
706
+ return {
707
+ // Always the slot, never undefined once installed. Gating this on readiness
708
+ // is what put a null into the consumer's closure and kept it there.
709
+ surface: handleRef.current,
710
+ generation: readiness.generation,
711
+ error: readiness.error
712
+ };
713
+ }
714
+
715
+ // src/useCompositionPlayback.ts
716
+ var import_react2 = require("react");
717
+ var import_video_core2 = require("@pictor-sdk/video-core");
718
+
719
+ // src/compositionSpike.ts
720
+ var import_video_core = require("@pictor-sdk/video-core");
721
+ function compositionSpikeSpec(project, mix, resolve) {
722
+ const playlist = (0, import_video_core.playlistOf)(project);
723
+ const items = [];
724
+ const unsupported = playlist.unsupported.map((u) => `${u.clipId}: ${u.reason}`);
725
+ for (const item of playlist.items) {
726
+ const uri = resolve(item.source);
727
+ if (uri === null) {
728
+ unsupported.push(`${item.clipId}: unresolved`);
729
+ continue;
730
+ }
731
+ items.push({
732
+ uri,
733
+ startUs: Number(item.startUs),
734
+ endUs: Number(item.endUs),
735
+ speed: item.speed
736
+ });
737
+ }
738
+ return {
739
+ spec: {
740
+ items,
741
+ mixUri: mix.uri,
742
+ mixDurationUs: mix.durationUs,
743
+ width: project.size.width,
744
+ height: project.size.height
745
+ },
746
+ unsupported
747
+ };
748
+ }
749
+ function call(fn, label) {
750
+ try {
751
+ return JSON.parse(fn());
752
+ } catch (error) {
753
+ return { ok: false, error: `${label}: ${String(error)}` };
754
+ }
755
+ }
756
+ function spikeClaim() {
757
+ const result = call(() => requireNative().spikeClaim(), "spikeClaim");
758
+ return typeof result.requestId === "number" ? result.requestId : 0;
759
+ }
760
+ function spikeStart(spec) {
761
+ return call(() => requireNative().spikeStart(JSON.stringify(spec)), "spikeStart");
762
+ }
763
+ function spikePlay() {
764
+ return call(() => requireNative().spikePlay(), "spikePlay");
765
+ }
766
+ function spikePause() {
767
+ return call(() => requireNative().spikePause(), "spikePause");
768
+ }
769
+ function spikeSeek(positionUs) {
770
+ return call(() => requireNative().spikeSeek(positionUs), "spikeSeek");
771
+ }
772
+ function spikeState() {
773
+ return call(() => requireNative().spikeState(), "spikeState");
774
+ }
775
+ function spikeDrain(fromSeq, limit = 400) {
776
+ const page = call(() => requireNative().spikeDrain(fromSeq, limit), "spikeDrain");
777
+ return {
778
+ ok: page.ok === true,
779
+ rows: (page.rows ?? []).map((r) => ({
780
+ seq: r[0],
781
+ atNs: r[1],
782
+ positionUs: r[2],
783
+ playing: (r[3] & 1) === 1,
784
+ ready: (r[3] & 2) === 2,
785
+ epoch: r[4],
786
+ sinceEpochNs: r[5],
787
+ frameId: r[6],
788
+ frameGeneration: r[7],
789
+ frameWindow: r[8],
790
+ framePtsUs: r[9],
791
+ frameReleaseNs: r[10],
792
+ frameConsumedNs: r[11],
793
+ frameAvailableNs: r[12],
794
+ coalesced: r[13],
795
+ offered: r[14],
796
+ available: r[15],
797
+ taken: r[16],
798
+ readSpanNs: r[17],
799
+ frameMetadataNs: r[18] ?? -1
800
+ })),
801
+ nextSeq: page.nextSeq ?? fromSeq,
802
+ total: page.total ?? 0,
803
+ dropped: page.dropped ?? 0
804
+ };
805
+ }
806
+ function spikeRelease() {
807
+ return call(() => requireNative().spikeRelease(), "spikeRelease");
808
+ }
809
+
810
+ // src/playbackLifecycle.ts
811
+ function createPlaybackLifecycle(effects) {
812
+ let active = true;
813
+ let released = false;
814
+ let reason = null;
815
+ let epoch = 1;
816
+ return {
817
+ get active() {
818
+ return active;
819
+ },
820
+ get released() {
821
+ return released;
822
+ },
823
+ get reason() {
824
+ return reason;
825
+ },
826
+ get epoch() {
827
+ return epoch;
828
+ },
829
+ accepts(at) {
830
+ if (released) return false;
831
+ return active && at === epoch;
832
+ },
833
+ suspend(why) {
834
+ if (released || !active) return;
835
+ active = false;
836
+ reason = why;
837
+ epoch += 1;
838
+ effects.pause();
839
+ },
840
+ resume() {
841
+ if (released || active) return;
842
+ active = true;
843
+ reason = null;
844
+ epoch += 1;
845
+ },
846
+ teardown(why) {
847
+ if (released) return;
848
+ const wasActive = active;
849
+ released = true;
850
+ active = false;
851
+ reason = why;
852
+ epoch += 1;
853
+ if (wasActive) effects.pause();
854
+ effects.release();
855
+ }
856
+ };
857
+ }
858
+
859
+ // src/useCompositionPlayback.ts
860
+ var POLL_MS = 100;
861
+ function composedState(snapshot) {
862
+ const playbackState = snapshot.playbackState ?? 1;
863
+ return {
864
+ playing: snapshot.playing === true,
865
+ ready: playbackState === 3,
866
+ buffering: playbackState === 2,
867
+ ended: playbackState === 4,
868
+ // `released` counts releases; a positive count with nothing attached is the
869
+ // released state this means.
870
+ released: snapshot.attached !== true && (snapshot.released ?? 0) > 0,
871
+ positionUs: snapshot.positionUs ?? 0,
872
+ durationUs: snapshot.durationUs ?? 0,
873
+ /*
874
+ Zero, and honestly so.
875
+
876
+ A composition renders into the presentation surface at the size the host
877
+ gave it; it does not report a decoded size the way the video player did.
878
+ The host already knows the size it asked for, and a fabricated number here
879
+ would be worse than none.
880
+ */
881
+ width: 0,
882
+ height: 0,
883
+ error: snapshot.error ?? null
884
+ };
885
+ }
886
+ function useCompositionPlayback(options) {
887
+ const { project, mix, enabled = true } = options;
888
+ const surfaceGeneration = options.surfaceGeneration ?? 0;
889
+ const resolveSource = (0, import_react2.useRef)(options.resolveSource);
890
+ resolveSource.current = options.resolveSource;
891
+ const [state, setState] = (0, import_react2.useState)(null);
892
+ const [playing, setPlaying] = (0, import_react2.useState)(false);
893
+ const playheadRef = (0, import_react2.useRef)(options.playheadUs);
894
+ playheadRef.current = options.playheadUs;
895
+ const playlist = (0, import_react2.useMemo)(() => (0, import_video_core2.playlistOf)(project), [project]);
896
+ const items = playlist.items;
897
+ const durationUs = (0, import_react2.useMemo)(() => (0, import_video_core2.projectDuration)(project), [project]);
898
+ const needsMix = project.audio.length > 0;
899
+ const signature = (0, import_react2.useMemo)(() => {
900
+ if (needsMix && mix === null) return "";
901
+ const parts = items.map(
902
+ (i) => `${JSON.stringify(i.source)}|${i.startUs}|${i.endUs}|${i.speed}|${i.muted}`
903
+ );
904
+ const sound = mix === null ? "silent" : `${mix.uri}:${mix.durationUs}`;
905
+ return `${parts.join(";")}#${playlist.unsupported.length}@${sound}`;
906
+ }, [items, playlist.unsupported.length, mix, needsMix]);
907
+ const [unsupported, setUnsupported] = (0, import_react2.useState)([]);
908
+ const lastSeekRef = (0, import_react2.useRef)(null);
909
+ const lifecycleRef = (0, import_react2.useRef)(null);
910
+ if (lifecycleRef.current === null) {
911
+ lifecycleRef.current = createPlaybackLifecycle({
912
+ pause: () => void spikePause(),
913
+ release: () => void spikeRelease()
914
+ });
915
+ }
916
+ const lifecycle = lifecycleRef.current;
917
+ const [lifecycleEpoch, setLifecycleEpoch] = (0, import_react2.useState)(lifecycle.epoch);
918
+ const loaded = (0, import_react2.useRef)("");
919
+ (0, import_react2.useEffect)(() => {
920
+ if (!enabled || signature === "") return;
921
+ if (surfaceGeneration === 0) return;
922
+ if (loaded.current === signature) return;
923
+ const { spec, unsupported: refused } = compositionSpikeSpec(
924
+ project,
925
+ mix ?? { uri: "", durationUs: 0 },
926
+ (s) => resolveSource.current(s)
927
+ );
928
+ setUnsupported(
929
+ refused.map((reason) => ({ clipId: reason.split(":")[0] ?? "", reason }))
930
+ );
931
+ if (spec.items.length === 0) return;
932
+ const started = spikeStart(spec);
933
+ if (started.ok === true) loaded.current = signature;
934
+ }, [enabled, signature, surfaceGeneration, project, mix]);
935
+ (0, import_react2.useEffect)(() => {
936
+ return () => {
937
+ lifecycle.teardown("unmount");
938
+ };
939
+ }, [lifecycle]);
940
+ (0, import_react2.useEffect)(() => {
941
+ if (!enabled) return;
942
+ const epoch = lifecycle.epoch;
943
+ const id = setInterval(() => {
944
+ if (!lifecycle.accepts(epoch)) return;
945
+ const next = composedState(spikeState());
946
+ setState(
947
+ (previous) => previous !== null && previous.playing === next.playing && previous.ready === next.ready && previous.buffering === next.buffering && previous.ended === next.ended && previous.released === next.released && previous.error === next.error ? previous : next
948
+ );
949
+ setPlaying(next.playing);
950
+ }, POLL_MS);
951
+ return () => clearInterval(id);
952
+ }, [enabled, lifecycle, lifecycleEpoch]);
953
+ const seek = (0, import_react2.useCallback)((timeUs) => {
954
+ const at = Math.max(0, Math.round(timeUs));
955
+ lastSeekRef.current = { globalUs: at, windowIndex: 0, localUs: at };
956
+ spikeSeek(at);
957
+ }, []);
958
+ const positionUs = (0, import_react2.useCallback)(() => {
959
+ const snapshot = spikeState();
960
+ if (!snapshot.attached) return null;
961
+ const at = snapshot.positionUs;
962
+ return typeof at === "number" ? at : null;
963
+ }, []);
964
+ const play = (0, import_react2.useCallback)(() => {
965
+ const snapshot = spikeState();
966
+ const intent = (0, import_video_core2.playIntent)({
967
+ positionUs: typeof snapshot.positionUs === "number" ? snapshot.positionUs : 0,
968
+ durationUs,
969
+ playbackState: typeof snapshot.playbackState === "number" ? snapshot.playbackState : 1,
970
+ ended: snapshot.playbackState === 4
971
+ });
972
+ if (intent.seekToUs !== null) seek(intent.seekToUs);
973
+ if (!intent.play) return intent;
974
+ if (lifecycle.released) return intent;
975
+ spikePlay();
976
+ setPlaying(true);
977
+ return intent;
978
+ }, [durationUs, lifecycle, seek]);
979
+ (0, import_react2.useEffect)(() => {
980
+ setLifecycleEpoch(lifecycle.epoch);
981
+ }, [lifecycle, surfaceGeneration]);
982
+ return {
983
+ playing,
984
+ play,
985
+ get lastSeek() {
986
+ return lastSeekRef.current;
987
+ },
988
+ positionUs,
989
+ effectiveRate: 1,
990
+ pause: (0, import_react2.useCallback)(() => {
991
+ spikePause();
992
+ setPlaying(false);
993
+ }, []),
994
+ /*
995
+ Rate is the document's, not the transport's.
996
+
997
+ Clip speed is compiled into the composition and into the mix, and a
998
+ preview-wide rate on top of it would be a second speed nobody asked for.
999
+ The contract keeps the method; this path has nothing to do with it.
1000
+ */
1001
+ setRate: (0, import_react2.useCallback)(() => {
1002
+ }, []),
1003
+ seek,
1004
+ state,
1005
+ items,
1006
+ unsupported,
1007
+ divergenceUs: 0,
1008
+ lifecycle,
1009
+ // One player, so there is no half to stall: the recovery this reported was
1010
+ // the video player running while the audio player had stopped, or the
1011
+ // reverse. @see docs/research/preview-acceptance.md
1012
+ stall: null
1013
+ };
1014
+ }
1015
+
1016
+ // src/useMedia3Playback.ts
1017
+ var import_video_core3 = require("@pictor-sdk/video-core");
1018
+ var import_react3 = require("react");
1019
+ var import_react_native3 = require("react-native");
1020
+
1021
+ // src/surfaceRecovery.ts
1022
+ var RECOVERY_GRACE_MS = 1500;
1023
+ function classifyStall(since, now) {
1024
+ if (now.positionUs <= since.positionUs) return null;
1025
+ if (now.pendingGeneration !== now.consumerGeneration || now.framesRejected > since.framesRejected) {
1026
+ return "handshake";
1027
+ }
1028
+ const latched = now.framesLatched > since.framesLatched;
1029
+ if (latched) {
1030
+ return "resource";
1031
+ }
1032
+ const arrived = now.framesAvailable > since.framesAvailable;
1033
+ if (arrived) return "consumer";
1034
+ return "attachment";
1035
+ }
1036
+ function shouldRecover(since, now, graceMs = RECOVERY_GRACE_MS) {
1037
+ if (now.atMs - since.atMs < graceMs) return false;
1038
+ const kind = classifyStall(since, now);
1039
+ return kind === "consumer" || kind === "handshake" || kind === "attachment";
1040
+ }
1041
+
1042
+ // src/useMedia3Playback.ts
1043
+ var DEFAULT_POLL_MS = 100;
1044
+ var RETRY_INTERVAL_MS = 250;
1045
+ function useMedia3Playback(options) {
1046
+ const { project, playheadUs, enabled = true } = options;
1047
+ const surfaceGeneration = options.surfaceGeneration ?? NO_SURFACE;
1048
+ const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
1049
+ const [state, setState] = (0, import_react3.useState)(null);
1050
+ const [playing, setPlaying] = (0, import_react3.useState)(false);
1051
+ const playlist = (0, import_react3.useMemo)(() => (0, import_video_core3.playlistOf)(project), [project]);
1052
+ const items = playlist.items;
1053
+ const divergenceUs = (0, import_react3.useMemo)(
1054
+ () => (0, import_video_core3.projectDuration)(project) - (0, import_video_core3.playableDuration)(playlist),
1055
+ [project, playlist]
1056
+ );
1057
+ const signature = (0, import_react3.useMemo)(
1058
+ () => items.map((i) => `${JSON.stringify(i.source)}|${i.startUs}|${i.endUs}|${i.speed}|${i.muted}`).join(";") + // Unsupported clips belong in the signature even though they are not
1059
+ // windows. Adding a photo between two videos leaves the video windows
1060
+ // identical, so a signature built from items alone would not change and
1061
+ // the player would keep playing a timeline that had grown underneath it —
1062
+ // the exact divergence this is all meant to prevent.
1063
+ `!${playlist.unsupported.map((u) => `${u.clipId}:${u.reason}`).join(",")}`,
1064
+ [items, playlist.unsupported]
1065
+ );
1066
+ const playheadRef = (0, import_react3.useRef)(playheadUs);
1067
+ playheadRef.current = playheadUs;
1068
+ const itemsRef = (0, import_react3.useRef)(items);
1069
+ itemsRef.current = items;
1070
+ const session = (0, import_react3.useRef)(idlePlaylistSession());
1071
+ const [retry, setRetry] = (0, import_react3.useState)(0);
1072
+ (0, import_react3.useEffect)(() => {
1073
+ if (!enabled) return;
1074
+ const intent = { signature, generation: surfaceGeneration };
1075
+ if (!shouldPrepare(session.current, intent)) return;
1076
+ let ok;
1077
+ try {
1078
+ if (playlist.unsupported.length > 0 || items.length === 0) {
1079
+ ok = Media3.setPlaylist({ items: [], unsupported: [] }) !== false;
1080
+ } else {
1081
+ const start = (0, import_video_core3.projectTimeToWindow)(items, playheadRef.current) ?? {
1082
+ windowIndex: 0,
1083
+ positionUs: 0
1084
+ };
1085
+ ok = Media3.setPlaylist(playlist, start) !== false;
1086
+ }
1087
+ } catch {
1088
+ ok = false;
1089
+ }
1090
+ session.current = afterPrepare(session.current, intent, ok);
1091
+ }, [signature, items, playlist, enabled, surfaceGeneration, retry]);
1092
+ (0, import_react3.useEffect)(() => {
1093
+ if (!enabled) return;
1094
+ if (!session.current.pending) return;
1095
+ const id = setInterval(() => setRetry((n) => n + 1), RETRY_INTERVAL_MS);
1096
+ return () => clearInterval(id);
1097
+ }, [enabled, retry, surfaceGeneration, signature]);
1098
+ const stallRef = (0, import_react3.useRef)(null);
1099
+ const surfaceGenerationRef = (0, import_react3.useRef)(surfaceGeneration ?? 0);
1100
+ surfaceGenerationRef.current = surfaceGeneration ?? 0;
1101
+ const lifecycleRef = (0, import_react3.useRef)(null);
1102
+ if (lifecycleRef.current === null) {
1103
+ lifecycleRef.current = createPlaybackLifecycle({
1104
+ pause: () => Media3.pause(),
1105
+ release: () => Media3.release()
1106
+ });
1107
+ }
1108
+ const lifecycle = lifecycleRef.current;
1109
+ const [lifecycleEpoch, setLifecycleEpoch] = (0, import_react3.useState)(lifecycle.epoch);
1110
+ (0, import_react3.useEffect)(() => {
1111
+ const subscription = import_react_native3.AppState.addEventListener("change", (next) => {
1112
+ if (next === "active") {
1113
+ lifecycle.resume();
1114
+ setLifecycleEpoch(lifecycle.epoch);
1115
+ return;
1116
+ }
1117
+ lifecycle.suspend("background");
1118
+ setLifecycleEpoch(lifecycle.epoch);
1119
+ setPlaying(false);
1120
+ });
1121
+ return () => subscription.remove();
1122
+ }, [lifecycle]);
1123
+ (0, import_react3.useEffect)(() => {
1124
+ return () => {
1125
+ lifecycle.teardown("unmount");
1126
+ };
1127
+ }, [lifecycle]);
1128
+ const healthy = (0, import_react3.useRef)(null);
1129
+ const [stall, setStall] = (0, import_react3.useState)(null);
1130
+ const lastMeaning = (0, import_react3.useRef)("");
1131
+ (0, import_react3.useEffect)(() => {
1132
+ if (!enabled) return;
1133
+ const epoch = lifecycle.epoch;
1134
+ const id = setInterval(() => {
1135
+ if (!lifecycle.accepts(epoch)) return;
1136
+ const snapshot = Media3.state();
1137
+ const meaning = [
1138
+ snapshot.playing,
1139
+ snapshot.ready,
1140
+ snapshot.buffering,
1141
+ snapshot.ended,
1142
+ snapshot.released,
1143
+ snapshot.width,
1144
+ snapshot.height,
1145
+ snapshot.windowIndex,
1146
+ snapshot.windowCount,
1147
+ snapshot.errorCount,
1148
+ snapshot.errorMessage,
1149
+ snapshot.rateConstrained
1150
+ ].join("|");
1151
+ if (meaning !== lastMeaning.current) {
1152
+ lastMeaning.current = meaning;
1153
+ setState(snapshot);
1154
+ setPlaying(snapshot.playing);
1155
+ }
1156
+ const sample = {
1157
+ atMs: Date.now(),
1158
+ positionUs: snapshot.windowPositionUs,
1159
+ framesOffered: snapshot.framesOffered,
1160
+ framesAvailable: snapshot.framesAvailable,
1161
+ // Latched is not reported directly: what arrived, less what is still
1162
+ // waiting, is the count the consumer has actually taken.
1163
+ framesLatched: snapshot.framesAvailable - snapshot.pendingDepth,
1164
+ framesRejected: snapshot.framesRejected,
1165
+ pendingGeneration: snapshot.pendingGeneration,
1166
+ consumerGeneration: surfaceGenerationRef.current
1167
+ };
1168
+ const since = healthy.current;
1169
+ if (since === null) {
1170
+ healthy.current = sample;
1171
+ } else if (classifyStall(since, sample) === null) {
1172
+ healthy.current = sample;
1173
+ if (stallRef.current !== null) setStall(null);
1174
+ } else if (shouldRecover(since, sample)) {
1175
+ const kind = classifyStall(since, sample);
1176
+ healthy.current = sample;
1177
+ stallRef.current = kind;
1178
+ setStall(kind);
1179
+ session.current = { ...session.current, pending: true };
1180
+ setRetry((n) => n + 1);
1181
+ }
1182
+ if (!snapshot.playing) return;
1183
+ const projectUs = (0, import_video_core3.windowToProjectTime)(
1184
+ itemsRef.current,
1185
+ snapshot.windowIndex,
1186
+ snapshot.windowPositionUs
1187
+ );
1188
+ if (projectUs !== null) options.onPositionUs?.(projectUs);
1189
+ }, pollMs);
1190
+ return () => clearInterval(id);
1191
+ }, [enabled, pollMs, lifecycle, lifecycleEpoch]);
1192
+ const lastSeekRef = (0, import_react3.useRef)(null);
1193
+ const seek = (0, import_react3.useCallback)((timeUs) => {
1194
+ const target = (0, import_video_core3.projectTimeToWindow)(itemsRef.current, timeUs);
1195
+ if (!target) return;
1196
+ lastSeekRef.current = {
1197
+ globalUs: Math.round(timeUs),
1198
+ windowIndex: target.windowIndex,
1199
+ localUs: target.positionUs
1200
+ };
1201
+ Media3.seekToWindow(target.windowIndex, target.positionUs);
1202
+ }, []);
1203
+ const positionUs = (0, import_react3.useCallback)(() => {
1204
+ const snapshot = Media3.state();
1205
+ if (snapshot.released || itemsRef.current.length === 0) return null;
1206
+ return (0, import_video_core3.windowToProjectTime)(
1207
+ itemsRef.current,
1208
+ snapshot.windowIndex,
1209
+ snapshot.windowPositionUs
1210
+ );
1211
+ }, []);
1212
+ const durationUsRef = (0, import_react3.useRef)(0);
1213
+ durationUsRef.current = (0, import_video_core3.projectDuration)(project);
1214
+ const play = (0, import_react3.useCallback)(() => {
1215
+ const snapshot = Media3.state();
1216
+ const intent = (0, import_video_core3.playIntent)({
1217
+ positionUs: (0, import_video_core3.windowToProjectTime)(
1218
+ itemsRef.current,
1219
+ snapshot.windowIndex,
1220
+ snapshot.windowPositionUs
1221
+ ) ?? 0,
1222
+ durationUs: durationUsRef.current,
1223
+ playbackState: snapshot.playbackState,
1224
+ ended: snapshot.ended
1225
+ });
1226
+ if (intent.seekToUs !== null) {
1227
+ const target = (0, import_video_core3.projectTimeToWindow)(itemsRef.current, intent.seekToUs);
1228
+ if (target) Media3.seekToWindow(target.windowIndex, target.positionUs);
1229
+ }
1230
+ if (!intent.play) return intent;
1231
+ if (lifecycle.released) return intent;
1232
+ Media3.play();
1233
+ setPlaying(true);
1234
+ return intent;
1235
+ }, [lifecycle]);
1236
+ return {
1237
+ playing,
1238
+ play,
1239
+ get lastSeek() {
1240
+ return lastSeekRef.current;
1241
+ },
1242
+ positionUs,
1243
+ effectiveRate: state?.effectiveRate ?? 1,
1244
+ pause: (0, import_react3.useCallback)(() => {
1245
+ Media3.pause();
1246
+ setPlaying(false);
1247
+ }, []),
1248
+ setRate: (0, import_react3.useCallback)((rate) => Media3.setRate(rate), []),
1249
+ seek,
1250
+ state,
1251
+ items,
1252
+ unsupported: playlist.unsupported,
1253
+ divergenceUs,
1254
+ lifecycle,
1255
+ stall
1256
+ };
1257
+ }
1258
+
1259
+ // src/thumbnailProvider.ts
1260
+ var import_video_core4 = require("@pictor-sdk/video-core");
1261
+ var DEFAULT_CAPACITY = 240;
1262
+ var DEFAULT_POLL_MS2 = 16;
1263
+ function createAndroidThumbnailProvider(options = {}) {
1264
+ const policy = (0, import_video_core4.createThumbnailPolicy)({
1265
+ capacity: options.capacity ?? DEFAULT_CAPACITY
1266
+ });
1267
+ const pollMs = options.pollMs ?? DEFAULT_POLL_MS2;
1268
+ const tokens = /* @__PURE__ */ new Map();
1269
+ function uriOf2(source) {
1270
+ if (typeof source === "string") return source;
1271
+ if (source && typeof source === "object" && "uri" in source) {
1272
+ const uri = source.uri;
1273
+ if (typeof uri === "string") return uri;
1274
+ }
1275
+ throw new Error(
1276
+ `@pictor-sdk/video-android: a thumbnail source is not a URI. Normalise host media to { kind: 'uri', uri } first. Received: ${JSON.stringify(source)}`
1277
+ );
1278
+ }
1279
+ return {
1280
+ async request(request, signal) {
1281
+ const key = (0, import_video_core4.thumbnailKey)(request);
1282
+ const cached = policy.get(request);
1283
+ if (cached !== void 0) {
1284
+ return { image: cached, sourceTimeUs: request.sourceTimeUs, size: { width: 0, height: 0 } };
1285
+ }
1286
+ if (signal?.aborted) return null;
1287
+ const native = requireNative();
1288
+ policy.begin(request);
1289
+ const token = native.thumbnailRequest(
1290
+ uriOf2(request.source),
1291
+ request.sourceTimeUs,
1292
+ request.widthPx
1293
+ );
1294
+ tokens.set(key, token);
1295
+ const stop = () => {
1296
+ native.thumbnailCancel(token);
1297
+ tokens.delete(key);
1298
+ policy.cancel(request);
1299
+ };
1300
+ signal?.addEventListener("abort", stop, { once: true });
1301
+ try {
1302
+ for (; ; ) {
1303
+ if (signal?.aborted) return null;
1304
+ const data = native.thumbnailTake(token);
1305
+ if (data !== null && data !== void 0) {
1306
+ tokens.delete(key);
1307
+ const evicted = policy.store(request, `data:image/jpeg;base64,${data}`);
1308
+ void evicted;
1309
+ return {
1310
+ image: policy.get(request),
1311
+ sourceTimeUs: request.sourceTimeUs,
1312
+ size: { width: request.widthPx, height: 0 }
1313
+ };
1314
+ }
1315
+ const error = native.thumbnailError(token);
1316
+ if (error !== null && error !== void 0) {
1317
+ tokens.delete(key);
1318
+ policy.cancel(request);
1319
+ return null;
1320
+ }
1321
+ if (!native.thumbnailRunning(token)) {
1322
+ tokens.delete(key);
1323
+ policy.cancel(request);
1324
+ return null;
1325
+ }
1326
+ await new Promise((resolve) => setTimeout(() => resolve(), pollMs));
1327
+ }
1328
+ } finally {
1329
+ signal?.removeEventListener("abort", stop);
1330
+ }
1331
+ },
1332
+ releaseSource(source) {
1333
+ policy.evictSource(source);
1334
+ requireNative().thumbnailReleaseSource(uriOf2(source));
1335
+ },
1336
+ release() {
1337
+ for (const token of tokens.values()) requireNative().thumbnailCancel(token);
1338
+ tokens.clear();
1339
+ policy.clear();
1340
+ requireNative().thumbnailReleaseAll();
1341
+ }
1342
+ };
1343
+ }
1344
+ function thumbnailCounters() {
1345
+ return requireNative().thumbnailCounters();
1346
+ }
1347
+
1348
+ // src/convergence.ts
1349
+ var CONVERGED_RUN = 8;
1350
+ var MAX_RECOVERY_SAMPLES = 600;
1351
+ function beginPhase(phase) {
1352
+ return {
1353
+ phase,
1354
+ samples: [],
1355
+ run: 0,
1356
+ worstUs: 0,
1357
+ recoverySamples: 0,
1358
+ converged: phase === "steady",
1359
+ timedOut: false
1360
+ };
1361
+ }
1362
+ function observe(state, offsetUs, boundUs) {
1363
+ if (state.converged) {
1364
+ return { state, bucket: "steady" };
1365
+ }
1366
+ const magnitude = Math.abs(offsetUs);
1367
+ const next = {
1368
+ ...state,
1369
+ samples: [...state.samples, offsetUs],
1370
+ recoverySamples: state.recoverySamples + 1,
1371
+ worstUs: Math.max(state.worstUs, magnitude),
1372
+ run: magnitude <= boundUs ? state.run + 1 : 0
1373
+ };
1374
+ if (next.run >= CONVERGED_RUN) {
1375
+ next.converged = true;
1376
+ } else if (next.recoverySamples >= MAX_RECOVERY_SAMPLES) {
1377
+ next.converged = true;
1378
+ next.timedOut = true;
1379
+ }
1380
+ return { state: next, bucket: state.phase };
1381
+ }
1382
+ function recoveryFrames(state) {
1383
+ return state.converged ? state.recoverySamples : null;
1384
+ }
1385
+
1386
+ // src/exportJob.ts
1387
+ var import_video_core5 = require("@pictor-sdk/video-core");
1388
+ var EXPORT_STAGES = [
1389
+ "resolving",
1390
+ "overlays",
1391
+ "shards",
1392
+ "cadence",
1393
+ "audio",
1394
+ "concatenating",
1395
+ "finalising",
1396
+ "publishing"
1397
+ ];
1398
+ var counter = 0;
1399
+ function createExportJobs(options) {
1400
+ const now = options.now ?? (() => Date.now());
1401
+ let active = null;
1402
+ let cancelledAt = null;
1403
+ let cancelRequested = false;
1404
+ let settled = false;
1405
+ let accepted = 0;
1406
+ let succeeded = 0;
1407
+ let cancelled = 0;
1408
+ let failed = 0;
1409
+ const clear = () => {
1410
+ active = null;
1411
+ cancelRequested = false;
1412
+ cancelledAt = null;
1413
+ settled = true;
1414
+ };
1415
+ return {
1416
+ get active() {
1417
+ return active;
1418
+ },
1419
+ get counts() {
1420
+ return {
1421
+ accepted,
1422
+ succeeded,
1423
+ cancelled,
1424
+ failed,
1425
+ active: active === null ? 0 : 1
1426
+ };
1427
+ },
1428
+ async run(body) {
1429
+ if (active !== null) {
1430
+ throw new Error(
1431
+ `createMedia3VideoExporter: an export is already running (${active}); cancel it before starting another`
1432
+ );
1433
+ }
1434
+ counter += 1;
1435
+ accepted += 1;
1436
+ const id = `export_${now().toString(36)}_${counter.toString(36)}`;
1437
+ active = id;
1438
+ cancelRequested = false;
1439
+ cancelledAt = null;
1440
+ settled = false;
1441
+ const job = {
1442
+ id,
1443
+ checkpoint(stage) {
1444
+ if (cancelRequested) {
1445
+ cancelledAt = stage;
1446
+ throw new import_video_core5.VideoExportCancelled(id, stage);
1447
+ }
1448
+ },
1449
+ progress(value, deliver) {
1450
+ if (settled || cancelRequested) return;
1451
+ deliver(value);
1452
+ }
1453
+ };
1454
+ try {
1455
+ const result = await body(job);
1456
+ if (cancelRequested) {
1457
+ throw new import_video_core5.VideoExportCancelled(id, cancelledAt ?? "finalising");
1458
+ }
1459
+ clear();
1460
+ succeeded += 1;
1461
+ return result;
1462
+ } catch (error) {
1463
+ const wasCancelled = cancelRequested;
1464
+ const stage = cancelledAt ?? "finalising";
1465
+ clear();
1466
+ if (wasCancelled) cancelled += 1;
1467
+ else failed += 1;
1468
+ if (wasCancelled) {
1469
+ if (error instanceof import_video_core5.VideoExportCancelled) throw error;
1470
+ throw new import_video_core5.VideoExportCancelled(id, stage);
1471
+ }
1472
+ throw error;
1473
+ }
1474
+ },
1475
+ cancel() {
1476
+ if (active === null || cancelRequested) return;
1477
+ cancelRequested = true;
1478
+ options.cancelNative(active);
1479
+ }
1480
+ };
1481
+ }
1482
+
1483
+ // src/mixSources.ts
1484
+ function mixSpanContext(project, spanId) {
1485
+ const isClip = project.clips.some((clip) => clip.id === spanId);
1486
+ const isAudio = project.audio.some((item) => item.id === spanId);
1487
+ if (isClip === isAudio) return null;
1488
+ return isClip ? { kind: "clip", clipId: spanId } : { kind: "audio", audioId: spanId };
1489
+ }
1490
+ function resolveMixSources(project, program, resolveSource) {
1491
+ const uris = {};
1492
+ const unresolved = [];
1493
+ for (const span of program.spans) {
1494
+ if (span.silent || span.source === null) continue;
1495
+ const context = mixSpanContext(project, span.id);
1496
+ if (context === null) {
1497
+ unresolved.push(
1498
+ `audio span '${span.id}' matches no clip or audio item, so it has no owner to resolve against`
1499
+ );
1500
+ continue;
1501
+ }
1502
+ const uri = resolveSource(span.source, context);
1503
+ if (typeof uri !== "string" || uri.length === 0) {
1504
+ unresolved.push(`audio span '${span.id}' could not be resolved to a readable source`);
1505
+ continue;
1506
+ }
1507
+ uris[span.id] = uri;
1508
+ }
1509
+ return { uris, unresolved };
1510
+ }
1511
+
1512
+ // src/exportDestination.ts
1513
+ function describeTarget(path) {
1514
+ const trimmed = path.replace(/\/+$/, "");
1515
+ const name = trimmed.slice(trimmed.lastIndexOf("/") + 1);
1516
+ return name.length > 0 ? name : "(unnamed)";
1517
+ }
1518
+ var KNOWN_SCHEMES = {
1519
+ "content:": "a content uri is not a filesystem path, so it cannot be published atomically",
1520
+ "http:": "a remote destination cannot be written to directly",
1521
+ "https:": "a remote destination cannot be written to directly",
1522
+ "android.resource:": "app resources are read-only",
1523
+ "asset:": "bundled assets are read-only"
1524
+ };
1525
+ function resolveDestination(destination) {
1526
+ if (destination === void 0) return { destination: { kind: "temporary" }, reasons: [] };
1527
+ if (destination.kind === "temporary") return { destination: { kind: "temporary" }, reasons: [] };
1528
+ if (destination.kind !== "file") {
1529
+ return {
1530
+ destination: null,
1531
+ reasons: [`destination kind '${String(destination.kind)}' is not supported`]
1532
+ };
1533
+ }
1534
+ const uri = destination.uri;
1535
+ if (typeof uri !== "string" || uri.trim().length === 0) {
1536
+ return { destination: null, reasons: ["the file destination has no uri"] };
1537
+ }
1538
+ const scheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.exec(uri)?.[0]?.toLowerCase();
1539
+ if (scheme !== void 0 && scheme !== "file:") {
1540
+ const known = KNOWN_SCHEMES[scheme];
1541
+ return {
1542
+ destination: null,
1543
+ reasons: [
1544
+ known !== void 0 ? `destination scheme '${scheme}' is not supported: ${known}` : `destination scheme '${scheme}' is not supported; only file: is`
1545
+ ]
1546
+ };
1547
+ }
1548
+ if (scheme === void 0) {
1549
+ return { destination: null, reasons: ["the file destination must be a file: uri"] };
1550
+ }
1551
+ let path;
1552
+ try {
1553
+ const withoutScheme = uri.slice("file:".length);
1554
+ const rest = withoutScheme.startsWith("//") ? withoutScheme.slice(2) : withoutScheme;
1555
+ if (withoutScheme.startsWith("//") && !rest.startsWith("/")) {
1556
+ return {
1557
+ destination: null,
1558
+ reasons: ["the file destination names a remote host, which is not supported"]
1559
+ };
1560
+ }
1561
+ path = decodeURIComponent(rest);
1562
+ } catch {
1563
+ return { destination: null, reasons: ["the file destination uri is malformed"] };
1564
+ }
1565
+ if (!path.startsWith("/")) {
1566
+ return { destination: null, reasons: ["the file destination must be an absolute path"] };
1567
+ }
1568
+ if (path.endsWith("/")) {
1569
+ return {
1570
+ destination: null,
1571
+ reasons: ["the file destination is a directory; it must name the file to write"]
1572
+ };
1573
+ }
1574
+ if (path.includes("\0")) {
1575
+ return { destination: null, reasons: ["the file destination uri is malformed"] };
1576
+ }
1577
+ if (path.split("/").some((segment) => segment === "..")) {
1578
+ return { destination: null, reasons: ['the file destination must not contain ".."'] };
1579
+ }
1580
+ const name = describeTarget(path);
1581
+ if (name === "." || name === "(unnamed)") {
1582
+ return {
1583
+ destination: null,
1584
+ reasons: ["the file destination is a directory; it must name the file to write"]
1585
+ };
1586
+ }
1587
+ return {
1588
+ destination: { kind: "file", path, overwrite: destination.overwrite === true },
1589
+ reasons: []
1590
+ };
1591
+ }
1592
+
1593
+ // src/exportProgress.ts
1594
+ var STALL_GRACE_MS = 7e3;
1595
+ var MAX_OBSERVED_HEARTBEAT_GAP_MS = 2046;
1596
+ var AUDIO_UNIT = 1 / 512;
1597
+ var MUX_UNITS = 24;
1598
+ var PUBLISH_UNITS = 8;
1599
+ function planProgressWeights(input) {
1600
+ const shardUnits = input.shards.map((s) => Math.max(0, s.frames));
1601
+ const audioUnits = input.audioSamples > 0 ? input.audioSamples * AUDIO_UNIT : 0;
1602
+ const total = shardUnits.reduce((a, b) => a + b, 0) + audioUnits + MUX_UNITS + PUBLISH_UNITS;
1603
+ return {
1604
+ shardWeights: shardUnits.map((u) => u / total),
1605
+ audioWeight: audioUnits / total,
1606
+ muxWeight: MUX_UNITS / total,
1607
+ publishWeight: PUBLISH_UNITS / total,
1608
+ durationUs: Math.max(0, input.durationUs),
1609
+ shardFrames: shardUnits,
1610
+ audioSamples: Math.max(0, input.audioSamples)
1611
+ };
1612
+ }
1613
+ function createProgressReporter(options) {
1614
+ const now = options.now ?? (() => Date.now());
1615
+ const throttleMs = options.throttleMs ?? 120;
1616
+ const plan = options.plan;
1617
+ const shardDone = plan.shardWeights.map(() => 0);
1618
+ let audioDone = 0;
1619
+ let muxDone = 0;
1620
+ let publishDone = 0;
1621
+ let highest = 0;
1622
+ let began = false;
1623
+ let finished = false;
1624
+ let stopped = false;
1625
+ let lastEmitMs = Number.NEGATIVE_INFINITY;
1626
+ let batching = 0;
1627
+ let batchForce = false;
1628
+ const fraction = () => {
1629
+ let sum = 0;
1630
+ for (let i = 0; i < shardDone.length; i += 1) {
1631
+ sum += plan.shardWeights[i] * shardDone[i];
1632
+ }
1633
+ return sum + plan.audioWeight * audioDone + plan.muxWeight * muxDone + plan.publishWeight * publishDone;
1634
+ };
1635
+ const emit = (value, force) => {
1636
+ if (stopped || !began) return;
1637
+ if (batching > 0) {
1638
+ batchForce = batchForce || force;
1639
+ return;
1640
+ }
1641
+ const next = Math.max(highest, Math.min(1, value));
1642
+ const at = now();
1643
+ if (!force && at - lastEmitMs < throttleMs) return;
1644
+ if (!force && began && next === highest) return;
1645
+ highest = next;
1646
+ lastEmitMs = at;
1647
+ try {
1648
+ options.onProgress?.({
1649
+ progress: highest,
1650
+ time: Math.min(plan.durationUs, Math.round(plan.durationUs * highest))
1651
+ });
1652
+ } catch {
1653
+ }
1654
+ };
1655
+ return {
1656
+ begin() {
1657
+ if (began || stopped) return;
1658
+ began = true;
1659
+ emit(0, true);
1660
+ },
1661
+ shard(index, framesDone) {
1662
+ if (finished || stopped) return;
1663
+ if (index < 0 || index >= shardDone.length) return;
1664
+ const total = Math.max(1, plan.shardFrames[index] ?? 1);
1665
+ shardDone[index] = Math.max(shardDone[index], Math.min(1, framesDone / total));
1666
+ emit(fraction(), false);
1667
+ },
1668
+ audio(samplesDone) {
1669
+ if (finished || stopped) return;
1670
+ const total = Math.max(1, plan.audioSamples);
1671
+ audioDone = Math.max(audioDone, Math.min(1, samplesDone / total));
1672
+ emit(fraction(), false);
1673
+ },
1674
+ muxed() {
1675
+ if (finished || stopped) return;
1676
+ muxDone = 1;
1677
+ emit(fraction(), true);
1678
+ },
1679
+ complete() {
1680
+ if (finished || stopped) return;
1681
+ finished = true;
1682
+ publishDone = 1;
1683
+ emit(1, true);
1684
+ },
1685
+ batch(body) {
1686
+ batching += 1;
1687
+ try {
1688
+ body();
1689
+ } finally {
1690
+ batching -= 1;
1691
+ }
1692
+ const force = batchForce;
1693
+ batchForce = false;
1694
+ emit(finished ? 1 : fraction(), force);
1695
+ },
1696
+ stop() {
1697
+ stopped = true;
1698
+ }
1699
+ };
1700
+ }
1701
+ function createStallWatchdog(options = {}) {
1702
+ const thresholdMs = options.thresholdMs ?? STALL_GRACE_MS;
1703
+ let lastStage = null;
1704
+ let lastUnits = 0;
1705
+ let lastAtMs = 0;
1706
+ let fired = false;
1707
+ return {
1708
+ advanced(stage, units, atMs) {
1709
+ lastStage = stage;
1710
+ lastUnits = units;
1711
+ lastAtMs = atMs;
1712
+ },
1713
+ stalledFor(atMs) {
1714
+ if (fired || lastStage === null) return null;
1715
+ const idleMs = atMs - lastAtMs;
1716
+ if (idleMs < thresholdMs) return null;
1717
+ fired = true;
1718
+ return { stage: lastStage, units: lastUnits, idleMs };
1719
+ }
1720
+ };
1721
+ }
1722
+
1723
+ // src/exportProgressPump.ts
1724
+ function stageOf(snapshot, plan) {
1725
+ if (snapshot.muxed) return "publishing";
1726
+ const shardsDone = plan.shardFrames.every(
1727
+ (total, index) => (snapshot.shards[index] ?? 0) >= total
1728
+ );
1729
+ if (!shardsDone) return "shards";
1730
+ if (snapshot.audioPlanned > 0 && snapshot.audioSamples < snapshot.audioPlanned) return "audio";
1731
+ return "concatenating";
1732
+ }
1733
+ function createProgressPump(options) {
1734
+ const now = options.now ?? (() => Date.now());
1735
+ const pollMs = options.pollMs ?? 125;
1736
+ const plan = options.plan;
1737
+ const reporter = createProgressReporter({
1738
+ plan,
1739
+ durationUs: options.durationUs,
1740
+ onProgress: options.onProgress,
1741
+ // The reporter's own throttle is the poll interval: a tick that finds
1742
+ // nothing new should not be able to spend a crossing saying so.
1743
+ throttleMs: pollMs,
1744
+ now
1745
+ });
1746
+ const watchdog = createStallWatchdog({ thresholdMs: options.thresholdMs ?? STALL_GRACE_MS });
1747
+ let lastBeats = -1;
1748
+ let stopped = false;
1749
+ let stalled = false;
1750
+ return {
1751
+ pollMs,
1752
+ begin() {
1753
+ reporter.begin();
1754
+ watchdog.advanced("resolving", 0, now());
1755
+ },
1756
+ tick() {
1757
+ if (stopped || stalled) return;
1758
+ const snapshot = options.read();
1759
+ reporter.batch(() => {
1760
+ for (let i = 0; i < plan.shardFrames.length; i += 1) {
1761
+ reporter.shard(i, snapshot.shards[i] ?? 0);
1762
+ }
1763
+ if (snapshot.audioPlanned > 0 && plan.audioSamples > 0) {
1764
+ const fraction = Math.min(1, snapshot.audioSamples / snapshot.audioPlanned);
1765
+ reporter.audio(plan.audioSamples * fraction);
1766
+ }
1767
+ if (snapshot.muxed) reporter.muxed();
1768
+ });
1769
+ const at = now();
1770
+ if (snapshot.beats !== lastBeats) {
1771
+ lastBeats = snapshot.beats;
1772
+ watchdog.advanced(stageOf(snapshot, plan), snapshot.beats, at);
1773
+ }
1774
+ const verdict = watchdog.stalledFor(at);
1775
+ if (verdict) {
1776
+ stalled = true;
1777
+ options.onStall(verdict);
1778
+ }
1779
+ },
1780
+ complete() {
1781
+ if (stopped) return;
1782
+ reporter.complete();
1783
+ },
1784
+ stop() {
1785
+ stopped = true;
1786
+ reporter.stop();
1787
+ }
1788
+ };
1789
+ }
1790
+
1791
+ // src/exporter.ts
1792
+ var import_video_core6 = require("@pictor-sdk/video-core");
1793
+ function nativeModule() {
1794
+ if (!NativePictorVideo_default) {
1795
+ throw new VideoExportRejected(["the Pictor video native module is not installed"]);
1796
+ }
1797
+ return NativePictorVideo_default;
1798
+ }
1799
+ var VideoExportRejected = class extends Error {
1800
+ constructor(reasons) {
1801
+ super(`export rejected: ${reasons.join("; ")}`);
1802
+ this.name = "VideoExportRejected";
1803
+ this.reasons = reasons;
1804
+ }
1805
+ };
1806
+ var DEFAULT_FPS = 30;
1807
+ function describeUnsupported(project, spec) {
1808
+ const reasons = [];
1809
+ for (const clip of project.clips) {
1810
+ if (clip.kind === "photo") {
1811
+ reasons.push(`clip '${clip.id}' is a photo, which this exporter does not encode yet`);
1812
+ continue;
1813
+ }
1814
+ }
1815
+ if (spec.format !== void 0 && spec.format !== "mp4") {
1816
+ reasons.push(`format '${spec.format}' is not supported; this exporter writes mp4`);
1817
+ }
1818
+ if (spec.scale !== void 0 && spec.scale !== 1) {
1819
+ reasons.push(`scale ${spec.scale} is not supported yet; only 1 is`);
1820
+ }
1821
+ if (spec.fps !== void 0 && spec.fps !== DEFAULT_FPS) {
1822
+ reasons.push(`fps ${spec.fps} is not supported yet; only ${DEFAULT_FPS} is`);
1823
+ }
1824
+ if (spec.bitrate !== void 0) {
1825
+ reasons.push("bitrate is not supported yet; the encoder default is used");
1826
+ }
1827
+ return reasons;
1828
+ }
1829
+ function createMedia3VideoExporter(options) {
1830
+ const jobs = createExportJobs({
1831
+ cancelNative: (jobId) => {
1832
+ nativeModule().exportCancelJob(jobId);
1833
+ }
1834
+ });
1835
+ return {
1836
+ async export(project, spec, onProgress) {
1837
+ return jobs.run(async (job) => {
1838
+ job.checkpoint("resolving");
1839
+ const issues = (0, import_video_core6.validateProject)(project);
1840
+ if (issues.length > 0) {
1841
+ throw new VideoExportRejected(issues.map((issue) => issue.message));
1842
+ }
1843
+ const unsupported = describeUnsupported(project, spec);
1844
+ if (unsupported.length > 0) throw new VideoExportRejected(unsupported);
1845
+ const decision = resolveDestination(spec.destination);
1846
+ if (decision.destination === null) throw new VideoExportRejected(decision.reasons);
1847
+ const destination = decision.destination;
1848
+ const playable = project.clips.filter((clip) => (0, import_video_core6.isPlayableWindow)(clip));
1849
+ if (playable.length === 0) throw new VideoExportRejected(["the project has no playable clips"]);
1850
+ if (!((0, import_video_core6.projectDuration)(project) > 0)) {
1851
+ throw new VideoExportRejected(["the project has no duration"]);
1852
+ }
1853
+ const uris = [];
1854
+ const unresolved = [];
1855
+ for (const clip of playable) {
1856
+ const uri = options.resolveSource(clip.source, { kind: "clip", clipId: clip.id });
1857
+ if (typeof uri !== "string" || uri.length === 0) {
1858
+ unresolved.push(`clip '${clip.id}' could not be resolved to a readable source`);
1859
+ continue;
1860
+ }
1861
+ uris.push(uri);
1862
+ }
1863
+ if (unresolved.length > 0) throw new VideoExportRejected(unresolved);
1864
+ const fps = DEFAULT_FPS;
1865
+ const placements = (0, import_video_core6.layout)(project);
1866
+ const requests = playable.map((clip) => ({
1867
+ sourceStartUs: clip.sourceIn,
1868
+ sourceEndUs: clip.sourceOut,
1869
+ speed: clip.speed,
1870
+ sourceFps: clip.metadata.fps ?? fps,
1871
+ targetFps: fps
1872
+ }));
1873
+ const plan = (0, import_video_core6.planClipShards)(requests, fps);
1874
+ const program = (0, import_video_core6.compileVideoSceneProgram)(project);
1875
+ if (program.segments.length === 0) {
1876
+ throw new VideoExportRejected(["the compiled program has no segments"]);
1877
+ }
1878
+ void placements;
1879
+ const named = /* @__PURE__ */ new Set();
1880
+ for (const segment of program.segments) {
1881
+ for (const command of segment.commands) {
1882
+ if (command.op === "overlay") named.add(command.overlayId);
1883
+ }
1884
+ }
1885
+ const overlayPlan = options.prepareOverlays ? options.prepareOverlays(program, project.size, 1) : {
1886
+ resources: [],
1887
+ pending: [],
1888
+ invalid: [...named].map((overlayId) => ({
1889
+ overlayId,
1890
+ kind: "unknown",
1891
+ reason: "no prepareOverlays was supplied, so this overlay has no portable form"
1892
+ }))
1893
+ };
1894
+ job.checkpoint("overlays");
1895
+ const stagedNow = overlayPlan.resources.map((resource) => resource.file);
1896
+ if (overlayPlan.pending.length > 0) {
1897
+ options.releaseResources?.(stagedNow);
1898
+ throw new VideoExportRejected(
1899
+ overlayPlan.pending.map(
1900
+ (id) => `overlay '${id}' is still preparing; export would omit it`
1901
+ )
1902
+ );
1903
+ }
1904
+ if (overlayPlan.invalid.length > 0) {
1905
+ options.releaseResources?.(stagedNow);
1906
+ throw new VideoExportRejected(
1907
+ overlayPlan.invalid.map(
1908
+ (bad) => `overlay '${bad.overlayId}' (${bad.kind}): ${bad.reason}`
1909
+ )
1910
+ );
1911
+ }
1912
+ const resources = overlayPlan.resources;
1913
+ const shards = plan.map((shard, index) => {
1914
+ const clip = playable[index];
1915
+ const cadence = cadenceFor(shard.plan.frames);
1916
+ return {
1917
+ uri: uris[index],
1918
+ startMs: Math.round(clip.sourceIn / 1e3),
1919
+ endMs: Math.round(clip.sourceOut / 1e3),
1920
+ speed: clip.speed,
1921
+ timelineOffsetUs: shard.timelineOffsetUs,
1922
+ /*
1923
+ The shard's own audio is dropped and replaced with generated silence.
1924
+
1925
+ Not because anything wants that silence — the final mux takes only
1926
+ the video track from this pipeline — but because Media3 drives its
1927
+ speed path through audio, and a video-only item at any rate other
1928
+ than 1 stalls with "no output sample written". So the track exists to
1929
+ satisfy the re-timing and is discarded.
1930
+
1931
+ What matters is that the *source's* audio never enters this pipeline:
1932
+ a per-shard audio track has to be re-encoded to concatenate, which is
1933
+ what put a second generation on everything the user hears.
1934
+ */
1935
+ forceAudioTrack: true,
1936
+ removeAudio: true,
1937
+ emitsPerInput: cadence.emitsPerInput,
1938
+ outputPtsUs: cadence.outputPtsUs,
1939
+ spanUs: Math.round(shard.plan.frameCount * 1e6 / fps)
1940
+ };
1941
+ });
1942
+ const mixProgram = (0, import_video_core6.compileAudioMixProgram)(project);
1943
+ const mixProblems = (0, import_video_core6.validateAudioMixProgram)(mixProgram);
1944
+ if (mixProblems.length > 0) throw new VideoExportRejected(mixProblems);
1945
+ const resolved = resolveMixSources(project, mixProgram, options.resolveSource);
1946
+ const mixUris = resolved.uris;
1947
+ if (resolved.unresolved.length > 0) throw new VideoExportRejected(resolved.unresolved);
1948
+ job.checkpoint("resolving");
1949
+ job.checkpoint("overlays");
1950
+ const staged = resources.map((resource) => resource.file);
1951
+ const durationUs = (0, import_video_core6.projectDuration)(project);
1952
+ const progressPlan = planProgressWeights({
1953
+ shards: plan.map((shard) => ({ frames: shard.plan.frameCount })),
1954
+ audioSamples: mixProgram.spans.some((span) => !span.silent) ? Math.max(0, mixProgram.durationUs) : 0,
1955
+ durationUs
1956
+ });
1957
+ const stalled = { at: null };
1958
+ const pump = createProgressPump({
1959
+ plan: progressPlan,
1960
+ durationUs,
1961
+ read: () => JSON.parse(nativeModule().exportProgressSnapshot()),
1962
+ // Delivered through the job, so nothing arrives after settlement: a
1963
+ // host that has already shown a result must not be told 90%.
1964
+ onProgress: onProgress ? (value) => {
1965
+ job.progress(value.progress, () => onProgress(value));
1966
+ } : void 0,
1967
+ onStall: (verdict) => {
1968
+ stalled.at = { stage: String(verdict.stage), idleMs: verdict.idleMs };
1969
+ nativeModule().exportCancelJob(job.id);
1970
+ },
1971
+ thresholdMs: options.stallGraceMs,
1972
+ pollMs: options.progressPollMs
1973
+ });
1974
+ pump.begin();
1975
+ const ticker = setInterval(() => {
1976
+ pump.tick();
1977
+ }, pump.pollMs);
1978
+ let outcome;
1979
+ try {
1980
+ outcome = JSON.parse(
1981
+ await nativeModule().exportRunProjectAsync(
1982
+ job.id,
1983
+ JSON.stringify(shards),
1984
+ JSON.stringify({
1985
+ version: import_video_core6.VIDEO_SCENE_PROGRAM_VERSION,
1986
+ projectSize: { width: project.size.width, height: project.size.height },
1987
+ durationUs: program.durationUs,
1988
+ segments: program.segments
1989
+ }),
1990
+ JSON.stringify(resources),
1991
+ JSON.stringify(mixProgram),
1992
+ JSON.stringify(mixUris),
1993
+ /* timeoutSeconds= */
1994
+ 600,
1995
+ destination.kind === "file" ? JSON.stringify({
1996
+ kind: "file",
1997
+ path: destination.path,
1998
+ overwrite: destination.overwrite
1999
+ }) : ""
2000
+ )
2001
+ );
2002
+ } finally {
2003
+ clearInterval(ticker);
2004
+ options.releaseResources?.(staged);
2005
+ }
2006
+ const wedged = stalled.at;
2007
+ if (wedged) {
2008
+ pump.stop();
2009
+ throw new import_video_core6.VideoExportStalled(job.id, wedged.stage, wedged.idleMs);
2010
+ }
2011
+ const cancelledDuring = /^cancelled during (\w+)/.exec(outcome.error ?? "");
2012
+ if (!outcome.ok && cancelledDuring) {
2013
+ pump.stop();
2014
+ throw new import_video_core6.VideoExportCancelled(job.id, cancelledDuring[1]);
2015
+ }
2016
+ if (!outcome.ok) {
2017
+ pump.stop();
2018
+ const error = outcome.error || "the export failed";
2019
+ if (isRefusal(error)) throw new VideoExportRejected([error]);
2020
+ throw new import_video_core6.VideoExportFailed(job.id, "encoding", error);
2021
+ }
2022
+ job.checkpoint("publishing");
2023
+ const probe = JSON.parse(nativeModule().exportProbeOutput(outcome.uri));
2024
+ if (!probe.readable) {
2025
+ pump.stop();
2026
+ throw new import_video_core6.VideoExportFailed(
2027
+ job.id,
2028
+ "publishing",
2029
+ `${describeTarget(outcome.uri)} could not be read back: ${probe.error ?? "no video track"}`
2030
+ );
2031
+ }
2032
+ pump.complete();
2033
+ pump.stop();
2034
+ if (destination.kind === "file") return { kind: "file", uri: outcome.uri };
2035
+ const temporaryUri = outcome.uri;
2036
+ return {
2037
+ kind: "temporary",
2038
+ uri: temporaryUri,
2039
+ release: async () => {
2040
+ await Promise.resolve();
2041
+ nativeModule().exportReleaseTemporaryOutput(temporaryUri);
2042
+ }
2043
+ };
2044
+ });
2045
+ },
2046
+ /**
2047
+ * Ask the running export to stop.
2048
+ *
2049
+ * Idempotent, and a no-op when nothing is running. Reaches the native job
2050
+ * before the promise settles: a Transformer nobody is listening to still
2051
+ * encodes, still writes and still holds its codecs, so rejecting in
2052
+ * JavaScript alone would show a cancelled state over a running export.
2053
+ */
2054
+ cancel() {
2055
+ jobs.cancel();
2056
+ }
2057
+ };
2058
+ }
2059
+ var REFUSAL_PATTERNS = [
2060
+ /^program rejected/,
2061
+ /^overlay resources? /,
2062
+ /^overlay resource /,
2063
+ /^no shards/,
2064
+ /already exists; pass overwrite/,
2065
+ /^the directory for /,
2066
+ /^the destination /,
2067
+ /cannot be written without overwrite/,
2068
+ /^the staged file could not be read back/
2069
+ ];
2070
+ function isRefusal(error) {
2071
+ return REFUSAL_PATTERNS.some((pattern) => pattern.test(error));
2072
+ }
2073
+ function cadenceFor(frames) {
2074
+ const first = frames[0].sourceFrameIndex;
2075
+ const last = frames[frames.length - 1].sourceFrameIndex;
2076
+ const emitsPerInput = [];
2077
+ for (let i = 0; i <= last - first; i += 1) emitsPerInput.push(0);
2078
+ const outputPtsUs = [];
2079
+ for (const frame of frames) {
2080
+ const slot = frame.sourceFrameIndex - first;
2081
+ emitsPerInput[slot] = (emitsPerInput[slot] ?? 0) + 1;
2082
+ outputPtsUs.push(frame.outputPtsUs);
2083
+ }
2084
+ return { emitsPerInput, outputPtsUs };
2085
+ }
2086
+
2087
+ // src/mediaStore.ts
2088
+ var MediaSaveFailed = class extends Error {
2089
+ constructor(reason) {
2090
+ super(`save failed: ${reason}`);
2091
+ this.name = "MediaSaveFailed";
2092
+ this.reason = reason;
2093
+ }
2094
+ };
2095
+ var MediaSaveCancelled = class extends Error {
2096
+ constructor() {
2097
+ super("save cancelled");
2098
+ this.name = "MediaSaveCancelled";
2099
+ }
2100
+ };
2101
+ function shareSavedMedia(uri) {
2102
+ if (!NativePictorVideo_default) {
2103
+ return { ok: false, noTargets: false, reason: "sharing is not available" };
2104
+ }
2105
+ if (!uri.startsWith("content://")) {
2106
+ return { ok: false, noTargets: false, reason: "only a saved video can be shared" };
2107
+ }
2108
+ const result = JSON.parse(NativePictorVideo_default.mediaShare(uri));
2109
+ return {
2110
+ ok: result.ok === true,
2111
+ noTargets: result.noTargets === true,
2112
+ reason: result.error ?? ""
2113
+ };
2114
+ }
2115
+ var counter2 = 0;
2116
+ function createMediaStoreSaver() {
2117
+ let active = null;
2118
+ const native = () => {
2119
+ if (!NativePictorVideo_default) {
2120
+ throw new MediaSaveFailed("the Pictor video native module is not installed");
2121
+ }
2122
+ return NativePictorVideo_default;
2123
+ };
2124
+ return {
2125
+ async save(sourceUri) {
2126
+ if (active !== null) {
2127
+ throw new MediaSaveFailed("a save is already running");
2128
+ }
2129
+ counter2 += 1;
2130
+ const id = `save_${counter2.toString(36)}`;
2131
+ active = id;
2132
+ let outcome;
2133
+ try {
2134
+ outcome = JSON.parse(await native().mediaSaveAsync(id, sourceUri));
2135
+ } finally {
2136
+ active = null;
2137
+ }
2138
+ if (outcome.cancelled === true) throw new MediaSaveCancelled();
2139
+ if (!outcome.ok || typeof outcome.uri !== "string") {
2140
+ throw new MediaSaveFailed(outcome.error ?? "the file could not be saved");
2141
+ }
2142
+ return {
2143
+ uri: outcome.uri,
2144
+ displayName: outcome.displayName ?? "",
2145
+ location: outcome.location ?? ""
2146
+ };
2147
+ },
2148
+ cancel() {
2149
+ if (active === null) return;
2150
+ native().mediaSaveCancel(active);
2151
+ }
2152
+ };
2153
+ }
2154
+ function sweepPendingMediaSaves() {
2155
+ if (!NativePictorVideo_default) return { swept: 0, inspected: 0, skipped: 0 };
2156
+ const result = JSON.parse(NativePictorVideo_default.mediaSaveSweepPending());
2157
+ return {
2158
+ swept: result.swept ?? 0,
2159
+ inspected: result.inspected ?? 0,
2160
+ skipped: result.skipped ?? 0
2161
+ };
2162
+ }
2163
+
2164
+ // src/overlayStaging.ts
2165
+ var installed = false;
2166
+ function installOverlayStaging() {
2167
+ if (installed && global.__pictorOverlayStaging !== void 0) return true;
2168
+ installed = requireNative().installOverlayStaging();
2169
+ return installed && global.__pictorOverlayStaging !== void 0;
2170
+ }
2171
+ function host() {
2172
+ const h = global.__pictorOverlayStaging;
2173
+ if (h === void 0) {
2174
+ throw new Error("@pictor-sdk/video-android: overlay staging is not installed");
2175
+ }
2176
+ return h;
2177
+ }
2178
+ function sweepOverlayStaging() {
2179
+ if (!installOverlayStaging()) return 0;
2180
+ return host().sweep();
2181
+ }
2182
+ function createOverlayStagingSink() {
2183
+ if (!installOverlayStaging()) {
2184
+ throw new Error("@pictor-sdk/video-android: overlay staging could not be installed");
2185
+ }
2186
+ const tokens = /* @__PURE__ */ new Map();
2187
+ return {
2188
+ stage(bytes, meta) {
2189
+ const exact = bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength ? bytes.buffer : bytes.slice().buffer;
2190
+ const staged = host().stage(exact, meta.rasterWidth, meta.rasterHeight);
2191
+ tokens.set(staged.file, staged.token);
2192
+ return {
2193
+ file: staged.file,
2194
+ byteLength: staged.byteLength,
2195
+ checksum: staged.checksum
2196
+ };
2197
+ },
2198
+ remove(file) {
2199
+ const token = tokens.get(file);
2200
+ if (token === void 0) return;
2201
+ tokens.delete(file);
2202
+ host().release(token);
2203
+ },
2204
+ releaseAll() {
2205
+ tokens.clear();
2206
+ return host().releaseAll();
2207
+ },
2208
+ ledger() {
2209
+ return host().ledger();
2210
+ }
2211
+ };
2212
+ }
2213
+ function measureStagingStall(sink, width, height) {
2214
+ const bytes = new Uint8Array(width * height * 4);
2215
+ const at = Date.now();
2216
+ const staged = sink.stage(bytes, {
2217
+ overlayId: "measure",
2218
+ resourceId: "measure",
2219
+ rasterWidth: width,
2220
+ rasterHeight: height
2221
+ });
2222
+ const ms = Date.now() - at;
2223
+ sink.remove(staged.file);
2224
+ return { ms, bytes: bytes.byteLength };
2225
+ }
2226
+
2227
+ // src/audioProbe.ts
2228
+ function probeAudioSource(uri) {
2229
+ let raw;
2230
+ try {
2231
+ raw = requireNative().probeAudioSource(uri);
2232
+ } catch (error) {
2233
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
2234
+ }
2235
+ let parsed;
2236
+ try {
2237
+ parsed = JSON.parse(raw);
2238
+ } catch {
2239
+ return { ok: false, reason: "probe returned unparseable JSON" };
2240
+ }
2241
+ const value = parsed;
2242
+ if (value.ok !== true) {
2243
+ return { ok: false, reason: typeof value.reason === "string" ? value.reason : "probe failed" };
2244
+ }
2245
+ return {
2246
+ ok: true,
2247
+ durationUs: Number.isFinite(value.durationUs) ? Number(value.durationUs) : 0,
2248
+ hasAudioTrack: value.hasAudioTrack === true,
2249
+ seekable: value.seekable === true,
2250
+ mimeType: typeof value.mimeType === "string" ? value.mimeType : ""
2251
+ };
2252
+ }
2253
+
2254
+ // src/managedSources.ts
2255
+ function ingestManagedSource(uri) {
2256
+ return parse(
2257
+ () => requireNative().ingestManagedSource(uri),
2258
+ (value) => ({
2259
+ ok: true,
2260
+ uri: String(value.uri ?? ""),
2261
+ token: String(value.token ?? ""),
2262
+ byteLength: Number(value.byteLength ?? 0),
2263
+ sha256: String(value.sha256 ?? ""),
2264
+ created: value.created === true
2265
+ })
2266
+ );
2267
+ }
2268
+ function releaseManagedSource(token) {
2269
+ try {
2270
+ const value = JSON.parse(requireNative().releaseManagedSource(token));
2271
+ return value.removed === true;
2272
+ } catch {
2273
+ return false;
2274
+ }
2275
+ }
2276
+ function sweepManagedSources(keepTokens) {
2277
+ try {
2278
+ const value = JSON.parse(
2279
+ requireNative().sweepManagedSources(JSON.stringify([...keepTokens]))
2280
+ );
2281
+ if (value.ok !== true) return { removed: 0, kept: 0, partials: 0, skipped: 0 };
2282
+ return {
2283
+ removed: Number(value.removed ?? 0),
2284
+ kept: Number(value.kept ?? 0),
2285
+ partials: Number(value.partials ?? 0),
2286
+ skipped: Number(value.skipped ?? 0)
2287
+ };
2288
+ } catch {
2289
+ return { removed: 0, kept: 0, partials: 0, skipped: 0 };
2290
+ }
2291
+ }
2292
+ function managedSourceList() {
2293
+ try {
2294
+ const value = JSON.parse(requireNative().managedSourceList());
2295
+ return value.items ?? [];
2296
+ } catch {
2297
+ return [];
2298
+ }
2299
+ }
2300
+ function seedManagedSourceForGate(name, bytes) {
2301
+ try {
2302
+ const value = JSON.parse(requireNative().managedSourceGateSeed(name, bytes));
2303
+ return value.ok === true;
2304
+ } catch {
2305
+ return false;
2306
+ }
2307
+ }
2308
+ function sweepManagedSourcesRawForGate(rawKeep) {
2309
+ try {
2310
+ const value = JSON.parse(requireNative().managedSourceGateSweepRaw(rawKeep));
2311
+ return {
2312
+ ok: value.ok === true,
2313
+ removed: Number(value.removed ?? 0),
2314
+ kept: Number(value.kept ?? 0),
2315
+ partials: Number(value.partials ?? 0),
2316
+ skipped: Number(value.skipped ?? 0)
2317
+ };
2318
+ } catch {
2319
+ return { ok: false, removed: 0, kept: 0, partials: 0, skipped: 0 };
2320
+ }
2321
+ }
2322
+ function managedSourceLedger() {
2323
+ try {
2324
+ const value = JSON.parse(requireNative().managedSourceLedger());
2325
+ return {
2326
+ count: Number(value.count ?? 0),
2327
+ bytes: Number(value.bytes ?? 0),
2328
+ items: value.items ?? []
2329
+ };
2330
+ } catch {
2331
+ return { count: 0, bytes: 0, items: [] };
2332
+ }
2333
+ }
2334
+ function managedSourceToken(uri) {
2335
+ const match = /\/([0-9a-f]{64})\.bin$/.exec(uri);
2336
+ return match ? match[1] : null;
2337
+ }
2338
+ function parse(call2, build) {
2339
+ let raw;
2340
+ try {
2341
+ raw = call2();
2342
+ } catch (error) {
2343
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
2344
+ }
2345
+ let value;
2346
+ try {
2347
+ value = JSON.parse(raw);
2348
+ } catch {
2349
+ return { ok: false, reason: "native returned unparseable JSON" };
2350
+ }
2351
+ if (value.ok !== true) {
2352
+ return { ok: false, reason: typeof value.reason === "string" ? value.reason : "failed" };
2353
+ }
2354
+ return build(value);
2355
+ }
2356
+
2357
+ // src/previewMix.ts
2358
+ async function preparePreviewMix(jobId, programJson, urisJson) {
2359
+ let raw;
2360
+ try {
2361
+ raw = await requireNative().previewMixPrepare(jobId, programJson, urisJson);
2362
+ } catch (error) {
2363
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
2364
+ }
2365
+ let value;
2366
+ try {
2367
+ value = JSON.parse(raw);
2368
+ } catch {
2369
+ return { ok: false, reason: "native returned unparseable JSON" };
2370
+ }
2371
+ if (value.ok === true) {
2372
+ return {
2373
+ ok: true,
2374
+ uri: String(value.uri ?? ""),
2375
+ key: String(value.key ?? ""),
2376
+ cached: value.cached === true,
2377
+ byteLength: Number(value.byteLength ?? 0),
2378
+ ...value.profile ? { profile: value.profile } : {}
2379
+ };
2380
+ }
2381
+ if (value.superseded === true) {
2382
+ return { ok: false, superseded: true, key: String(value.key ?? "") };
2383
+ }
2384
+ return {
2385
+ ok: false,
2386
+ ...typeof value.key === "string" ? { key: value.key } : {},
2387
+ reason: typeof value.error === "string" ? value.error : "the mix could not be prepared"
2388
+ };
2389
+ }
2390
+ function cancelPreviewMix() {
2391
+ try {
2392
+ requireNative().previewMixCancel();
2393
+ } catch {
2394
+ }
2395
+ }
2396
+ function previewMixKey(programJson, urisJson) {
2397
+ try {
2398
+ return requireNative().previewMixKey(programJson, urisJson);
2399
+ } catch {
2400
+ return "";
2401
+ }
2402
+ }
2403
+ function releasePreviewMix(key) {
2404
+ try {
2405
+ const value = JSON.parse(requireNative().previewMixRelease(key));
2406
+ return value.removed === true;
2407
+ } catch {
2408
+ return false;
2409
+ }
2410
+ }
2411
+ function sweepPreviewMixes(keepKeys) {
2412
+ const none = { removed: 0, kept: 0, partials: 0, skipped: 0 };
2413
+ try {
2414
+ const value = JSON.parse(requireNative().previewMixSweep(JSON.stringify([...keepKeys])));
2415
+ if (value.ok !== true) return none;
2416
+ return {
2417
+ removed: Number(value.removed ?? 0),
2418
+ kept: Number(value.kept ?? 0),
2419
+ partials: Number(value.partials ?? 0),
2420
+ skipped: Number(value.skipped ?? 0)
2421
+ };
2422
+ } catch {
2423
+ return none;
2424
+ }
2425
+ }
2426
+ function previewMixLedger() {
2427
+ try {
2428
+ const value = JSON.parse(requireNative().previewMixLedger());
2429
+ return {
2430
+ count: Number(value.count ?? 0),
2431
+ bytes: Number(value.bytes ?? 0),
2432
+ items: value.items ?? []
2433
+ };
2434
+ } catch {
2435
+ return { count: 0, bytes: 0, items: [] };
2436
+ }
2437
+ }
2438
+
2439
+ // src/previewAudioSession.ts
2440
+ var import_video_core7 = require("@pictor-sdk/video-core");
2441
+ function previewAudioNeeded(project) {
2442
+ return project.audio.length > 0;
2443
+ }
2444
+ function decidePreviewAudio(inputs) {
2445
+ const { wantedKey, readyKey, preparingKey, failedKey } = inputs;
2446
+ if (wantedKey === "") {
2447
+ return {
2448
+ status: "source",
2449
+ wantedKey,
2450
+ shouldPrepare: false,
2451
+ muteSourceAudio: false,
2452
+ canPlay: true
2453
+ };
2454
+ }
2455
+ if (readyKey === wantedKey) {
2456
+ return {
2457
+ status: "ready",
2458
+ wantedKey,
2459
+ shouldPrepare: false,
2460
+ // The mix contains the clips' audio too. Leaving the source audible
2461
+ // would play the whole timeline twice.
2462
+ muteSourceAudio: true,
2463
+ canPlay: true
2464
+ };
2465
+ }
2466
+ if (failedKey === wantedKey) {
2467
+ return {
2468
+ status: "failed",
2469
+ wantedKey,
2470
+ shouldPrepare: false,
2471
+ muteSourceAudio: true,
2472
+ canPlay: false
2473
+ };
2474
+ }
2475
+ return {
2476
+ status: "preparing",
2477
+ wantedKey,
2478
+ shouldPrepare: preparingKey !== wantedKey,
2479
+ muteSourceAudio: true,
2480
+ canPlay: false
2481
+ };
2482
+ }
2483
+ function previewProgramJson(project) {
2484
+ return JSON.stringify((0, import_video_core7.compileAudioMixProgram)(project));
2485
+ }
2486
+ var PREVIEW_DRIFT_TOLERANCE_US = 6e4;
2487
+
2488
+ // src/tracePhases.ts
2489
+ var PHASE_MARK_PREFIX = "phase:";
2490
+ function num(v) {
2491
+ return typeof v === "number" ? v : Number(v);
2492
+ }
2493
+ function empty(label, index) {
2494
+ return {
2495
+ label,
2496
+ index,
2497
+ samples: 0,
2498
+ playingSamples: 0,
2499
+ freshPlayingSamples: 0,
2500
+ stalePlayingSamples: 0,
2501
+ longestStaleRun: 0,
2502
+ minUs: 0,
2503
+ maxUs: 0,
2504
+ meanUs: 0,
2505
+ worstAbsUs: 0,
2506
+ freshWorstAbsUs: 0,
2507
+ overToleranceSamples: 0,
2508
+ settledAfterUs: 0
2509
+ };
2510
+ }
2511
+ function summarisePhases(events, toleranceUs) {
2512
+ const phases = [];
2513
+ let current = empty("start", 0);
2514
+ let sum = 0;
2515
+ let staleRun = 0;
2516
+ let firstPlayingNs = 0;
2517
+ let lastOverNs = 0;
2518
+ let sawPlaying = false;
2519
+ let stillOver = false;
2520
+ const close = () => {
2521
+ current.meanUs = current.playingSamples > 0 ? Math.round(sum / current.playingSamples) : 0;
2522
+ current.settledAfterUs = current.overToleranceSamples === 0 ? 0 : stillOver ? -1 : Math.round((lastOverNs - firstPlayingNs) / 1e3);
2523
+ phases.push(current);
2524
+ sum = 0;
2525
+ staleRun = 0;
2526
+ firstPlayingNs = 0;
2527
+ lastOverNs = 0;
2528
+ sawPlaying = false;
2529
+ stillOver = false;
2530
+ };
2531
+ for (const e of events) {
2532
+ if (e.type !== "sample") {
2533
+ const label = e.type === "mark" ? String(e.v.label ?? "") : "";
2534
+ if (label.startsWith(PHASE_MARK_PREFIX)) {
2535
+ close();
2536
+ current = empty(label.slice(PHASE_MARK_PREFIX.length), phases.length);
2537
+ }
2538
+ continue;
2539
+ }
2540
+ current.samples += 1;
2541
+ if (e.v.videoPlaying !== true || e.v.audioPlaying !== true) {
2542
+ staleRun = 0;
2543
+ continue;
2544
+ }
2545
+ const offset = num(e.v.offsetUs);
2546
+ if (!Number.isFinite(offset)) continue;
2547
+ current.playingSamples += 1;
2548
+ if (!sawPlaying) {
2549
+ sawPlaying = true;
2550
+ firstPlayingNs = num(e.atNs);
2551
+ }
2552
+ sum += offset;
2553
+ if (current.playingSamples === 1) {
2554
+ current.minUs = offset;
2555
+ current.maxUs = offset;
2556
+ } else {
2557
+ current.minUs = Math.min(current.minUs, offset);
2558
+ current.maxUs = Math.max(current.maxUs, offset);
2559
+ }
2560
+ const abs = Math.abs(offset);
2561
+ if (abs > current.worstAbsUs) current.worstAbsUs = abs;
2562
+ if (abs > toleranceUs) {
2563
+ current.overToleranceSamples += 1;
2564
+ lastOverNs = num(e.atNs);
2565
+ stillOver = true;
2566
+ } else {
2567
+ stillOver = false;
2568
+ }
2569
+ if (e.v.pfFresh === true) {
2570
+ current.freshPlayingSamples += 1;
2571
+ if (abs > current.freshWorstAbsUs) current.freshWorstAbsUs = abs;
2572
+ staleRun = 0;
2573
+ } else {
2574
+ current.stalePlayingSamples += 1;
2575
+ staleRun += 1;
2576
+ if (staleRun > current.longestStaleRun) current.longestStaleRun = staleRun;
2577
+ }
2578
+ }
2579
+ close();
2580
+ const worstAbsUs = phases.reduce((w, p) => Math.max(w, p.worstAbsUs), 0);
2581
+ const freshWorstAbsUs = phases.reduce((w, p) => Math.max(w, p.freshWorstAbsUs), 0);
2582
+ const overToleranceSamples = phases.reduce((n, p) => n + p.overToleranceSamples, 0);
2583
+ return {
2584
+ toleranceUs,
2585
+ phases,
2586
+ worstAbsUs,
2587
+ freshWorstAbsUs,
2588
+ overToleranceSamples,
2589
+ pass: overToleranceSamples === 0
2590
+ };
2591
+ }
2592
+
2593
+ // src/spikeExcursions.ts
2594
+ var SPIKE_TOLERANCE_US = 6e4;
2595
+ function offsetAt(sample, releaseAtNs = 0) {
2596
+ if (releaseAtNs > 0 && sample.frameConsumedNs < releaseAtNs) {
2597
+ const raw = sample.positionUs - sample.framePtsUs;
2598
+ return { atLatch: raw, atIntended: raw };
2599
+ }
2600
+ return {
2601
+ atLatch: sample.positionUs - (sample.atNs - sample.frameConsumedNs) / 1e3 - sample.framePtsUs,
2602
+ atIntended: sample.positionUs - (sample.atNs - sample.frameReleaseNs) / 1e3 - sample.framePtsUs
2603
+ };
2604
+ }
2605
+ function classifyExcursions(samples, options = {}) {
2606
+ const toleranceUs = options.toleranceUs ?? SPIKE_TOLERANCE_US;
2607
+ const frameIntervalUs = options.frameIntervalUs ?? 33333;
2608
+ const ADVANCE_WINDOW_NS = 2e8;
2609
+ const advancing = samples.map((s, i) => {
2610
+ for (let j = i - 1; j >= 0; j -= 1) {
2611
+ if (s.atNs - samples[j].atNs > ADVANCE_WINDOW_NS) break;
2612
+ if (samples[j].positionUs !== s.positionUs) return true;
2613
+ }
2614
+ return false;
2615
+ });
2616
+ const releaseAtNs = [];
2617
+ let intervalStart = 0;
2618
+ for (let i = 0; i < samples.length; i += 1) {
2619
+ if (!advancing[i]) intervalStart = 0;
2620
+ else if (intervalStart === 0) intervalStart = samples[i].atNs;
2621
+ releaseAtNs.push(intervalStart);
2622
+ }
2623
+ const isActive = (s, i) => s.playing && s.ready && s.framePtsUs >= 0 && s.frameConsumedNs > 0 && // The picture for this epoch has arrived.
2624
+ s.frameConsumedNs >= s.atNs - s.sinceEpochNs && advancing[i] === true;
2625
+ const active = samples.filter(isActive);
2626
+ const preparing = samples.length - active.length;
2627
+ const excursions = [];
2628
+ const byCause = {};
2629
+ let worstAbsUs = 0;
2630
+ let worstAtIntendedUs = 0;
2631
+ let overAtIntended = 0;
2632
+ let readSpanUs = 0;
2633
+ let latchLagSum = 0;
2634
+ let preReleaseSamples = 0;
2635
+ let preReleaseWorstUs = 0;
2636
+ const releaseFor = /* @__PURE__ */ new Map();
2637
+ samples.forEach((s, i) => releaseFor.set(s.seq, releaseAtNs[i]));
2638
+ for (let i = 0; i < active.length; i += 1) {
2639
+ const s = active[i];
2640
+ const previous = i > 0 ? active[i - 1] : null;
2641
+ readSpanUs = Math.max(readSpanUs, s.readSpanNs / 1e3);
2642
+ const releasedAt = releaseFor.get(s.seq) ?? 0;
2643
+ const both = offsetAt(s, releasedAt);
2644
+ const offsetUs = both.atLatch;
2645
+ const abs = Math.abs(offsetUs);
2646
+ const absIntended = Math.abs(both.atIntended);
2647
+ if (releasedAt > 0 && s.frameConsumedNs < releasedAt) {
2648
+ preReleaseSamples += 1;
2649
+ if (abs > preReleaseWorstUs) preReleaseWorstUs = abs;
2650
+ }
2651
+ if (abs > worstAbsUs) worstAbsUs = abs;
2652
+ if (absIntended > worstAtIntendedUs) worstAtIntendedUs = absIntended;
2653
+ if (absIntended > toleranceUs) overAtIntended += 1;
2654
+ if (abs <= toleranceUs) continue;
2655
+ const frameAgeUs = (s.atNs - s.frameConsumedNs) / 1e3;
2656
+ const latchLagUs = (s.frameConsumedNs - s.frameReleaseNs) / 1e3;
2657
+ latchLagSum += latchLagUs;
2658
+ const positionStepUs = previous ? s.positionUs - previous.positionUs : 0;
2659
+ const wallStepUs = previous ? (s.atNs - previous.atNs) / 1e3 : 0;
2660
+ let cause;
2661
+ if (releasedAt > 0 && s.frameConsumedNs < releasedAt) {
2662
+ cause = "pre-release-frame";
2663
+ } else if (s.frameConsumedNs > 0 && s.frameConsumedNs < s.atNs - s.sinceEpochNs) {
2664
+ cause = "cross-epoch";
2665
+ } else if (previous && s.frameGeneration !== previous.frameGeneration) {
2666
+ cause = "cross-epoch";
2667
+ } else if (frameAgeUs > 2 * frameIntervalUs) {
2668
+ cause = "stale-pairing";
2669
+ } else if (s.sinceEpochNs / 1e3 < 5e5) {
2670
+ cause = "first-frame-after-seek";
2671
+ } else if (latchLagUs > frameIntervalUs) {
2672
+ cause = "delayed-presentation";
2673
+ } else if (previous && Math.abs(positionStepUs - wallStepUs) > toleranceUs) {
2674
+ cause = "audio-discontinuity";
2675
+ } else if (previous && s.frameWindow !== previous.frameWindow) {
2676
+ cause = "mapping";
2677
+ } else {
2678
+ cause = "unexplained";
2679
+ }
2680
+ byCause[cause] = (byCause[cause] ?? 0) + 1;
2681
+ excursions.push({
2682
+ seq: s.seq,
2683
+ offsetUs: Math.round(offsetUs),
2684
+ offsetAtIntendedUs: Math.round(both.atIntended),
2685
+ cause,
2686
+ epoch: s.epoch,
2687
+ sinceEpochMs: Math.round(s.sinceEpochNs / 1e6),
2688
+ frameWindow: s.frameWindow,
2689
+ framePtsUs: s.framePtsUs,
2690
+ latchLagUs: Math.round(latchLagUs),
2691
+ frameAgeUs: Math.round(frameAgeUs),
2692
+ positionStepUs: Math.round(positionStepUs),
2693
+ wallStepUs: Math.round(wallStepUs)
2694
+ });
2695
+ }
2696
+ return {
2697
+ toleranceUs,
2698
+ activeSamples: active.length,
2699
+ preparingSamples: preparing,
2700
+ excursions,
2701
+ byCause,
2702
+ preReleaseSamples,
2703
+ preReleaseWorstUs: Math.round(preReleaseWorstUs),
2704
+ worstAbsUs: Math.round(worstAbsUs),
2705
+ worstAtIntendedUs: Math.round(worstAtIntendedUs),
2706
+ overAtIntended,
2707
+ passAtIntended: active.length > 0 && overAtIntended === 0,
2708
+ meanLatchLagUs: excursions.length ? Math.round(latchLagSum / excursions.length) : 0,
2709
+ // No samples is not a pass. A run that never played has not been judged.
2710
+ pass: active.length > 0 && excursions.length === 0,
2711
+ uncertainty: {
2712
+ frameQuantumUs: Math.round(frameIntervalUs / 2),
2713
+ readSpanUs: Math.round(readSpanUs),
2714
+ positionQuantumUs: 1e3
2715
+ }
2716
+ };
2717
+ }
2718
+ function windowAround(samples, seq, spanUs = 25e4) {
2719
+ const centre = samples.find((s) => s.seq === seq);
2720
+ if (!centre) return [];
2721
+ const spanNs = spanUs * 1e3;
2722
+ return samples.filter((s) => Math.abs(s.atNs - centre.atNs) <= spanNs);
2723
+ }
2724
+
2725
+ // src/previewCadence.ts
2726
+ var import_video_core8 = require("@pictor-sdk/video-core");
2727
+ var PREVIEW_TOLERANCE_US = 6e4;
2728
+ var PARITY_LEVEL_TOLERANCE = 12;
2729
+ var PARITY_BAR_TOLERANCE = 4 / 960;
2730
+ var PREVIEW_OUTPUT_FPS = 30;
2731
+ function wallClockAt(samples, projectUs, intervalUs) {
2732
+ let before = null;
2733
+ let after = null;
2734
+ for (const s of samples) {
2735
+ if (!s.playing || s.positionUs < 0 || s.atNs <= 0) continue;
2736
+ if (s.positionUs <= projectUs && (before === null || s.positionUs > before.positionUs)) before = s;
2737
+ if (s.positionUs >= projectUs && (after === null || s.positionUs < after.positionUs)) after = s;
2738
+ }
2739
+ if (after === null) {
2740
+ if (before === null || projectUs - before.positionUs > intervalUs) return null;
2741
+ return before.atNs + (projectUs - before.positionUs) * 1e3;
2742
+ }
2743
+ if (before === null) {
2744
+ if (after.positionUs - projectUs > intervalUs) return null;
2745
+ return after.atNs - (after.positionUs - projectUs) * 1e3;
2746
+ }
2747
+ if (before.epoch !== after.epoch) return null;
2748
+ if (after.positionUs === before.positionUs) return before.atNs;
2749
+ const t = (projectUs - before.positionUs) / (after.positionUs - before.positionUs);
2750
+ return before.atNs + t * (after.atNs - before.atNs);
2751
+ }
2752
+ function displayedAt(submissions, projectUs, atNs, intervalNs) {
2753
+ if (atNs === null) return null;
2754
+ const deadlineNs = atNs + intervalNs;
2755
+ let best = null;
2756
+ for (const s of submissions) {
2757
+ if (s.ptsUs > projectUs) continue;
2758
+ if (s.submittedNs > deadlineNs) continue;
2759
+ if (best === null || s.submittedNs > best.submittedNs) best = s;
2760
+ }
2761
+ return best;
2762
+ }
2763
+ function checkCadence(clip, submissions, outputFps = PREVIEW_OUTPUT_FPS, toleranceUs = PREVIEW_TOLERANCE_US, samples = []) {
2764
+ const request = {
2765
+ sourceStartUs: clip.sourceStartUs,
2766
+ sourceEndUs: clip.sourceEndUs,
2767
+ speed: clip.speed,
2768
+ sourceFps: clip.sourceFps,
2769
+ targetFps: outputFps
2770
+ };
2771
+ const plan = (0, import_video_core8.resampleClip)(request);
2772
+ const outputIntervalUs = Math.round(1e6 / outputFps);
2773
+ const inClip = submissions.filter((s) => s.ptsUs >= clip.timelineStartUs && s.ptsUs < clip.timelineEndUs).sort((a, b) => a.ptsUs - b.ptsUs);
2774
+ const skews = inClip.filter((s) => s.texturePtsUs >= 0).map((s) => Math.abs(s.texturePtsUs - s.ptsUs)).sort((a, b) => a - b);
2775
+ const medianSkewUs = skews.length > 0 ? skews[Math.floor(skews.length / 2)] : -1;
2776
+ const timestampDomain = skews.length === 0 ? "unknown" : medianSkewUs < 1e6 ? "media" : "system";
2777
+ const unique = new Set(inClip.map((s) => s.ptsUs));
2778
+ const delivered = /* @__PURE__ */ new Set();
2779
+ let markersRead = 0;
2780
+ let markersFailed = 0;
2781
+ for (const record2 of inClip) {
2782
+ if (record2.acquiredMarker >= 0) delivered.add(record2.acquiredMarker);
2783
+ if (record2.submittedMarker >= 0) markersRead += 1;
2784
+ else if (record2.markerFailure !== "" && record2.markerFailure !== "unread") markersFailed += 1;
2785
+ }
2786
+ const pixelIdentity = markersRead > 0;
2787
+ const first = inClip.length > 0 ? inClip[0].ptsUs : 0;
2788
+ const last = inClip.length > 0 ? inClip[inClip.length - 1].ptsUs : 0;
2789
+ const clockOffsets = inClip.filter((r) => r.submittedNs > 0).map((r) => r.submittedNs - r.ptsUs * 1e3).sort((a, b) => a - b);
2790
+ const playerOffsets = samples.filter((s) => s.playing && s.atNs > 0 && s.positionUs >= 0).map((s) => s.atNs - s.positionUs * 1e3).sort((a, b) => a - b);
2791
+ const frameDerivedOffsetNs = clockOffsets.length > 0 && playerOffsets.length > 0 ? clockOffsets[Math.floor(clockOffsets.length / 2)] - playerOffsets[Math.floor(playerOffsets.length / 2)] : 0;
2792
+ let expectedInstants = 0;
2793
+ let leadingExcluded = 0;
2794
+ let trailingExcluded = 0;
2795
+ let covered = 0;
2796
+ let wrong = 0;
2797
+ let unverified = 0;
2798
+ let unplaceable = 0;
2799
+ let exact = 0;
2800
+ const offsets = {};
2801
+ const sourceIntervalUs = clip.sourceFps > 0 ? 1e6 / clip.sourceFps / clip.speed : 0;
2802
+ const wrongInstants = [];
2803
+ for (const frame of plan.frames) {
2804
+ const projectUs = clip.timelineStartUs + Number(frame.outputPtsUs);
2805
+ if (inClip.length === 0) continue;
2806
+ const boundary = projectUs < first || projectUs > last;
2807
+ if (projectUs < first) leadingExcluded += 1;
2808
+ else if (projectUs > last) trailingExcluded += 1;
2809
+ else expectedInstants += 1;
2810
+ const instantNs = wallClockAt(samples, projectUs, outputIntervalUs);
2811
+ if (instantNs === null && !boundary) unplaceable += 1;
2812
+ const shown = instantNs === null ? null : displayedAt(inClip, projectUs, instantNs, outputIntervalUs * 1e3);
2813
+ if (shown === null) {
2814
+ if (boundary) {
2815
+ wrongInstants.push({
2816
+ projectUs,
2817
+ expectedSourceFrame: frame.sourceFrameIndex,
2818
+ expectedSourcePtsUs: Number(frame.sourceTimeUs),
2819
+ shownSourceFrame: -1,
2820
+ shownPtsUs: -1,
2821
+ acquiredTexturePtsUs: -1,
2822
+ acquiredMarker: -1,
2823
+ submittedMarker: -1,
2824
+ slot: -1,
2825
+ frameId: -1,
2826
+ submittedNs: 0,
2827
+ cause: "uncovered",
2828
+ boundary
2829
+ });
2830
+ }
2831
+ continue;
2832
+ }
2833
+ if (!boundary) covered += 1;
2834
+ const shownProjectUs = timestampDomain === "media" ? shown.texturePtsUs : shown.ptsUs;
2835
+ const shownSourceUs = clip.sourceStartUs + (shownProjectUs - clip.timelineStartUs) * clip.speed;
2836
+ const derivedIndex = Math.floor(shownSourceUs * clip.sourceFps / 1e6);
2837
+ const shownIndex = shown.submittedMarker >= 0 ? shown.submittedMarker : derivedIndex;
2838
+ const verified = shown.submittedMarker >= 0 || timestampDomain === "media";
2839
+ if (!verified) {
2840
+ if (!boundary) unverified += 1;
2841
+ continue;
2842
+ }
2843
+ const offset = shownIndex - frame.sourceFrameIndex;
2844
+ if (!boundary) {
2845
+ const key = String(offset);
2846
+ offsets[key] = (offsets[key] ?? 0) + 1;
2847
+ }
2848
+ const offsetUs = Math.abs(offset) * sourceIntervalUs;
2849
+ const slack = shown.submittedMarker >= 0 ? 0 : sourceIntervalUs;
2850
+ if (offset === 0) exact += 1;
2851
+ if (offsetUs <= Math.max(toleranceUs, slack)) continue;
2852
+ if (!boundary) wrong += 1;
2853
+ const acquired = shown.texturePtsUs;
2854
+ const late = shown.submittedNs > 0 && shown.ptsUs < projectUs - outputIntervalUs;
2855
+ const hadItByThen = inClip.some(
2856
+ (r) => r.acquiredMarker === frame.sourceFrameIndex && r.acquiredNs > 0 && r.acquiredNs <= shown.submittedNs
2857
+ );
2858
+ let cause;
2859
+ if (shown.submittedMarker >= 0) {
2860
+ if (shown.acquiredMarker >= 0 && shown.submittedMarker !== shown.acquiredMarker) {
2861
+ cause = "storage-aliased";
2862
+ } else if (!hadItByThen) cause = "not-delivered";
2863
+ else cause = "wrong-selection";
2864
+ } else if (late) cause = "late-delivery";
2865
+ else cause = "wrong-submitted-identity";
2866
+ wrongInstants.push({
2867
+ projectUs,
2868
+ expectedSourceFrame: frame.sourceFrameIndex,
2869
+ expectedSourcePtsUs: Number(frame.sourceTimeUs),
2870
+ shownSourceFrame: shownIndex,
2871
+ shownPtsUs: shownProjectUs,
2872
+ acquiredTexturePtsUs: acquired,
2873
+ acquiredMarker: shown.acquiredMarker,
2874
+ submittedMarker: shown.submittedMarker,
2875
+ slot: shown.slot,
2876
+ frameId: shown.frameId,
2877
+ submittedNs: shown.submittedNs,
2878
+ cause,
2879
+ boundary
2880
+ });
2881
+ }
2882
+ const walls = [];
2883
+ for (let i = 1; i < inClip.length; i += 1) {
2884
+ const gap = (inClip[i].submittedNs - inClip[i - 1].submittedNs) / 1e3;
2885
+ if (gap > 0) walls.push(gap);
2886
+ }
2887
+ walls.sort((a, b) => a - b);
2888
+ const tail = (fraction) => walls.length === 0 ? 0 : Math.round(walls[Math.min(walls.length - 1, Math.floor(walls.length * fraction))]);
2889
+ const frameTimes = {
2890
+ p50: tail(0.5),
2891
+ p95: tail(0.95),
2892
+ p99: tail(0.99),
2893
+ max: walls.length > 0 ? Math.round(walls[walls.length - 1]) : 0,
2894
+ samples: walls.length
2895
+ };
2896
+ let parityMeasured = 0;
2897
+ let worstLevelDelta = 0;
2898
+ let worstBarDelta = 0;
2899
+ let parityFailures = 0;
2900
+ for (const record2 of inClip) {
2901
+ if (record2.levelDelta < 0 || record2.barDelta < 0) continue;
2902
+ parityMeasured += 1;
2903
+ if (record2.levelDelta > worstLevelDelta) worstLevelDelta = record2.levelDelta;
2904
+ if (record2.barDelta > worstBarDelta) worstBarDelta = record2.barDelta;
2905
+ if (record2.levelDelta > PARITY_LEVEL_TOLERANCE || record2.barDelta > PARITY_BAR_TOLERANCE) {
2906
+ parityFailures += 1;
2907
+ }
2908
+ }
2909
+ const stamps = [...unique].sort((a, b) => a - b);
2910
+ const holds = [];
2911
+ for (let i = 1; i < stamps.length; i += 1) holds.push(stamps[i] - stamps[i - 1]);
2912
+ const repeats = Math.max(1, Math.round(1 / clip.speed));
2913
+ const expectedHoldUs = outputIntervalUs * repeats;
2914
+ const missedDeadlines = holds.filter((h) => h > expectedHoldUs * 1.5).length;
2915
+ holds.sort((a, b) => a - b);
2916
+ let verdict = "pass";
2917
+ let verdictReason = "";
2918
+ if (expectedInstants === 0) {
2919
+ verdict = "invalid";
2920
+ verdictReason = "no instants covered";
2921
+ } else if (!pixelIdentity) {
2922
+ verdict = "invalid";
2923
+ verdictReason = "identity not read from pixels";
2924
+ } else if (parityMeasured === 0) {
2925
+ verdict = "invalid";
2926
+ verdictReason = "rendering parity not measured";
2927
+ } else if (unplaceable > 0) {
2928
+ verdict = "invalid";
2929
+ verdictReason = `${unplaceable} instants could not be placed on the clock`;
2930
+ } else if (unverified > 0) {
2931
+ verdict = "invalid";
2932
+ verdictReason = `${unverified} instants unverified`;
2933
+ } else if (wrong > 0) {
2934
+ verdict = "fail";
2935
+ verdictReason = `${wrong} wrong instants`;
2936
+ } else if (covered !== expectedInstants) {
2937
+ verdict = "fail";
2938
+ verdictReason = `${expectedInstants - covered} instants uncovered`;
2939
+ } else if (missedDeadlines > 0) {
2940
+ verdict = "fail";
2941
+ verdictReason = `${missedDeadlines} missed deadlines`;
2942
+ } else if (parityFailures > 0) {
2943
+ verdict = "fail";
2944
+ verdictReason = `${parityFailures} parity failures`;
2945
+ }
2946
+ return {
2947
+ clip: clip.name,
2948
+ nominalInstants: plan.frames.length,
2949
+ leadingExcluded,
2950
+ trailingExcluded,
2951
+ expectedInstants,
2952
+ coveredInstants: covered,
2953
+ wrongFrameInstants: wrong,
2954
+ uncoveredInstants: expectedInstants - covered,
2955
+ submittedUnique: unique.size,
2956
+ ruleSkippedSourceFrames: plan.droppedSourceFrameCount,
2957
+ outputIntervalUs,
2958
+ medianHoldUs: holds.length > 0 ? holds[Math.floor(holds.length / 2)] : 0,
2959
+ maxHoldUs: holds.length > 0 ? holds[holds.length - 1] : 0,
2960
+ expectedHoldUs,
2961
+ missedDeadlines,
2962
+ parity: {
2963
+ measured: parityMeasured,
2964
+ worstLevelDelta,
2965
+ worstBarDelta: Number(worstBarDelta.toFixed(5)),
2966
+ failures: parityFailures,
2967
+ // No measurement is not a pass: a run that never looked has not looked.
2968
+ pass: parityMeasured > 0 && parityFailures === 0
2969
+ },
2970
+ offsets,
2971
+ unplaceableInstants: unplaceable,
2972
+ frameDerivedOffsetUs: Math.round(frameDerivedOffsetNs / 1e3),
2973
+ exactInstants: exact,
2974
+ worstOffsetUs: Math.round(
2975
+ Object.keys(offsets).reduce((worst, k) => Math.max(worst, Math.abs(Number(k))), 0) * (clip.sourceFps > 0 ? 1e6 / clip.sourceFps / clip.speed : 0)
2976
+ ),
2977
+ identity: {
2978
+ timestampDomain,
2979
+ medianTextureSkewUs: medianSkewUs,
2980
+ pixelIdentity,
2981
+ identityUnverified: unverified,
2982
+ deliveredFrames: delivered.size,
2983
+ markersRead,
2984
+ markersFailed
2985
+ },
2986
+ frameTimeUs: frameTimes,
2987
+ wrong: wrongInstants,
2988
+ // Every expected instant covered, by the frame the shared rule names, and
2989
+ // no picture held past its turn.
2990
+ /*
2991
+ An unverified instant is not a passing one.
2992
+
2993
+ With the probe off and the timestamps in the system domain there is no
2994
+ evidence about which frame was shown, and a check that reported that
2995
+ absence as success would be the same mistake as the one the band was
2996
+ built to correct.
2997
+ */
2998
+ /*
2999
+ The preview contract: temporally accurate, correctly rendered, covered.
3000
+
3001
+ Exact export-frame identity is deliberately absent. `exactInstants` and
3002
+ `offsets` record it, and `@see PREVIEW_TOLERANCE_US` for why requiring it
3003
+ here would be requiring a per-frame seek.
3004
+ */
3005
+ pass: verdict === "pass",
3006
+ verdict,
3007
+ verdictReason
3008
+ };
3009
+ }
3010
+
3011
+ // src/submissionScoring.ts
3012
+ function audioAt(samples, atNs, windowNs) {
3013
+ let before = null;
3014
+ let after = null;
3015
+ for (const s of samples) {
3016
+ if (!s.playing) continue;
3017
+ if (s.atNs <= atNs && (before === null || s.atNs > before.atNs)) before = s;
3018
+ if (s.atNs >= atNs && (after === null || s.atNs < after.atNs)) after = s;
3019
+ }
3020
+ if (before !== null && after !== null && after.atNs > before.atNs) {
3021
+ const spanUs = (after.atNs - before.atNs) / 1e3;
3022
+ const movedUs = after.positionUs - before.positionUs;
3023
+ if (Math.abs(movedUs - spanUs) <= DISCONTINUITY_US) {
3024
+ const t = (atNs - before.atNs) / (after.atNs - before.atNs);
3025
+ return before.positionUs + t * movedUs;
3026
+ }
3027
+ return before.positionUs + (atNs - before.atNs) / 1e3;
3028
+ }
3029
+ const nearest = before ?? after;
3030
+ if (nearest === null) return null;
3031
+ if (Math.abs(nearest.atNs - atNs) > windowNs) return null;
3032
+ return nearest.positionUs + (atNs - nearest.atNs) / 1e3;
3033
+ }
3034
+ var DISCONTINUITY_US = 1e5;
3035
+ function scoreSubmissions(submissions, samples, options = {}) {
3036
+ const toleranceUs = options.toleranceUs ?? 6e4;
3037
+ const pairWindowNs = options.pairWindowNs ?? 5e7;
3038
+ const scored = [];
3039
+ const excursions = [];
3040
+ let unpaired = 0;
3041
+ let worstErrorUs = 0;
3042
+ let peakOccupancy = 0;
3043
+ for (const record2 of submissions) {
3044
+ if (record2.ptsUs < 0 || record2.submittedNs <= 0) continue;
3045
+ if (record2.occupancy > peakOccupancy) peakOccupancy = record2.occupancy;
3046
+ const audioUs = audioAt(samples, record2.submittedNs, pairWindowNs);
3047
+ if (audioUs === null) {
3048
+ unpaired += 1;
3049
+ continue;
3050
+ }
3051
+ const start = record2.ptsUs;
3052
+ const end = record2.ptsUs + Math.max(record2.intervalUs, 0);
3053
+ const signedUs = audioUs < start ? audioUs - start : audioUs > end ? audioUs - end : 0;
3054
+ const errorUs = Math.abs(signedUs);
3055
+ const entry = {
3056
+ frameId: record2.frameId,
3057
+ ptsUs: record2.ptsUs,
3058
+ intervalUs: record2.intervalUs,
3059
+ submittedNs: record2.submittedNs,
3060
+ audioUs: Math.round(audioUs),
3061
+ errorUs: Math.round(errorUs),
3062
+ signedUs: Math.round(signedUs),
3063
+ occupancy: record2.occupancy
3064
+ };
3065
+ scored.push(entry);
3066
+ if (errorUs > worstErrorUs) worstErrorUs = errorUs;
3067
+ if (errorUs > toleranceUs) excursions.push(entry);
3068
+ }
3069
+ const gaps = [];
3070
+ const frameTimes = [];
3071
+ for (let i = 1; i < scored.length; i += 1) {
3072
+ const gap = scored[i].ptsUs - scored[i - 1].ptsUs;
3073
+ if (gap > 0) gaps.push(gap);
3074
+ const wall = (scored[i].submittedNs - scored[i - 1].submittedNs) / 1e3;
3075
+ if (wall > 0) frameTimes.push(wall);
3076
+ }
3077
+ gaps.sort((a, b) => a - b);
3078
+ frameTimes.sort((a, b) => a - b);
3079
+ const at = (fraction) => frameTimes.length === 0 ? 0 : Math.round(frameTimes[Math.min(frameTimes.length - 1, Math.floor(frameTimes.length * fraction))]);
3080
+ return {
3081
+ toleranceUs,
3082
+ scored: scored.length,
3083
+ unpaired,
3084
+ excursions,
3085
+ worstErrorUs: Math.round(worstErrorUs),
3086
+ // No submissions is not a pass: a run that showed nothing was not judged.
3087
+ pass: scored.length > 0 && excursions.length === 0,
3088
+ verdict: scored.length === 0 ? "invalid" : excursions.length === 0 ? "pass" : "fail",
3089
+ verdictReason: scored.length === 0 ? "no submissions scored" : excursions.length === 0 ? "" : `${excursions.length} excursions, worst ${Math.round(worstErrorUs / 1e3)} ms`,
3090
+ cadenceUs: {
3091
+ median: gaps.length > 0 ? gaps[Math.floor(gaps.length / 2)] : 0,
3092
+ min: gaps.length > 0 ? gaps[0] : 0,
3093
+ max: gaps.length > 0 ? gaps[gaps.length - 1] : 0,
3094
+ samples: gaps.length
3095
+ },
3096
+ frameTimeUs: {
3097
+ p50: at(0.5),
3098
+ p95: at(0.95),
3099
+ p99: at(0.99),
3100
+ max: frameTimes.length > 0 ? Math.round(frameTimes[frameTimes.length - 1]) : 0,
3101
+ samples: frameTimes.length
3102
+ },
3103
+ peakOccupancy
3104
+ };
3105
+ }
3106
+ function traceSubmission(records, scored, frameId, span = 8) {
3107
+ const ordered = records.filter((r) => r.submittedNs > 0).sort((a, b) => a.submittedNs - b.submittedNs);
3108
+ const centre = ordered.findIndex((r) => r.frameId === frameId);
3109
+ if (centre < 0) {
3110
+ return {
3111
+ frameId,
3112
+ rows: [],
3113
+ spentOn: "unknown",
3114
+ spentUs: 0,
3115
+ invalid: true,
3116
+ reason: "frame not retained"
3117
+ };
3118
+ }
3119
+ const from = Math.max(0, centre - span);
3120
+ const window = ordered.slice(from, centre + span + 1);
3121
+ const base = window[0].submittedNs;
3122
+ const ms = (ns) => ns > 0 ? Math.round((ns - base) / 1e6) : -1;
3123
+ const rows = window.map((r) => {
3124
+ const match = scored.find((s) => s.frameId === r.frameId);
3125
+ return {
3126
+ frameId: r.frameId,
3127
+ acquiredMs: ms(r.acquiredNs),
3128
+ dueMs: ms(r.dueNs),
3129
+ promotedMs: ms(r.promotedNs),
3130
+ submittedMs: ms(r.submittedNs),
3131
+ waitUs: r.acquiredNs > 0 && r.dueNs > 0 ? Math.round((r.dueNs - r.acquiredNs) / 1e3) : -1,
3132
+ promoteUs: r.dueNs > 0 && r.promotedNs > 0 ? Math.round((r.promotedNs - r.dueNs) / 1e3) : -1,
3133
+ submitUs: r.promotedNs > 0 && r.submittedNs > 0 ? Math.round((r.submittedNs - r.promotedNs) / 1e3) : -1,
3134
+ ptsUs: r.ptsUs,
3135
+ audioUs: match === void 0 ? -1 : match.audioUs,
3136
+ errorUs: match === void 0 ? -1 : match.errorUs,
3137
+ slot: r.slot,
3138
+ occupancy: r.occupancy,
3139
+ marker: r.submittedMarker
3140
+ };
3141
+ });
3142
+ const target = rows.find((r) => r.frameId === frameId);
3143
+ const raw = window.find((r) => r.frameId === frameId);
3144
+ if (raw.acquiredNs <= 0 || raw.dueNs <= 0 || raw.promotedNs <= 0) {
3145
+ return {
3146
+ frameId,
3147
+ rows,
3148
+ spentOn: "unknown",
3149
+ spentUs: 0,
3150
+ invalid: true,
3151
+ reason: "incomplete stamps"
3152
+ };
3153
+ }
3154
+ const index = rows.indexOf(target);
3155
+ const previous = index > 0 ? rows[index - 1] : null;
3156
+ const arrivalUs = previous === null ? 0 : Math.round((target.acquiredMs - previous.submittedMs) * 1e3);
3157
+ const promoteUs = target.waitUs < 0 ? target.promoteUs + target.waitUs : target.promoteUs;
3158
+ const legs = [
3159
+ ["arrival", arrivalUs],
3160
+ ["due", Math.max(0, target.waitUs)],
3161
+ ["promotion", promoteUs],
3162
+ ["submission", target.submitUs]
3163
+ ];
3164
+ let spentOn = "unknown";
3165
+ let worst = -1;
3166
+ for (const [name, value] of legs) {
3167
+ if (value > worst) {
3168
+ worst = value;
3169
+ spentOn = name;
3170
+ }
3171
+ }
3172
+ return { frameId, rows, spentOn, spentUs: Math.max(0, worst), invalid: false, reason: "" };
3173
+ }
3174
+ function traceSeam(samples, atUs, spanUs = 6e5) {
3175
+ const near = samples.filter(
3176
+ (s) => s.framePtsUs >= 0 && s.framePtsUs >= atUs - spanUs && s.framePtsUs <= atUs + spanUs && s.frameConsumedNs > 0
3177
+ ).sort((a, b) => a.frameConsumedNs - b.frameConsumedNs);
3178
+ const unique = [];
3179
+ for (const s of near) {
3180
+ if (unique.length === 0 || unique[unique.length - 1].frameId !== s.frameId) unique.push(s);
3181
+ }
3182
+ if (unique.length < 3) {
3183
+ return {
3184
+ atUs,
3185
+ outputGapUs: -1,
3186
+ acquireGapUs: -1,
3187
+ owner: "unknown",
3188
+ stages: [],
3189
+ invalid: true,
3190
+ reason: "too few frames around the boundary"
3191
+ };
3192
+ }
3193
+ const base = unique[0].frameConsumedNs;
3194
+ const ms = (ns) => ns > 0 ? Math.round((ns - base) / 1e6) : -1;
3195
+ const stages = unique.map((s) => ({
3196
+ framePtsUs: s.framePtsUs,
3197
+ metadataMs: ms(s.frameMetadataNs),
3198
+ availableMs: ms(s.frameAvailableNs),
3199
+ consumedMs: ms(s.frameConsumedNs),
3200
+ graphUs: s.frameMetadataNs > 0 && s.frameAvailableNs > 0 ? Math.round((s.frameAvailableNs - s.frameMetadataNs) / 1e3) : -1,
3201
+ latchUs: s.frameAvailableNs > 0 && s.frameConsumedNs > 0 ? Math.round((s.frameConsumedNs - s.frameAvailableNs) / 1e3) : -1
3202
+ }));
3203
+ let outputGapUs = 0;
3204
+ let acquireGapUs = 0;
3205
+ let at = -1;
3206
+ for (let i = 1; i < unique.length; i += 1) {
3207
+ const acquire = Math.round((unique[i].frameConsumedNs - unique[i - 1].frameConsumedNs) / 1e3);
3208
+ if (acquire > acquireGapUs) {
3209
+ acquireGapUs = acquire;
3210
+ at = i;
3211
+ }
3212
+ const a = unique[i].frameMetadataNs;
3213
+ const b = unique[i - 1].frameMetadataNs;
3214
+ if (a > 0 && b > 0) outputGapUs = Math.max(outputGapUs, Math.round((a - b) / 1e3));
3215
+ }
3216
+ let owner = "unknown";
3217
+ if (at > 0) {
3218
+ const s = stages[at];
3219
+ if (s.graphUs < 0 || s.latchUs < 0) owner = "unknown";
3220
+ else if (s.graphUs >= s.latchUs && s.graphUs > 16e3) owner = "graph";
3221
+ else if (s.latchUs > 16e3) owner = "consumer";
3222
+ else owner = "production";
3223
+ }
3224
+ return { atUs, outputGapUs, acquireGapUs, owner, stages, invalid: false, reason: "" };
3225
+ }
3226
+ function traceStartup(samples, records, options) {
3227
+ const count = options.count ?? 10;
3228
+ if (options.sourceFps <= 0 || options.speed <= 0) {
3229
+ return { rows: [], missingSourceFrames: [], misassociated: [], invalid: true, reason: "no clip rate" };
3230
+ }
3231
+ const stepUs = 1e6 / options.sourceFps / options.speed;
3232
+ const byFrame = /* @__PURE__ */ new Map();
3233
+ for (const s of samples) {
3234
+ if (s.frameId < 0 || s.frameConsumedNs <= 0) continue;
3235
+ const seen = byFrame.get(s.frameId);
3236
+ if (seen === void 0 || s.frameConsumedNs < seen.frameConsumedNs) byFrame.set(s.frameId, s);
3237
+ }
3238
+ const ordered = records.filter((r) => r.submittedNs > 0).sort((a, b) => a.submittedNs - b.submittedNs).slice(0, count);
3239
+ if (ordered.length === 0) {
3240
+ return { rows: [], missingSourceFrames: [], misassociated: [], invalid: true, reason: "no submissions" };
3241
+ }
3242
+ const base = ordered[0].submittedNs;
3243
+ const ms = (ns) => ns > 0 ? Math.round((ns - base) / 1e6) : -1;
3244
+ const rows = ordered.map((r) => {
3245
+ const s = byFrame.get(r.frameId);
3246
+ return {
3247
+ frameId: r.frameId,
3248
+ renderedPtsUs: s === void 0 ? -1 : s.framePtsUs,
3249
+ recordedPtsUs: r.ptsUs,
3250
+ marker: r.submittedMarker,
3251
+ markerPtsUs: r.submittedMarker >= 0 ? Math.round(r.submittedMarker * stepUs) : -1,
3252
+ metadataMs: s === void 0 ? -1 : ms(s.frameMetadataNs),
3253
+ consumedMs: s === void 0 ? -1 : ms(s.frameConsumedNs),
3254
+ submittedMs: ms(r.submittedNs)
3255
+ };
3256
+ });
3257
+ const misassociated = [];
3258
+ const rendered = /* @__PURE__ */ new Set();
3259
+ for (const row of rows) {
3260
+ if (row.renderedPtsUs >= 0) rendered.add(Math.round(row.renderedPtsUs / stepUs));
3261
+ if (row.marker < 0 || row.renderedPtsUs < 0) continue;
3262
+ if (Math.abs(row.markerPtsUs - row.renderedPtsUs) > stepUs / 2) misassociated.push(row.frameId);
3263
+ }
3264
+ const last = rows[rows.length - 1];
3265
+ const highest = Math.max(
3266
+ last.marker >= 0 ? last.marker : 0,
3267
+ last.renderedPtsUs >= 0 ? Math.round(last.renderedPtsUs / stepUs) : 0
3268
+ );
3269
+ const missingSourceFrames = [];
3270
+ for (let i = 0; i <= highest; i += 1) if (!rendered.has(i)) missingSourceFrames.push(i);
3271
+ return { rows, missingSourceFrames, misassociated, invalid: false, reason: "" };
3272
+ }
3273
+
3274
+ // src/usePreviewAudio.ts
3275
+ var import_video_core9 = require("@pictor-sdk/video-core");
3276
+ var import_react4 = require("react");
3277
+
3278
+ // src/previewAudioDiagnostics.ts
3279
+ var previewAudioDiagnostics = {
3280
+ suppressPublications: false,
3281
+ logPublications: false,
3282
+ published: 0,
3283
+ suppressed: 0
3284
+ };
3285
+ function publishState(name, set) {
3286
+ return (value) => {
3287
+ if (previewAudioDiagnostics.suppressPublications) {
3288
+ previewAudioDiagnostics.suppressed += 1;
3289
+ return;
3290
+ }
3291
+ previewAudioDiagnostics.published += 1;
3292
+ if (previewAudioDiagnostics.logPublications) {
3293
+ console.log(`AUDIOPUB ${name} ${JSON.stringify(value ?? null)}`);
3294
+ }
3295
+ set(value);
3296
+ };
3297
+ }
3298
+
3299
+ // src/usePreviewAudio.ts
3300
+ function usePreviewAudio(options) {
3301
+ const { project, enabled = true } = options;
3302
+ const resolveSource = (0, import_react4.useRef)(options.resolveSource);
3303
+ resolveSource.current = options.resolveSource;
3304
+ const [status, setStatusState] = (0, import_react4.useState)("source");
3305
+ const [error, setErrorState] = (0, import_react4.useState)(null);
3306
+ const [readyKey, setReadyKeyState] = (0, import_react4.useState)(null);
3307
+ const [lastPrepareMs, setLastPrepareMsState] = (0, import_react4.useState)(null);
3308
+ const [lastProfile, setLastProfileState] = (0, import_react4.useState)(null);
3309
+ const [mix, setMix] = (0, import_react4.useState)(null);
3310
+ const setStatus = (0, import_react4.useMemo)(() => publishState("status", setStatusState), [setStatusState]);
3311
+ const setError = (0, import_react4.useMemo)(() => publishState("error", setErrorState), [setErrorState]);
3312
+ const setReadyKey = (0, import_react4.useMemo)(
3313
+ () => publishState("readyKey", setReadyKeyState),
3314
+ [setReadyKeyState]
3315
+ );
3316
+ const setLastPrepareMs = (0, import_react4.useMemo)(
3317
+ () => publishState("lastPrepareMs", setLastPrepareMsState),
3318
+ [setLastPrepareMsState]
3319
+ );
3320
+ const setLastProfile = (0, import_react4.useMemo)(
3321
+ () => publishState("lastProfile", setLastProfileState),
3322
+ [setLastProfileState]
3323
+ );
3324
+ const preparingKey = (0, import_react4.useRef)(null);
3325
+ const failedKey = (0, import_react4.useRef)(null);
3326
+ const epoch = (0, import_react4.useRef)(0);
3327
+ const mounted = (0, import_react4.useRef)(true);
3328
+ const wanted = (0, import_react4.useMemo)(() => {
3329
+ if (!enabled || project.audio.length === 0) {
3330
+ return {
3331
+ key: "",
3332
+ programJson: "",
3333
+ urisJson: "",
3334
+ durationUs: 0,
3335
+ unresolved: []
3336
+ };
3337
+ }
3338
+ const program = (0, import_video_core9.compileAudioMixProgram)(project);
3339
+ const resolved = resolveMixSources(project, program, (s, c) => resolveSource.current(s, c));
3340
+ const programJson = JSON.stringify(program);
3341
+ const urisJson = JSON.stringify(resolved.uris);
3342
+ return {
3343
+ key: resolved.unresolved.length > 0 ? "" : previewMixKey(programJson, urisJson),
3344
+ programJson,
3345
+ urisJson,
3346
+ durationUs: Number(program.durationUs),
3347
+ unresolved: resolved.unresolved
3348
+ };
3349
+ }, [project, enabled]);
3350
+ const decision = decidePreviewAudio({
3351
+ wantedKey: wanted.key,
3352
+ readyKey,
3353
+ preparingKey: preparingKey.current,
3354
+ failedKey: failedKey.current
3355
+ });
3356
+ const unresolved = wanted.unresolved.length > 0 && enabled && project.audio.length > 0;
3357
+ (0, import_react4.useEffect)(() => {
3358
+ mounted.current = true;
3359
+ return () => {
3360
+ mounted.current = false;
3361
+ };
3362
+ }, []);
3363
+ (0, import_react4.useEffect)(() => {
3364
+ if (!enabled) return;
3365
+ try {
3366
+ requireNative().previewMuteSourceAudio(decision.muteSourceAudio || unresolved);
3367
+ } catch {
3368
+ }
3369
+ }, [decision.muteSourceAudio, unresolved, enabled]);
3370
+ (0, import_react4.useEffect)(() => {
3371
+ if (unresolved) {
3372
+ setStatus("failed");
3373
+ setError(wanted.unresolved[0] ?? "a sound could not be read");
3374
+ return;
3375
+ }
3376
+ setStatus(decision.status);
3377
+ if (decision.status !== "failed") setError(null);
3378
+ }, [decision.status, unresolved, wanted.unresolved, setError, setStatus]);
3379
+ (0, import_react4.useEffect)(() => {
3380
+ if (!decision.shouldPrepare || unresolved) return;
3381
+ const key = wanted.key;
3382
+ preparingKey.current = key;
3383
+ const jobId = `preview-${key.slice(0, 8)}-${epoch.current + 1}`;
3384
+ const startedAt = Date.now();
3385
+ void preparePreviewMix(jobId, wanted.programJson, wanted.urisJson).then((result) => {
3386
+ if (!mounted.current) return;
3387
+ if (result.ok === false && result.superseded) return;
3388
+ if (key !== preparingKey.current) return;
3389
+ preparingKey.current = null;
3390
+ if (result.ok === false) {
3391
+ failedKey.current = key;
3392
+ setError(result.reason);
3393
+ setStatus("failed");
3394
+ return;
3395
+ }
3396
+ failedKey.current = null;
3397
+ epoch.current += 1;
3398
+ setLastPrepareMs(result.cached ? null : Date.now() - startedAt);
3399
+ setLastProfile(result.cached ? null : result.profile ?? null);
3400
+ try {
3401
+ requireNative().previewAudioLoad(result.uri, epoch.current);
3402
+ } catch {
3403
+ failedKey.current = key;
3404
+ setError("the prepared mix could not be opened");
3405
+ setStatus("failed");
3406
+ return;
3407
+ }
3408
+ setMix({ uri: result.uri, durationUs: Number(wanted.durationUs) });
3409
+ setReadyKey(result.key);
3410
+ sweepPreviewMixes([result.key]);
3411
+ });
3412
+ }, [
3413
+ decision.shouldPrepare,
3414
+ wanted.key,
3415
+ wanted.programJson,
3416
+ wanted.urisJson,
3417
+ unresolved,
3418
+ setError,
3419
+ setLastPrepareMs,
3420
+ setLastProfile,
3421
+ setReadyKey,
3422
+ setStatus
3423
+ ]);
3424
+ const release = (0, import_react4.useCallback)(() => {
3425
+ cancelPreviewMix();
3426
+ try {
3427
+ requireNative().previewSyncStop("release");
3428
+ requireNative().previewAudioRelease();
3429
+ requireNative().previewMuteSourceAudio(false);
3430
+ } catch {
3431
+ }
3432
+ preparingKey.current = null;
3433
+ setReadyKey(null);
3434
+ setMix(null);
3435
+ }, [setReadyKey]);
3436
+ (0, import_react4.useEffect)(() => release, [release]);
3437
+ return {
3438
+ status: unresolved ? "failed" : status,
3439
+ error,
3440
+ canPlay: !unresolved && decision.canPlay,
3441
+ ready: decision.status === "ready",
3442
+ lastPrepareMs,
3443
+ lastProfile,
3444
+ // Null until a mix matching the document is ready, so a composition built
3445
+ // from it is never built from a stale one.
3446
+ mix: decision.status === "ready" ? mix : null,
3447
+ release
3448
+ };
3449
+ }
3450
+
3451
+ // src/encoderGate.ts
3452
+ var ENCODER_GATE_FIXTURES = [
3453
+ "short",
3454
+ "acceptance",
3455
+ "silence",
3456
+ "saturated",
3457
+ "markers"
3458
+ ];
3459
+ function runEncoderGate(fixture, maxInputSize = 65536) {
3460
+ try {
3461
+ return JSON.parse(requireNative().encoderGateRun(fixture, maxInputSize));
3462
+ } catch (error) {
3463
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
3464
+ }
3465
+ }
3466
+
3467
+ // src/previewTrace.ts
3468
+ var EMPTY2 = {
3469
+ ok: false,
3470
+ valid: false,
3471
+ fault: "trace unavailable",
3472
+ events: [],
3473
+ nextSeq: 0,
3474
+ total: 0,
3475
+ dropped: 0
3476
+ };
3477
+ function armPreviewTrace(on, intervalMs = 25) {
3478
+ try {
3479
+ const value = JSON.parse(requireNative().previewTraceArm(on, intervalMs));
3480
+ return value.ok === true;
3481
+ } catch {
3482
+ return false;
3483
+ }
3484
+ }
3485
+ function markPreviewTrace(label) {
3486
+ try {
3487
+ requireNative().previewTraceMark(label);
3488
+ } catch {
3489
+ }
3490
+ }
3491
+ function drainPreviewTrace(fromSeq, limit = 200) {
3492
+ try {
3493
+ const value = JSON.parse(requireNative().previewTraceDrain(fromSeq, limit));
3494
+ return value.ok ? value : EMPTY2;
3495
+ } catch {
3496
+ return EMPTY2;
3497
+ }
3498
+ }
3499
+ function drainAllPreviewTrace(limit = 200) {
3500
+ const events = [];
3501
+ let seq = 0;
3502
+ let dropped = 0;
3503
+ let total = 0;
3504
+ let valid = true;
3505
+ let fault = null;
3506
+ for (let guard = 0; guard < 200; guard += 1) {
3507
+ const page = drainPreviewTrace(seq, limit);
3508
+ if (page.fault) {
3509
+ valid = false;
3510
+ fault = page.fault;
3511
+ }
3512
+ if (!page.ok || page.events.length === 0) {
3513
+ total = page.total || total;
3514
+ break;
3515
+ }
3516
+ events.push(...page.events);
3517
+ dropped += page.dropped;
3518
+ total = page.total;
3519
+ if (page.nextSeq === seq) break;
3520
+ seq = page.nextSeq;
3521
+ }
3522
+ return { ok: true, valid, fault, events, nextSeq: seq, total, dropped };
3523
+ }
3524
+ function summariseOffsets(events) {
3525
+ let n = 0;
3526
+ let stale = 0;
3527
+ let absent = 0;
3528
+ let fresh = 0;
3529
+ let staleRun = 0;
3530
+ let longestStaleRun = 0;
3531
+ let min = Number.POSITIVE_INFINITY;
3532
+ let max = Number.NEGATIVE_INFINITY;
3533
+ let sum = 0;
3534
+ let samples = 0;
3535
+ for (const e of events) {
3536
+ if (e.type !== "sample") continue;
3537
+ samples += 1;
3538
+ if (e.v.pfFresh === void 0) {
3539
+ absent += 1;
3540
+ staleRun += 1;
3541
+ if (staleRun > longestStaleRun) longestStaleRun = staleRun;
3542
+ } else if (e.v.pfFresh === false) {
3543
+ stale += 1;
3544
+ staleRun += 1;
3545
+ if (staleRun > longestStaleRun) longestStaleRun = staleRun;
3546
+ } else {
3547
+ fresh += 1;
3548
+ staleRun = 0;
3549
+ }
3550
+ if (e.v.videoPlaying !== true || e.v.audioPlaying !== true) continue;
3551
+ const offset = Number(e.v.offsetUs);
3552
+ if (!Number.isFinite(offset)) continue;
3553
+ n += 1;
3554
+ sum += offset;
3555
+ if (offset < min) min = offset;
3556
+ if (offset > max) max = offset;
3557
+ }
3558
+ return {
3559
+ samples,
3560
+ playingSamples: n,
3561
+ staleFrameSamples: stale,
3562
+ absentFrameSamples: absent,
3563
+ freshFrameSamples: fresh,
3564
+ longestStaleRun,
3565
+ minUs: n > 0 ? min : 0,
3566
+ maxUs: n > 0 ? max : 0,
3567
+ meanUs: n > 0 ? Math.round(sum / n) : 0,
3568
+ worstAbsUs: n > 0 ? Math.max(Math.abs(min), Math.abs(max)) : 0
3569
+ };
3570
+ }
3571
+ function previewSyncCounters() {
3572
+ try {
3573
+ return JSON.parse(requireNative().previewSyncCounters());
3574
+ } catch {
3575
+ return { ok: false };
3576
+ }
3577
+ }
3578
+ function stopPreviewForHost(reason) {
3579
+ try {
3580
+ requireNative().previewSyncStop(reason);
3581
+ } catch {
3582
+ }
3583
+ }
3584
+ function previewFramePipeline() {
3585
+ try {
3586
+ return JSON.parse(requireNative().previewFramePipeline());
3587
+ } catch {
3588
+ return {};
3589
+ }
3590
+ }
3591
+ function setPreviewParts(parts) {
3592
+ try {
3593
+ const v = JSON.parse(requireNative().previewConfigSet(JSON.stringify(parts)));
3594
+ return v.ok ? v : null;
3595
+ } catch {
3596
+ return null;
3597
+ }
3598
+ }
3599
+ function previewParts() {
3600
+ try {
3601
+ return JSON.parse(requireNative().previewConfigGet());
3602
+ } catch {
3603
+ return null;
3604
+ }
3605
+ }
3606
+ function muteSourceAudio(muted) {
3607
+ try {
3608
+ requireNative().previewMuteSourceAudio(muted);
3609
+ } catch {
3610
+ }
3611
+ }
3612
+ function previewCodecLedger() {
3613
+ try {
3614
+ return JSON.parse(requireNative().previewCodecLedger());
3615
+ } catch {
3616
+ return { live: -1, peakLive: -1, total: 0, entries: [] };
3617
+ }
3618
+ }
3619
+ // Annotate the CommonJS export names for ESM import in node:
3620
+ 0 && (module.exports = {
3621
+ AndroidVideoPlayer,
3622
+ CONVERGED_RUN,
3623
+ DEFAULT_CEILING_BYTES,
3624
+ DEFAULT_RISE_BYTES,
3625
+ DEFAULT_WINDOW_SAMPLES,
3626
+ ENCODER_GATE_FIXTURES,
3627
+ EXPORT_STAGES,
3628
+ MAX_OBSERVED_HEARTBEAT_GAP_MS,
3629
+ MAX_RECOVERY_SAMPLES,
3630
+ Media3,
3631
+ MediaSaveCancelled,
3632
+ MediaSaveFailed,
3633
+ NO_SURFACE,
3634
+ PHASE_MARK_PREFIX,
3635
+ PREVIEW_DRIFT_TOLERANCE_US,
3636
+ PREVIEW_OUTPUT_FPS,
3637
+ PREVIEW_TOLERANCE_US,
3638
+ RECOVERY_GRACE_MS,
3639
+ SPIKE_TOLERANCE_US,
3640
+ STALL_GRACE_MS,
3641
+ VideoExportRejected,
3642
+ afterPrepare,
3643
+ armPreviewTrace,
3644
+ beginPhase,
3645
+ cancelPreviewMix,
3646
+ checkCadence,
3647
+ classifyExcursions,
3648
+ classifyStall,
3649
+ compositionSpikeSpec,
3650
+ createAndroidThumbnailProvider,
3651
+ createExportJobs,
3652
+ createMedia3VideoExporter,
3653
+ createMediaStoreSaver,
3654
+ createMemoryGuard,
3655
+ createOverlayStagingSink,
3656
+ createPlaybackLifecycle,
3657
+ createProgressReporter,
3658
+ createStallWatchdog,
3659
+ createSurfaceOwner,
3660
+ createSyncTrace,
3661
+ decidePreviewAudio,
3662
+ describeTrip,
3663
+ distribution,
3664
+ drainAllPreviewTrace,
3665
+ drainPreviewTrace,
3666
+ expectedPtsAt,
3667
+ idlePlaylistSession,
3668
+ ingestManagedSource,
3669
+ installOverlayStaging,
3670
+ isCurrentGeneration,
3671
+ managedSourceLedger,
3672
+ managedSourceList,
3673
+ managedSourceToken,
3674
+ markPreviewTrace,
3675
+ measureStagingStall,
3676
+ mixSpanContext,
3677
+ muteSourceAudio,
3678
+ nextOwnerToken,
3679
+ observe,
3680
+ observeMemory,
3681
+ offsetAt,
3682
+ planProgressWeights,
3683
+ preparePreviewMix,
3684
+ previewAudioDiagnostics,
3685
+ previewAudioNeeded,
3686
+ previewCodecLedger,
3687
+ previewFramePipeline,
3688
+ previewMixKey,
3689
+ previewMixLedger,
3690
+ previewParts,
3691
+ previewProgramJson,
3692
+ previewSyncCounters,
3693
+ probeAudioSource,
3694
+ recordTick,
3695
+ recoveryFrames,
3696
+ releaseManagedSource,
3697
+ releasePreviewMix,
3698
+ resetSurfaceOwnerForTests,
3699
+ resolveMixSources,
3700
+ retireSurface,
3701
+ runEncoderGate,
3702
+ scoreSubmissions,
3703
+ seedManagedSourceForGate,
3704
+ setPreviewParts,
3705
+ shareSavedMedia,
3706
+ shouldPrepare,
3707
+ shouldRecover,
3708
+ spikeClaim,
3709
+ spikeDrain,
3710
+ spikePause,
3711
+ spikePlay,
3712
+ spikeRelease,
3713
+ spikeSeek,
3714
+ spikeStart,
3715
+ spikeState,
3716
+ stopPreviewForHost,
3717
+ summariseOffsets,
3718
+ summarisePhases,
3719
+ surfaceOwner,
3720
+ sweepManagedSources,
3721
+ sweepManagedSourcesRawForGate,
3722
+ sweepOverlayStaging,
3723
+ sweepPendingMediaSaves,
3724
+ sweepPreviewMixes,
3725
+ syncReport,
3726
+ thumbnailCounters,
3727
+ traceSeam,
3728
+ traceStartup,
3729
+ traceSubmission,
3730
+ useCompositionPlayback,
3731
+ useMedia3Playback,
3732
+ usePreviewAudio,
3733
+ useVideoSurface,
3734
+ videoSurfaceSlot,
3735
+ windowAround
3736
+ });
3737
+ //# sourceMappingURL=index.js.map