@pictor-sdk/video-android 0.1.4 → 0.1.6
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/CompositionSpike.kt +49 -9
- package/android/src/main/java/dev/pictor/video/ResumeReadiness.kt +82 -0
- package/dist/index.d.mts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +22 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +22 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/compositionSpike.ts +10 -0
- package/src/useCompositionPlayback.ts +69 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pictor-sdk/video-android",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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.
|
|
32
|
+
"@pictor-sdk/video-core": "^0.1.2"
|
|
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.
|
|
43
|
+
"@pictor-sdk/video-core": "0.1.2"
|
|
44
44
|
},
|
|
45
45
|
"codegenConfig": {
|
|
46
46
|
"name": "PictorVideoSpec",
|
package/src/compositionSpike.ts
CHANGED
|
@@ -106,6 +106,16 @@ export interface CompositionSpikeState {
|
|
|
106
106
|
releases?: number
|
|
107
107
|
readinessTimeouts?: number
|
|
108
108
|
readinessFailure?: string | null
|
|
109
|
+
/**
|
|
110
|
+
* How many resumes had to be re-primed because no frame could satisfy them.
|
|
111
|
+
*
|
|
112
|
+
* A resume whose target sits past the newest frame cannot be released on
|
|
113
|
+
* anything that exists, so playback seeks to the frame on screen to make the
|
|
114
|
+
* decoder produce one. Counted because it is a recovery, not a normal path:
|
|
115
|
+
* a number that climbs steadily is the renderer falling behind the transport
|
|
116
|
+
* more often than it should.
|
|
117
|
+
*/
|
|
118
|
+
primedResumes?: number
|
|
109
119
|
/** How long the last hold lasted before playback was released. */
|
|
110
120
|
holdMs?: number
|
|
111
121
|
/**
|
|
@@ -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,
|
|
@@ -104,6 +105,19 @@ export interface CompositionPlaybackState {
|
|
|
104
105
|
width: number
|
|
105
106
|
height: number
|
|
106
107
|
error: string | null
|
|
108
|
+
/**
|
|
109
|
+
* Why the transport refused to start, or null.
|
|
110
|
+
*
|
|
111
|
+
* Playback is released on a frame that shows the position asked for, and if
|
|
112
|
+
* no such frame arrives the hold times out and the transport stays stopped
|
|
113
|
+
* rather than starting on an unverified picture. That used to be invisible:
|
|
114
|
+
* the control stayed enabled, the press was accepted, and nothing happened,
|
|
115
|
+
* with no way for a host to tell that apart from a person who had not pressed
|
|
116
|
+
* it yet. Pressing play again clears it and tries afresh.
|
|
117
|
+
*
|
|
118
|
+
* @experimental
|
|
119
|
+
*/
|
|
120
|
+
readinessFailure: string | null
|
|
107
121
|
}
|
|
108
122
|
|
|
109
123
|
function composedState(snapshot: CompositionSpikeState): CompositionPlaybackState {
|
|
@@ -129,6 +143,7 @@ function composedState(snapshot: CompositionSpikeState): CompositionPlaybackStat
|
|
|
129
143
|
width: 0,
|
|
130
144
|
height: 0,
|
|
131
145
|
error: snapshot.error ?? null,
|
|
146
|
+
readinessFailure: snapshot.readinessFailure ?? null,
|
|
132
147
|
}
|
|
133
148
|
}
|
|
134
149
|
|
|
@@ -213,13 +228,66 @@ export function useCompositionPlayback(
|
|
|
213
228
|
setUnsupported(
|
|
214
229
|
refused.map((reason) => ({ clipId: reason.split(':')[0] ?? '', reason }) as UnsupportedSource),
|
|
215
230
|
)
|
|
216
|
-
|
|
231
|
+
/*
|
|
232
|
+
A project with nothing in it is a reconciliation too.
|
|
233
|
+
|
|
234
|
+
This used to return here, and returning is not nothing: the player is a
|
|
235
|
+
process-wide singleton that goes on playing whatever it last loaded. So
|
|
236
|
+
removing the only clip *while it was playing* left the removed clip
|
|
237
|
+
audible until it reached its own end — measured on a Pixel 7 as roughly
|
|
238
|
+
four more seconds of a six-second clip, three times out of three — with
|
|
239
|
+
the picture already cleared and the clock already reading 0:00.000,
|
|
240
|
+
because those follow the document and only the player does not.
|
|
241
|
+
|
|
242
|
+
Pausing is the whole fix. The composition is deliberately **not**
|
|
243
|
+
released: emptying a timeline is usually a step on the way to putting
|
|
244
|
+
something else in it, and `loaded.current` is left alone so that undoing
|
|
245
|
+
the removal reuses the composition already loaded instead of rebuilding
|
|
246
|
+
one that has not changed. Releasing is what `PlaybackLifecycle` does when
|
|
247
|
+
the *screen* goes, which is a different question from what the document
|
|
248
|
+
currently holds.
|
|
249
|
+
|
|
250
|
+
Nothing here touches the document. Playback stopping says nothing about
|
|
251
|
+
where the playhead should be.
|
|
252
|
+
*/
|
|
253
|
+
if (spec.items.length === 0) {
|
|
254
|
+
spikePause()
|
|
255
|
+
return
|
|
256
|
+
}
|
|
217
257
|
const started = spikeStart(spec) as { ok?: boolean }
|
|
218
258
|
// A refusal is not a load. Recording it as one is what left an editor
|
|
219
259
|
// waiting for an edit the person had already made.
|
|
220
260
|
if (started.ok === true) loaded.current = signature
|
|
221
261
|
}, [enabled, signature, surfaceGeneration, project, mix])
|
|
222
262
|
|
|
263
|
+
/*
|
|
264
|
+
Backgrounding pauses; returning re-arms.
|
|
265
|
+
|
|
266
|
+
The same wiring `useMedia3Playback` has, and its absence here was the same
|
|
267
|
+
bug `PlaybackLifecycle` was written to fix, reintroduced by a newer playback
|
|
268
|
+
path that never got it. `useVideoSurface` already retires the presentation
|
|
269
|
+
surface when the app leaves, so the picture stopped and the sound carried
|
|
270
|
+
on — measured on a Pixel 7 as a composition still reporting `playing` with
|
|
271
|
+
its position advancing from 1.3 s to 4.4 s across a press of Home.
|
|
272
|
+
|
|
273
|
+
Suspend rather than teardown: the picker comes up over the editor as a
|
|
274
|
+
background transition, and rebuilding a player the screen never lost would
|
|
275
|
+
be a stall nobody asked for. The document is not touched either way.
|
|
276
|
+
*/
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
const subscription = AppState.addEventListener('change', (next) => {
|
|
279
|
+
if (next === 'active') {
|
|
280
|
+
lifecycle.resume()
|
|
281
|
+
setLifecycleEpoch(lifecycle.epoch)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
lifecycle.suspend('background')
|
|
285
|
+
setLifecycleEpoch(lifecycle.epoch)
|
|
286
|
+
setPlaying(false)
|
|
287
|
+
})
|
|
288
|
+
return () => subscription.remove()
|
|
289
|
+
}, [lifecycle])
|
|
290
|
+
|
|
223
291
|
useEffect(() => {
|
|
224
292
|
return () => {
|
|
225
293
|
lifecycle.teardown('unmount')
|