@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,591 @@
|
|
|
1
|
+
package dev.pictor.video
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One canonical sample per displayed frame — the only source of timing truth.
|
|
5
|
+
*
|
|
6
|
+
* Everything reported about synchronisation comes from here: the percentiles
|
|
7
|
+
* and the on-screen readout read the *same* sample, matched by [Sample.ID]. An
|
|
8
|
+
* independent readout calculation was how a 22 ms disagreement went unnoticed
|
|
9
|
+
* between two views of one quantity.
|
|
10
|
+
*
|
|
11
|
+
* ## What is paired with what
|
|
12
|
+
*
|
|
13
|
+
* `VideoFrameMetadataListener` fires just before a frame is released to the
|
|
14
|
+
* surface; `onFrameAvailable` follows once it is queued, and **always after**
|
|
15
|
+
* the metadata for that frame. `updateTexImage()` then latches the *newest*
|
|
16
|
+
* buffer, discarding anything behind it.
|
|
17
|
+
*
|
|
18
|
+
* **The pairing happens when availability fires, not when the renderer ticks.**
|
|
19
|
+
* `onFrameAvailable` coalesces: several queued frames can raise one callback.
|
|
20
|
+
* Draining the two queues independently — one metadata per availability —
|
|
21
|
+
* therefore leaves metadata behind whenever that happens, and the surplus never
|
|
22
|
+
* clears. The reported PTS falls further behind with every coalesced callback,
|
|
23
|
+
* which measured as a *growing* offset: 21 ms after twenty-five seconds and
|
|
24
|
+
* 616 ms after five minutes, while the picture stayed correct.
|
|
25
|
+
*
|
|
26
|
+
* So availability claims **all** metadata queued at that moment and keeps the
|
|
27
|
+
* newest as the pending pair. That is sound precisely because metadata always
|
|
28
|
+
* precedes its own availability: everything queued when the callback fires
|
|
29
|
+
* belongs to a frame already handed to the surface. The renderer then consumes
|
|
30
|
+
* one complete pair, and metadata cannot accumulate.
|
|
31
|
+
*
|
|
32
|
+
* ## Time bases
|
|
33
|
+
*
|
|
34
|
+
* Every nanosecond value here is `System.nanoTime`. `releaseTimeNs` from Media3
|
|
35
|
+
* is documented on that clock, `onFrameAvailable` is stamped with it, and the
|
|
36
|
+
* anchor is published with it, so differences between them are meaningful. The
|
|
37
|
+
* only microsecond values are media times. `TIME_UNSET` never reaches this
|
|
38
|
+
* class; the player maps it to zero before publishing.
|
|
39
|
+
*
|
|
40
|
+
* **Every method is `@JvmStatic`.** A Kotlin `object`'s members are instance
|
|
41
|
+
* methods on `INSTANCE`, so `GetStaticMethodID` returns null without it and JNI
|
|
42
|
+
* aborts the process with `JNI DETECTED ERROR IN APPLICATION: mid == null` — no
|
|
43
|
+
* Java stack, and a message naming the JNI call rather than the annotation.
|
|
44
|
+
*/
|
|
45
|
+
internal object FrameClock {
|
|
46
|
+
|
|
47
|
+
/** A few frames of pipeline depth, and no more. */
|
|
48
|
+
const val CAPACITY = 32
|
|
49
|
+
|
|
50
|
+
/** Fields of one canonical sample, in the order [take] returns them. */
|
|
51
|
+
const val FIELDS = 18
|
|
52
|
+
|
|
53
|
+
/** Indices into the returned array. Mirrored in `pictor_videosurface.cpp`. */
|
|
54
|
+
object Field {
|
|
55
|
+
const val ID = 0
|
|
56
|
+
const val GENERATION = 1
|
|
57
|
+
const val PTS_US = 2
|
|
58
|
+
const val METADATA_NS = 3
|
|
59
|
+
const val RELEASE_NS = 4
|
|
60
|
+
const val AVAILABLE_NS = 5
|
|
61
|
+
const val CONSUMED_NS = 6
|
|
62
|
+
const val ANCHOR_POSITION_US = 7
|
|
63
|
+
const val ANCHOR_NS = 8
|
|
64
|
+
const val RATE_MILLI = 9
|
|
65
|
+
const val POSITION_AT_RELEASE_US = 10
|
|
66
|
+
const val POSITION_AT_CONSUMPTION_US = 11
|
|
67
|
+
const val FLAGS = 12
|
|
68
|
+
const val ANCHOR_AGE_NS = 13
|
|
69
|
+
const val COALESCED = 14
|
|
70
|
+
/**
|
|
71
|
+
* Which playlist window the frame came from.
|
|
72
|
+
*
|
|
73
|
+
* Carried per frame rather than read from player state, because state
|
|
74
|
+
* is published on a timer and a seam is exactly where a frame and a
|
|
75
|
+
* timer-sampled window index disagree. A consumer uses this to refuse a
|
|
76
|
+
* frame belonging to a clip the playhead has already left.
|
|
77
|
+
*/
|
|
78
|
+
const val WINDOW_INDEX = 15
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The acquired buffer's own timestamp, after `updateTexImage`.
|
|
82
|
+
*
|
|
83
|
+
* Not media time. Measured, this is a `System.nanoTime`-based value in
|
|
84
|
+
* the same domain as `RELEASE_NS`, so it identifies *which buffer* was
|
|
85
|
+
* acquired rather than which frame it depicts.
|
|
86
|
+
*
|
|
87
|
+
* The metadata deque is coalesced to its newest entry on every
|
|
88
|
+
* availability callback, so the pairing between "the sample we kept"
|
|
89
|
+
* and "the buffer `updateTexImage` took" is an assumption. This is the
|
|
90
|
+
* one fact that can check it: `SurfaceTexture.getTimestamp()` is the
|
|
91
|
+
* acquired buffer's own presentation time. A disagreement means the
|
|
92
|
+
* frame on screen is not the frame the record describes, which is the
|
|
93
|
+
* difference between a late frame and a mislabelled one.
|
|
94
|
+
*/
|
|
95
|
+
const val TEXTURE_PTS_US = 16
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether this sample's pixels and its timestamps are the same frame.
|
|
99
|
+
*
|
|
100
|
+
* 1 when the latched buffer's own timestamp matched a metadata entry
|
|
101
|
+
* inside the pairing window, and matched exactly one. 0 otherwise: no
|
|
102
|
+
* timestamp from the producer, no candidate close enough, or two
|
|
103
|
+
* candidates equally close.
|
|
104
|
+
*
|
|
105
|
+
* A 0 is not a small error. It means the pair is a guess — the claimed
|
|
106
|
+
* metadata with whatever buffer happened to be latched — which is the
|
|
107
|
+
* shape that put frame 7's pixels under frame 2's presentation time.
|
|
108
|
+
* Nothing may gate readiness or judge frame identity on one.
|
|
109
|
+
*/
|
|
110
|
+
const val ASSOCIATION = 17
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private val lock = Any()
|
|
114
|
+
private val metadata = ArrayDeque<LongArray>()
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The newest complete pair, waiting for the renderer.
|
|
118
|
+
*
|
|
119
|
+
* One slot, not a queue: `updateTexImage` shows the newest buffer, so an
|
|
120
|
+
* older pending pair describes a frame nobody will see. A newer pair
|
|
121
|
+
* replaces it and the replaced one counts as [coalesced].
|
|
122
|
+
*/
|
|
123
|
+
private var pending: LongArray? = null
|
|
124
|
+
|
|
125
|
+
@Volatile
|
|
126
|
+
var generation: Int = 0
|
|
127
|
+
private set
|
|
128
|
+
|
|
129
|
+
@Volatile
|
|
130
|
+
private var nextId: Long = 1
|
|
131
|
+
|
|
132
|
+
/** The sample the renderer most recently consumed. Readout reads this. */
|
|
133
|
+
@Volatile
|
|
134
|
+
private var latest: LongArray? = null
|
|
135
|
+
|
|
136
|
+
@Volatile var overflowed: Long = 0; private set
|
|
137
|
+
@Volatile var unpaired: Long = 0; private set
|
|
138
|
+
@Volatile var stale: Long = 0; private set
|
|
139
|
+
@Volatile var coalesced: Long = 0; private set
|
|
140
|
+
@Volatile var highWater: Int = 0; private set
|
|
141
|
+
|
|
142
|
+
/** Everything queued belongs to the clip that has just been replaced. */
|
|
143
|
+
/**
|
|
144
|
+
* Whether the transport is in continuous playback.
|
|
145
|
+
*
|
|
146
|
+
* Set by whoever owns the transport, not inferred. A gap between frame
|
|
147
|
+
* callbacks is not a transition: a clip seam produces one and playback is
|
|
148
|
+
* continuous across it, so pacing must stay engaged. What ends a stream is
|
|
149
|
+
* a pause, a seek or a start — and only the transport knows which of those
|
|
150
|
+
* just happened.
|
|
151
|
+
*/
|
|
152
|
+
@Volatile private var streaming = false
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Whether the latch enforces the intended presentation time.
|
|
156
|
+
*
|
|
157
|
+
* Off, and measured rather than assumed: enforcing it at the latch costs a
|
|
158
|
+
* frame per deferral. `markAvailable` coalesces the metadata deque whenever
|
|
159
|
+
* something is already pending, so a frame held for its due time has its
|
|
160
|
+
* record discarded by the next arrival and `updateTexImage` takes the newer
|
|
161
|
+
* buffer regardless. The displayed cadence in a 1x clip went from 33.3 ms
|
|
162
|
+
* to 66.7 ms — half the frame rate — to remove a 34 ms systematic lead.
|
|
163
|
+
*
|
|
164
|
+
* That is the wrong trade, so the enforcement stays available and stays
|
|
165
|
+
* off. Honouring the schedule without dropping frames needs the previous
|
|
166
|
+
* picture retained, and the external texture cannot do that: one
|
|
167
|
+
* `updateTexImage` overwrites it, and a recorded `SkPicture` samples the
|
|
168
|
+
* texture at draw time rather than holding its pixels. Retaining it means
|
|
169
|
+
* an owned copy — one 1080x1920 RGBA texture, about 8.3 MB, plus a
|
|
170
|
+
* full-frame blit per held frame.
|
|
171
|
+
*/
|
|
172
|
+
@Volatile var enforcePresentationSchedule = false
|
|
173
|
+
|
|
174
|
+
@JvmStatic
|
|
175
|
+
fun setStreaming(on: Boolean) {
|
|
176
|
+
streaming = on
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@JvmStatic
|
|
180
|
+
fun isStreaming(): Boolean = streaming
|
|
181
|
+
|
|
182
|
+
@JvmStatic
|
|
183
|
+
fun bump() {
|
|
184
|
+
// A discontinuity ends the stream: the frames in flight belong to the
|
|
185
|
+
// position being left, and their release times describe it.
|
|
186
|
+
streaming = false
|
|
187
|
+
synchronized(lock) {
|
|
188
|
+
generation += 1
|
|
189
|
+
stale += metadata.size + (if (pending == null) 0 else 1)
|
|
190
|
+
metadata.clear()
|
|
191
|
+
pending = null
|
|
192
|
+
latest = null
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Which playlist window is playing, as the player last reported it.
|
|
198
|
+
*
|
|
199
|
+
* Lives here rather than being passed in, because the only place frames are
|
|
200
|
+
* offered is a factory that builds the player before the instance holding
|
|
201
|
+
* that number exists. Set on the player's own thread at a media-item
|
|
202
|
+
* transition — which is the instant it changes — and read on the playback
|
|
203
|
+
* thread when a frame is offered.
|
|
204
|
+
*/
|
|
205
|
+
@Volatile
|
|
206
|
+
@JvmStatic
|
|
207
|
+
var windowIndex: Int = 0
|
|
208
|
+
|
|
209
|
+
/** Metadata callbacks so far. With [available], separates the two halves. */
|
|
210
|
+
@Volatile var offered: Long = 0; private set
|
|
211
|
+
|
|
212
|
+
/** From `VideoFrameMetadataListener`, before the frame is released. */
|
|
213
|
+
@JvmStatic
|
|
214
|
+
fun offer(ptsUs: Long, scheduledReleaseNs: Long) {
|
|
215
|
+
val now = PlaybackAnchor.nowNs()
|
|
216
|
+
/*
|
|
217
|
+
How far ahead the renderer scheduled this frame.
|
|
218
|
+
|
|
219
|
+
`releaseOutputBuffer(index, releaseTimeNs)` hands the buffer to the
|
|
220
|
+
surface's frame scheduler with a desired present time. A lead of a
|
|
221
|
+
frame or two is normal; a lead measured in seconds means the frame was
|
|
222
|
+
handed over but is not going to be presented for a very long time, and
|
|
223
|
+
from outside that is indistinguishable from a producer that stopped.
|
|
224
|
+
*/
|
|
225
|
+
lastReleaseLeadNs = scheduledReleaseNs - now
|
|
226
|
+
lastOfferedPtsUs = ptsUs
|
|
227
|
+
if (lastReleaseLeadNs > 1_000_000_000L) farFutureOffers += 1
|
|
228
|
+
if (lastReleaseLeadNs < -1_000_000_000L) farPastOffers += 1
|
|
229
|
+
val anchor = PlaybackAnchor.latest()
|
|
230
|
+
offered += 1
|
|
231
|
+
val sample = LongArray(FIELDS)
|
|
232
|
+
sample[Field.GENERATION] = generation.toLong()
|
|
233
|
+
sample[Field.PTS_US] = ptsUs
|
|
234
|
+
sample[Field.METADATA_NS] = now
|
|
235
|
+
sample[Field.RELEASE_NS] = scheduledReleaseNs
|
|
236
|
+
sample[Field.ANCHOR_POSITION_US] = anchor.positionUs
|
|
237
|
+
sample[Field.ANCHOR_NS] = anchor.monotonicNs
|
|
238
|
+
sample[Field.RATE_MILLI] = (anchor.rate * 1000).toLong()
|
|
239
|
+
sample[Field.POSITION_AT_RELEASE_US] = PlaybackAnchor.positionAt(scheduledReleaseNs)
|
|
240
|
+
sample[Field.FLAGS] = stateBits(anchor)
|
|
241
|
+
sample[Field.ANCHOR_AGE_NS] = PlaybackAnchor.ageNs(scheduledReleaseNs)
|
|
242
|
+
sample[Field.WINDOW_INDEX] = windowIndex.toLong()
|
|
243
|
+
synchronized(lock) {
|
|
244
|
+
if (metadata.size >= CAPACITY) {
|
|
245
|
+
metadata.removeFirst()
|
|
246
|
+
overflowed += 1
|
|
247
|
+
}
|
|
248
|
+
metadata.addLast(sample)
|
|
249
|
+
if (metadata.size > highWater) highWater = metadata.size
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* From `SurfaceTexture.OnFrameAvailableListener`, once queued.
|
|
255
|
+
*
|
|
256
|
+
* Claims every metadata event queued at this instant and keeps the newest.
|
|
257
|
+
* Anything older describes a buffer the next `updateTexImage` will skip.
|
|
258
|
+
*/
|
|
259
|
+
/** Every availability callback, paired or not. A liveness counter. */
|
|
260
|
+
@Volatile var available: Long = 0; private set
|
|
261
|
+
|
|
262
|
+
@JvmStatic
|
|
263
|
+
fun markAvailable() {
|
|
264
|
+
val now = PlaybackAnchor.nowNs()
|
|
265
|
+
available += 1
|
|
266
|
+
synchronized(lock) {
|
|
267
|
+
if (metadata.isEmpty()) {
|
|
268
|
+
// Availability with nothing to claim. Not fatal — a repeat
|
|
269
|
+
// callback, or a frame whose metadata was discarded by a
|
|
270
|
+
// generation change — but worth counting.
|
|
271
|
+
orphaned += 1
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
var dropped = 0
|
|
275
|
+
while (metadata.size > 1) {
|
|
276
|
+
metadata.removeFirst()
|
|
277
|
+
dropped += 1
|
|
278
|
+
}
|
|
279
|
+
if (pending != null) dropped += 1
|
|
280
|
+
coalesced += dropped.toLong()
|
|
281
|
+
val sample = metadata.removeFirst()
|
|
282
|
+
sample[Field.AVAILABLE_NS] = now
|
|
283
|
+
sample[Field.COALESCED] = dropped.toLong()
|
|
284
|
+
pending = sample
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Availability callbacks that found no metadata to claim. */
|
|
289
|
+
@Volatile var orphaned: Long = 0; private set
|
|
290
|
+
|
|
291
|
+
@JvmStatic
|
|
292
|
+
fun pendingFrames(): Int = synchronized(lock) { if (pending == null) 0 else 1 }
|
|
293
|
+
|
|
294
|
+
private fun stateBits(anchor: PlaybackAnchor.Sample): Long {
|
|
295
|
+
var bits = 0L
|
|
296
|
+
if (anchor.playing) bits = bits or 1L
|
|
297
|
+
if (anchor.buffering) bits = bits or 2L
|
|
298
|
+
if (anchor.discontinuity) bits = bits or 4L
|
|
299
|
+
return bits
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Complete the sample for the frame the renderer is about to show.
|
|
304
|
+
*
|
|
305
|
+
* Drains to the newest pending frame, because that is the one
|
|
306
|
+
* `updateTexImage` will produce. Returns null when nothing of the current
|
|
307
|
+
* generation is pending — the honest answer after a seek, and better than
|
|
308
|
+
* a stale pairing.
|
|
309
|
+
*/
|
|
310
|
+
/**
|
|
311
|
+
* Every entry to [take], counted before the lock is attempted.
|
|
312
|
+
*
|
|
313
|
+
* The one measurement that separates a consumer which has stopped ticking
|
|
314
|
+
* from one blocked inside the latch. `taken` and `unpaired` are both
|
|
315
|
+
* recorded *after* the lock is held, so a consumer waiting on it looks
|
|
316
|
+
* exactly like a consumer that never arrived — and a frozen picture with
|
|
317
|
+
* `unpaired` at zero could mean either.
|
|
318
|
+
*/
|
|
319
|
+
@Volatile var takeCalls: Long = 0; private set
|
|
320
|
+
|
|
321
|
+
@JvmStatic
|
|
322
|
+
/** Acquisitions whose texture timestamp did not match the paired metadata. */
|
|
323
|
+
@Volatile var mismatched: Long = 0; private set
|
|
324
|
+
|
|
325
|
+
/** Acquisitions where the texture reported no usable timestamp. */
|
|
326
|
+
@Volatile var unverified: Long = 0; private set
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* How far the texture's timestamp may sit from the paired metadata.
|
|
330
|
+
*
|
|
331
|
+
* Half a frame at 30 fps. The two describe the same buffer, so they should
|
|
332
|
+
* agree exactly; the tolerance is for the rounding between microseconds and
|
|
333
|
+
* nanoseconds and for producers that offset their timestamps.
|
|
334
|
+
*/
|
|
335
|
+
private const val IDENTITY_TOLERANCE_US = 16_000L
|
|
336
|
+
|
|
337
|
+
@JvmStatic
|
|
338
|
+
fun take(consumedNs: Long): LongArray? = take(consumedNs, 0L)
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* How far a buffer's timestamp may sit from a metadata entry's release time
|
|
342
|
+
* and still be the same buffer.
|
|
343
|
+
*
|
|
344
|
+
* Half a frame at 30 fps. Both are `System.nanoTime` values describing the
|
|
345
|
+
* same handover, so they agree closely; the tolerance covers the scheduler's
|
|
346
|
+
* own rounding.
|
|
347
|
+
*/
|
|
348
|
+
private const val PAIRING_TOLERANCE_NS = 16_000_000L
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* How much closer the best candidate must be than the next.
|
|
352
|
+
*
|
|
353
|
+
* Below this the two are indistinguishable and neither can be claimed. A
|
|
354
|
+
* quarter of the pairing window: wide enough that ordinary jitter still
|
|
355
|
+
* resolves, narrow enough that two frames a whole interval apart do.
|
|
356
|
+
*/
|
|
357
|
+
private const val AMBIGUITY_MARGIN_NS = 4_000_000L
|
|
358
|
+
|
|
359
|
+
/** Latches whose pixels could not be tied to any metadata entry. */
|
|
360
|
+
@Volatile var unassociated: Long = 0; private set
|
|
361
|
+
|
|
362
|
+
/** Latches where the buffer was not the one the claimed metadata described. */
|
|
363
|
+
@Volatile var repaired: Long = 0; private set
|
|
364
|
+
|
|
365
|
+
@JvmStatic
|
|
366
|
+
fun take(consumedNs: Long, texturePtsNs: Long): LongArray? {
|
|
367
|
+
takeCalls += 1
|
|
368
|
+
synchronized(lock) {
|
|
369
|
+
/*
|
|
370
|
+
Pair with the buffer that was actually latched, not the one that
|
|
371
|
+
was claimed when availability first fired.
|
|
372
|
+
|
|
373
|
+
`markAvailable` claims the newest metadata *at that instant* and
|
|
374
|
+
`updateTexImage` takes the newest buffer *at its instant*, and
|
|
375
|
+
between the two more frames can arrive. Measured at startup: the
|
|
376
|
+
renderer released frames 2 to 7 in a burst, availability claimed
|
|
377
|
+
frame 2, the latch took frame 7's buffer, and the pair went out
|
|
378
|
+
as frame 7's pixels labelled 33.3 ms — a wrong picture that every
|
|
379
|
+
timestamp in the system agreed was right.
|
|
380
|
+
|
|
381
|
+
The texture's own timestamp settles it. It is a `System.nanoTime`
|
|
382
|
+
value on this device, the same domain as `RELEASE_NS`, so the
|
|
383
|
+
entry describing the latched buffer is the one whose release time
|
|
384
|
+
it matches. Without a usable timestamp this falls back to the
|
|
385
|
+
claimed pair, which is the old behaviour.
|
|
386
|
+
*/
|
|
387
|
+
var sample = pending
|
|
388
|
+
var associated = false
|
|
389
|
+
if (texturePtsNs > 0L) {
|
|
390
|
+
var best: LongArray? = null
|
|
391
|
+
var bestSkew = Long.MAX_VALUE
|
|
392
|
+
var runnerUp = Long.MAX_VALUE
|
|
393
|
+
val claimed = pending
|
|
394
|
+
if (claimed != null) {
|
|
395
|
+
bestSkew = Math.abs(claimed[Field.RELEASE_NS] - texturePtsNs)
|
|
396
|
+
best = claimed
|
|
397
|
+
}
|
|
398
|
+
for (entry in metadata) {
|
|
399
|
+
val skew = Math.abs(entry[Field.RELEASE_NS] - texturePtsNs)
|
|
400
|
+
if (skew < bestSkew) {
|
|
401
|
+
runnerUp = bestSkew
|
|
402
|
+
bestSkew = skew
|
|
403
|
+
best = entry
|
|
404
|
+
} else if (skew < runnerUp) {
|
|
405
|
+
runnerUp = skew
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/*
|
|
409
|
+
Unique, or it is not an association.
|
|
410
|
+
|
|
411
|
+
Two entries equally close to the latched timestamp cannot both
|
|
412
|
+
describe it, and picking the nearer by a nanosecond is a coin
|
|
413
|
+
toss dressed as a measurement. Ambiguity is reported as
|
|
414
|
+
unverified, like absence.
|
|
415
|
+
*/
|
|
416
|
+
if (best != null && bestSkew <= PAIRING_TOLERANCE_NS && runnerUp - bestSkew >= AMBIGUITY_MARGIN_NS) {
|
|
417
|
+
associated = true
|
|
418
|
+
}
|
|
419
|
+
if (associated && best != null && best !== pending) {
|
|
420
|
+
/*
|
|
421
|
+
The match is ahead of the claimed pair. Everything between
|
|
422
|
+
describes a buffer this latch skipped, so it is dropped and
|
|
423
|
+
counted rather than shown later out of order.
|
|
424
|
+
*/
|
|
425
|
+
var dropped = if (pending != null) 1 else 0
|
|
426
|
+
while (metadata.isNotEmpty() && metadata.first() !== best) {
|
|
427
|
+
metadata.removeFirst()
|
|
428
|
+
dropped += 1
|
|
429
|
+
}
|
|
430
|
+
if (metadata.isNotEmpty() && metadata.first() === best) metadata.removeFirst()
|
|
431
|
+
coalesced += dropped.toLong()
|
|
432
|
+
best!![Field.AVAILABLE_NS] = consumedNs
|
|
433
|
+
best[Field.COALESCED] = dropped.toLong()
|
|
434
|
+
pending = best
|
|
435
|
+
sample = best
|
|
436
|
+
repaired += 1
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (sample == null) {
|
|
440
|
+
unpaired += 1
|
|
441
|
+
return null
|
|
442
|
+
}
|
|
443
|
+
if (sample[Field.GENERATION] != generation.toLong()) {
|
|
444
|
+
// The clip moved on between pairing and consumption.
|
|
445
|
+
pending = null
|
|
446
|
+
stale += 1
|
|
447
|
+
unpaired += 1
|
|
448
|
+
return null
|
|
449
|
+
}
|
|
450
|
+
pending = null
|
|
451
|
+
taken += 1
|
|
452
|
+
/*
|
|
453
|
+
Stamped on every sample, including the ones that fell back.
|
|
454
|
+
|
|
455
|
+
The fallback keeps the claimed pair so a producer without
|
|
456
|
+
timestamps still gets a picture, but it is a guess and has to
|
|
457
|
+
travel as one: readiness will not release on it and the identity
|
|
458
|
+
gates will not count it.
|
|
459
|
+
*/
|
|
460
|
+
sample[Field.ASSOCIATION] = if (associated) 1L else 0L
|
|
461
|
+
if (!associated) unassociated += 1
|
|
462
|
+
sample[Field.ID] = nextId++
|
|
463
|
+
sample[Field.CONSUMED_NS] = consumedNs
|
|
464
|
+
sample[Field.POSITION_AT_CONSUMPTION_US] = PlaybackAnchor.positionAt(consumedNs)
|
|
465
|
+
/*
|
|
466
|
+
The acquired texture, labelled with what it actually holds.
|
|
467
|
+
|
|
468
|
+
Recorded rather than assumed, and the disagreement is counted
|
|
469
|
+
rather than smoothed: a sample carrying another frame's timestamp
|
|
470
|
+
would make a correctly-delivered frame look late, and there is no
|
|
471
|
+
way to tell those apart afterwards from the numbers alone.
|
|
472
|
+
*/
|
|
473
|
+
if (texturePtsNs > 0L) {
|
|
474
|
+
sample[Field.TEXTURE_PTS_US] = texturePtsNs / 1000
|
|
475
|
+
/*
|
|
476
|
+
Compared against the release time, not the media timestamp.
|
|
477
|
+
|
|
478
|
+
Measured on a Pixel 7, `SurfaceTexture.getTimestamp` returns a
|
|
479
|
+
`System.nanoTime`-based value here — 175,110,822,173 us, about
|
|
480
|
+
forty-nine hours of uptime — and not the buffer's presentation
|
|
481
|
+
time. Comparing it to `PTS_US` counted every single frame as
|
|
482
|
+
mismatched, which is a broken check reporting a broken
|
|
483
|
+
pipeline. Against `RELEASE_NS` the two share a domain, and the
|
|
484
|
+
question it answers is the useful one: is the buffer that was
|
|
485
|
+
acquired the one whose release we paired?
|
|
486
|
+
*/
|
|
487
|
+
val skewUs = Math.abs(texturePtsNs - sample[Field.RELEASE_NS]) / 1000
|
|
488
|
+
if (skewUs > IDENTITY_TOLERANCE_US) mismatched += 1
|
|
489
|
+
} else {
|
|
490
|
+
sample[Field.TEXTURE_PTS_US] = -1
|
|
491
|
+
unverified += 1
|
|
492
|
+
}
|
|
493
|
+
latest = sample
|
|
494
|
+
return sample
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Samples actually consumed by the renderer. A liveness counter. */
|
|
499
|
+
@Volatile var lastReleaseLeadNs: Long = 0; private set
|
|
500
|
+
@Volatile var lastOfferedPtsUs: Long = 0; private set
|
|
501
|
+
@Volatile var farFutureOffers: Long = 0; private set
|
|
502
|
+
@Volatile var farPastOffers: Long = 0; private set
|
|
503
|
+
@Volatile var taken: Long = 0; private set
|
|
504
|
+
|
|
505
|
+
/** The most recent canonical sample, for a readout. Never recomputed. */
|
|
506
|
+
@JvmStatic
|
|
507
|
+
fun latestSample(): LongArray? = latest
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Everything needed to say *which stage* stopped, in one read.
|
|
511
|
+
*
|
|
512
|
+
* `latest` freezing can mean the picture froze, the renderer stopped
|
|
513
|
+
* consuming, or metadata stopped arriving — and an offset summary cannot
|
|
514
|
+
* tell those apart. These are the counters that can.
|
|
515
|
+
*
|
|
516
|
+
* **Volatiles only, and deliberately no `depth` or `pendingFrames`.** Those
|
|
517
|
+
* take [lock], which `offer`, `markAvailable` and `take` all hold on the
|
|
518
|
+
* paths that decode and draw. A sampler reading them at 40 Hz contended
|
|
519
|
+
* with the renderer badly enough to stop it consuming frames entirely —
|
|
520
|
+
* the instrument froze the picture it was measuring. Anything here must be
|
|
521
|
+
* readable without waiting for a frame.
|
|
522
|
+
*/
|
|
523
|
+
/**
|
|
524
|
+
* When the pending frame was asked to be presented, or 0 if none.
|
|
525
|
+
*
|
|
526
|
+
* Exposed so the consumer can honour that time instead of latching the
|
|
527
|
+
* instant a buffer is queued. Measured, it latched about 34 ms early:
|
|
528
|
+
* `releaseOutputBuffer(index, releaseTimeNs)` queues immediately with a
|
|
529
|
+
* desired present time in the future, and nothing downstream was enforcing
|
|
530
|
+
* it — so the picture ran ahead of the sound by that much and the
|
|
531
|
+
* comparison against the intended time described an intention nobody kept.
|
|
532
|
+
*/
|
|
533
|
+
/**
|
|
534
|
+
* When the pending frame was asked to be presented, or 0 if none.
|
|
535
|
+
*
|
|
536
|
+
* Zero while the transport is not streaming, which is the whole of the
|
|
537
|
+
* first-frame readiness path: a start or a seek needs its first frame
|
|
538
|
+
* latched as soon as it exists, and there is nothing yet for it to be early
|
|
539
|
+
* relative to. Pacing resumes when playback does.
|
|
540
|
+
*/
|
|
541
|
+
@JvmStatic
|
|
542
|
+
fun pendingDueNs(): Long =
|
|
543
|
+
if (!streaming || !enforcePresentationSchedule) 0L
|
|
544
|
+
else synchronized(lock) { pending?.get(Field.RELEASE_NS) ?: 0L }
|
|
545
|
+
|
|
546
|
+
@JvmStatic
|
|
547
|
+
fun pipeline(): LongArray = longArrayOf(
|
|
548
|
+
offered, available, taken, orphaned, unpaired, stale, coalesced,
|
|
549
|
+
generation.toLong(), takeCalls,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
@JvmStatic
|
|
553
|
+
fun depth(): Int = synchronized(lock) { metadata.size }
|
|
554
|
+
|
|
555
|
+
/** overflowed, unpaired, stale, highWater, generation, coalesced, orphaned. */
|
|
556
|
+
@JvmStatic
|
|
557
|
+
fun counters(): LongArray = longArrayOf(
|
|
558
|
+
overflowed, unpaired, stale, highWater.toLong(), generation.toLong(), coalesced,
|
|
559
|
+
orphaned,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
@JvmStatic
|
|
563
|
+
fun reset() {
|
|
564
|
+
synchronized(lock) {
|
|
565
|
+
generation = 0
|
|
566
|
+
nextId = 1
|
|
567
|
+
overflowed = 0
|
|
568
|
+
available = 0
|
|
569
|
+
offered = 0
|
|
570
|
+
unpaired = 0
|
|
571
|
+
stale = 0
|
|
572
|
+
coalesced = 0
|
|
573
|
+
orphaned = 0
|
|
574
|
+
// Identity counters too: a static tally that survives a reset makes
|
|
575
|
+
// every test after the first read another test's evidence.
|
|
576
|
+
mismatched = 0
|
|
577
|
+
repaired = 0
|
|
578
|
+
unassociated = 0
|
|
579
|
+
unverified = 0
|
|
580
|
+
taken = 0
|
|
581
|
+
takeCalls = 0
|
|
582
|
+
streaming = false
|
|
583
|
+
enforcePresentationSchedule = false
|
|
584
|
+
highWater = 0
|
|
585
|
+
metadata.clear()
|
|
586
|
+
pending = null
|
|
587
|
+
latest = null
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
}
|