@pictor-sdk/video-android 0.1.3 → 0.1.5
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/android/src/main/java/dev/pictor/video/CompositionSeek.kt +8 -0
- package/android/src/main/java/dev/pictor/video/CompositionSpike.kt +23 -13
- package/android/src/main/java/dev/pictor/video/CompositionVideoItem.kt +16 -0
- package/android/src/main/java/dev/pictor/video/CompositionWindow.kt +17 -0
- package/android/src/main/java/dev/pictor/video/FrameClock.kt +6 -1
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +23 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/compositionSpike.ts +9 -0
- package/src/useCompositionPlayback.ts +55 -1
package/package.json
CHANGED
package/src/compositionSpike.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { AppState } from 'react-native'
|
|
2
3
|
import {
|
|
3
4
|
playIntent,
|
|
4
5
|
playlistOf,
|
|
@@ -213,13 +214,66 @@ export function useCompositionPlayback(
|
|
|
213
214
|
setUnsupported(
|
|
214
215
|
refused.map((reason) => ({ clipId: reason.split(':')[0] ?? '', reason }) as UnsupportedSource),
|
|
215
216
|
)
|
|
216
|
-
|
|
217
|
+
/*
|
|
218
|
+
A project with nothing in it is a reconciliation too.
|
|
219
|
+
|
|
220
|
+
This used to return here, and returning is not nothing: the player is a
|
|
221
|
+
process-wide singleton that goes on playing whatever it last loaded. So
|
|
222
|
+
removing the only clip *while it was playing* left the removed clip
|
|
223
|
+
audible until it reached its own end — measured on a Pixel 7 as roughly
|
|
224
|
+
four more seconds of a six-second clip, three times out of three — with
|
|
225
|
+
the picture already cleared and the clock already reading 0:00.000,
|
|
226
|
+
because those follow the document and only the player does not.
|
|
227
|
+
|
|
228
|
+
Pausing is the whole fix. The composition is deliberately **not**
|
|
229
|
+
released: emptying a timeline is usually a step on the way to putting
|
|
230
|
+
something else in it, and `loaded.current` is left alone so that undoing
|
|
231
|
+
the removal reuses the composition already loaded instead of rebuilding
|
|
232
|
+
one that has not changed. Releasing is what `PlaybackLifecycle` does when
|
|
233
|
+
the *screen* goes, which is a different question from what the document
|
|
234
|
+
currently holds.
|
|
235
|
+
|
|
236
|
+
Nothing here touches the document. Playback stopping says nothing about
|
|
237
|
+
where the playhead should be.
|
|
238
|
+
*/
|
|
239
|
+
if (spec.items.length === 0) {
|
|
240
|
+
spikePause()
|
|
241
|
+
return
|
|
242
|
+
}
|
|
217
243
|
const started = spikeStart(spec) as { ok?: boolean }
|
|
218
244
|
// A refusal is not a load. Recording it as one is what left an editor
|
|
219
245
|
// waiting for an edit the person had already made.
|
|
220
246
|
if (started.ok === true) loaded.current = signature
|
|
221
247
|
}, [enabled, signature, surfaceGeneration, project, mix])
|
|
222
248
|
|
|
249
|
+
/*
|
|
250
|
+
Backgrounding pauses; returning re-arms.
|
|
251
|
+
|
|
252
|
+
The same wiring `useMedia3Playback` has, and its absence here was the same
|
|
253
|
+
bug `PlaybackLifecycle` was written to fix, reintroduced by a newer playback
|
|
254
|
+
path that never got it. `useVideoSurface` already retires the presentation
|
|
255
|
+
surface when the app leaves, so the picture stopped and the sound carried
|
|
256
|
+
on — measured on a Pixel 7 as a composition still reporting `playing` with
|
|
257
|
+
its position advancing from 1.3 s to 4.4 s across a press of Home.
|
|
258
|
+
|
|
259
|
+
Suspend rather than teardown: the picker comes up over the editor as a
|
|
260
|
+
background transition, and rebuilding a player the screen never lost would
|
|
261
|
+
be a stall nobody asked for. The document is not touched either way.
|
|
262
|
+
*/
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
const subscription = AppState.addEventListener('change', (next) => {
|
|
265
|
+
if (next === 'active') {
|
|
266
|
+
lifecycle.resume()
|
|
267
|
+
setLifecycleEpoch(lifecycle.epoch)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
lifecycle.suspend('background')
|
|
271
|
+
setLifecycleEpoch(lifecycle.epoch)
|
|
272
|
+
setPlaying(false)
|
|
273
|
+
})
|
|
274
|
+
return () => subscription.remove()
|
|
275
|
+
}, [lifecycle])
|
|
276
|
+
|
|
223
277
|
useEffect(() => {
|
|
224
278
|
return () => {
|
|
225
279
|
lifecycle.teardown('unmount')
|