@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,299 @@
|
|
|
1
|
+
package dev.pictor.video
|
|
2
|
+
|
|
3
|
+
import org.json.JSONArray
|
|
4
|
+
import org.json.JSONObject
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A compiled scene program, validated, with segment selection by binary search.
|
|
8
|
+
*
|
|
9
|
+
* **This is the whole of native's timeline knowledge.** Given a presentation
|
|
10
|
+
* timestamp it finds the half-open interval containing it and executes the
|
|
11
|
+
* commands stored there. It performs no trim arithmetic, no speed arithmetic,
|
|
12
|
+
* no overlay-span arithmetic and no playlist placement: the compiler in
|
|
13
|
+
* TypeScript settled all of that, and any of it here would be the second
|
|
14
|
+
* interpreter the architecture exists to prevent.
|
|
15
|
+
*
|
|
16
|
+
* The transport matters as much as the selection. A timeline resolves to a
|
|
17
|
+
* different scene at every instant, but its *structure* changes only at clip
|
|
18
|
+
* placements and overlay boundaries — so a program is as long as the number of
|
|
19
|
+
* distinct scenes rather than the number of frames, and one document covers a
|
|
20
|
+
* whole export.
|
|
21
|
+
*/
|
|
22
|
+
internal class SceneProgramNative private constructor(
|
|
23
|
+
val durationUs: Long,
|
|
24
|
+
val projectWidth: Int,
|
|
25
|
+
val projectHeight: Int,
|
|
26
|
+
private val fromUs: LongArray,
|
|
27
|
+
private val toUs: LongArray,
|
|
28
|
+
private val clearColor: IntArray,
|
|
29
|
+
private val clipIds: Array<String?>,
|
|
30
|
+
private val windowIndex: IntArray,
|
|
31
|
+
/**
|
|
32
|
+
* Six affine values per segment, exactly as `resolveVideoScene` emitted them.
|
|
33
|
+
*
|
|
34
|
+
* Native never receives x, y, scale, rotation or flip and never reconstructs
|
|
35
|
+
* them. Those are the resolver's inputs; the matrix is its output, and
|
|
36
|
+
* decomposing it here would be a second placement rule that could disagree
|
|
37
|
+
* with the first.
|
|
38
|
+
*/
|
|
39
|
+
private val matrices: FloatArray,
|
|
40
|
+
/** Per-segment clip opacity, exactly as the resolver emitted it. */
|
|
41
|
+
private val opacities: FloatArray,
|
|
42
|
+
/** Overlay draws per segment, already in the resolver's z order. */
|
|
43
|
+
private val overlays: Array<List<Overlay>>,
|
|
44
|
+
) {
|
|
45
|
+
/**
|
|
46
|
+
* One overlay draw: a name, a placement and an alpha.
|
|
47
|
+
*
|
|
48
|
+
* No extent. The resolver cannot know how big a text block is, so it places
|
|
49
|
+
* the centre and the backend applies the measured half-extent that arrived
|
|
50
|
+
* with the resource — the same split preview uses.
|
|
51
|
+
*/
|
|
52
|
+
class Overlay(
|
|
53
|
+
val overlayId: String,
|
|
54
|
+
val resourceId: String,
|
|
55
|
+
val matrix: FloatArray,
|
|
56
|
+
val opacity: Float,
|
|
57
|
+
val z: Int,
|
|
58
|
+
)
|
|
59
|
+
val segmentCount: Int get() = fromUs.size
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The segment containing `timeUs`, or `-1`.
|
|
63
|
+
*
|
|
64
|
+
* Binary search over sorted, contiguous, half-open intervals — a lookup, and
|
|
65
|
+
* the only selection rule there is.
|
|
66
|
+
*/
|
|
67
|
+
fun segmentAt(timeUs: Long): Int {
|
|
68
|
+
var lo = 0
|
|
69
|
+
var hi = fromUs.size - 1
|
|
70
|
+
while (lo <= hi) {
|
|
71
|
+
val mid = (lo + hi) ushr 1
|
|
72
|
+
when {
|
|
73
|
+
timeUs < fromUs[mid] -> hi = mid - 1
|
|
74
|
+
timeUs >= toUs[mid] -> lo = mid + 1
|
|
75
|
+
else -> return mid
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return -1
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun clearColorAt(index: Int): Int = clearColor[index]
|
|
82
|
+
|
|
83
|
+
/** The window this segment's video command names, or -1 when it has none. */
|
|
84
|
+
fun windowAt(index: Int): Int = windowIndex[index]
|
|
85
|
+
|
|
86
|
+
fun clipAt(index: Int): String? = clipIds[index]
|
|
87
|
+
|
|
88
|
+
/** Overlay draws for this segment, in z order. */
|
|
89
|
+
fun overlaysAt(index: Int): List<Overlay> = overlays[index]
|
|
90
|
+
|
|
91
|
+
/** Every resource id the program references, for preloading. */
|
|
92
|
+
fun requiredResourceIds(): Set<String> {
|
|
93
|
+
val ids = LinkedHashSet<String>()
|
|
94
|
+
for (list in overlays) for (o in list) ids.add(o.resourceId)
|
|
95
|
+
return ids
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Clip opacity for this segment, in `[0, 1]`. */
|
|
99
|
+
fun opacityAt(index: Int): Float = opacities[index]
|
|
100
|
+
|
|
101
|
+
/** `[a, b, c, d, tx, ty]` for this segment, in project space. */
|
|
102
|
+
fun matrixAt(index: Int, out: FloatArray) {
|
|
103
|
+
System.arraycopy(matrices, index * 6, out, 0, 6)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class Invalid(val reason: String)
|
|
107
|
+
|
|
108
|
+
companion object {
|
|
109
|
+
const val SUPPORTED_VERSION = 2
|
|
110
|
+
const val SUPPORTED_SCENE_VERSION = 2
|
|
111
|
+
private val SUPPORTED_OPS = setOf("clear", "video", "overlay")
|
|
112
|
+
|
|
113
|
+
fun parse(json: String): Pair<SceneProgramNative?, Invalid?> {
|
|
114
|
+
val root = try {
|
|
115
|
+
JSONObject(json)
|
|
116
|
+
} catch (e: Exception) {
|
|
117
|
+
return null to Invalid("malformed JSON: ${e.message}")
|
|
118
|
+
}
|
|
119
|
+
val version = root.optInt("version", -1)
|
|
120
|
+
if (version != SUPPORTED_VERSION) {
|
|
121
|
+
return null to Invalid("unsupported program version $version, expected $SUPPORTED_VERSION")
|
|
122
|
+
}
|
|
123
|
+
val durationUs = root.optLong("durationUs", -1)
|
|
124
|
+
if (durationUs <= 0) return null to Invalid("durationUs is not positive")
|
|
125
|
+
val size = root.optJSONObject("projectSize")
|
|
126
|
+
?: return null to Invalid("projectSize is not an object")
|
|
127
|
+
val width = size.optDouble("width", Double.NaN)
|
|
128
|
+
val height = size.optDouble("height", Double.NaN)
|
|
129
|
+
if (width.isNaN() || height.isNaN() || width <= 0 || height <= 0) {
|
|
130
|
+
return null to Invalid("projectSize is not a positive finite size")
|
|
131
|
+
}
|
|
132
|
+
val segments = root.optJSONArray("segments")
|
|
133
|
+
?: return null to Invalid("segments is not an array")
|
|
134
|
+
val count = segments.length()
|
|
135
|
+
if (count == 0) return null to Invalid("program has no segments")
|
|
136
|
+
|
|
137
|
+
val from = LongArray(count)
|
|
138
|
+
val to = LongArray(count)
|
|
139
|
+
val clears = IntArray(count)
|
|
140
|
+
val clips = arrayOfNulls<String>(count)
|
|
141
|
+
val windows = IntArray(count) { -1 }
|
|
142
|
+
val opacities = FloatArray(count) { 1f }
|
|
143
|
+
val overlays = Array(count) { mutableListOf<Overlay>() }
|
|
144
|
+
val matrices = FloatArray(count * 6)
|
|
145
|
+
for (i in 0 until count) matrices[i * 6] = 1f
|
|
146
|
+
for (i in 0 until count) matrices[i * 6 + 3] = 1f
|
|
147
|
+
|
|
148
|
+
for (i in 0 until count) {
|
|
149
|
+
val segment = segments.optJSONObject(i)
|
|
150
|
+
?: return null to Invalid("segment $i is not an object")
|
|
151
|
+
val f = segment.optLong("fromUs", -1)
|
|
152
|
+
val t = segment.optLong("toUs", -1)
|
|
153
|
+
if (f < 0 || t < 0) return null to Invalid("segment $i has no interval")
|
|
154
|
+
if (t <= f) return null to Invalid("segment $i is empty or inverted")
|
|
155
|
+
// Sorted, contiguous, gap-free — checked rather than trusted,
|
|
156
|
+
// because a backend that selects by binary search cannot detect
|
|
157
|
+
// any of these at execution time. It would simply draw the wrong
|
|
158
|
+
// scene, or none, and encode that permanently.
|
|
159
|
+
if (i == 0 && f != 0L) return null to Invalid("first segment starts at $f, not 0")
|
|
160
|
+
if (i > 0) {
|
|
161
|
+
if (f < to[i - 1]) return null to Invalid("segment $i overlaps ${i - 1}")
|
|
162
|
+
if (f > to[i - 1]) return null to Invalid("gap between segments ${i - 1} and $i")
|
|
163
|
+
}
|
|
164
|
+
if (t > durationUs) return null to Invalid("segment $i ends past the duration")
|
|
165
|
+
from[i] = f
|
|
166
|
+
to[i] = t
|
|
167
|
+
|
|
168
|
+
val commands = segment.optJSONArray("commands")
|
|
169
|
+
?: return null to Invalid("segment $i has no commands")
|
|
170
|
+
val problem = readCommands(i, commands, clears, clips, windows, matrices, opacities, overlays[i])
|
|
171
|
+
if (problem != null) return null to problem
|
|
172
|
+
}
|
|
173
|
+
if (to[count - 1] != durationUs) {
|
|
174
|
+
return null to Invalid("last segment ends at ${to[count - 1]}, not $durationUs")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return SceneProgramNative(
|
|
178
|
+
durationUs, width.toInt(), height.toInt(), from, to, clears, clips, windows, matrices,
|
|
179
|
+
opacities,
|
|
180
|
+
Array(count) { overlays[it].sortedBy { o -> o.z } },
|
|
181
|
+
) to null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private fun readCommands(
|
|
185
|
+
index: Int,
|
|
186
|
+
commands: JSONArray,
|
|
187
|
+
clears: IntArray,
|
|
188
|
+
clips: Array<String?>,
|
|
189
|
+
windows: IntArray,
|
|
190
|
+
matrices: FloatArray,
|
|
191
|
+
opacities: FloatArray,
|
|
192
|
+
overlays: MutableList<Overlay>,
|
|
193
|
+
): Invalid? {
|
|
194
|
+
var sawClear = false
|
|
195
|
+
for (k in 0 until commands.length()) {
|
|
196
|
+
val command = commands.optJSONObject(k)
|
|
197
|
+
?: return Invalid("segment $index command $k is not an object")
|
|
198
|
+
val op = command.optString("op", "")
|
|
199
|
+
if (op !in SUPPORTED_OPS) {
|
|
200
|
+
// Refused, never skipped: a dropped command is a frame
|
|
201
|
+
// quietly missing something the timeline said was there.
|
|
202
|
+
return Invalid("segment $index command $k has unsupported op '$op'")
|
|
203
|
+
}
|
|
204
|
+
if (op == "clear") {
|
|
205
|
+
if (k != 0) return Invalid("segment $index has clear at $k, not first")
|
|
206
|
+
val color = parseColor(command.optString("color", ""))
|
|
207
|
+
?: return Invalid("segment $index has an unparseable clear color")
|
|
208
|
+
clears[index] = color
|
|
209
|
+
sawClear = true
|
|
210
|
+
} else if (op == "overlay") {
|
|
211
|
+
if (!sawClear) return Invalid("segment $index has an overlay before clear")
|
|
212
|
+
val overlayId = command.optString("overlayId", "")
|
|
213
|
+
if (overlayId.isEmpty()) return Invalid("segment $index overlay has no overlayId")
|
|
214
|
+
val resourceId = command.optString("resourceId", "")
|
|
215
|
+
if (resourceId.isEmpty()) return Invalid("segment $index overlay has no resourceId")
|
|
216
|
+
val matrix = command.optJSONArray("matrix")
|
|
217
|
+
?: return Invalid("segment $index overlay has no matrix")
|
|
218
|
+
if (matrix.length() != 6) return Invalid("segment $index overlay matrix is not six numbers")
|
|
219
|
+
val m = FloatArray(6)
|
|
220
|
+
for (k in 0 until 6) {
|
|
221
|
+
val v = matrix.optDouble(k, Double.NaN)
|
|
222
|
+
if (!finite(v)) return Invalid("segment $index overlay matrix[$k] is not finite")
|
|
223
|
+
m[k] = v.toFloat()
|
|
224
|
+
}
|
|
225
|
+
val det = m[0] * m[3] - m[1] * m[2]
|
|
226
|
+
if (det == 0f) return Invalid("segment $index overlay matrix is singular")
|
|
227
|
+
val alpha = command.optDouble("opacity", Double.NaN)
|
|
228
|
+
if (!finite(alpha)) return Invalid("segment $index overlay opacity is not finite")
|
|
229
|
+
if (alpha < 0.0 || alpha > 1.0) {
|
|
230
|
+
return Invalid("segment $index overlay opacity $alpha is outside [0, 1]")
|
|
231
|
+
}
|
|
232
|
+
overlays.add(
|
|
233
|
+
Overlay(overlayId, resourceId, m, alpha.toFloat(), command.optInt("z", overlays.size)),
|
|
234
|
+
)
|
|
235
|
+
} else {
|
|
236
|
+
if (!sawClear) return Invalid("segment $index has video before clear")
|
|
237
|
+
if (clips[index] != null) return Invalid("segment $index has two video commands")
|
|
238
|
+
val id = command.optString("clipId", "")
|
|
239
|
+
if (id.isEmpty()) return Invalid("segment $index video has no clipId")
|
|
240
|
+
val window = command.optInt("windowIndex", -1)
|
|
241
|
+
if (window < 0) return Invalid("segment $index video has no windowIndex")
|
|
242
|
+
val matrix = command.optJSONArray("matrix")
|
|
243
|
+
?: return Invalid("segment $index video has no matrix")
|
|
244
|
+
if (matrix.length() != 6) return Invalid("segment $index matrix is not six numbers")
|
|
245
|
+
for (m in 0 until 6) {
|
|
246
|
+
val value = matrix.optDouble(m, Double.NaN)
|
|
247
|
+
if (value.isNaN() || value.isInfinite()) {
|
|
248
|
+
return Invalid("segment $index matrix[$m] is not finite")
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/*
|
|
252
|
+
Singular matrices are refused before anything starts.
|
|
253
|
+
|
|
254
|
+
A zero determinant collapses the source to a line or a
|
|
255
|
+
point. GL will draw it without complaint — a degenerate
|
|
256
|
+
quad rasterises to nothing — and the export would succeed
|
|
257
|
+
with a clip silently missing. There is no picture that
|
|
258
|
+
correctly represents it, so it is not drawn at all.
|
|
259
|
+
*/
|
|
260
|
+
val det = matrix.optDouble(0) * matrix.optDouble(3) -
|
|
261
|
+
matrix.optDouble(1) * matrix.optDouble(2)
|
|
262
|
+
if (!finite(det) || det == 0.0) {
|
|
263
|
+
return Invalid("segment $index has a singular matrix")
|
|
264
|
+
}
|
|
265
|
+
for (m in 0 until 6) matrices[index * 6 + m] = matrix.optDouble(m).toFloat()
|
|
266
|
+
/*
|
|
267
|
+
Taken as given, never clamped.
|
|
268
|
+
|
|
269
|
+
The resolver clamps to [0, 1] and substitutes 1 for a
|
|
270
|
+
non-finite value; that is the single place the rule lives.
|
|
271
|
+
A backend that clamped as well would silently accept a
|
|
272
|
+
scene the resolver would have rejected, and the two would
|
|
273
|
+
disagree about what a malformed document draws. So anything
|
|
274
|
+
outside the range is refused before the export starts.
|
|
275
|
+
*/
|
|
276
|
+
val opacity = command.optDouble("opacity", Double.NaN)
|
|
277
|
+
if (!finite(opacity)) return Invalid("segment $index opacity is not finite")
|
|
278
|
+
if (opacity < 0.0 || opacity > 1.0) {
|
|
279
|
+
return Invalid("segment $index opacity $opacity is outside [0, 1]")
|
|
280
|
+
}
|
|
281
|
+
opacities[index] = opacity.toFloat()
|
|
282
|
+
clips[index] = id
|
|
283
|
+
windows[index] = window
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (!sawClear) return Invalid("segment $index has no clear")
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private fun finite(value: Double) = !value.isNaN() && !value.isInfinite()
|
|
291
|
+
|
|
292
|
+
private fun parseColor(hex: String): Int? {
|
|
293
|
+
val raw = hex.removePrefix("#")
|
|
294
|
+
if (raw.length != 6 && raw.length != 8) return null
|
|
295
|
+
val value = raw.toLongOrNull(16) ?: return null
|
|
296
|
+
return if (raw.length == 8) value.toInt() else (0xff000000L or value).toInt()
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|