@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
@@ -0,0 +1,574 @@
1
+ /* eslint-disable @typescript-eslint/no-wrapper-object-types --
2
+ * React Native's codegen recognises the upper-cased `Object` as its
3
+ * `UnsafeObject` type and rejects the primitive `object`. This file is a
4
+ * codegen spec before it is TypeScript, so the generator's spelling wins.
5
+ */
6
+ import { TurboModuleRegistry, type TurboModule } from 'react-native'
7
+
8
+ /**
9
+ * The native surface. **Internal** until the API stabilises.
10
+ *
11
+ * Numbers, strings and plain objects only. `ExoPlayer`, `MediaItem`, `Surface`
12
+ * and every other Android or Media3 type stops at the Kotlin side of this
13
+ * boundary and never appears in a Pictor signature.
14
+ *
15
+ * Nothing here is per frame. Frames reach the renderer through the
16
+ * `__pictorVideoSurface` host object on the UI runtime, so the JS thread is not
17
+ * in the picture's path at all; these calls are controls and a throttled
18
+ * snapshot.
19
+ */
20
+ export interface Spec extends TurboModule {
21
+ /** Publishes `__pictorVideoSurface` on the JS runtime. */
22
+ installVideoSurface(): boolean
23
+ /**
24
+ * Publishes `__pictorOverlayStaging` on the JS runtime.
25
+ *
26
+ * Native chooses the directory; JS never supplies a destination.
27
+ */
28
+ installOverlayStaging(): boolean
29
+
30
+ /** Open a source — local `android.resource:`/`file:` or remote — and render it. */
31
+ media3Prepare(uri: string): boolean
32
+ /** Diagnostics: a player with no video output, for heap attribution. */
33
+ media3PrepareNoSurface(uri: string): boolean
34
+ /** Diagnostics: point the existing player at another source. */
35
+ media3Rebind(uri: string): boolean
36
+ /** Diagnostics: end the source, keep the player. */
37
+ media3StopKeepingPlayer(): void
38
+ /**
39
+ * Replace the playlist. Two parallel arrays: sources, then four numbers per
40
+ * entry — clipping start and end in microseconds, speed in milli-units, and
41
+ * muted as 0 or 1.
42
+ */
43
+ media3SetPlaylist(
44
+ uris: string[],
45
+ bounds: number[],
46
+ startWindow: number,
47
+ startPositionUs: number,
48
+ ): boolean
49
+ /** Seek in the player's coordinates: window index plus offset. */
50
+ media3SeekToWindow(windowIndex: number, positionUs: number): void
51
+ /**
52
+ * **V3.3 gate only.** Encode one source through Transformer, once.
53
+ *
54
+ * Not an exporter and not reachable from `VideoExporter`: it takes a single
55
+ * URI rather than a project, writes to an adapter-private temporary, and
56
+ * exists to answer whether Skia can draw inside Transformer's frame processor
57
+ * without changing the picture. `withSkia` selects the effect chain — false is
58
+ * the vanilla run that establishes what the codec alone costs.
59
+ *
60
+ * Returns a JSON string rather than a shaped object because the shape is a
61
+ * gate's report, not an API, and freezing it here would be pretending
62
+ * otherwise.
63
+ *
64
+ * @experimental
65
+ */
66
+ exportGateRun(uri: string, withSkia: boolean, timeoutSeconds: number): string
67
+ /**
68
+ * **V3.3 gate only.** Execute one serialized, versioned `VideoScene`.
69
+ *
70
+ * Native receives a scene, never a project: ids, indices and numbers that
71
+ * `resolveVideoScene` already settled. It validates the schema version, the
72
+ * command order and every identity before Transformer starts, and refuses
73
+ * anything it cannot execute faithfully rather than skipping it.
74
+ *
75
+ * @experimental
76
+ */
77
+ exportGateRunScene(
78
+ uri: string,
79
+ sceneJson: string,
80
+ timeoutSeconds: number,
81
+ /** `-1` never fails; `0` fails after GL init before the first frame; `n` after n frames. */
82
+ failAfterFrames: number,
83
+ ): string
84
+ /**
85
+ * **V3.3 gate only.** Execute a compiled scene *program* over a sequence.
86
+ *
87
+ * One document for the whole export: sorted, contiguous, half-open segments
88
+ * compiled from the project's clip placements and overlay spans. Native
89
+ * validates coverage, then selects a segment per frame by binary search — a
90
+ * lookup, not an interpretation.
91
+ *
92
+ * @experimental
93
+ */
94
+ exportGateRunProgram(uris: string[], programJson: string, timeoutSeconds: number): string
95
+ /** **V3.3 gate only.** Delete every temporary the gate created. @experimental */
96
+ /**
97
+ * **V3.3 gate only.** A sequence whose clipping and speed TypeScript chose.
98
+ *
99
+ * `itemsJson` is `{ uri, startMs, endMs, speed, durationUs?, forceAudioTrack? }[]`.
100
+ * Native hands these to Media3 and computes none of them: which part of a
101
+ * source plays, and how fast, is timeline interpretation and belongs to the
102
+ * compiler.
103
+ *
104
+ * `forceAudioTrack` is sequence-level and honoured if any item sets it. A
105
+ * source with no audio track fails outright at any rate other than 1 without
106
+ * it — Media3 drives its speed path through audio.
107
+ *
108
+ * @experimental
109
+ */
110
+ exportGateRunItems(itemsJson: string, programJson: string, timeoutSeconds: number): string
111
+ exportGateReleaseTemporary(): number
112
+ /**
113
+ * **V3.3 gate only.** Compare two files in **display space**.
114
+ *
115
+ * Display space is the only space the question means anything in: a file
116
+ * storing 1080×1920 with a rotation flag and one storing 1920×1080 with the
117
+ * orientation baked in display identically, and comparing stored dimensions
118
+ * would call that a difference.
119
+ *
120
+ * Returns a JSON report of per-timestamp statistics. It states no verdict —
121
+ * the threshold is frozen by the caller, from the control, before the thing
122
+ * being tested is looked at.
123
+ *
124
+ * @experimental
125
+ */
126
+ exportGateCompare(leftUri: string, rightUri: string, timestampsUs: number[]): string
127
+ /**
128
+ * **V3.3 gate only.** Mean colour of a central region, per timestamp.
129
+ *
130
+ * Reports numbers and draws no conclusion. Which source a colour implies, and
131
+ * which frame of it, is arithmetic the TypeScript oracle owns.
132
+ *
133
+ * @experimental
134
+ */
135
+ exportGateSample(uri: string, timestampsUs: number[]): string
136
+ /**
137
+ * **V3.3 gate only.** Muxed sample timestamps and per-track durations.
138
+ *
139
+ * A frame count and a whole-file duration cannot tell a held tail frame from a
140
+ * correctly-timed one, or a gap from a slow rate. The written timestamps can.
141
+ * Reports them; judges nothing.
142
+ *
143
+ * @experimental
144
+ */
145
+ /**
146
+ * **V3.4 gate only.** Mean and extremes over caller-chosen rectangles.
147
+ *
148
+ * `probesJson` is `{ timeUs, x, y, w, h }[]`. The fixed centre region
149
+ * {@link exportGateSample} uses straddles a transformed clip's edge and
150
+ * averages source pixels with cleared ones; the caller has the corner oracle
151
+ * and passes rectangles it has confirmed are covered.
152
+ *
153
+ * @experimental
154
+ */
155
+ /**
156
+ * **V3.5 gate only.** Decoded audio: loudness and spectrum per window.
157
+ *
158
+ * A pitch-shifting speed change and a pitch-preserving one produce files whose
159
+ * duration and sample count are identical, so counting cannot tell them apart.
160
+ * Also reports AAC encoder delay, padding and the first presentation time,
161
+ * which a seam alignment check needs.
162
+ *
163
+ * @experimental
164
+ */
165
+ exportGateAudio(uri: string, windowsJson: string): string
166
+ exportGateProbe(uri: string, probesJson: string): string
167
+ exportGateTracks(uri: string): string
168
+ /**
169
+ * **V3.4 gate only.** One decoded frame as base64 PNG, in display space.
170
+ *
171
+ * @experimental
172
+ */
173
+ exportGateFramePng(uri: string, timeUs: number): string
174
+ /**
175
+ * **V3.4 gate only.** Encode a still through the treatment's encoder.
176
+ *
177
+ * A comparison between two renderers is only about the renderers if
178
+ * everything after them is the same, so the reference is encoded and decoded
179
+ * exactly as the treatment is.
180
+ *
181
+ * @experimental
182
+ */
183
+ exportGateEncodeStill(pngBase64: string, frames: number): string
184
+ /**
185
+ * **V3.4 gate only.** Geometry and interior colour, measured separately.
186
+ *
187
+ * A maximum channel delta is near-useless at a transformed edge: one row of
188
+ * pixels on the far side of a bright/black boundary reads 255 while the
189
+ * picture is right. Returns mask overlap, edge displacement and interior
190
+ * colour error, and draws no conclusion about any of them.
191
+ *
192
+ * @experimental
193
+ */
194
+ exportGateGeometry(
195
+ leftUri: string,
196
+ leftUs: number,
197
+ rightUri: string,
198
+ rightUs: number,
199
+ maskThreshold: number,
200
+ ): string
201
+ /**
202
+ * **V3.3 gate only.** Per-sample checksums of the compressed video payload.
203
+ *
204
+ * Transmuxing must move encoded bytes and change only timestamps and container
205
+ * metadata. Decoded pixels cannot show that — a re-encode that happens to look
206
+ * identical would pass — so the payload itself is checksummed.
207
+ *
208
+ * @experimental
209
+ */
210
+ exportGateSampleHashes(uri: string): string
211
+ /**
212
+ * **V3.3 gate only.** One Transformer job per clip, then a transmux concat.
213
+ *
214
+ * `shardsJson` is `{ uri, startMs, endMs, speed, timelineOffsetUs,
215
+ * forceAudioTrack, frameDropFps?, encoderRepeatUs?, emitsPerInput?,
216
+ * outputPtsUs?, cadenceFailAfterEmits? }[]`. The cadence arrays come straight
217
+ * from `resampleClip`; native walks them and computes no grid arithmetic. Native adds `timelineOffsetUs` to each local
218
+ * presentation timestamp and does segment lookup; it computes no placement,
219
+ * duration or rate. Intermediates are deleted on every path.
220
+ *
221
+ * @experimental
222
+ */
223
+ exportGateRunShards(
224
+ shardsJson: string,
225
+ programJson: string,
226
+ timeoutSeconds: number,
227
+ overlaysJson: string,
228
+ ): string
229
+ /**
230
+ * **V3.4 gate only.** Write raw RGBA bytes to a sidecar and checksum them.
231
+ *
232
+ * Pixels never travel in scene JSON. This stages them beside it and returns
233
+ * the path, length and CRC32 a descriptor must declare.
234
+ *
235
+ * @experimental
236
+ */
237
+ /**
238
+ * **V3.5 gate only.** Video shards, an audio timeline, then a muxer copy.
239
+ *
240
+ * The two pipelines never meet until the last step, and that step decodes
241
+ * nothing: `MediaMuxer` writes both sets of encoded samples unchanged, so
242
+ * neither takes a second generation.
243
+ *
244
+ * @experimental
245
+ */
246
+ /**
247
+ * Run an export off the bridge so it can be cancelled while it runs.
248
+ *
249
+ * Resolves with the same JSON outcome the blocking call returns. @experimental
250
+ */
251
+ exportRunProjectAsync(
252
+ jobId: string,
253
+ shardsJson: string,
254
+ programJson: string,
255
+ overlaysJson: string,
256
+ mixProgramJson: string,
257
+ mixUrisJson: string,
258
+ timeoutSeconds: number,
259
+ /** `""` for a temporary, or `{kind:'file',path,overwrite}`. @experimental */
260
+ destinationJson: string,
261
+ ): Promise<string>
262
+ /**
263
+ * Save a finished export into the device's video collection.
264
+ *
265
+ * Inserts pending, streams, verifies, then publishes. Settles once: the
266
+ * published `content:` uri, or an error with the pending row already
267
+ * deleted. @experimental
268
+ */
269
+ mediaSaveAsync(jobId: string, sourceUri: string): Promise<string>
270
+ /**
271
+ * Offer a saved video to the system share sheet.
272
+ *
273
+ * The durable `content:` uri only, with a read grant and nothing else.
274
+ * Returns what the intent carried, so a gate can inspect it. @experimental
275
+ */
276
+ mediaShare(uri: string): string
277
+ /** Ask a running save to stop. Idempotent. @experimental */
278
+ mediaSaveCancel(jobId: string): string
279
+ /** Delete this app's own stale pending rows. Never a published item. @experimental */
280
+ mediaSaveSweepPending(): string
281
+ /** Confirm a saved item still opens. @experimental */
282
+ mediaSaveReadable(uri: string): string
283
+ /**
284
+ * Release one temporary output this module produced. Idempotent.
285
+ *
286
+ * Refuses any path it did not create, so it cannot delete a caller's file.
287
+ * @experimental
288
+ */
289
+ exportReleaseTemporaryOutput(uri: string): string
290
+ /**
291
+ * Remove `.pictor-staging-*.tmp` orphans a killed process left behind.
292
+ *
293
+ * Positively identified by prefix and suffix; never a wildcard. @experimental
294
+ */
295
+ exportSweepStagingOrphans(directory: string): string
296
+ /** Counted work plus the beat total, for the progress pump. @experimental */
297
+ exportProgressSnapshot(): string
298
+ /**
299
+ * What a picked audio file actually contains.
300
+ *
301
+ * Duration, audio-track presence, seekability and MIME, read by opening the
302
+ * file. The host asks this before a sound may enter a project: the mix
303
+ * compiler has no decoder and believes whatever the document declares, and a
304
+ * document that claimed audio a file lacked cost a whole export. Returns
305
+ * `{ok, durationUs, hasAudioTrack, seekable, mimeType}` or `{ok:false,
306
+ * reason}` — never throws, because an unreadable pick is an answer.
307
+ * @experimental
308
+ */
309
+ /**
310
+ * Prepare the audio a preview plays, off the JS thread.
311
+ *
312
+ * The same mixer an export runs, over the same compiled program. Resolves
313
+ * `{ok, uri, key, cached, byteLength}`, or `{ok:false, superseded}` when a
314
+ * newer request replaced it — which a caller must never publish.
315
+ * @experimental
316
+ */
317
+ previewMixPrepare(
318
+ jobId: string,
319
+ programJson: string,
320
+ urisJson: string,
321
+ ): Promise<string>
322
+ /**
323
+ * Load a prepared mix into the mixed-audio player, stamped with an epoch.
324
+ *
325
+ * The epoch is how a start or a seek proves it belongs to the mix currently
326
+ * playing; one carrying an older epoch is dropped. @experimental
327
+ */
328
+ previewAudioLoad(uri: string, epoch: number): string
329
+ /** Start, if the epoch still matches. @experimental */
330
+ previewAudioPlay(epoch: number): string
331
+ /** @experimental */
332
+ previewAudioPause(): string
333
+ /** Seek to a global timeline position, if the epoch still matches. @experimental */
334
+ previewAudioSeek(positionUs: number, epoch: number): string
335
+ /** Preview rate. Clip speed is already compiled into the mix. @experimental */
336
+ previewAudioSetRate(rate: number): string
337
+ /** Position and the wall clock it was sampled at, for drift. @experimental */
338
+ previewAudioState(): string
339
+ /**
340
+ * Silence the video player's own audio while a mixed preview plays.
341
+ *
342
+ * The mix contains the clips' audio too, so both audible plays the timeline
343
+ * twice. @experimental
344
+ */
345
+ previewMuteSourceAudio(muted: boolean): string
346
+ /** @experimental */
347
+ previewAudioRelease(): string
348
+ /**
349
+ * Gate-only: encode a fixed, natively generated PCM fixture and describe the
350
+ * result — access units, timestamps, CSD, payload hashes and decoded metrics.
351
+ *
352
+ * The inputs are formulas rather than files, so a baseline taken before a
353
+ * change to the encoder loop is comparable with one taken after it.
354
+ * @experimental
355
+ */
356
+ encoderGateRun(fixture: string, maxInputSize: number): string
357
+ /** Gate-only: where samples go, stage by stage, for one marker source. @experimental */
358
+ primingAttribution(): string
359
+ /** Gate-only: the file-measurable Music gates, in one run. @experimental */
360
+ musicGates(): string
361
+
362
+ /**
363
+ * Gate-only: mix the marker fixture at a chosen timeline start, decode the
364
+ * result and report where the markers actually landed. @experimental
365
+ */
366
+ previewAlignmentGate(startUs: number): string
367
+
368
+ /** Gate-only: arm the paired player trace and sample every `intervalMs`. @experimental */
369
+ previewTraceArm(on: boolean, intervalMs: number): string
370
+ /** Gate-only: label the trace so a harness can find its own cases. @experimental */
371
+ previewTraceMark(label: string): string
372
+ /** Gate-only: drain trace events from a sequence number, paged. @experimental */
373
+ previewTraceDrain(fromSeq: number, limit: number): string
374
+ /**
375
+ * Compare both players on one clock and correct the mix past a tolerance.
376
+ *
377
+ * Native because that is where both anchors are published; measuring across
378
+ * the bridge would carry the hop's own jitter into the answer. @experimental
379
+ */
380
+ previewDriftCheck(toleranceUs: number): string
381
+ /**
382
+ * Stop playback for a host reason — background, picker, unmount.
383
+ *
384
+ * Overrides any automatic resume the stall coordinator is holding, so
385
+ * playback cannot restart at a moment nobody asked for. @experimental
386
+ */
387
+ previewSyncStop(reason: string): string
388
+ /** What the stall coordinator has done, for assertions. @experimental */
389
+ previewSyncCounters(): string
390
+ /**
391
+ * Frames offered, queued and drawn.
392
+ *
393
+ * Read-only accounting, available in release, so the question "is the trace
394
+ * disturbing the picture?" can be asked without the trace. @experimental
395
+ */
396
+ previewFramePipeline(): string
397
+ /**
398
+ * Gate-only: the shared-clock playback spike.
399
+ *
400
+ * `spikeStart` takes `{items:[{uri,startUs,endUs,speed}], mixUri, width,
401
+ * height}` straight from `playlistOf()`, so the compiler decides the timing
402
+ * and native maps it onto media items without interpreting the project.
403
+ */
404
+ spikeStart(specJson: string): string
405
+ spikePlay(): string
406
+ spikePause(): string
407
+ spikeSeek(positionUs: number): string
408
+ spikeClaim(): string
409
+ spikeState(): string
410
+ spikeDrain(fromSeq: number, limit: number): string
411
+ spikeRelease(): string
412
+ /**
413
+ * Gate-only: turn parts of the music preview off, to bisect a failure.
414
+ *
415
+ * `{loadPlayer, startMix, positionPump, coordinator}`, all true in
416
+ * production. @experimental
417
+ */
418
+ previewConfigSet(json: string): string
419
+ /** What the configuration currently is. @experimental */
420
+ previewConfigGet(): string
421
+ /** Every codec mix preparation owns, and what became of it. @experimental */
422
+ previewCodecLedger(): string
423
+ /** Stop whatever is preparing. @experimental */
424
+ previewMixCancel(): string
425
+ /** The cache key for a program, without preparing it. @experimental */
426
+ previewMixKey(programJson: string, urisJson: string): string
427
+ /** Drop one prepared mix. @experimental */
428
+ previewMixRelease(key: string): string
429
+ /** Keep only these keys; remove the rest and every partial. @experimental */
430
+ previewMixSweep(keepKeysJson: string): string
431
+ /** What is prepared, for assertions. @experimental */
432
+ previewMixLedger(): string
433
+ /**
434
+ * Copy a picked source into app-owned durable storage.
435
+ *
436
+ * A picker returns a reference, not a file: a `content://` grant dies with
437
+ * the activity result and a cache copy dies whenever Android wants storage
438
+ * back. The project persists what this returns, never what a picker did.
439
+ * Content-addressed, so the same song added twice is stored once. Returns
440
+ * `{ok, uri, token, byteLength, sha256, created}` or `{ok:false, reason}`.
441
+ * @experimental
442
+ */
443
+ ingestManagedSource(uri: string): string
444
+ /** Drop one managed source by token. @experimental */
445
+ releaseManagedSource(token: string): string
446
+ /**
447
+ * Remove every managed source not named in `keepTokensJson`, and every
448
+ * partial a killed process left. The only thing that deletes media.
449
+ * @experimental
450
+ */
451
+ sweepManagedSources(keepTokensJson: string): string
452
+ /**
453
+ * Every filename in the managed directory, including partials and strays.
454
+ *
455
+ * Read-only accounting. `managedSourceLedger` reports only well-formed
456
+ * sources, which cannot show what a sweep left behind. @experimental
457
+ */
458
+ managedSourceList(): string
459
+ /** Gate-only: sweep with an unparsed keep list. @experimental */
460
+ managedSourceGateSweepRaw(rawKeep: string): string
461
+ /** Gate-only: seed a file with an exact name, to exercise the sweep. @experimental */
462
+ managedSourceGateSeed(name: string, bytes: number): string
463
+ /** Tokens and sizes of what is stored, for assertions. @experimental */
464
+ managedSourceLedger(): string
465
+ probeAudioSource(uri: string): string
466
+ /** Confirm the destination is closed and parseable. @experimental */
467
+ exportProbeOutput(uri: string): string
468
+ /** Gate-only: hold inside the media copy, so a kill can land there. @experimental */
469
+ exportGateHoldMediaCopy(enabled: boolean): string
470
+ /** Gate-only: wedge the cadence program after N frames. -1 is off. @experimental */
471
+ exportGateStallAfterFrames(frames: number): string
472
+ /**
473
+ * Arm the gate-only controls. Everything gate-only is inert until this is
474
+ * called, and the production exporter never calls it. @experimental
475
+ */
476
+ exportGateEnableDiagnostics(enabled: boolean): string
477
+ /** Gate-only: fail publication at a chosen point. @experimental */
478
+ exportGatePublishFault(mode: string): string
479
+ /** Gate-only: does this directory's filesystem have an atomic no-replace? @experimental */
480
+ exportGateProbeNoReplace(directory: string): string
481
+ /** Gate-only: size and CRC32, to prove a file did or did not change. @experimental */
482
+ exportGateFileDigest(path: string): string
483
+ /** Gate-only: which temporaries are still registered, by name. @experimental */
484
+ exportGateLiveTemporaries(): string
485
+ /** Gate-only: remove a file, so a case can start from a known state. @experimental */
486
+ exportGateDeleteFile(path: string): string
487
+ /** Gate-only: a writable directory that is not the gallery. @experimental */
488
+ exportGateExternalFilesDir(): string
489
+ /** Gate-only: the current heartbeat epoch. @experimental */
490
+ exportGateBeatEpoch(): string
491
+ /** Gate-only: offer a beat for `epoch`; reports acceptance. @experimental */
492
+ exportGateTestBeat(source: string, epoch: number): string
493
+ /** Gate-only: enable sample hashing for conformance checks. @experimental */
494
+ exportGateProvenance(enabled: boolean): string
495
+ /** Heartbeat counts and the maximum gap per source. @experimental */
496
+ exportGateHeartbeats(): string
497
+ /** Gate-only: started/completed/failed/released accounting. @experimental */
498
+ exportGateAccounting(): string
499
+ /** Stop `jobId` if it is the one running. Idempotent. @experimental */
500
+ exportCancelJob(jobId: string): string
501
+ exportGateRunProject(
502
+ shardsJson: string,
503
+ programJson: string,
504
+ overlaysJson: string,
505
+ mixProgramJson: string,
506
+ mixUrisJson: string,
507
+ timeoutSeconds: number,
508
+ destinationJson: string,
509
+ ): string
510
+ /**
511
+ * **V3.5 gate only.** Mix a compiled `AudioMixProgram` and encode it once.
512
+ *
513
+ * `urisJson` maps span id to a host-resolved URI. Native reads only the plan:
514
+ * no audio kinds, no duration policy, no project.
515
+ *
516
+ * @experimental
517
+ */
518
+ exportGateMixAudio(programJson: string, urisJson: string): string
519
+ /**
520
+ * **V3.5 gate only.** Decode one window and report every stage.
521
+ *
522
+ * Running the identical window at 1x and another rate settles which stage is
523
+ * responsible: matching cropped counts with differing stretched counts point
524
+ * at the stretch, differing cropped counts at the decode.
525
+ *
526
+ * @experimental
527
+ */
528
+ exportGateDecodeProbe(uri: string, fromUs: number, toUs: number, speed: number): string
529
+ /** Per-window dominant-frequency trajectory and harmonic band energies. @experimental */
530
+ exportGateTrajectory(uri: string, spec: string): string
531
+ /** Copy a gate output to external files, for the human gates. @experimental */
532
+ exportGateStage(uri: string, name: string): string
533
+ /** Measure this encoder configuration's priming from a burst at `at`. @experimental */
534
+ exportGatePrimingProbe(rate: number, channels: number, bitrate: number, at: number): string
535
+ /** Decode a window under a simulated decoder warm-up requirement. @experimental */
536
+ exportGatePrerollProbe(uri: string, fromUs: number, toUs: number, warmupUs: number): string
537
+ /**
538
+ * **V3.5 gate only.** Run the time-stretch alone on synthetic PCM.
539
+ *
540
+ * @experimental
541
+ */
542
+ exportGateStretchProbe(rate: number, channels: number, frames: number, speed: number): string
543
+ exportGateStageResource(base64: string, width: number, height: number): string
544
+ media3Play(): void
545
+ media3Pause(): void
546
+ /** Microseconds, like every other time in Pictor. */
547
+ media3Seek(positionUs: number): void
548
+ media3SetRate(rate: number): void
549
+ /** Media3's own recovery after a source failure: prepare again. */
550
+ media3Retry(): void
551
+ media3Release(): void
552
+ /** A throttled snapshot: position, size, counters and any source error. */
553
+ media3State(): Object
554
+
555
+ /**
556
+ * Start decoding one still frame. Returns a token to poll or cancel with.
557
+ *
558
+ * Not a Promise: a strip issues and abandons requests in bursts, and a
559
+ * rejected Promise per abandoned request is noisy and hard to cancel. A
560
+ * token the caller owns makes "stop this one" expressible.
561
+ */
562
+ thumbnailRequest(uri: string, sourceTimeUs: number, widthPx: number): number
563
+ thumbnailCancel(token: number): void
564
+ /** Base64 JPEG, or null while it is still decoding. Consumes the result. */
565
+ thumbnailTake(token: number): string | null
566
+ thumbnailError(token: number): string | null
567
+ thumbnailRunning(token: number): boolean
568
+ thumbnailReleaseSource(uri: string): void
569
+ thumbnailReleaseAll(): void
570
+ /** `{ decoded, cancelled, failed, running, retrievers }`. */
571
+ thumbnailCounters(): Object
572
+ }
573
+
574
+ export default TurboModuleRegistry.get<Spec>('PictorVideo')
@@ -0,0 +1,72 @@
1
+ import type { AudioProbe } from '@pictor-sdk/video-core'
2
+ import { requireNative } from './player'
3
+
4
+ /**
5
+ * Reading what a picked audio file actually contains.
6
+ *
7
+ * The structural half of {@link AudioProbe} lives in `@pictor-sdk/video-core`,
8
+ * which has no decoder and never will; this is the adapter that can answer it.
9
+ * A host calls this once, before adding, and passes the result to
10
+ * `acceptAudioSource`.
11
+ *
12
+ * Nothing here identifies the file. The display name is the host's to carry —
13
+ * a picker URI embeds an id and sometimes a user's own folder names.
14
+ *
15
+ * @experimental
16
+ */
17
+
18
+ /** A probe that failed, with a reason fit for a log rather than a person. @experimental */
19
+ export interface AudioProbeFailure {
20
+ ok: false
21
+ /** Why the file could not be read. Diagnostic, not a user-facing sentence. */
22
+ reason: string
23
+ }
24
+
25
+ /** What native learned, minus the display name only the host knows. @experimental */
26
+ export type AudioProbeResult = ({ ok: true } & Omit<AudioProbe, 'displayName'>) | AudioProbeFailure
27
+
28
+ /**
29
+ * Open `uri` and report its audio.
30
+ *
31
+ * Never throws for an unreadable file: a pick that cannot be read is an answer,
32
+ * and a host that had to wrap this in a `try` would be handling a normal
33
+ * outcome as an exception. It throws only if the adapter is not installed,
34
+ * which is a wiring mistake rather than a file.
35
+ *
36
+ * @experimental
37
+ */
38
+ export function probeAudioSource(uri: string): AudioProbeResult {
39
+ let raw: string
40
+ try {
41
+ raw = requireNative().probeAudioSource(uri)
42
+ } catch (error) {
43
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) }
44
+ }
45
+
46
+ let parsed: unknown
47
+ try {
48
+ parsed = JSON.parse(raw)
49
+ } catch {
50
+ return { ok: false, reason: 'probe returned unparseable JSON' }
51
+ }
52
+
53
+ const value = parsed as Partial<AudioProbe> & { ok?: boolean; reason?: string }
54
+ if (value.ok !== true) {
55
+ return { ok: false, reason: typeof value.reason === 'string' ? value.reason : 'probe failed' }
56
+ }
57
+
58
+ /*
59
+ Normalised here, not trusted as-is.
60
+
61
+ Everything downstream treats these as decided facts — `hasAudioTrack` ends
62
+ up in `metadata.hasAudio`, which is what the encoder is told. A missing or
63
+ malformed field must not arrive as `undefined` and read as permission.
64
+ */
65
+ return {
66
+ ok: true,
67
+ durationUs: Number.isFinite(value.durationUs) ? Number(value.durationUs) : 0,
68
+ hasAudioTrack: value.hasAudioTrack === true,
69
+ seekable: value.seekable === true,
70
+ mimeType: typeof value.mimeType === 'string' ? value.mimeType : '',
71
+ }
72
+ }