@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,212 @@
1
+ import { requireNative } from './player'
2
+
3
+ /**
4
+ * The audio a preview plays.
5
+ *
6
+ * ## Preparation, not playback
7
+ *
8
+ * A preview mix is produced by the same code an export runs: every contributing
9
+ * source is decoded, re-timed by its clip's speed, ramped, summed and saturated
10
+ * once, then encoded. That takes real time proportional to the project, and no
11
+ * amount of naming it "preview" makes it realtime. A caller has to show that it
12
+ * is preparing and must not pretend otherwise.
13
+ *
14
+ * ## Why not mix in the player
15
+ *
16
+ * Because then there would be two definitions of what a project sounds like,
17
+ * and the one a person hears while editing would be the one that was never
18
+ * tested against an export. Gain, fades, trims, speed and overlap are decided
19
+ * by `compileAudioMixProgram` and executed by `AudioMixer` — here and in the
20
+ * exporter, from the same plan.
21
+ *
22
+ * ## Superseding
23
+ *
24
+ * One job at a time. A newer request cancels the one in flight, which unwinds
25
+ * at its next checkpoint, deletes its partial and reports `superseded`. A
26
+ * superseded result is not a failure and must never be published: it is the
27
+ * answer to a question nobody is asking any more.
28
+ *
29
+ * @experimental
30
+ */
31
+
32
+ /** A prepared mix. @experimental */
33
+ export interface PreparedMix {
34
+ ok: true
35
+ /** A `file://` URI in app-private storage. */
36
+ uri: string
37
+ /** The content key: mix program, sources and mixer configuration. */
38
+ key: string
39
+ /** True when nothing had to be encoded — the file was already there. */
40
+ cached: boolean
41
+ byteLength: number
42
+ /**
43
+ * Where the time went, when it was actually prepared.
44
+ *
45
+ * Milliseconds per stage and frames per source. Absent for a cached mix,
46
+ * which did no work to attribute.
47
+ */
48
+ profile?: {
49
+ stageMs: Record<string, number>
50
+ counts: Record<string, number>
51
+ sources: Record<string, Record<string, number>>
52
+ }
53
+ }
54
+
55
+ /** @experimental */
56
+ export type PreviewMixResult =
57
+ | PreparedMix
58
+ /** A newer request replaced this one. Not a failure; never publish it. */
59
+ | { ok: false; superseded: true; key: string }
60
+ /** It ran and failed. The reason is diagnostic and may name a path. */
61
+ | { ok: false; superseded?: false; key?: string; reason: string }
62
+
63
+ /**
64
+ * Prepare the mix for a compiled program, or hand back the one already
65
+ * prepared.
66
+ *
67
+ * `urisJson` maps span id to a resolved, durable source URI — the same
68
+ * resolution an export performs, so the two agree about which file a span
69
+ * means.
70
+ *
71
+ * Never rejects: a mix that cannot be produced is an outcome.
72
+ *
73
+ * @experimental
74
+ */
75
+ export async function preparePreviewMix(
76
+ jobId: string,
77
+ programJson: string,
78
+ urisJson: string,
79
+ ): Promise<PreviewMixResult> {
80
+ let raw: string
81
+ try {
82
+ raw = await requireNative().previewMixPrepare(jobId, programJson, urisJson)
83
+ } catch (error) {
84
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) }
85
+ }
86
+
87
+ let value: Record<string, unknown>
88
+ try {
89
+ value = JSON.parse(raw) as Record<string, unknown>
90
+ } catch {
91
+ return { ok: false, reason: 'native returned unparseable JSON' }
92
+ }
93
+
94
+ if (value.ok === true) {
95
+ return {
96
+ ok: true,
97
+ uri: String(value.uri ?? ''),
98
+ key: String(value.key ?? ''),
99
+ cached: value.cached === true,
100
+ byteLength: Number(value.byteLength ?? 0),
101
+ ...(value.profile ? { profile: value.profile as PreparedMix['profile'] } : {}),
102
+ }
103
+ }
104
+ if (value.superseded === true) {
105
+ return { ok: false, superseded: true, key: String(value.key ?? '') }
106
+ }
107
+ return {
108
+ ok: false,
109
+ ...(typeof value.key === 'string' ? { key: value.key } : {}),
110
+ reason: typeof value.error === 'string' ? value.error : 'the mix could not be prepared',
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Stop whatever is preparing.
116
+ *
117
+ * Safe to call when nothing is: cancelling twice, or cancelling after a job
118
+ * already finished, is how a caller's teardown paths overlap on purpose.
119
+ *
120
+ * @experimental
121
+ */
122
+ export function cancelPreviewMix(): void {
123
+ try {
124
+ requireNative().previewMixCancel()
125
+ } catch {
126
+ // Nothing to stop, or no adapter. Neither is worth raising during teardown.
127
+ }
128
+ }
129
+
130
+ /**
131
+ * The key a program would produce, without preparing it.
132
+ *
133
+ * Lets a caller ask "is this the mix I am already playing?" before starting a
134
+ * job — which is what stops a selection change, or any edit that does not
135
+ * change the sound, from rebuilding anything.
136
+ *
137
+ * @experimental
138
+ */
139
+ export function previewMixKey(programJson: string, urisJson: string): string {
140
+ try {
141
+ return requireNative().previewMixKey(programJson, urisJson)
142
+ } catch {
143
+ return ''
144
+ }
145
+ }
146
+
147
+ /** Drop one prepared mix. @experimental */
148
+ export function releasePreviewMix(key: string): boolean {
149
+ try {
150
+ const value = JSON.parse(requireNative().previewMixRelease(key)) as { removed?: boolean }
151
+ return value.removed === true
152
+ } catch {
153
+ return false
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Keep only these mixes; remove the rest and every partial.
159
+ *
160
+ * The bound on the cache. A sweep that cannot read its own result removes
161
+ * nothing, rather than assuming the directory should be empty.
162
+ *
163
+ * @experimental
164
+ */
165
+ export function sweepPreviewMixes(keepKeys: readonly string[]): {
166
+ removed: number
167
+ kept: number
168
+ partials: number
169
+ skipped: number
170
+ } {
171
+ const none = { removed: 0, kept: 0, partials: 0, skipped: 0 }
172
+ try {
173
+ const value = JSON.parse(requireNative().previewMixSweep(JSON.stringify([...keepKeys]))) as {
174
+ ok?: boolean
175
+ removed?: number
176
+ kept?: number
177
+ partials?: number
178
+ skipped?: number
179
+ }
180
+ if (value.ok !== true) return none
181
+ return {
182
+ removed: Number(value.removed ?? 0),
183
+ kept: Number(value.kept ?? 0),
184
+ partials: Number(value.partials ?? 0),
185
+ skipped: Number(value.skipped ?? 0),
186
+ }
187
+ } catch {
188
+ return none
189
+ }
190
+ }
191
+
192
+ /** What is prepared, for assertions. @experimental */
193
+ export function previewMixLedger(): {
194
+ count: number
195
+ bytes: number
196
+ items: { name: string; byteLength: number }[]
197
+ } {
198
+ try {
199
+ const value = JSON.parse(requireNative().previewMixLedger()) as {
200
+ count?: number
201
+ bytes?: number
202
+ items?: { name: string; byteLength: number }[]
203
+ }
204
+ return {
205
+ count: Number(value.count ?? 0),
206
+ bytes: Number(value.bytes ?? 0),
207
+ items: value.items ?? [],
208
+ }
209
+ } catch {
210
+ return { count: 0, bytes: 0, items: [] }
211
+ }
212
+ }
@@ -0,0 +1,348 @@
1
+ import { requireNative } from './player'
2
+
3
+ /**
4
+ * The paired player trace.
5
+ *
6
+ * Both players sampled on one monotonic clock, buffered natively and drained
7
+ * afterwards. It exists because the question — do the picture and the sound
8
+ * agree — needs samples closer together than a bridge hop, and far closer than
9
+ * `uiautomator` can time.
10
+ *
11
+ * Diagnostics-only in native: a build without them refuses, and even with them
12
+ * it does nothing until armed.
13
+ *
14
+ * @experimental
15
+ */
16
+
17
+ /** One sample or event. @experimental */
18
+ export interface TraceEvent {
19
+ seq: number
20
+ /** `System.nanoTime()`, the clock both players publish on. */
21
+ atNs: number
22
+ type: string
23
+ v: Record<string, unknown>
24
+ }
25
+
26
+ /** @experimental */
27
+ export interface TracePage {
28
+ ok: boolean
29
+ /**
30
+ * Whether the *run* is trustworthy.
31
+ *
32
+ * False when the sampler threw. The events before the fault are real and the
33
+ * silence after it is not evidence of anything — a summary computed over a
34
+ * truncated run reads exactly like one computed over a complete one.
35
+ */
36
+ valid: boolean
37
+ /** Why it stopped being trustworthy, or null. */
38
+ fault: string | null
39
+ events: TraceEvent[]
40
+ nextSeq: number
41
+ total: number
42
+ /** Events overwritten before they could be read. Non-zero means a hole. */
43
+ dropped: number
44
+ }
45
+
46
+ const EMPTY: TracePage = {
47
+ ok: false,
48
+ valid: false,
49
+ fault: 'trace unavailable',
50
+ events: [],
51
+ nextSeq: 0,
52
+ total: 0,
53
+ dropped: 0,
54
+ }
55
+
56
+ /** Arm the trace and start sampling. @experimental */
57
+ export function armPreviewTrace(on: boolean, intervalMs = 25): boolean {
58
+ try {
59
+ const value = JSON.parse(requireNative().previewTraceArm(on, intervalMs)) as { ok?: boolean }
60
+ return value.ok === true
61
+ } catch {
62
+ return false
63
+ }
64
+ }
65
+
66
+ /** Label the trace, so a harness can find its own cases. @experimental */
67
+ export function markPreviewTrace(label: string): void {
68
+ try {
69
+ requireNative().previewTraceMark(label)
70
+ } catch {
71
+ // Unarmed or unavailable; a mark that lands nowhere is not worth raising.
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Drain a page of events.
77
+ *
78
+ * Paged rather than whole: a full trace is far past what a single log line
79
+ * survives, and truncation in the middle is the failure this is meant to
80
+ * detect rather than cause.
81
+ *
82
+ * @experimental
83
+ */
84
+ export function drainPreviewTrace(fromSeq: number, limit = 200): TracePage {
85
+ try {
86
+ const value = JSON.parse(requireNative().previewTraceDrain(fromSeq, limit)) as TracePage
87
+ return value.ok ? value : EMPTY
88
+ } catch {
89
+ return EMPTY
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Every event, drained in pages until the buffer is exhausted.
95
+ *
96
+ * @experimental
97
+ */
98
+ export function drainAllPreviewTrace(limit = 200): TracePage {
99
+ const events: TraceEvent[] = []
100
+ let seq = 0
101
+ let dropped = 0
102
+ let total = 0
103
+ let valid = true
104
+ let fault: string | null = null
105
+ // Bounded: the ring is finite, so this cannot page forever even if the
106
+ // sampler is still running while it drains.
107
+ for (let guard = 0; guard < 200; guard += 1) {
108
+ const page = drainPreviewTrace(seq, limit)
109
+ // Carried from every page: a fault recorded mid-drain still invalidates
110
+ // the run, and the last page is not special.
111
+ if (page.fault) {
112
+ valid = false
113
+ fault = page.fault
114
+ }
115
+ if (!page.ok || page.events.length === 0) {
116
+ total = page.total || total
117
+ break
118
+ }
119
+ events.push(...page.events)
120
+ dropped += page.dropped
121
+ total = page.total
122
+ if (page.nextSeq === seq) break
123
+ seq = page.nextSeq
124
+ }
125
+ return { ok: true, valid, fault, events, nextSeq: seq, total, dropped }
126
+ }
127
+
128
+ /**
129
+ * The offset between the players, summarised over the samples that mattered.
130
+ *
131
+ * Only samples where **both** players were playing are counted: a paused
132
+ * player's position does not advance, so including it would average in a
133
+ * divergence that is not drift.
134
+ *
135
+ * @experimental
136
+ */
137
+ export function summariseOffsets(events: readonly TraceEvent[]): {
138
+ samples: number
139
+ playingSamples: number
140
+ /** Samples whose presented frame was the same one as the tick before. */
141
+ staleFrameSamples: number
142
+ /**
143
+ * Samples with no presented frame at all.
144
+ *
145
+ * `FrameClock.latest` is null until the renderer consumes something, and
146
+ * again after a generation bump. Counting these as "stale" hid them: a run
147
+ * reported 56 stale of 506 while every sampled point in the same run had no
148
+ * record whatsoever, because an absent field is not a false one.
149
+ */
150
+ absentFrameSamples: number
151
+ /** Samples with a frame that had advanced since the previous tick. */
152
+ freshFrameSamples: number
153
+ /** The longest run of consecutive stale samples. */
154
+ longestStaleRun: number
155
+ minUs: number
156
+ maxUs: number
157
+ meanUs: number
158
+ worstAbsUs: number
159
+ } {
160
+ let n = 0
161
+ let stale = 0
162
+ let absent = 0
163
+ let fresh = 0
164
+ let staleRun = 0
165
+ let longestStaleRun = 0
166
+ let min = Number.POSITIVE_INFINITY
167
+ let max = Number.NEGATIVE_INFINITY
168
+ let sum = 0
169
+ let samples = 0
170
+ for (const e of events) {
171
+ if (e.type !== 'sample') continue
172
+ samples += 1
173
+ /*
174
+ Stale frames are counted, never filtered.
175
+
176
+ A presented time that has not moved since the last tick describes a frame
177
+ the renderer already showed. Dropping those samples and reporting the
178
+ percentage that remain would be reporting a pass over the interval where
179
+ the picture was least trustworthy.
180
+ */
181
+ if (e.v.pfFresh === undefined) {
182
+ absent += 1
183
+ staleRun += 1
184
+ if (staleRun > longestStaleRun) longestStaleRun = staleRun
185
+ } else if (e.v.pfFresh === false) {
186
+ stale += 1
187
+ staleRun += 1
188
+ if (staleRun > longestStaleRun) longestStaleRun = staleRun
189
+ } else {
190
+ fresh += 1
191
+ staleRun = 0
192
+ }
193
+ if (e.v.videoPlaying !== true || e.v.audioPlaying !== true) continue
194
+ const offset = Number(e.v.offsetUs)
195
+ if (!Number.isFinite(offset)) continue
196
+ n += 1
197
+ sum += offset
198
+ if (offset < min) min = offset
199
+ if (offset > max) max = offset
200
+ }
201
+ return {
202
+ samples,
203
+ playingSamples: n,
204
+ staleFrameSamples: stale,
205
+ absentFrameSamples: absent,
206
+ freshFrameSamples: fresh,
207
+ longestStaleRun,
208
+ minUs: n > 0 ? min : 0,
209
+ maxUs: n > 0 ? max : 0,
210
+ meanUs: n > 0 ? Math.round(sum / n) : 0,
211
+ worstAbsUs: n > 0 ? Math.max(Math.abs(min), Math.abs(max)) : 0,
212
+ }
213
+ }
214
+
215
+ /**
216
+ * What the stall coordinator has done.
217
+ *
218
+ * Holds, releases and realigning seeks. A gate asserts that seams produced
219
+ * holds and that every hold resolved — a coordinator that never engaged and one
220
+ * that engaged and never released look identical in an offset summary.
221
+ *
222
+ * @experimental
223
+ */
224
+ export function previewSyncCounters(): Record<string, unknown> {
225
+ try {
226
+ return JSON.parse(requireNative().previewSyncCounters()) as Record<string, unknown>
227
+ } catch {
228
+ return { ok: false }
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Stop playback for a host reason — background, picker, unmount.
234
+ *
235
+ * Overrides a resume the coordinator is holding, so playback cannot restart at
236
+ * a moment nobody asked for.
237
+ *
238
+ * @experimental
239
+ */
240
+ export function stopPreviewForHost(reason: string): void {
241
+ try {
242
+ requireNative().previewSyncStop(reason)
243
+ } catch {
244
+ // No adapter, or nothing playing. Neither is worth raising in teardown.
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Frames offered, queued and drawn.
250
+ *
251
+ * The instrument-disruption check reads this with tracing off and again with it
252
+ * on: if the drawn count advances at the same rate either way, the trace is not
253
+ * changing what it measures. Three defects in this area were the measurement
254
+ * rather than the product, so that check comes before the numbers do.
255
+ *
256
+ * @experimental
257
+ */
258
+ export function previewFramePipeline(): Record<string, number> {
259
+ try {
260
+ return JSON.parse(requireNative().previewFramePipeline()) as Record<string, number>
261
+ } catch {
262
+ return {}
263
+ }
264
+ }
265
+
266
+ /** Which parts of the music preview exist. All true in production. @experimental */
267
+ export interface PreviewParts {
268
+ /** Whether the drift check may act. Off measures without correcting. */
269
+ driftCorrection: boolean
270
+ loadPlayer: boolean
271
+ startMix: boolean
272
+ positionPump: boolean
273
+ coordinator: boolean
274
+ }
275
+
276
+ /**
277
+ * Turn parts of the music preview off, to bisect a failure.
278
+ *
279
+ * Gate-only in native. Guessing between a second player, its position pump, the
280
+ * stall coordinator and the mix actually playing is how the previous attempts
281
+ * in this area became tuning; this is how one of them gets ruled out at a time.
282
+ *
283
+ * @experimental
284
+ */
285
+ export function setPreviewParts(parts: Partial<PreviewParts>): PreviewParts | null {
286
+ try {
287
+ const v = JSON.parse(requireNative().previewConfigSet(JSON.stringify(parts))) as
288
+ PreviewParts & { ok?: boolean }
289
+ return v.ok ? v : null
290
+ } catch {
291
+ return null
292
+ }
293
+ }
294
+
295
+ /** @experimental */
296
+ export function previewParts(): PreviewParts | null {
297
+ try {
298
+ return JSON.parse(requireNative().previewConfigGet()) as PreviewParts
299
+ } catch {
300
+ return null
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Silence the video player's own audio.
306
+ *
307
+ * Exposed because a bisect control needs the *same* mute in every
308
+ * configuration: without it, the video-only row would be the only one with
309
+ * audible clip audio, and any difference against the others would be
310
+ * confounded by a whole track the rest do not have.
311
+ *
312
+ * @experimental
313
+ */
314
+ export function muteSourceAudio(muted: boolean): void {
315
+ try {
316
+ requireNative().previewMuteSourceAudio(muted)
317
+ } catch {
318
+ // No adapter. The caller's other work still stands.
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Every codec mix preparation owns, and what became of it.
324
+ *
325
+ * A `release()` that was called is not a release that finished, so both ends
326
+ * are recorded along with the exception path. The preview video decoder is not
327
+ * here: it belongs to the player and outlives any job, and counting it would
328
+ * make "live after publication" always true.
329
+ *
330
+ * @experimental
331
+ */
332
+ export function previewCodecLedger(): {
333
+ live: number
334
+ peakLive: number
335
+ total: number
336
+ entries: Record<string, unknown>[]
337
+ } {
338
+ try {
339
+ return JSON.parse(requireNative().previewCodecLedger()) as {
340
+ live: number
341
+ peakLive: number
342
+ total: number
343
+ entries: Record<string, unknown>[]
344
+ }
345
+ } catch {
346
+ return { live: -1, peakLive: -1, total: 0, entries: [] }
347
+ }
348
+ }