@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,190 @@
1
+ import type { VideoExportProgress } from '@pictor-sdk/video-core'
2
+ import type { ExportStage } from './exportJob'
3
+ import {
4
+ STALL_GRACE_MS,
5
+ createProgressReporter,
6
+ createStallWatchdog,
7
+ type ProgressPlan,
8
+ type StallVerdict,
9
+ } from './exportProgress'
10
+
11
+ /**
12
+ * Reading native counters, and turning them into a progress stream.
13
+ *
14
+ * The native run is one call that resolves when the whole export is done, so
15
+ * there is nothing to subscribe to: progress has to be *read*. That is the whole
16
+ * reason this exists as a pump rather than an event stream, and it is also why
17
+ * the two signals stay separate on either side of the boundary.
18
+ *
19
+ * **Natively, nothing is throttled.** Every emitted frame, every block of mixed
20
+ * audio and every muxed sample batch increments a counter or beats, at whatever
21
+ * rate the encoder runs. Throttling there would be throttling the evidence.
22
+ *
23
+ * **Across the bridge, everything is throttled.** This polls at 8 Hz and the
24
+ * reporter emits at most that often, because nobody can read a progress bar
25
+ * faster and every update costs a crossing. A 0.1× export that beats a thousand
26
+ * times a second still produces eight callbacks.
27
+ *
28
+ * The watchdog reads the *beat total* rather than the progress fraction. A long
29
+ * mux moves the bar by almost nothing while beating steadily, and treating that
30
+ * as a stall would kill a healthy export — which is the failure mode the
31
+ * separation exists to prevent.
32
+ *
33
+ * @experimental
34
+ */
35
+
36
+ /** What native reports about work completed so far. @experimental */
37
+ export interface ProgressSnapshot {
38
+ /** Output frames written, per shard index. */
39
+ shards: readonly number[]
40
+ /** Mixed audio samples handed to the encoder. */
41
+ audioSamples: number
42
+ /** What `audioSamples` counts up to. Zero before the mix starts. */
43
+ audioPlanned: number
44
+ /** Concatenation and mux finished. */
45
+ muxed: boolean
46
+ /**
47
+ * Every heartbeat from every source, summed.
48
+ *
49
+ * Liveness only. It is deliberately not a work total — a mux batch and an
50
+ * output frame are not the same amount of anything — so it feeds the watchdog
51
+ * and never the bar.
52
+ */
53
+ beats: number
54
+ }
55
+
56
+ /** @experimental */
57
+ export interface ProgressPumpOptions {
58
+ plan: ProgressPlan
59
+ durationUs: number
60
+ /** Read the native counters. Called on every tick. */
61
+ read(): ProgressSnapshot
62
+ onProgress?: (progress: VideoExportProgress) => void
63
+ /** Called once, when nothing has reported for the threshold. */
64
+ onStall(verdict: StallVerdict): void
65
+ /** Bridge delivery rate. 125 ms is 8 Hz. */
66
+ pollMs?: number
67
+ thresholdMs?: number
68
+ now?: () => number
69
+ }
70
+
71
+ /** @experimental */
72
+ export interface ProgressPump {
73
+ /** Emit `{ progress: 0, time: 0 }`, once. */
74
+ begin(): void
75
+ /** Read native once and report what changed. */
76
+ tick(): void
77
+ /**
78
+ * The destination is closed and verified readable. Emits 1, once.
79
+ *
80
+ * Separate from the mux on purpose: a bar that fills when the encoder stops
81
+ * produces a "done" over a file that cannot yet be opened.
82
+ */
83
+ complete(): void
84
+ /** Deliver nothing further, whatever arrives. */
85
+ stop(): void
86
+ /** How often {@link tick} expects to be called. */
87
+ readonly pollMs: number
88
+ }
89
+
90
+ /**
91
+ * Where the export was when it went quiet.
92
+ *
93
+ * Derived from what has finished rather than from a native stage string, so a
94
+ * stall report cannot claim a stage the counters contradict.
95
+ */
96
+ function stageOf(snapshot: ProgressSnapshot, plan: ProgressPlan): ExportStage {
97
+ if (snapshot.muxed) return 'publishing'
98
+ const shardsDone = plan.shardFrames.every(
99
+ (total, index) => (snapshot.shards[index] ?? 0) >= total,
100
+ )
101
+ if (!shardsDone) return 'shards'
102
+ if (snapshot.audioPlanned > 0 && snapshot.audioSamples < snapshot.audioPlanned) return 'audio'
103
+ return 'concatenating'
104
+ }
105
+
106
+ /** @experimental */
107
+ export function createProgressPump(options: ProgressPumpOptions): ProgressPump {
108
+ const now = options.now ?? (() => Date.now())
109
+ const pollMs = options.pollMs ?? 125
110
+ const plan = options.plan
111
+ const reporter = createProgressReporter({
112
+ plan,
113
+ durationUs: options.durationUs,
114
+ onProgress: options.onProgress,
115
+ // The reporter's own throttle is the poll interval: a tick that finds
116
+ // nothing new should not be able to spend a crossing saying so.
117
+ throttleMs: pollMs,
118
+ now,
119
+ })
120
+ const watchdog = createStallWatchdog({ thresholdMs: options.thresholdMs ?? STALL_GRACE_MS })
121
+ let lastBeats = -1
122
+ let stopped = false
123
+ let stalled = false
124
+
125
+ return {
126
+ pollMs,
127
+
128
+ begin() {
129
+ reporter.begin()
130
+ // Seeded here rather than on the first tick. Without it the threshold
131
+ // would be measured from whenever polling happened to start, which for a
132
+ // slow first shard is a different moment from when work began.
133
+ watchdog.advanced('resolving', 0, now())
134
+ },
135
+
136
+ tick() {
137
+ if (stopped || stalled) return
138
+ const snapshot = options.read()
139
+ /*
140
+ One read of native state is one update to the bar.
141
+
142
+ Without the batch the first shard spends the throttle slot and the rest
143
+ — including the whole audio mix — are dropped until the next tick, so
144
+ the last thing to change before an export ends could never be delivered
145
+ at all.
146
+ */
147
+ reporter.batch(() => {
148
+ for (let i = 0; i < plan.shardFrames.length; i += 1) {
149
+ reporter.shard(i, snapshot.shards[i] ?? 0)
150
+ }
151
+ if (snapshot.audioPlanned > 0 && plan.audioSamples > 0) {
152
+ /*
153
+ Rescaled rather than counted twice.
154
+
155
+ Native counts interleaved samples at its own rate and channel count;
156
+ the plan counts what the compiler predicted. Passing native's number
157
+ straight through would weight audio by whichever of the two happened
158
+ to be larger, so what crosses is the fraction.
159
+ */
160
+ const fraction = Math.min(1, snapshot.audioSamples / snapshot.audioPlanned)
161
+ reporter.audio(plan.audioSamples * fraction)
162
+ }
163
+ if (snapshot.muxed) reporter.muxed()
164
+ })
165
+
166
+ const at = now()
167
+ if (snapshot.beats !== lastBeats) {
168
+ lastBeats = snapshot.beats
169
+ watchdog.advanced(stageOf(snapshot, plan), snapshot.beats, at)
170
+ }
171
+ const verdict = watchdog.stalledFor(at)
172
+ if (verdict) {
173
+ // Once. The export is torn down from here and there is nothing further
174
+ // to observe; continuing to poll a dead run would report it again.
175
+ stalled = true
176
+ options.onStall(verdict)
177
+ }
178
+ },
179
+
180
+ complete() {
181
+ if (stopped) return
182
+ reporter.complete()
183
+ },
184
+
185
+ stop() {
186
+ stopped = true
187
+ reporter.stop()
188
+ },
189
+ }
190
+ }