@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,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
|
+
}
|