@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,261 @@
1
+ package dev.pictor.video
2
+
3
+ import android.media.MediaCodec
4
+ import android.media.MediaCodecInfo
5
+ import android.media.MediaFormat
6
+ import java.nio.ByteBuffer
7
+ import java.nio.ByteOrder
8
+ import kotlin.math.abs
9
+ import org.json.JSONObject
10
+
11
+ /**
12
+ * How many samples of priming this encoder prepends — measured, not assumed.
13
+ *
14
+ * A lossy encoder emits its filter-bank warm-up before any content, so a track
15
+ * muxed without compensating plays late by that much. Measured on a Pixel 7: a
16
+ * uniform 42.7 ms A/V offset, well past a video frame at 30 fps, identical at
17
+ * every marker in a ten-second timeline.
18
+ *
19
+ * Three things this deliberately does **not** do.
20
+ *
21
+ * It does not read `KEY_ENCODER_DELAY`: this encoder leaves it unset, and a
22
+ * value that is absent on the device you have is not a value you can build on.
23
+ *
24
+ * It does not hard-code 2048. That is the number this encoder happens to use,
25
+ * and a constant that is right on one device is indistinguishable from a
26
+ * measurement until it reaches a device where it is wrong.
27
+ *
28
+ * It does not look for the first loud sample in the user's audio. A clip that
29
+ * legitimately begins with silence would then be diagnosed as heavily primed and
30
+ * trimmed into its own content. Delay is a property of the codec and its
31
+ * configuration, so it is measured from a signal whose position is already
32
+ * known, and cached against that configuration.
33
+ */
34
+ internal object AacPriming {
35
+
36
+ /** Frames of calibration signal; long enough that the burst is unambiguous. */
37
+ private const val LENGTH = 16_384
38
+
39
+ /** The burst itself: short, full-scale, and easy to find after a codec. */
40
+ private const val BURST = 256
41
+
42
+ private val cache = HashMap<String, Result>()
43
+
44
+ /** What the measurement found, and how. */
45
+ class Result(
46
+ val delayFrames: Int,
47
+ /** `"timestamps"` or `"calibration"` — which method identified it. */
48
+ val method: String,
49
+ /** Onset the calibration observed, before subtracting the known position. */
50
+ val observedOnset: Int,
51
+ val expectedOnset: Int,
52
+ )
53
+
54
+ private fun key(rate: Int, channels: Int, bitrate: Int) = "$rate/$channels/$bitrate"
55
+
56
+ /**
57
+ * Measure (or recall) the priming for one encoder configuration.
58
+ *
59
+ * @param at where the calibration burst is placed. Callers use 0; the gate
60
+ * also runs the middle and the final frames, because a correct measurement
61
+ * must not depend on where the content sits.
62
+ */
63
+ fun measure(rate: Int, channels: Int, bitrate: Int, at: Int = 0, useCache: Boolean = true): Result {
64
+ val k = key(rate, channels, bitrate)
65
+ if (useCache && at == 0) cache[k]?.let { return it }
66
+
67
+ val pcm = ShortArray(LENGTH * channels)
68
+ for (i in 0 until BURST) {
69
+ val f = at + i
70
+ if (f >= LENGTH) break
71
+ // Alternating full scale: survives the codec as an unmistakable
72
+ // burst of energy, and a tone would ring past its own edge.
73
+ val v = if (i % 2 == 0) 30_000 else -30_000
74
+ for (c in 0 until channels) pcm[f * channels + c] = v.toShort()
75
+ }
76
+
77
+ val encoded = encode(pcm, rate, channels, bitrate)
78
+ /*
79
+ The timestamp relationship first, because it needs no signal at all.
80
+
81
+ If an encoder reports output timestamps shifted from the inputs that
82
+ produced them, that shift *is* the delay and is exact. Most Android AAC
83
+ encoders copy input timestamps straight through, in which case the
84
+ relationship carries no information and the calibration below has to
85
+ answer instead -- so the outcome is recorded either way rather than
86
+ assumed.
87
+ */
88
+ if (encoded.timestampShiftFrames > 0) {
89
+ val r = Result(encoded.timestampShiftFrames, "timestamps", -1, -1)
90
+ if (at == 0) cache[k] = r
91
+ return r
92
+ }
93
+
94
+ val decoded = decode(encoded.units, encoded.csd, rate, channels)
95
+ var peak = 0
96
+ for (v in decoded) if (abs(v.toInt()) > peak) peak = abs(v.toInt())
97
+ val threshold = (peak * 0.25).toInt().coerceAtLeast(1)
98
+ var onset = -1
99
+ var f = 0
100
+ while (f * channels < decoded.size) {
101
+ if (abs(decoded[f * channels].toInt()) >= threshold) { onset = f; break }
102
+ f += 1
103
+ }
104
+ if (onset < 0) throw IllegalStateException("calibration burst did not survive the encoder")
105
+ val r = Result(onset - at, "calibration", onset, at)
106
+ if (at == 0) cache[k] = r
107
+ return r
108
+ }
109
+
110
+ private class Encoded(
111
+ val units: List<ByteArray>,
112
+ val csd: ByteBuffer,
113
+ val timestampShiftFrames: Int,
114
+ )
115
+
116
+ private fun encode(pcm: ShortArray, rate: Int, channels: Int, bitrate: Int): Encoded {
117
+ val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, rate, channels).apply {
118
+ setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
119
+ setInteger(MediaFormat.KEY_BIT_RATE, bitrate)
120
+ }
121
+ val ledgerEntry = CodecLedger.open("priming.encoder")
122
+ val codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
123
+ codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
124
+ codec.start()
125
+ val units = ArrayList<ByteArray>()
126
+ var csd: ByteBuffer? = null
127
+ val info = MediaCodec.BufferInfo()
128
+ var offset = 0
129
+ var inputDone = false
130
+ var outputDone = false
131
+ var firstInputPts = Long.MIN_VALUE
132
+ var firstOutputPts = Long.MIN_VALUE
133
+ var guard = 0
134
+ try {
135
+ while (!outputDone && guard < 100_000) {
136
+ guard += 1
137
+ if (!inputDone) {
138
+ val index = codec.dequeueInputBuffer(10_000)
139
+ if (index >= 0) {
140
+ val buffer = codec.getInputBuffer(index)!!
141
+ buffer.clear()
142
+ val room = buffer.capacity() / 2
143
+ val take = minOf(room, pcm.size - offset)
144
+ if (take <= 0) {
145
+ codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
146
+ inputDone = true
147
+ } else {
148
+ val pts = (offset / channels).toLong() * 1_000_000L / rate
149
+ if (firstInputPts == Long.MIN_VALUE) firstInputPts = pts
150
+ buffer.order(ByteOrder.nativeOrder()).asShortBuffer().put(pcm, offset, take)
151
+ codec.queueInputBuffer(index, 0, take * 2, pts, 0)
152
+ offset += take
153
+ }
154
+ }
155
+ }
156
+ val index = codec.dequeueOutputBuffer(info, 10_000)
157
+ if (index >= 0) {
158
+ val buffer = codec.getOutputBuffer(index)!!
159
+ buffer.position(info.offset)
160
+ buffer.limit(info.offset + info.size)
161
+ if ((info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
162
+ val copy = ByteBuffer.allocate(info.size)
163
+ copy.put(buffer); copy.flip(); csd = copy
164
+ } else if (info.size > 0) {
165
+ if (firstOutputPts == Long.MIN_VALUE) firstOutputPts = info.presentationTimeUs
166
+ val bytes = ByteArray(info.size)
167
+ buffer.get(bytes)
168
+ units.add(bytes)
169
+ }
170
+ codec.releaseOutputBuffer(index, false)
171
+ if ((info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) outputDone = true
172
+ }
173
+ }
174
+ } finally {
175
+ runCatching { codec.stop() }
176
+ ledgerEntry.releaseCalledNs = System.nanoTime()
177
+ runCatching { codec.release() }
178
+ .onFailure { ledgerEntry.failure = "release: " + it.javaClass.simpleName }
179
+ ledgerEntry.releasedNs = System.nanoTime()
180
+ }
181
+ val shift =
182
+ if (firstInputPts != Long.MIN_VALUE && firstOutputPts != Long.MIN_VALUE &&
183
+ firstOutputPts > firstInputPts
184
+ ) ((firstOutputPts - firstInputPts) * rate / 1_000_000L).toInt()
185
+ else 0
186
+ return Encoded(units, csd ?: ByteBuffer.allocate(0), shift)
187
+ }
188
+
189
+ private fun decode(units: List<ByteArray>, csd: ByteBuffer, rate: Int, channels: Int): ShortArray {
190
+ val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, rate, channels).apply {
191
+ setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
192
+ if (csd.remaining() > 0) setByteBuffer("csd-0", csd.duplicate())
193
+ }
194
+ val ledgerEntry = CodecLedger.open("priming.decoder")
195
+ val codec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
196
+ codec.configure(format, null, null, 0)
197
+ codec.start()
198
+ val out = ArrayList<Short>()
199
+ val info = MediaCodec.BufferInfo()
200
+ var next = 0
201
+ var inputDone = false
202
+ var outputDone = false
203
+ var guard = 0
204
+ try {
205
+ while (!outputDone && guard < 100_000) {
206
+ guard += 1
207
+ if (!inputDone) {
208
+ val index = codec.dequeueInputBuffer(10_000)
209
+ if (index >= 0) {
210
+ if (next >= units.size) {
211
+ codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
212
+ inputDone = true
213
+ } else {
214
+ val unit = units[next]
215
+ val buffer = codec.getInputBuffer(index)!!
216
+ buffer.clear(); buffer.put(unit)
217
+ codec.queueInputBuffer(
218
+ index, 0, unit.size,
219
+ next.toLong() * 1024L * 1_000_000L / rate, 0,
220
+ )
221
+ next += 1
222
+ }
223
+ }
224
+ }
225
+ val index = codec.dequeueOutputBuffer(info, 10_000)
226
+ if (index >= 0) {
227
+ if (info.size > 0) {
228
+ val buffer = codec.getOutputBuffer(index)!!
229
+ buffer.position(info.offset)
230
+ buffer.limit(info.offset + info.size)
231
+ val shorts = buffer.order(ByteOrder.nativeOrder()).asShortBuffer()
232
+ while (shorts.hasRemaining()) out.add(shorts.get())
233
+ }
234
+ codec.releaseOutputBuffer(index, false)
235
+ if ((info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) outputDone = true
236
+ }
237
+ }
238
+ } finally {
239
+ runCatching { codec.stop() }
240
+ ledgerEntry.releaseCalledNs = System.nanoTime()
241
+ runCatching { codec.release() }
242
+ .onFailure { ledgerEntry.failure = "release: " + it.javaClass.simpleName }
243
+ ledgerEntry.releasedNs = System.nanoTime()
244
+ }
245
+ return ShortArray(out.size) { out[it] }
246
+ }
247
+
248
+ /** Report the measurement for a configuration and burst position. */
249
+ fun probe(rate: Int, channels: Int, bitrate: Int, at: Int): String {
250
+ val out = JSONObject().put("rate", rate).put("channels", channels)
251
+ .put("bitrate", bitrate).put("at", at)
252
+ return try {
253
+ val r = measure(rate, channels, bitrate, at, useCache = false)
254
+ out.put("ok", true).put("delayFrames", r.delayFrames).put("method", r.method)
255
+ .put("observedOnset", r.observedOnset).put("expectedOnset", r.expectedOnset)
256
+ .put("delayUs", r.delayFrames.toLong() * 1_000_000L / rate).toString()
257
+ } catch (e: Exception) {
258
+ out.put("ok", false).put("error", e.javaClass.simpleName + ": " + e.message).toString()
259
+ }
260
+ }
261
+ }