@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,72 @@
|
|
|
1
|
+
package dev.pictor.video
|
|
2
|
+
|
|
3
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Evidence that an export is still working, as distinct from how far it has got.
|
|
7
|
+
*
|
|
8
|
+
* Progress is what a person sees, throttled to a few updates a second because
|
|
9
|
+
* that is all anyone can read. A heartbeat is not throttled and does not have to
|
|
10
|
+
* move the visible percentage: one muxed sample batch, one repeated cadence
|
|
11
|
+
* copy, one block of copied bytes. Keeping the two apart is what stops a long
|
|
12
|
+
* mux from looking stalled because its percentage barely moves, and a 0.1x shard
|
|
13
|
+
* from looking stalled because it legitimately runs for minutes.
|
|
14
|
+
*
|
|
15
|
+
* ## Beats are owned
|
|
16
|
+
*
|
|
17
|
+
* A process-wide sink with no identity is a hazard: a decoder thread from a run
|
|
18
|
+
* that has already been released can still be unwinding, and its beat would land
|
|
19
|
+
* on whatever job happens to be current — keeping a genuinely stalled export
|
|
20
|
+
* alive with the dying breaths of the previous one. That is the exact failure a
|
|
21
|
+
* watchdog exists to catch, so beats carry the epoch of the work that produced
|
|
22
|
+
* them and a beat from any other epoch is discarded.
|
|
23
|
+
*
|
|
24
|
+
* Producers capture the epoch once, when they start, and hand it back with every
|
|
25
|
+
* beat. They never read the current epoch at beat time — that would defeat the
|
|
26
|
+
* check by construction.
|
|
27
|
+
*
|
|
28
|
+
* ## No fabricated liveness
|
|
29
|
+
*
|
|
30
|
+
* Nothing here beats on a timer. A "still alive" tick from a scheduler would
|
|
31
|
+
* keep the watchdog quiet precisely when a worker is blocked, which is the one
|
|
32
|
+
* situation it is meant to report. Every beat is a piece of work that happened
|
|
33
|
+
* or a blocking step that completed.
|
|
34
|
+
*/
|
|
35
|
+
internal object ExportHeartbeat {
|
|
36
|
+
private val epoch = AtomicLong(0)
|
|
37
|
+
|
|
38
|
+
@Volatile private var sink: ((String) -> Unit)? = null
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Take ownership for a new job.
|
|
42
|
+
*
|
|
43
|
+
* Returns the epoch producers must carry. Bumped rather than reused so beats
|
|
44
|
+
* already in flight from the previous job cannot be mistaken for this one's.
|
|
45
|
+
*/
|
|
46
|
+
fun attach(to: (String) -> Unit): Long {
|
|
47
|
+
val next = epoch.incrementAndGet()
|
|
48
|
+
sink = to
|
|
49
|
+
return next
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Give it up. Beats afterwards are ignored until the next `attach`. */
|
|
53
|
+
fun detach() {
|
|
54
|
+
epoch.incrementAndGet()
|
|
55
|
+
sink = null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The epoch a producer should capture when it starts. */
|
|
59
|
+
fun currentEpoch(): Long = epoch.get()
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Record a beat from `source`, on behalf of `atEpoch`.
|
|
63
|
+
*
|
|
64
|
+
* Ignored when nothing is exporting, and ignored when the epoch is not the
|
|
65
|
+
* current one — a late beat from a released run proves nothing about the job
|
|
66
|
+
* running now.
|
|
67
|
+
*/
|
|
68
|
+
fun beat(source: String, atEpoch: Long) {
|
|
69
|
+
if (atEpoch != epoch.get()) return
|
|
70
|
+
sink?.invoke(source)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
package dev.pictor.video
|
|
2
|
+
|
|
3
|
+
import org.json.JSONArray
|
|
4
|
+
import org.json.JSONObject
|
|
5
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
6
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Completed work, counted where it happens.
|
|
10
|
+
*
|
|
11
|
+
* The sibling of [ExportHeartbeat], and deliberately not the same thing. A beat
|
|
12
|
+
* says the pipeline is alive; these counters say how much of the export exists.
|
|
13
|
+
* A long mux beats hundreds of times and completes one unit of work, so a bar
|
|
14
|
+
* driven by beats would race, and a watchdog driven by these counters would kill
|
|
15
|
+
* a healthy 0.1× shard.
|
|
16
|
+
*
|
|
17
|
+
* Owned by the same epoch as the heartbeat, for the same reason: the export
|
|
18
|
+
* before this one may still have a shader program running on a thread Media3
|
|
19
|
+
* has not finished with, and a frame it emits must not be credited to the job
|
|
20
|
+
* that is running now. Ownership is taken in `ExportGate.beginJob` and given
|
|
21
|
+
* back in `endJob`; anything arriving on an older epoch is dropped.
|
|
22
|
+
*
|
|
23
|
+
* Read across the bridge by polling rather than pushed, because the native run
|
|
24
|
+
* is one call that resolves at the end. Counting here is unthrottled; the
|
|
25
|
+
* throttle belongs on the JavaScript side, where the crossings are.
|
|
26
|
+
*/
|
|
27
|
+
internal object ExportProgress {
|
|
28
|
+
/** The epoch these counters belong to, or -1 when no job owns them. */
|
|
29
|
+
@Volatile private var activeEpoch: Long = -1L
|
|
30
|
+
|
|
31
|
+
private val shardFrames = ConcurrentHashMap<Int, AtomicLong>()
|
|
32
|
+
@Volatile private var currentShard: Int = -1
|
|
33
|
+
private val audioSamples = AtomicLong(0)
|
|
34
|
+
private val audioPlanned = AtomicLong(0)
|
|
35
|
+
@Volatile private var muxed: Boolean = false
|
|
36
|
+
|
|
37
|
+
fun begin(epoch: Long) {
|
|
38
|
+
synchronized(this) {
|
|
39
|
+
shardFrames.clear()
|
|
40
|
+
currentShard = -1
|
|
41
|
+
audioSamples.set(0)
|
|
42
|
+
audioPlanned.set(0)
|
|
43
|
+
muxed = false
|
|
44
|
+
activeEpoch = epoch
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fun end() {
|
|
49
|
+
synchronized(this) { activeEpoch = -1L }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private fun owned(atEpoch: Long): Boolean = atEpoch == activeEpoch
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Which shard the frames arriving now belong to.
|
|
56
|
+
*
|
|
57
|
+
* Shards run one at a time, so the producer does not have to know its own
|
|
58
|
+
* index — which is just as well, because a `GlShaderProgram` is constructed
|
|
59
|
+
* by Media3 and told nothing about the timeline.
|
|
60
|
+
*/
|
|
61
|
+
fun enterShard(index: Int, atEpoch: Long) {
|
|
62
|
+
if (!owned(atEpoch)) return
|
|
63
|
+
currentShard = index
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fun frame(atEpoch: Long) {
|
|
67
|
+
if (!owned(atEpoch)) return
|
|
68
|
+
val shard = currentShard
|
|
69
|
+
if (shard < 0) return
|
|
70
|
+
shardFrames.getOrPut(shard) { AtomicLong(0) }.incrementAndGet()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Mixed samples handed to the encoder, and what they count up to. */
|
|
74
|
+
fun audio(done: Long, planned: Long, atEpoch: Long) {
|
|
75
|
+
if (!owned(atEpoch)) return
|
|
76
|
+
audioPlanned.set(planned)
|
|
77
|
+
// Monotonic here as well as in the reporter: the encoder's input offset
|
|
78
|
+
// is already monotonic, but a retried mix would restart it.
|
|
79
|
+
while (true) {
|
|
80
|
+
val seen = audioSamples.get()
|
|
81
|
+
if (done <= seen || audioSamples.compareAndSet(seen, done)) break
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fun muxed(atEpoch: Long) {
|
|
86
|
+
if (!owned(atEpoch)) return
|
|
87
|
+
muxed = true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `shards` is dense from 0, so a shard that has emitted nothing reads 0. */
|
|
91
|
+
fun snapshotJson(beats: Long): String {
|
|
92
|
+
val highest = shardFrames.keys.maxOrNull() ?: -1
|
|
93
|
+
val shards = JSONArray()
|
|
94
|
+
for (i in 0..highest) shards.put(shardFrames[i]?.get() ?: 0L)
|
|
95
|
+
return JSONObject()
|
|
96
|
+
.put("shards", shards)
|
|
97
|
+
.put("audioSamples", audioSamples.get())
|
|
98
|
+
.put("audioPlanned", audioPlanned.get())
|
|
99
|
+
.put("muxed", muxed)
|
|
100
|
+
.put("beats", beats)
|
|
101
|
+
.toString()
|
|
102
|
+
}
|
|
103
|
+
}
|