@pictor-sdk/video-android 0.1.0 → 0.1.4

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 (29) hide show
  1. package/android/build.gradle +36 -9
  2. package/android/consumer-rules.pro +47 -0
  3. package/android/src/main/AndroidManifest.xml +15 -1
  4. package/android/src/main/cpp/pictor_videosurface.cpp +88 -8
  5. package/android/src/main/java/dev/pictor/video/AudioMixer.kt +39 -4
  6. package/android/src/main/java/dev/pictor/video/BoundedWait.kt +89 -0
  7. package/android/src/main/java/dev/pictor/video/CadenceShaderProgram.kt +29 -0
  8. package/android/src/main/java/dev/pictor/video/CompositionSeek.kt +8 -0
  9. package/android/src/main/java/dev/pictor/video/CompositionSpike.kt +23 -13
  10. package/android/src/main/java/dev/pictor/video/CompositionVideoItem.kt +16 -0
  11. package/android/src/main/java/dev/pictor/video/CompositionWindow.kt +17 -0
  12. package/android/src/main/java/dev/pictor/video/ExportGate.kt +421 -44
  13. package/android/src/main/java/dev/pictor/video/FrameClock.kt +6 -1
  14. package/android/src/main/java/dev/pictor/video/GateLadder.kt +405 -0
  15. package/android/src/main/java/dev/pictor/video/MediaStoreSave.kt +216 -9
  16. package/android/src/main/java/dev/pictor/video/PictorVideoModule.kt +13 -0
  17. package/android/src/main/java/dev/pictor/video/RunRegistry.kt +108 -0
  18. package/android/src/main/java/dev/pictor/video/SkiaPassThroughEffect.kt +4 -0
  19. package/dist/index.d.mts +27 -0
  20. package/dist/index.d.ts +27 -0
  21. package/dist/index.js +15 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +15 -3
  24. package/dist/index.mjs.map +1 -1
  25. package/package.json +3 -3
  26. package/plugin/gradleProperties.js +9 -6
  27. package/src/NativePictorVideo.ts +11 -0
  28. package/src/compositionSpike.ts +9 -0
  29. package/src/exporter.ts +41 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pictor-sdk/video-android",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
4
4
  "description": "Optional Android video playback and export adapter for Pictor. Requires Android API 29+.",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -29,7 +29,7 @@
29
29
  "react": ">=18.0.0",
30
30
  "@expo/config-plugins": ">=54.0.0",
31
31
  "react-native-reanimated": ">=3.16.0",
32
- "@pictor-sdk/video-core": "^0.1.0"
32
+ "@pictor-sdk/video-core": "^0.1.1"
33
33
  },
34
34
  "devDependencies": {
35
35
  "react-native": "^0.81.0",
@@ -40,7 +40,7 @@
40
40
  "@types/react": "^19.0.0",
41
41
  "@expo/config-plugins": "^54.0.5",
42
42
  "react-native-reanimated": "^4.1.0",
43
- "@pictor-sdk/video-core": "0.1.0"
43
+ "@pictor-sdk/video-core": "0.1.1"
44
44
  },
