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