@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.
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/android/CMakeLists.txt +30 -0
- package/android/build.gradle +119 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/pictor_overlaystage.cpp +372 -0
- package/android/src/main/cpp/pictor_publish.cpp +87 -0
- package/android/src/main/cpp/pictor_videosurface.cpp +735 -0
- package/android/src/main/java/dev/pictor/video/AacPriming.kt +261 -0
- package/android/src/main/java/dev/pictor/video/AudioMixer.kt +1112 -0
- package/android/src/main/java/dev/pictor/video/AudioProbe.kt +162 -0
- package/android/src/main/java/dev/pictor/video/CadenceShaderProgram.kt +337 -0
- package/android/src/main/java/dev/pictor/video/CodecLedger.kt +126 -0
- package/android/src/main/java/dev/pictor/video/CompositionSpike.kt +978 -0
- package/android/src/main/java/dev/pictor/video/Diagnostics.kt +54 -0
- package/android/src/main/java/dev/pictor/video/EncoderGate.kt +476 -0
- package/android/src/main/java/dev/pictor/video/ExportGate.kt +1974 -0
- package/android/src/main/java/dev/pictor/video/ExportHeartbeat.kt +72 -0
- package/android/src/main/java/dev/pictor/video/ExportProgress.kt +103 -0
- package/android/src/main/java/dev/pictor/video/FrameClock.kt +591 -0
- package/android/src/main/java/dev/pictor/video/FramePending.kt +181 -0
- package/android/src/main/java/dev/pictor/video/GateAudio.kt +456 -0
- package/android/src/main/java/dev/pictor/video/GateDestination.kt +445 -0
- package/android/src/main/java/dev/pictor/video/GateFrames.kt +662 -0
- package/android/src/main/java/dev/pictor/video/GateMux.kt +185 -0
- package/android/src/main/java/dev/pictor/video/ManagedSources.kt +333 -0
- package/android/src/main/java/dev/pictor/video/Media3Player.kt +934 -0
- package/android/src/main/java/dev/pictor/video/MediaStoreSave.kt +419 -0
- package/android/src/main/java/dev/pictor/video/OverlayResources.kt +169 -0
- package/android/src/main/java/dev/pictor/video/PictorVideoModule.kt +1612 -0
- package/android/src/main/java/dev/pictor/video/PictorVideoPackage.kt +26 -0
- package/android/src/main/java/dev/pictor/video/PlaybackAnchor.kt +161 -0
- package/android/src/main/java/dev/pictor/video/PreviewAudioPlayer.kt +291 -0
- package/android/src/main/java/dev/pictor/video/PreviewConfig.kt +96 -0
- package/android/src/main/java/dev/pictor/video/PreviewMix.kt +396 -0
- package/android/src/main/java/dev/pictor/video/PreviewSync.kt +374 -0
- package/android/src/main/java/dev/pictor/video/PreviewTrace.kt +478 -0
- package/android/src/main/java/dev/pictor/video/PrimingTrim.kt +337 -0
- package/android/src/main/java/dev/pictor/video/RepeatingEncoderFactory.kt +97 -0
- package/android/src/main/java/dev/pictor/video/SceneProgram.kt +169 -0
- package/android/src/main/java/dev/pictor/video/SceneProgramNative.kt +299 -0
- package/android/src/main/java/dev/pictor/video/SkiaPassThroughEffect.kt +695 -0
- package/android/src/main/java/dev/pictor/video/SurfaceAttachment.kt +110 -0
- package/android/src/main/java/dev/pictor/video/ThumbnailDecoder.kt +171 -0
- package/android/src/main/java/dev/pictor/video/VideoSurface.kt +394 -0
- package/app.plugin.js +43 -0
- package/dist/index.d.mts +3622 -0
- package/dist/index.d.ts +3622 -0
- package/dist/index.js +3737 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3625 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +82 -0
- package/plugin/gradleProperties.d.ts +19 -0
- package/plugin/gradleProperties.js +64 -0
- package/react-native.config.js +13 -0
- package/src/NativePictorVideo.ts +574 -0
- package/src/audioProbe.ts +72 -0
- package/src/compositionSpike.ts +305 -0
- package/src/convergence.ts +112 -0
- package/src/encoderGate.ts +66 -0
- package/src/exportDestination.ts +149 -0
- package/src/exportJob.ts +232 -0
- package/src/exportProgress.ts +391 -0
- package/src/exportProgressPump.ts +190 -0
- package/src/exporter.ts +655 -0
- package/src/index.ts +276 -0
- package/src/managedSources.ts +252 -0
- package/src/media3.ts +412 -0
- package/src/mediaStore.ts +177 -0
- package/src/memoryGuard.ts +118 -0
- package/src/mixSources.ts +82 -0
- package/src/overlayStaging.ts +216 -0
- package/src/playbackLifecycle.ts +137 -0
- package/src/player.ts +39 -0
- package/src/playlistSession.ts +125 -0
- package/src/previewAudioDiagnostics.ts +67 -0
- package/src/previewAudioSession.ts +168 -0
- package/src/previewCadence.ts +950 -0
- package/src/previewMix.ts +212 -0
- package/src/previewTrace.ts +348 -0
- package/src/spikeExcursions.ts +405 -0
- package/src/submissionScoring.ts +680 -0
- package/src/surfaceOwner.ts +264 -0
- package/src/surfaceRecovery.ts +121 -0
- package/src/syncTrace.ts +189 -0
- package/src/thumbnailProvider.ts +163 -0
- package/src/tracePhases.ts +217 -0
- package/src/useCompositionPlayback.ts +327 -0
- package/src/useMedia3Playback.ts +492 -0
- package/src/usePreviewAudio.ts +305 -0
- 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
|
+
}
|