@xorgate/react-native 0.1.0 → 0.2.1
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/CHANGELOG.md +117 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/dist/replay-engine.d.ts +193 -0
- package/dist/replay-engine.d.ts.map +1 -0
- package/dist/replay-engine.js +843 -0
- package/dist/replay-engine.js.map +1 -0
- package/dist/replay-video.d.ts +13 -0
- package/dist/replay-video.d.ts.map +1 -0
- package/dist/replay-video.js +66 -0
- package/dist/replay-video.js.map +1 -0
- package/dist/use-replay-player.d.ts +58 -0
- package/dist/use-replay-player.d.ts.map +1 -0
- package/dist/use-replay-player.js +108 -0
- package/dist/use-replay-player.js.map +1 -0
- package/package.json +4 -2
- package/readme.md +66 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,122 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
- **Fix: a replay lane froze for a whole segment at a time on Android.** The
|
|
6
|
+
seam is an opacity flip between two overlapping views, and `expo-video` draws
|
|
7
|
+
into a `SurfaceView` by default — a layer the system composites rather than
|
|
8
|
+
one the view hierarchy draws, so `opacity: 0` does not hide it and `zIndex`
|
|
9
|
+
does not order it against its sibling. The hidden PRELOAD therefore won: it
|
|
10
|
+
parked on the first frame of the next segment, that still frame covered the
|
|
11
|
+
player that was actually running, and the picture held for the whole segment
|
|
12
|
+
while the clock, the map and the readouts carried on. It looked like the
|
|
13
|
+
engine had stalled; the engine was never involved. `ReplayVideo` now renders
|
|
14
|
+
into a `TextureView` on Android (`surfaceType`), which is an ordinary view,
|
|
15
|
+
so alpha and z-order mean what they say. iOS is untouched — `AVPlayerLayer`
|
|
16
|
+
is a real layer and the prop is Android's alone.
|
|
17
|
+
|
|
18
|
+
Measured on an API 36 emulator against a six-segment recording, sampling the
|
|
19
|
+
lane every ~1.5 s for three minutes: **11 distinct frames out of 80 before,
|
|
20
|
+
79 out of 80 after**, over three segment boundaries either way. Each freeze
|
|
21
|
+
began within ~1.5 s of the hidden player reporting ready, and the LAST
|
|
22
|
+
segment of a replay always played correctly — by then there is nothing left
|
|
23
|
+
to preload on top of it. The engine's own trace was clean throughout, every
|
|
24
|
+
boundary a 0-131 ms seam.
|
|
25
|
+
|
|
26
|
+
## 0.2.0
|
|
27
|
+
|
|
28
|
+
Replay: the recorded-media player, over `expo-video`.
|
|
29
|
+
|
|
30
|
+
- **`@xorgate/react` `^0.2.1`**, up from `^0.2.0-next.1`. 0.2.1 is where
|
|
31
|
+
`useReplayTelemetry` takes the player CORE rather than the browser wrapper —
|
|
32
|
+
the change that lets a React Native player drive replay telemetry at all. The
|
|
33
|
+
0.2.0 tarball shipped with the looser `^0.2.0-next.1`, which resolves to 0.2.1
|
|
34
|
+
on any fresh install and is wrong only for a lockfile pinned at the
|
|
35
|
+
pre-release; the range was tightened as soon as 0.2.1 existed to be named.
|
|
36
|
+
- **`expo-video` is a new required peer.** It is the native player; there is no
|
|
37
|
+
way to play a recorded segment without it, and an optional peer whose absence
|
|
38
|
+
fails deep inside a lane is worse than one line of install (the same call
|
|
39
|
+
`expo-crypto` got in 0.1.0). Installing it moves the native fingerprint.
|
|
40
|
+
- `useReplayPlayer(manifest, options)` — `@xorgate/react`'s
|
|
41
|
+
`useReplayPlayerCore` with native media bound to every lane. The transport,
|
|
42
|
+
the clock, the lanes and `laneState` are the web package's; what is added is
|
|
43
|
+
`lanePlayer(streamKey)` and `laneMedia(streamKey)`, and the pool of players
|
|
44
|
+
behind them. `createVideoPlayer` is manual memory, so the hook creates a
|
|
45
|
+
pair per lane lazily and releases it when the lane goes or the hook unmounts.
|
|
46
|
+
- `<ReplayVideo player streamKey />` — both of a lane's views, the hidden one
|
|
47
|
+
at zero opacity, `nativeControls` off.
|
|
48
|
+
- `ExpoVideoReplayEngine` — one lane over two players, implementing
|
|
49
|
+
`@xorgate/react`'s `ReplayEngine`. A phone has no Media Source Extensions:
|
|
50
|
+
`AVPlayer` and `ExoPlayer` play a FILE, and a recorded segment is a
|
|
51
|
+
self-contained fMP4 that cannot be concatenated at runtime. So a lane is two
|
|
52
|
+
players and a boundary is an opacity flip. Four decisions the real segments
|
|
53
|
+
forced, all measured before the code was written:
|
|
54
|
+
- **The boundary is the manifest's** `effectiveDurationMs`, not the file's
|
|
55
|
+
end — a segment's file runs past the next one's start and playing that tail
|
|
56
|
+
would show the same wall clock twice. `playToEnd` is the fallback for the
|
|
57
|
+
opposite case, a file shorter than the manifest claims.
|
|
58
|
+
- **Readiness is the `statusChange` event, never the `replaceAsync`
|
|
59
|
+
promise**, which resolves with `status` still `loading`; `bufferedPosition`
|
|
60
|
+
can answer for the previous item until then.
|
|
61
|
+
- **Prepare early, promote on time.** The next segment is loaded and parked
|
|
62
|
+
on its first frame half a second out, and played only when the outgoing
|
|
63
|
+
player actually reaches the boundary — swapping at the lead would silently
|
|
64
|
+
drop it, once per segment.
|
|
65
|
+
- **A cross-segment seek always loads onto the hidden player**, so the
|
|
66
|
+
outgoing picture stays up for the 1.3-5.3 s the download takes instead of
|
|
67
|
+
going black.
|
|
68
|
+
- A failed segment load is probed with a one-byte ranged GET on the same
|
|
69
|
+
presigned URL, because a native player never reports the HTTP status: 403
|
|
70
|
+
becomes `onAuthError` (the manifest refresh, as in the browser engine) and
|
|
71
|
+
404/410 becomes a media error. Anything else retries.
|
|
72
|
+
- `useReplayPlayer(manifest, { debug })` — every decision a lane makes, as one
|
|
73
|
+
line, off by default. A seam is tens of milliseconds wide and the native
|
|
74
|
+
player is the half you cannot step through, so a boundary that did NOT
|
|
75
|
+
happen has to be able to leave evidence. It is what found the four defects
|
|
76
|
+
below, none of which any test had caught.
|
|
77
|
+
|
|
78
|
+
**Four things the device corrected, all at the same place: a segment boundary.**
|
|
79
|
+
On the first real 3-segment session a whole 60 s segment vanished — 57.7 s of a
|
|
80
|
+
145.6 s replay, silently — and it took all four lining up:
|
|
81
|
+
|
|
82
|
+
1. **A preloaded player is not parked at 0.** One was caught at 15.68 s, and
|
|
83
|
+
promoting it would have started that segment a quarter of the way in. On
|
|
84
|
+
iOS a playback RATE is the transport, so setting the rate on a paused
|
|
85
|
+
preload starts it playing. The rate now goes only on the player that is
|
|
86
|
+
about to play, and the incoming player is seeked unconditionally rather
|
|
87
|
+
than only for a non-zero offset.
|
|
88
|
+
2. **Promoting does not move the playhead.** The clock is paced BY the lane
|
|
89
|
+
and only learns the new position on its next tick, so the very next
|
|
90
|
+
`ensureAt` arrives with a timestamp a few milliseconds BEFORE the boundary
|
|
91
|
+
— which dragged the lane back into the segment it had just left, which the
|
|
92
|
+
next tick undid, and so on. A move back into a just-crossed segment is now
|
|
93
|
+
ignored for a second.
|
|
94
|
+
3. **The boundary must be released whether or not the player says so.** The
|
|
95
|
+
last `timeUpdate` of a segment lands 60-80 ms short of the file's end, and
|
|
96
|
+
`playToEnd` / `playingChange` follow a beat later. A timer armed when the
|
|
97
|
+
lead-in is prepared — for the expected end plus 120 ms — closes that, and
|
|
98
|
+
deliberately fires LATE: early skips real video, late only holds a frame.
|
|
99
|
+
A segment's end is `min(manifest, file)`, because a file can fall short of
|
|
100
|
+
what the manifest claims (60.000 s claimed, 59.987 s of media).
|
|
101
|
+
4. **A lane waiting on a prepared lead-in must not report itself stalled.**
|
|
102
|
+
It did, so the player core's stall recovery ran — and with the playhead
|
|
103
|
+
already inside the NEXT segment, that recovery seeks to the END of it.
|
|
104
|
+
That is the line that actually ate the 57.7 s.
|
|
105
|
+
|
|
106
|
+
Two more it showed, away from the boundary:
|
|
107
|
+
|
|
108
|
+
- **A seek holds the outgoing picture STILL**, not merely on screen. The
|
|
109
|
+
outgoing player used to keep playing for the seconds a cross-segment
|
|
110
|
+
download takes — six of them, on one measured seek — while the transport bar
|
|
111
|
+
already read the seek target, so the picture and the clock were describing
|
|
112
|
+
different moments. A boundary lead-in is deliberately exempt: there the
|
|
113
|
+
outgoing segment is still the right one, right up to its last frame.
|
|
114
|
+
- **A torn-down lane dies quietly.** The players belong to the hook, and one
|
|
115
|
+
that has been released throws `NotFoundException` on every access; the hook
|
|
116
|
+
now destroys engines before releasing players rather than trusting React's
|
|
117
|
+
cleanup order, and an engine handed a dead pair goes inert instead of
|
|
118
|
+
throwing into the tree.
|
|
119
|
+
|
|
3
120
|
## 0.1.0
|
|
4
121
|
|
|
5
122
|
First release: live telemetry and live video on React Native, over
|
package/dist/index.d.ts
CHANGED
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
* re-exported here, so an app imports from ONE package: the data hooks, live
|
|
6
6
|
* telemetry, the live-block policy, the replay timeline and clock, the
|
|
7
7
|
* telemetry math, the types. What this package adds is the runtime underneath
|
|
8
|
-
* them (`nativePlatform`, `XorgateNativeProvider`) and the
|
|
9
|
-
*
|
|
10
|
-
* react-native-webrtc's `RTCView
|
|
8
|
+
* them (`nativePlatform`, `XorgateNativeProvider`) and the pieces that cannot
|
|
9
|
+
* be neutral because they are pixels: `useLiveVideo` / `LiveVideo` over
|
|
10
|
+
* react-native-webrtc's `RTCView`, and `useReplayPlayer` / `ReplayVideo` over
|
|
11
|
+
* expo-video.
|
|
11
12
|
*
|
|
12
13
|
* The browser-only exports of `@xorgate/react` — its `useLiveVideo`,
|
|
13
14
|
* `LiveVideo`, `useReplayPlayer`, `ReplayVideo` and the `browser*` platform
|
|
14
15
|
* helpers — are deliberately NOT re-exported: they need `HTMLVideoElement`,
|
|
15
|
-
* `window` or Media Source Extensions, none of which exist here.
|
|
16
|
+
* `window` or Media Source Extensions, none of which exist here. The four
|
|
17
|
+
* names are SHADOWED by the native implementations above, so an app that
|
|
18
|
+
* imports from this package gets the one that works on a phone; importing
|
|
16
19
|
* `@xorgate/react` directly still works and is how you reach them on web.
|
|
17
20
|
*/
|
|
18
21
|
export { nativePlatform, type NativePlatformOptions } from "./platform.js";
|
|
@@ -21,5 +24,8 @@ export { appStateWake, composeWake, type WakeSource } from "./wake.js";
|
|
|
21
24
|
export { XorgateNativeProvider, type XorgateNativeProviderProps, } from "./provider.js";
|
|
22
25
|
export { useLiveVideo, type UseLiveVideo, type UseLiveVideoOptions, } from "./use-live-video.js";
|
|
23
26
|
export { LiveVideo, type LiveVideoProps } from "./live-video.js";
|
|
27
|
+
export { ExpoVideoReplayEngine, type ExpoVideoReplayEngineOptions, } from "./replay-engine.js";
|
|
28
|
+
export { useReplayPlayer, type UseReplayPlayer, type UseReplayPlayerNativeOptions, type ReplayLaneMedia, type ReplayLaneState, type ReplayLaneStatus, type UseReplayPlayerOptions, } from "./use-replay-player.js";
|
|
29
|
+
export { ReplayVideo, type ReplayVideoProps } from "./replay-video.js";
|
|
24
30
|
export { XorgateProvider, useXorgate, useXorgateTenancy, DEFAULT_BASE_URL, type XorgateProviderProps, type XorgateConfig, type XorgateAuth, type FirstPartyAuth, type SessionTokenAuth, type ApiKeyAuth, type LiveCredentialsAuth, type LiveCredentials, type LiveUnavailableReason, type Awaitable, type XorgatePlatform, XorgateError, isXorgateError, type Device, type DeviceModel, type LatestByMetric, type LatestReading, type MediaSession, type Me, type MetricName, type Organization, type PageMeta, type RecordingRun, type ReplayGap, type ReplayManifest, type ReplaySegment, type ReplaySession, type StreamKey, type TelemetryHistory, type TelemetryReading, type VideoChannel, type Workspace, type XorgateClient, useMe, useOrganizations, useWorkspaces, useDevices, useDevice, useDeviceModel, useTelemetryLatest, useTelemetryHistory, useVideoChannels, useRecordingRuns, useSessions, type QueryResult, type QueryOptions, type UseDevicesParams, type UseDevicesResult, type UseTelemetryHistoryParams, type UseSessionsParams, type UseRunsResult, type UseSessionsResult, useLiveTelemetry, useLivePlane, useLiveCredentials, parseTelemetryPayload, presignIotWssUrl, type UseLiveTelemetry, type UseLiveTelemetryOptions, type LiveStatus, type TelemetryPoint, type TelemetrySnapshot, type TelemetryFeedDeps, type MqttLikeClient, type MqttConnectionSpec, type ResolvedLiveCredentials, type ParsedTelemetryPayload, type TelemetryRecordingStatus, type TelemetryMediaStatus, type TelemetryStreamStatus, type LiveBlockPayload, useLiveBlock, resolveLiveBlock, liveBlockElapsedMs, LIVE_BLOCK_STALE_MS, type LiveBlockReason, type LiveBlockOpenReason, type LiveBlockState, type ResolveLiveBlockInput, useLiveVideoSession, type UseLiveVideoSession, type UseLiveVideoSessionOptions, type LiveVideoStatus, type LiveVideoStats, type WebRtcPlatform, type PeerConnectionLike, type SignalingLike, type TimerHost, useReplayManifest, useReplayPlayerCore, useReplayTelemetry, buildTimeline, segmentEnd, segmentAt, hasCoverageAt, gapAt, nextSegmentAfter, prevBoundary, nextBoundary, clampTs, buildLanes, buildClockTimeline, segmentUrlKey, ReplayClock, REPLAY_RATES, parseSegmentMediaInfo, seriesFromReadings, floorIndex, latestWithin, metricGroup, stalenessMs, overviewStalenessMs, chooseIntervalSeconds, windowBoundsFor, windowNeedsRefetch, buildTrace, traceLines, traveledLines, positionAt, GROUP_STALENESS_MS, DEFAULT_STALENESS_MS, WINDOW_SPAN_MS, WINDOW_MIN_SPAN_MS, type UseReplayManifestParams, type UseReplayManifestResult, type UseReplayPlayerCore, type UseReplayTelemetry, type UseReplayTelemetryOptions, type ReplayEngine, type ReplayEngineOptions, type ReplayEngineFactory, type ReplayTimeline, type ReplayLane, type LaneSegment, type ReplayClockState, type ReplayRate, type PacerCandidate, type GapSkipEvent, type SegmentMediaInfo, type MetricSeries, type GpsTrace, type WindowBounds, type PositionResolution, } from "@xorgate/react";
|
|
25
31
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
* re-exported here, so an app imports from ONE package: the data hooks, live
|
|
6
6
|
* telemetry, the live-block policy, the replay timeline and clock, the
|
|
7
7
|
* telemetry math, the types. What this package adds is the runtime underneath
|
|
8
|
-
* them (`nativePlatform`, `XorgateNativeProvider`) and the
|
|
9
|
-
*
|
|
10
|
-
* react-native-webrtc's `RTCView
|
|
8
|
+
* them (`nativePlatform`, `XorgateNativeProvider`) and the pieces that cannot
|
|
9
|
+
* be neutral because they are pixels: `useLiveVideo` / `LiveVideo` over
|
|
10
|
+
* react-native-webrtc's `RTCView`, and `useReplayPlayer` / `ReplayVideo` over
|
|
11
|
+
* expo-video.
|
|
11
12
|
*
|
|
12
13
|
* The browser-only exports of `@xorgate/react` — its `useLiveVideo`,
|
|
13
14
|
* `LiveVideo`, `useReplayPlayer`, `ReplayVideo` and the `browser*` platform
|
|
14
15
|
* helpers — are deliberately NOT re-exported: they need `HTMLVideoElement`,
|
|
15
|
-
* `window` or Media Source Extensions, none of which exist here.
|
|
16
|
+
* `window` or Media Source Extensions, none of which exist here. The four
|
|
17
|
+
* names are SHADOWED by the native implementations above, so an app that
|
|
18
|
+
* imports from this package gets the one that works on a phone; importing
|
|
16
19
|
* `@xorgate/react` directly still works and is how you reach them on web.
|
|
17
20
|
*/
|
|
18
21
|
// ---------------------------------------------------------------------------
|
|
@@ -28,6 +31,12 @@ export { XorgateNativeProvider, } from "./provider.js";
|
|
|
28
31
|
export { useLiveVideo, } from "./use-live-video.js";
|
|
29
32
|
export { LiveVideo } from "./live-video.js";
|
|
30
33
|
// ---------------------------------------------------------------------------
|
|
34
|
+
// Replay (expo-video)
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
export { ExpoVideoReplayEngine, } from "./replay-engine.js";
|
|
37
|
+
export { useReplayPlayer, } from "./use-replay-player.js";
|
|
38
|
+
export { ReplayVideo } from "./replay-video.js";
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
31
40
|
// Re-exported unchanged from `@xorgate/react`
|
|
32
41
|
// ---------------------------------------------------------------------------
|
|
33
42
|
export {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,8EAA8E;AAC9E,2BAA2B;AAC3B,8EAA8E;AAE9E,OAAO,EAAE,cAAc,EAA8B,MAAM,eAAe,CAAC;AAC3E,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAmB,MAAM,WAAW,CAAC;AACvE,OAAO,EACL,qBAAqB,GAEtB,MAAM,eAAe,CAAC;AAEvB,8EAA8E;AAC9E,uBAAuB;AACvB,8EAA8E;AAE9E,OAAO,EACL,YAAY,GAGb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAuB,MAAM,iBAAiB,CAAC;AAEjE,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E,OAAO,EACL,qBAAqB,GAEtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,eAAe,GAOhB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,WAAW,EAAyB,MAAM,mBAAmB,CAAC;AAEvE,8EAA8E;AAC9E,8CAA8C;AAC9C,8EAA8E;AAE9E,OAAO;AACL,uBAAuB;AACvB,eAAe,EACf,UAAU,EACV,iBAAiB,EACjB,gBAAgB;AAahB,oCAAoC;AACpC,YAAY,EACZ,cAAc;AAsBd,aAAa;AACb,KAAK,EACL,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,SAAS,EACT,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,WAAW;AAUX,oCAAoC;AACpC,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,qBAAqB,EACrB,gBAAgB;AAgBhB,oBAAoB;AACpB,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB;AAMnB,mDAAmD;AACnD,mBAAmB;AAUnB,4EAA4E;AAC5E,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,aAAa,EACb,UAAU,EACV,SAAS,EACT,aAAa,EACb,KAAK,EACL,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,OAAO,EACP,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,WAAW,EACX,YAAY,EACZ,qBAAqB,EACrB,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,WAAW,EACX,WAAW,EACX,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,kBAAkB,EAClB,UAAU,EACV,UAAU,EACV,aAAa,EACb,UAAU,EACV,kBAAkB,EAClB,oBAAoB,EACpB,cAAc,EACd,kBAAkB,GAqBnB,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { type ReplayEngine, type ReplayEngineOptions } from "@xorgate/react";
|
|
2
|
+
import type { VideoPlayer } from "expo-video";
|
|
3
|
+
export interface ExpoVideoReplayEngineOptions extends ReplayEngineOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The lane's two players. The engine drives them but does not own them:
|
|
6
|
+
* `useReplayPlayer` creates and releases the pair, the same way the browser
|
|
7
|
+
* engine is handed a `<video>` it did not create.
|
|
8
|
+
*/
|
|
9
|
+
players: readonly [VideoPlayer, VideoPlayer];
|
|
10
|
+
/** The player on screen changed. The hook re-renders so the views swap. */
|
|
11
|
+
onActivePlayerChange?: () => void;
|
|
12
|
+
/**
|
|
13
|
+
* Every decision this lane makes, as one line. Off by default and costing
|
|
14
|
+
* nothing when unset. A replay seam is tens of milliseconds wide and the
|
|
15
|
+
* native player is the half you cannot step through, so the only way to
|
|
16
|
+
* find out why a boundary did or did not happen is to have the engine say
|
|
17
|
+
* so at the time.
|
|
18
|
+
*/
|
|
19
|
+
onDebug?: (message: string) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One replay lane over two `expo-video` players (replay option A).
|
|
23
|
+
*
|
|
24
|
+
* The browser plays a lane as ONE `<video>` fed by Media Source Extensions:
|
|
25
|
+
* segments are appended into a single continuous buffer and there is no seam
|
|
26
|
+
* to manage. A phone has no MSE. `AVPlayer` and `ExoPlayer` play a FILE, and
|
|
27
|
+
* every recorded segment is a self-contained fMP4 (own `ftyp`+`moov`, `tfdt`
|
|
28
|
+
* from 0) — an ordinary playable MP4, and nothing that can be concatenated at
|
|
29
|
+
* runtime. So a lane is two players: one showing segment N while the other
|
|
30
|
+
* already holds N+1, and a boundary is an opacity swap between two views that
|
|
31
|
+
* are both mounted.
|
|
32
|
+
*
|
|
33
|
+
* Four things shape the code, all measured on real segments before it was
|
|
34
|
+
* written (SPIKES.md §4):
|
|
35
|
+
*
|
|
36
|
+
* - **The boundary is the MANIFEST's** (`effectiveDurationMs`), not the file's
|
|
37
|
+
* end: a segment's file routinely runs past the next segment's start
|
|
38
|
+
* (`fstat` anchors), and playing that tail would show the same wall clock
|
|
39
|
+
* twice. `playToEnd` is the fallback for the opposite case, a file SHORTER
|
|
40
|
+
* than the manifest claims.
|
|
41
|
+
* - **Readiness is an event, not a promise.** `replaceAsync` resolves with
|
|
42
|
+
* `status` still `loading`, and `bufferedPosition` can report the previous
|
|
43
|
+
* item's value for a beat afterwards. Nothing here trusts a player until
|
|
44
|
+
* `statusChange → readyToPlay`.
|
|
45
|
+
* - **Prepare early, promote on time.** The incoming player is loaded and
|
|
46
|
+
* parked on its first frame half a second before the boundary, but it is
|
|
47
|
+
* not played until the outgoing one actually reaches the boundary. That is
|
|
48
|
+
* what makes the seam a picture change with no playhead jump — swapping
|
|
49
|
+
* early would silently skip the lead, once per segment.
|
|
50
|
+
* - **A cross-segment seek is download-bound** (1.3-5.3 s for a 6-13 MB
|
|
51
|
+
* segment), so it always loads onto the HIDDEN player and the outgoing
|
|
52
|
+
* picture stays up until the new one has a frame. Replacing the visible
|
|
53
|
+
* player's source instead is seconds of black.
|
|
54
|
+
*
|
|
55
|
+
* Positions are lane-local seconds, as `ReplayEngine` requires; the wall clock
|
|
56
|
+
* stays the player core's business.
|
|
57
|
+
*/
|
|
58
|
+
export declare class ExpoVideoReplayEngine implements ReplayEngine {
|
|
59
|
+
private readonly timeline;
|
|
60
|
+
private readonly getUrl;
|
|
61
|
+
private readonly onError;
|
|
62
|
+
private readonly onUpdate;
|
|
63
|
+
private readonly onAuthError;
|
|
64
|
+
private readonly onActivePlayerChange;
|
|
65
|
+
private readonly onDebug;
|
|
66
|
+
private readonly slots;
|
|
67
|
+
private activeIdx;
|
|
68
|
+
private activation;
|
|
69
|
+
private boundaryTimer;
|
|
70
|
+
private wantPlay;
|
|
71
|
+
private rate;
|
|
72
|
+
private destroyed;
|
|
73
|
+
private firstFrameFired;
|
|
74
|
+
/** Lane-local seconds, kept so `position` is honest while nothing is ready. */
|
|
75
|
+
private lastPos;
|
|
76
|
+
/** The last position the active player was seen at, and when — see `watchStall`. */
|
|
77
|
+
private stalledAt;
|
|
78
|
+
private stalledSince;
|
|
79
|
+
/** The segment most recently left at a boundary, and when. See `activate`. */
|
|
80
|
+
private crossed;
|
|
81
|
+
private readonly failedAt;
|
|
82
|
+
private readonly probing;
|
|
83
|
+
private readonly abort;
|
|
84
|
+
private readonly waitingCbs;
|
|
85
|
+
private readonly firstFrameCbs;
|
|
86
|
+
constructor(opts: ExpoVideoReplayEngineOptions);
|
|
87
|
+
private makeSlot;
|
|
88
|
+
get position(): number;
|
|
89
|
+
get paused(): boolean;
|
|
90
|
+
/** The player whose picture belongs on screen. */
|
|
91
|
+
get activePlayer(): VideoPlayer;
|
|
92
|
+
/** Which of the two players `activePlayer` is. */
|
|
93
|
+
get activeIndex(): 0 | 1;
|
|
94
|
+
buffered(): Array<{
|
|
95
|
+
start: number;
|
|
96
|
+
end: number;
|
|
97
|
+
}>;
|
|
98
|
+
isBufferedAt(posSec: number): boolean;
|
|
99
|
+
isStalled(): boolean;
|
|
100
|
+
seek(posSec: number): void;
|
|
101
|
+
play(): void;
|
|
102
|
+
pause(): void;
|
|
103
|
+
setRate(rate: number): void;
|
|
104
|
+
/**
|
|
105
|
+
* Keep the media around `ts` — and the segment after it — loaded. Called at
|
|
106
|
+
* clock-notify rate and from the core's 500 ms pump, so every path is
|
|
107
|
+
* guarded: steady state does no work.
|
|
108
|
+
*/
|
|
109
|
+
ensureAt(ts: number): void;
|
|
110
|
+
on(event: "waiting" | "firstFrame", cb: () => void): () => void;
|
|
111
|
+
destroy(): void;
|
|
112
|
+
private debug;
|
|
113
|
+
/** One line of state, for a debug message that has to explain a decision. */
|
|
114
|
+
private snapshot;
|
|
115
|
+
private segPos;
|
|
116
|
+
private slotHolding;
|
|
117
|
+
/**
|
|
118
|
+
* Only the player on screen ever plays; the other one is preload.
|
|
119
|
+
*
|
|
120
|
+
* Every write below goes through JSI to a native object the hook owns, and
|
|
121
|
+
* a call on one that has been released throws `NotFoundException` rather
|
|
122
|
+
* than returning — so nothing here may run after `destroy()`.
|
|
123
|
+
*/
|
|
124
|
+
private applyTransport;
|
|
125
|
+
/**
|
|
126
|
+
* Bring `seg` to the front at `offsetSec`, always through the HIDDEN player
|
|
127
|
+
* so the picture on screen survives the load.
|
|
128
|
+
*/
|
|
129
|
+
private activate;
|
|
130
|
+
private cancelActivation;
|
|
131
|
+
/** Park the incoming player on the frame it will be promoted with. */
|
|
132
|
+
private prepare;
|
|
133
|
+
private maybePromote;
|
|
134
|
+
/** One segment of look-ahead on the hidden player — what makes a boundary free. */
|
|
135
|
+
/**
|
|
136
|
+
* Cross the boundary on time even if the outgoing player never says it is
|
|
137
|
+
* finished. Armed when the lead-in is prepared, for the moment the segment
|
|
138
|
+
* is expected to run out plus a short grace, and re-armed rather than fired
|
|
139
|
+
* if the player is somehow still behind that (a pause, a slower rate). An
|
|
140
|
+
* event almost always beats it; what it buys is that none of them has to,
|
|
141
|
+
* and that the swap lands AFTER the end rather than before it — a timer
|
|
142
|
+
* that fires early skips real video, one that fires late holds a frame.
|
|
143
|
+
*/
|
|
144
|
+
private armBoundaryTimer;
|
|
145
|
+
private onBoundaryTimer;
|
|
146
|
+
private clearBoundaryTimer;
|
|
147
|
+
private prefetchAfter;
|
|
148
|
+
private load;
|
|
149
|
+
/**
|
|
150
|
+
* The player on screen has stopped advancing while the transport says play.
|
|
151
|
+
* That is a segment that has run out of file without saying so — and it is
|
|
152
|
+
* the state that must never be left alone, because the player core's stall
|
|
153
|
+
* recovery reads the playhead (already inside the NEXT segment) rather than
|
|
154
|
+
* the media, and jumps to the end of a segment that was never shown.
|
|
155
|
+
*/
|
|
156
|
+
private watchStall;
|
|
157
|
+
private markReady;
|
|
158
|
+
/**
|
|
159
|
+
* The one case `statusChange` cannot report: a player that was already
|
|
160
|
+
* `readyToPlay` and whose new item loaded without the status ever leaving
|
|
161
|
+
* that value. Runs off the core's pump, so it costs nothing in steady state.
|
|
162
|
+
*/
|
|
163
|
+
private reconcileReady;
|
|
164
|
+
private onTimeUpdate;
|
|
165
|
+
private onPlayToEnd;
|
|
166
|
+
/**
|
|
167
|
+
* The outgoing segment is finished, however we found out. With a lead-in
|
|
168
|
+
* already prepared this swaps; without one — the file ran out well before
|
|
169
|
+
* the manifest said it would, which `fstat`-anchored rows do — it starts
|
|
170
|
+
* the next segment now rather than leave the clock on a player that will
|
|
171
|
+
* never advance again.
|
|
172
|
+
*/
|
|
173
|
+
private releaseBoundary;
|
|
174
|
+
/**
|
|
175
|
+
* Where this player's segment really ends, in its own seconds: the manifest
|
|
176
|
+
* duration, or the file's if the file is shorter. A file that outruns the
|
|
177
|
+
* manifest must be cut at the manifest (its tail is the next segment's wall
|
|
178
|
+
* clock); a file that falls short can only be played to its own end, and
|
|
179
|
+
* waiting for a boundary it can never reach is what freezes a lane.
|
|
180
|
+
*/
|
|
181
|
+
private endOf;
|
|
182
|
+
private contiguousNext;
|
|
183
|
+
private onStatusChange;
|
|
184
|
+
/**
|
|
185
|
+
* A native player reports that a source would not load, and never why: the
|
|
186
|
+
* HTTP status is not on the error. A one-byte ranged GET against the same
|
|
187
|
+
* presigned URL is the cheapest way to tell an expired signature (403 —
|
|
188
|
+
* refresh the manifest, exactly as the browser engine does) from a network
|
|
189
|
+
* blip (retry), and it only ever runs on a failure.
|
|
190
|
+
*/
|
|
191
|
+
private onLoadFailure;
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=replay-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-engine.d.ts","sourceRoot":"","sources":["../src/replay-engine.ts"],"names":[],"mappings":"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"}
|