@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,374 @@
|
|
|
1
|
+
package dev.pictor.video
|
|
2
|
+
|
|
3
|
+
import org.json.JSONObject
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Keeping the picture and the mix together through a stall.
|
|
7
|
+
*
|
|
8
|
+
* ## What goes wrong without it
|
|
9
|
+
*
|
|
10
|
+
* A clip seam makes the video player buffer while it switches decoder. The mix
|
|
11
|
+
* is one continuous file and has no reason to stop, so it runs ahead by exactly
|
|
12
|
+
* the length of the stall and keeps the lead — measured at +256 µs·10³ from the
|
|
13
|
+
* first seam onward, and it never came back.
|
|
14
|
+
*
|
|
15
|
+
* Correcting that afterwards does not work. A seek into a compressed mix costs
|
|
16
|
+
* 200–340 ms on a Pixel 7, which is the same size as the error, so a monitor
|
|
17
|
+
* aiming at where the picture *was* lands behind it and then chases the other
|
|
18
|
+
* way. Measured, it oscillated +264, −194, −207, −178 for as long as playback
|
|
19
|
+
* continued.
|
|
20
|
+
*
|
|
21
|
+
* ## What this does instead
|
|
22
|
+
*
|
|
23
|
+
* Both transports are held for the stall and released together, aligned.
|
|
24
|
+
*
|
|
25
|
+
* - **A stall holds the mix and remembers the intent.** The picture is already
|
|
26
|
+
* stopped — that is what a stall is — so only the mix needs holding. The
|
|
27
|
+
* user's intent to be playing is kept separately, because a hold is not a
|
|
28
|
+
* pause.
|
|
29
|
+
*
|
|
30
|
+
* Holding the picture's own transport was tried first and deadlocked: taking
|
|
31
|
+
* `playWhenReady` away stops its position advancing, and an advancing
|
|
32
|
+
* position is the only evidence that the stall has ended. One hold, zero
|
|
33
|
+
* releases, and playback never resumed.
|
|
34
|
+
* - **Resume waits for alignment.** When the picture is ready again, the mix's
|
|
35
|
+
* position is compared against it. If they already agree, both start. If they
|
|
36
|
+
* do not, the mix seeks *while everything is held* — nothing is advancing, so
|
|
37
|
+
* the seek has nothing to chase — and the release waits for it to land.
|
|
38
|
+
* - **Epochs guard it.** A hold belongs to a generation; a ready callback from
|
|
39
|
+
* a mix that has since been replaced cannot release a hold that is no longer
|
|
40
|
+
* its own.
|
|
41
|
+
* - **The user always wins.** An explicit pause, a background or an unmount
|
|
42
|
+
* clears the intent, and a pending automatic resume is then dropped rather
|
|
43
|
+
* than fired later at a surprising moment.
|
|
44
|
+
*/
|
|
45
|
+
internal object PreviewSync {
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* How close counts as aligned when releasing a hold.
|
|
49
|
+
*
|
|
50
|
+
* Tighter than the drift backstop, because nothing is moving: the mix has
|
|
51
|
+
* been seeked while held and the only question is whether it landed. A
|
|
52
|
+
* value near the mix's own access-unit granularity is as good as this can
|
|
53
|
+
* be without decoding.
|
|
54
|
+
*/
|
|
55
|
+
private const val ALIGN_US = 30_000L
|
|
56
|
+
|
|
57
|
+
/** How long to wait for a seek to land before giving up and releasing. */
|
|
58
|
+
private const val CONVERGE_TIMEOUT_NS = 1_500_000_000L
|
|
59
|
+
|
|
60
|
+
private enum class State { IDLE, HELD, AWAITING_SEEK }
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How long the picture may stop advancing before it counts as stalled.
|
|
64
|
+
*
|
|
65
|
+
* Detected by measurement, not by a callback. `onPlaybackStateChanged`
|
|
66
|
+
* never reported `STATE_BUFFERING` at a clip seam on a Pixel 7 — the
|
|
67
|
+
* transition is seamless as far as the state machine is concerned — while
|
|
68
|
+
* the position stopped advancing for about 230 ms and the mix ran on. A
|
|
69
|
+
* hold keyed on that callback engaged zero times in a 22 s run.
|
|
70
|
+
*
|
|
71
|
+
* Two anchor publications is the floor: the pump runs at 50 ms, so one
|
|
72
|
+
* unchanged reading proves nothing.
|
|
73
|
+
*/
|
|
74
|
+
private const val STALL_NS = 110_000_000L
|
|
75
|
+
|
|
76
|
+
@Volatile private var state = State.IDLE
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether the person wants playback to be running.
|
|
80
|
+
*
|
|
81
|
+
* Separate from whether it *is* running. A hold stops both transports
|
|
82
|
+
* without changing this, which is what lets the release know it should
|
|
83
|
+
* start them again rather than leave them stopped.
|
|
84
|
+
*/
|
|
85
|
+
@Volatile var intentPlaying = false
|
|
86
|
+
private set
|
|
87
|
+
|
|
88
|
+
/** The mix generation a hold belongs to. */
|
|
89
|
+
@Volatile private var holdEpoch = 0L
|
|
90
|
+
|
|
91
|
+
/** When the realigning seek was issued, for the convergence timeout. */
|
|
92
|
+
@Volatile private var seekAtNs = 0L
|
|
93
|
+
|
|
94
|
+
/** Where the seek aimed, so convergence can be judged against it. */
|
|
95
|
+
@Volatile private var seekTargetUs = 0L
|
|
96
|
+
|
|
97
|
+
/** The last *raw* anchor position, and when it last changed. */
|
|
98
|
+
@Volatile private var lastRawUs = Long.MIN_VALUE
|
|
99
|
+
@Volatile private var lastRawChangeNs = 0L
|
|
100
|
+
|
|
101
|
+
/** True while the picture is measurably not advancing. */
|
|
102
|
+
@Volatile var stalled = false
|
|
103
|
+
private set
|
|
104
|
+
|
|
105
|
+
@Volatile var stalls = 0L
|
|
106
|
+
private set
|
|
107
|
+
|
|
108
|
+
/** Counters, so a gate can assert that holds happened and resolved. */
|
|
109
|
+
@Volatile var holds = 0L
|
|
110
|
+
private set
|
|
111
|
+
|
|
112
|
+
@Volatile var releases = 0L
|
|
113
|
+
private set
|
|
114
|
+
|
|
115
|
+
@Volatile var realigningSeeks = 0L
|
|
116
|
+
private set
|
|
117
|
+
|
|
118
|
+
/** The picture's position, as *presented* rather than as decoded. */
|
|
119
|
+
fun presentedUs(): Long? {
|
|
120
|
+
val sample = FrameClock.latestSample() ?: return null
|
|
121
|
+
val ptsUs = sample[FrameClock.Field.PTS_US]
|
|
122
|
+
val window = sample[FrameClock.Field.WINDOW_INDEX].toInt()
|
|
123
|
+
return Media3Player.globalPositionUs(window, ptsUs)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The presented frame, with everything needed to say whether it is fresh.
|
|
128
|
+
*
|
|
129
|
+
* A presented time that stops changing has three possible causes and they
|
|
130
|
+
* need different fixes: the renderer drew the same frame again, the record
|
|
131
|
+
* is stale because nothing new was consumed, or the source timestamp is
|
|
132
|
+
* being mapped to the wrong global time. Reading the position alone cannot
|
|
133
|
+
* tell them apart — the frame's own identity can.
|
|
134
|
+
*
|
|
135
|
+
* `id` is the canonical sample's sequence, `generation` changes with the
|
|
136
|
+
* media item, `ptsUs` is the source timestamp and `consumedNs` is when the
|
|
137
|
+
* renderer latched it, on the same clock as everything else here.
|
|
138
|
+
*/
|
|
139
|
+
fun presentedRecord(): LongArray? {
|
|
140
|
+
val sample = FrameClock.latestSample() ?: return null
|
|
141
|
+
val window = sample[FrameClock.Field.WINDOW_INDEX]
|
|
142
|
+
return longArrayOf(
|
|
143
|
+
sample[FrameClock.Field.ID],
|
|
144
|
+
sample[FrameClock.Field.GENERATION],
|
|
145
|
+
window,
|
|
146
|
+
sample[FrameClock.Field.PTS_US],
|
|
147
|
+
Media3Player.globalPositionUs(window.toInt(), sample[FrameClock.Field.PTS_US]),
|
|
148
|
+
sample[FrameClock.Field.CONSUMED_NS],
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The picture's position as the decoder reports it, extrapolated. */
|
|
153
|
+
fun decodedUs(atNs: Long): Long {
|
|
154
|
+
val anchor = PlaybackAnchor.latest()
|
|
155
|
+
return Media3Player.globalPositionUs(anchor.windowIndex, PlaybackAnchor.positionAt(atNs))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private fun audioUs(atNs: Long): Long? {
|
|
159
|
+
val anchor = PreviewAudioPlayer.current()?.anchor ?: return null
|
|
160
|
+
return if (anchor.playing) anchor.positionUs + (atNs - anchor.monotonicNs) / 1000
|
|
161
|
+
else anchor.positionUs
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private fun trace(type: String, extra: JSONObject.() -> Unit = {}) {
|
|
165
|
+
PreviewTrace.record(type) {
|
|
166
|
+
val atNs = System.nanoTime()
|
|
167
|
+
JSONObject()
|
|
168
|
+
.put("state", state.name)
|
|
169
|
+
.put("intent", intentPlaying)
|
|
170
|
+
.put("presentedUs", presentedUs() ?: -1)
|
|
171
|
+
.put("decodedUs", decodedUs(atNs))
|
|
172
|
+
.put("audioUs", audioUs(atNs) ?: -1)
|
|
173
|
+
.put("audioPlaying", PreviewAudioPlayer.current()?.anchor?.playing ?: false)
|
|
174
|
+
.also(extra)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The host is starting playback. */
|
|
179
|
+
fun onUserPlay() {
|
|
180
|
+
intentPlaying = true
|
|
181
|
+
state = State.IDLE
|
|
182
|
+
trace("sync.userPlay")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The host is stopping playback, or the screen is going away.
|
|
187
|
+
*
|
|
188
|
+
* Clears the intent, so a hold in flight releases into a stopped state
|
|
189
|
+
* rather than starting playback the person has already asked to stop.
|
|
190
|
+
*/
|
|
191
|
+
fun onUserStop(reason: String) {
|
|
192
|
+
intentPlaying = false
|
|
193
|
+
state = State.IDLE
|
|
194
|
+
trace("sync.userStop") { put("reason", reason) }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The picture's buffering state changed.
|
|
199
|
+
*
|
|
200
|
+
* Kept because it is the earliest signal when it does fire, and it costs
|
|
201
|
+
* nothing when it does not. It is no longer the only one: see [STALL_NS].
|
|
202
|
+
*/
|
|
203
|
+
fun onVideoBuffering(buffering: Boolean, holdVideo: (Boolean) -> Unit) {
|
|
204
|
+
if (!PreviewConfig.coordinator) return
|
|
205
|
+
if (buffering && intentPlaying) beginHold(holdVideo, "buffering")
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private fun beginHold(holdVideo: (Boolean) -> Unit, why: String) {
|
|
209
|
+
if (state != State.IDLE) return
|
|
210
|
+
val mix = PreviewAudioPlayer.current() ?: return
|
|
211
|
+
state = State.HELD
|
|
212
|
+
holdEpoch = mix.epoch
|
|
213
|
+
holds += 1
|
|
214
|
+
attempts = 0
|
|
215
|
+
trace("sync.hold") { put("why", why) }
|
|
216
|
+
/*
|
|
217
|
+
The mix only.
|
|
218
|
+
|
|
219
|
+
The picture is already stopped — that is what a stall is — and taking
|
|
220
|
+
its transport away as well removed the very signal the release waits
|
|
221
|
+
for. `holdVideo` stays in the signature because an explicit stop still
|
|
222
|
+
needs to reach it.
|
|
223
|
+
*/
|
|
224
|
+
mix.pause()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Realignment attempts within one hold, so a chase cannot run forever. */
|
|
228
|
+
@Volatile private var attempts = 0
|
|
229
|
+
|
|
230
|
+
/** At most this many seeks per hold. */
|
|
231
|
+
private const val MAX_ATTEMPTS = 2
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Called from the player's own anchor pump, so it exists in production
|
|
235
|
+
* rather than only while a trace is armed.
|
|
236
|
+
*/
|
|
237
|
+
fun tick(holdVideo: (Boolean) -> Unit) {
|
|
238
|
+
if (!PreviewConfig.coordinator) return
|
|
239
|
+
val nowNs = System.nanoTime()
|
|
240
|
+
val anchor = PlaybackAnchor.latest()
|
|
241
|
+
|
|
242
|
+
/*
|
|
243
|
+
Stall detection, from the raw position.
|
|
244
|
+
|
|
245
|
+
`positionAt` extrapolates, so it keeps advancing through a stall and
|
|
246
|
+
cannot see one. The published value is what actually stops moving.
|
|
247
|
+
*/
|
|
248
|
+
if (anchor.positionUs != lastRawUs) {
|
|
249
|
+
lastRawUs = anchor.positionUs
|
|
250
|
+
lastRawChangeNs = nowNs
|
|
251
|
+
if (stalled) {
|
|
252
|
+
stalled = false
|
|
253
|
+
PreviewTrace.record("sync.stallEnd") {
|
|
254
|
+
JSONObject().put("positionUs", anchor.positionUs)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} else if (
|
|
258
|
+
intentPlaying && anchor.playing && !stalled &&
|
|
259
|
+
lastRawChangeNs != 0L && nowNs - lastRawChangeNs > STALL_NS
|
|
260
|
+
) {
|
|
261
|
+
stalled = true
|
|
262
|
+
stalls += 1
|
|
263
|
+
PreviewTrace.record("sync.stallStart") {
|
|
264
|
+
JSONObject()
|
|
265
|
+
.put("positionUs", anchor.positionUs)
|
|
266
|
+
.put("stalledForMs", (nowNs - lastRawChangeNs) / 1_000_000)
|
|
267
|
+
}
|
|
268
|
+
beginHold(holdVideo, "stall")
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (state == State.IDLE) return
|
|
272
|
+
val mix = PreviewAudioPlayer.current() ?: run { state = State.IDLE; return }
|
|
273
|
+
|
|
274
|
+
if (!intentPlaying) {
|
|
275
|
+
// The person stopped while we were holding. Release the picture's
|
|
276
|
+
// transport but start nothing.
|
|
277
|
+
state = State.IDLE
|
|
278
|
+
holdVideo(false)
|
|
279
|
+
trace("sync.abandon")
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
if (mix.epoch != holdEpoch) {
|
|
283
|
+
// A new mix arrived mid-hold; the hold was about the old one.
|
|
284
|
+
state = State.IDLE
|
|
285
|
+
holdVideo(false)
|
|
286
|
+
trace("sync.staleHold")
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Still stalled, or the decoder says it is refilling: nothing to align
|
|
291
|
+
// against yet.
|
|
292
|
+
if (stalled || anchor.buffering) return
|
|
293
|
+
|
|
294
|
+
val atNs = nowNs
|
|
295
|
+
val videoUs = decodedUs(atNs)
|
|
296
|
+
val audio = audioUs(atNs) ?: return
|
|
297
|
+
|
|
298
|
+
when (state) {
|
|
299
|
+
State.HELD -> {
|
|
300
|
+
val gap = audio - videoUs
|
|
301
|
+
if (Math.abs(gap) <= ALIGN_US || attempts >= MAX_ATTEMPTS) {
|
|
302
|
+
release(mix, holdVideo, gap, if (attempts >= MAX_ATTEMPTS) "attemptsSpent" else "aligned")
|
|
303
|
+
} else {
|
|
304
|
+
/*
|
|
305
|
+
Seek while the mix is held.
|
|
306
|
+
|
|
307
|
+
It is paused, so the seek has nothing to chase on its own
|
|
308
|
+
side — the only thing still moving is the picture, and the
|
|
309
|
+
convergence check below re-reads it rather than assuming
|
|
310
|
+
the target is still current.
|
|
311
|
+
*/
|
|
312
|
+
state = State.AWAITING_SEEK
|
|
313
|
+
seekAtNs = atNs
|
|
314
|
+
seekTargetUs = videoUs
|
|
315
|
+
attempts += 1
|
|
316
|
+
realigningSeeks += 1
|
|
317
|
+
trace("sync.realign") { put("gapUs", gap).put("toUs", videoUs).put("attempt", attempts) }
|
|
318
|
+
mix.seekTo(videoUs, mix.epoch)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
State.AWAITING_SEEK -> {
|
|
322
|
+
val landed = Math.abs(audio - seekTargetUs) <= ALIGN_US
|
|
323
|
+
val timedOut = atNs - seekAtNs > CONVERGE_TIMEOUT_NS
|
|
324
|
+
if (timedOut) {
|
|
325
|
+
release(mix, holdVideo, audio - videoUs, "timeout")
|
|
326
|
+
} else if (landed) {
|
|
327
|
+
// Back to HELD rather than straight to release: the picture
|
|
328
|
+
// moved while the seek ran, so the gap is re-measured and
|
|
329
|
+
// may warrant one more attempt — bounded by MAX_ATTEMPTS.
|
|
330
|
+
state = State.HELD
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
State.IDLE -> Unit
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private fun release(
|
|
338
|
+
mix: PreviewAudioPlayer,
|
|
339
|
+
holdVideo: (Boolean) -> Unit,
|
|
340
|
+
gapUs: Long,
|
|
341
|
+
why: String,
|
|
342
|
+
) {
|
|
343
|
+
state = State.IDLE
|
|
344
|
+
releases += 1
|
|
345
|
+
attempts = 0
|
|
346
|
+
trace("sync.release") { put("gapUs", gapUs).put("why", why) }
|
|
347
|
+
// The picture was never held, so there is nothing to give back to it;
|
|
348
|
+
// the call is kept so an explicit stop can still reach the transport.
|
|
349
|
+
holdVideo(false)
|
|
350
|
+
mix.play(mix.epoch)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
fun counters(): JSONObject =
|
|
354
|
+
JSONObject()
|
|
355
|
+
.put("holds", holds)
|
|
356
|
+
.put("releases", releases)
|
|
357
|
+
.put("realigningSeeks", realigningSeeks)
|
|
358
|
+
.put("stalls", stalls)
|
|
359
|
+
.put("state", state.name)
|
|
360
|
+
.put("intentPlaying", intentPlaying)
|
|
361
|
+
|
|
362
|
+
fun reset() {
|
|
363
|
+
state = State.IDLE
|
|
364
|
+
intentPlaying = false
|
|
365
|
+
holds = 0
|
|
366
|
+
releases = 0
|
|
367
|
+
realigningSeeks = 0
|
|
368
|
+
attempts = 0
|
|
369
|
+
stalls = 0
|
|
370
|
+
stalled = false
|
|
371
|
+
lastRawUs = Long.MIN_VALUE
|
|
372
|
+
lastRawChangeNs = 0
|
|
373
|
+
}
|
|
374
|
+
}
|