45
45
  "codegenConfig": {
46
46
  "name": "PictorVideoSpec",
@@ -11,14 +11,17 @@
11
11
  */
12
12
 
13
13
  /**
14
- * The oldest Android the adapter is known to run on.
14
+ * The oldest Android the adapter builds against.
15
15
  *
16
- * Conservative, and deliberately so: the surviving code path needs 24, and 29 is
17
- * held only because nothing older than the one device here has actually run it.
18
- * Installing this adapter raises a host's `minSdkVersion`; not installing it
19
- * leaves image-only consumers exactly where they were.
16
+ * 24, matching `android/build.gradle` — the two have to agree or a prebuild
17
+ * writes a floor the library then rejects. React Native needs 24 and Media3
18
+ * 1.9.4 declares 23, so nothing below this is reachable anyway.
19
+ *
20
+ * This is a *build* floor. Whether 24..28 actually works is a separate claim,
21
+ * and `docs/research/min-sdk-24.md` records how far it has been taken. A host
22
+ * that wants a higher floor passes one: this raises, never lowers.
20
23
  */
21
- const MIN_SDK_VERSION = 29
24
+ const MIN_SDK_VERSION = 24
22
25
 
23
26
  const MIN_SDK_KEY = 'android.minSdkVersion'
24
27
 
@@ -469,6 +469,17 @@ export interface Spec extends TurboModule {
469
469
  exportGateHoldMediaCopy(enabled: boolean): string
470
470
  /** Gate-only: wedge the cadence program after N frames. -1 is off. @experimental */
471
471
  exportGateStallAfterFrames(frames: number): string
472
+ /**
473
+ * Gate-only: the smallest failing Media3 export, one stage at a time.
474
+ *
475
+ * Takes a JSON spec — uri, source span in microseconds, the configured speed
476
+ * and the emission plan `resampleClip` compiled — and runs a plain
477
+ * Transformer, then the same run with speed mapping, cadence and Skia added
478
+ * individually. Starts in the background and returns immediately; each rung
479
+ * and the whole ladder are reported to logcat under `PictorLadderRung` and
480
+ * `PictorLadder`. @experimental
481
+ */
482
+ exportGateLadder(specJson: string): string
472
483
  /**
473
484
  * Arm the gate-only controls. Everything gate-only is inert until this is
474
485
  * called, and the production exporter never calls it. @experimental
@@ -34,6 +34,10 @@ export interface CompositionSpikeItem {
34
34
  /** Clipping bounds in **source** time, which is where speed is applied. */
35
35
  startUs: number
36
36
  endUs: number
37
+ /** Full source duration, before trimming or speed. Omit only for legacy callers. */
38
+ sourceDurationUs?: number
39
+ /** Exclusive project-time end from the playlist compiler, after trim and speed. */
40
+ timelineEndUs?: number
37
41
  speed: number
38
42
  }
39
43
 
@@ -122,6 +126,8 @@ export interface CompositionSpikeState {
122
126
  playbackState?: number
123
127
  durationUs?: number
124
128
  presentedUs?: number | null
129
+ /** Playlist window belonging to the latched frame, not the current playhead. */
130
+ presentedWindowIndex?: number
125
131
  /** How old the presented frame was when it was read, so a stalled sample can be excluded. */
126
132
  presentedAgeNs?: number
127
133
  /** How long the paired read itself took. Bounds the pairing error. */
@@ -148,6 +154,7 @@ export function compositionSpikeSpec(
148
154
  const playlist = playlistOf(project)
149
155
  const items: CompositionSpikeItem[] = []
150
156
  const unsupported = playlist.unsupported.map((u) => `${u.clipId}: ${u.reason}`)
157
+ const clips = new Map(project.clips.map((clip) => [clip.id, clip]))
151
158
  for (const item of playlist.items) {
152
159
  const uri = resolve(item.source as unknown as { kind: string; uri?: string })
153
160
  if (uri === null) {
@@ -158,6 +165,8 @@ export function compositionSpikeSpec(
158
165
  uri,
159
166
  startUs: Number(item.startUs),
160
167
  endUs: Number(item.endUs),
168
+ sourceDurationUs: clips.get(item.clipId)?.metadata.duration,
169
+ timelineEndUs: Number(item.timelineEndUs),
161
170
  speed: item.speed,
162
171
  })
163
172
  }
package/src/exporter.ts CHANGED
@@ -169,6 +169,27 @@ export interface Media3ExporterOptions {
169
169
  * Native counting is unthrottled regardless; this is only the bridge rate.
170
170
  */
171
171
  progressPollMs?: number
172
+ /**
173
+ * Which mechanism re-times a clip's **video** when its speed is not 1.
174
+ *
175
+ * `'cadence'` (the default, and what has shipped) compiles an exact emit plan
176
+ * in `resampleClip` and a GL effect drops or repeats frames to match it. The
177
+ * plan's length is the clip's length by construction, so project time is
178
+ * exact — and above 1× the decoder still reads every source frame, which is
179
+ * what produces a burst of output followed immediately by end-of-stream.
180
+ *
181
+ * `'media3'` sends no plan, so no cadence effect is built and
182
+ * `EditedMediaItem.setSpeed` carries the video timing on its own.
183
+ *
184
+ * Exposed because the burst is implicated in a vendor encoder that drops its
185
+ * output end-of-stream at high rates — see
186
+ * `docs/research/export-speed-stall.md`. It is a comparison switch, not a
187
+ * recommendation: the two paths select different frames, and which one is
188
+ * correct is what the comparison is for.
189
+ *
190
+ * @experimental
191
+ */
192
+ videoSpeedVia?: 'cadence' | 'media3'
172
193
  }
173
194
 
174
195
  /** Why an export will not start. @experimental */
@@ -357,9 +378,18 @@ export function createMedia3VideoExporter(options: Media3ExporterOptions): Video
357
378
  }
358
379
  const resources = overlayPlan.resources
359
380
 
381
+ /*
382
+ Whether a re-timed clip gets an emit plan at all.
383
+
384
+ Omitting it is how the cadence effect is left out: native builds one
385
+ only when both arrays arrive, so there is no second switch to keep in
386
+ step. A clip at 1× has nothing to re-time either way.
387
+ */
388
+ const viaMedia3 = options.videoSpeedVia === 'media3'
360
389
  const shards = plan.map((shard, index) => {
361
390
  const clip = playable[index]!
362
- const cadence = cadenceFor(shard.plan.frames)
391
+ const cadence =
392
+ viaMedia3 && clip.speed !== 1 ? null : cadenceFor(shard.plan.frames)
363
393
  return {
364
394
  uri: uris[index]!,
365
395
  startMs: Math.round(clip.sourceIn / 1000),
@@ -378,11 +408,19 @@ export function createMedia3VideoExporter(options: Media3ExporterOptions): Video
378
408
  What matters is that the *source's* audio never enters this pipeline:
379
409
  a per-shard audio track has to be re-encoded to concatenate, which is
380
410
  what put a second generation on everything the user hears.
411
+
412
+ `ExportGate` notes that `setRemoveAudio` beside a speed change is a
413
+ combination Media3 warns about, and that was tried as a candidate
414
+ fix for the high-rate export stall: giving the re-timing the real
415
+ stream instead of silence. It made no difference to the fault — the
416
+ encoder stopped at exactly the same place — so the change was
417
+ reverted rather than kept for a benefit that could not be shown.
418
+ See `docs/research/export-speed-stall.md`.
381
419
  */
382
420
  forceAudioTrack: true,
383
421
  removeAudio: true,
384
- emitsPerInput: cadence.emitsPerInput,
385
- outputPtsUs: cadence.outputPtsUs,
422
+ emitsPerInput: cadence?.emitsPerInput,
423
+ outputPtsUs: cadence?.outputPtsUs,
386
424
  spanUs: Math.round((shard.plan.frameCount * 1_000_000) / fps),
387
425
  }
388
426
  })