react-native-tv-audio-description 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 (57) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +21 -0
  3. package/PLATFORM.md +262 -0
  4. package/README.md +324 -0
  5. package/lib/ADControls.d.ts +24 -0
  6. package/lib/ADControls.js +41 -0
  7. package/lib/ADControls.js.map +1 -0
  8. package/lib/CueScheduler.d.ts +49 -0
  9. package/lib/CueScheduler.js +114 -0
  10. package/lib/CueScheduler.js.map +1 -0
  11. package/lib/DescriptionAudio.d.ts +36 -0
  12. package/lib/DescriptionAudio.js +88 -0
  13. package/lib/DescriptionAudio.js.map +1 -0
  14. package/lib/MediaAdapter.d.ts +123 -0
  15. package/lib/MediaAdapter.js +3 -0
  16. package/lib/MediaAdapter.js.map +1 -0
  17. package/lib/TrackLoader.d.ts +48 -0
  18. package/lib/TrackLoader.js +129 -0
  19. package/lib/TrackLoader.js.map +1 -0
  20. package/lib/budget.d.ts +41 -0
  21. package/lib/budget.js +60 -0
  22. package/lib/budget.js.map +1 -0
  23. package/lib/duck.d.ts +21 -0
  24. package/lib/duck.js +40 -0
  25. package/lib/duck.js.map +1 -0
  26. package/lib/index.d.ts +10 -0
  27. package/lib/index.js +33 -0
  28. package/lib/index.js.map +1 -0
  29. package/lib/log.d.ts +17 -0
  30. package/lib/log.js +18 -0
  31. package/lib/log.js.map +1 -0
  32. package/lib/messages.d.ts +21 -0
  33. package/lib/messages.js +17 -0
  34. package/lib/messages.js.map +1 -0
  35. package/lib/track.d.ts +44 -0
  36. package/lib/track.js +22 -0
  37. package/lib/track.js.map +1 -0
  38. package/lib/vega/SegmentBuffer.d.ts +87 -0
  39. package/lib/vega/SegmentBuffer.js +133 -0
  40. package/lib/vega/SegmentBuffer.js.map +1 -0
  41. package/lib/vega/index.d.ts +33 -0
  42. package/lib/vega/index.js +280 -0
  43. package/lib/vega/index.js.map +1 -0
  44. package/package.json +89 -0
  45. package/src/ADControls.tsx +92 -0
  46. package/src/CueScheduler.ts +145 -0
  47. package/src/DescriptionAudio.ts +97 -0
  48. package/src/MediaAdapter.ts +136 -0
  49. package/src/TrackLoader.ts +156 -0
  50. package/src/budget.ts +61 -0
  51. package/src/duck.ts +42 -0
  52. package/src/index.ts +42 -0
  53. package/src/log.ts +30 -0
  54. package/src/messages.ts +27 -0
  55. package/src/track.ts +52 -0
  56. package/src/vega/SegmentBuffer.ts +195 -0
  57. package/src/vega/index.tsx +382 -0
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stateMessage = void 0;
4
+ exports.ADControls = ADControls;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("react");
7
+ const react_native_1 = require("react-native");
8
+ const track_1 = require("./track");
9
+ const log_1 = require("./log");
10
+ const messages_1 = require("./messages");
11
+ var messages_2 = require("./messages");
12
+ Object.defineProperty(exports, "stateMessage", { enumerable: true, get: function () { return messages_2.stateMessage; } });
13
+ function ADControls({ state, onToggle, onVerbosity, focusRef }) {
14
+ const lastSpoken = (0, react_1.useRef)('');
15
+ // Announce on entry and on every state change, once each. The guard is not
16
+ // an optimisation — re-announcing an unchanged state talks over the film for
17
+ // no reason, and this screen is already competing for the one channel its
18
+ // users have.
19
+ (0, react_1.useEffect)(() => {
20
+ const message = (0, messages_1.stateMessage)(state);
21
+ if (message === lastSpoken.current)
22
+ return;
23
+ lastSpoken.current = message;
24
+ react_native_1.AccessibilityInfo.announceForAccessibility(message);
25
+ (0, log_1.log)(`controls.announce kind=${state.kind}`);
26
+ }, [state]);
27
+ const toggle = (0, react_1.useCallback)(() => {
28
+ if (state.kind !== 'ready')
29
+ return;
30
+ (0, log_1.log)(`controls.toggle to=${!state.enabled}`);
31
+ onToggle(!state.enabled); // the scheduler flips; the video is untouched
32
+ }, [state, onToggle]);
33
+ if (state.kind !== 'ready') {
34
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { accessible: true, accessibilityRole: "alert", accessibilityLabel: (0, messages_1.stateMessage)(state), children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { children: (0, messages_1.stateMessage)(state) }) }));
35
+ }
36
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { children: [(0, jsx_runtime_1.jsx)(react_native_1.Pressable, { ref: focusRef, accessible: true, accessibilityRole: "switch", accessibilityLabel: "Audio description", accessibilityState: { checked: state.enabled }, onPress: toggle, hasTVPreferredFocus: true, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { children: state.enabled ? 'Description: on' : 'Description: off' }) }), track_1.VERBOSITY_LEVELS.map((level) => ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessible: true, accessibilityRole: "radio", accessibilityLabel: `${level} description`, accessibilityState: { selected: state.verbosity === level, disabled: !state.enabled }, disabled: !state.enabled, onPress: () => {
37
+ (0, log_1.log)(`controls.verbosity to=${level}`);
38
+ onVerbosity(level);
39
+ }, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { children: level }) }, level)))] }));
40
+ }
41
+ //# sourceMappingURL=ADControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ADControls.js","sourceRoot":"","sources":["../src/ADControls.tsx"],"names":[],"mappings":";;;AA6BA,gCA8DC;;AA3FD,iCAA8D;AAC9D,+CAAwE;AAExE,mCAA2C;AAC3C,+BAA4B;AAC5B,yCAAwD;AAExD,uCAAwD;AAA/C,wGAAA,YAAY,OAAA;AAsBrB,SAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAmB;IACpF,MAAM,UAAU,GAAG,IAAA,cAAM,EAAS,EAAE,CAAC,CAAC;IAEtC,2EAA2E;IAC3E,6EAA6E;IAC7E,0EAA0E;IAC1E,cAAc;IACd,IAAA,iBAAS,EAAC,GAAG,EAAE;QACb,MAAM,OAAO,GAAG,IAAA,uBAAY,EAAC,KAAK,CAAC,CAAC;QACpC,IAAI,OAAO,KAAK,UAAU,CAAC,OAAO;YAAE,OAAO;QAC3C,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC;QAC7B,gCAAiB,CAAC,wBAAwB,CAAC,OAAO,CAAC,CAAC;QACpD,IAAA,SAAG,EAAC,0BAA0B,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAC9C,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAEZ,MAAM,MAAM,GAAG,IAAA,mBAAW,EAAC,GAAG,EAAE;QAC9B,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;YAAE,OAAO;QACnC,IAAA,SAAG,EAAC,sBAAsB,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAC5C,QAAQ,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,8CAA8C;IAC1E,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;IAEtB,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC3B,OAAO,CACL,uBAAC,mBAAI,IAAC,UAAU,QAAC,iBAAiB,EAAC,OAAO,EAAC,kBAAkB,EAAE,IAAA,uBAAY,EAAC,KAAK,CAAC,YAChF,uBAAC,mBAAI,cAAE,IAAA,uBAAY,EAAC,KAAK,CAAC,GAAQ,GAC7B,CACR,CAAC;IACJ,CAAC;IAED,OAAO,CACL,wBAAC,mBAAI,eACH,uBAAC,wBAAS,IACR,GAAG,EAAE,QAAQ,EACb,UAAU,QACV,iBAAiB,EAAC,QAAQ,EAC1B,kBAAkB,EAAC,mBAAmB,EACtC,kBAAkB,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,EAC9C,OAAO,EAAE,MAAM,EACf,mBAAmB,kBAEnB,uBAAC,mBAAI,cAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,kBAAkB,GAAQ,GAC3D,EAGX,wBAAgB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAC/B,uBAAC,wBAAS,IAER,UAAU,QACV,iBAAiB,EAAC,OAAO,EACzB,kBAAkB,EAAE,GAAG,KAAK,cAAc,EAC1C,kBAAkB,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,KAAK,KAAK,EAAE,QAAQ,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EACrF,QAAQ,EAAE,CAAC,KAAK,CAAC,OAAO,EACxB,OAAO,EAAE,GAAG,EAAE;oBACZ,IAAA,SAAG,EAAC,yBAAyB,KAAK,EAAE,CAAC,CAAC;oBACtC,WAAW,CAAC,KAAK,CAAC,CAAC;gBACrB,CAAC,YAED,uBAAC,mBAAI,cAAE,KAAK,GAAQ,IAXf,KAAK,CAYA,CACb,CAAC,IACG,CACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,49 @@
1
+ import type { DescriptionCue } from './track';
2
+ /**
3
+ * Playback position becomes "fire this cue now, once".
4
+ *
5
+ * The invariant this file exists to hold: a cue fires only while position is
6
+ * inside its own window. A cue whose window has already passed is DROPPED,
7
+ * never played late — a late cue is a cue playing over dialogue, which is the
8
+ * one thing worse than no description.
9
+ */
10
+ export interface SchedulerEvents {
11
+ onFire: (cue: DescriptionCue) => void;
12
+ }
13
+ export interface SchedulerOptions {
14
+ /**
15
+ * How long a cue needs, from firing to the film being back at full. A cue
16
+ * is only fired if that much of its window is left. Default: its words at
17
+ * the narration pace plus the fade down and up — pass a better figure if you
18
+ * have the clips' real durations.
19
+ */
20
+ estimateMs?: (cue: DescriptionCue) => number;
21
+ }
22
+ /** words at the narration pace, plus the fade down and the fade back up */
23
+ export declare function estimateCueMs(cue: DescriptionCue): number;
24
+ export declare class CueScheduler {
25
+ private readonly events;
26
+ private cues;
27
+ private cursor;
28
+ private firing;
29
+ private enabled;
30
+ private readonly estimateMs;
31
+ constructor(events: SchedulerEvents, options?: SchedulerOptions);
32
+ /** `failed` cues carry no audio; they never enter the schedule. */
33
+ load(cues: DescriptionCue[]): void;
34
+ setEnabled(on: boolean): void;
35
+ /** Called on every position update from the platform. */
36
+ tick(positionMs: number): void;
37
+ /**
38
+ * Called with the SETTLED position after a seek — not with each
39
+ * intermediate position a held direction produces. Resets the cursor to the
40
+ * first cue that has not closed yet, so no backlog fires.
41
+ */
42
+ resync(positionMs: number): void;
43
+ }
44
+ /**
45
+ * The input half of seeking. A held D-pad direction emits a stream of key
46
+ * events; acting on each one produces a seek storm and a resync per event.
47
+ * This defers the action until the stream stops for `quietMs`.
48
+ */
49
+ export declare function coalesce(fn: (value: number) => void, quietMs?: number): (value: number) => void;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CueScheduler = void 0;
4
+ exports.estimateCueMs = estimateCueMs;
5
+ exports.coalesce = coalesce;
6
+ const budget_1 = require("./budget");
7
+ const log_1 = require("./log");
8
+ /** words at the narration pace, plus the fade down and the fade back up */
9
+ function estimateCueMs(cue) {
10
+ return (cue.words / budget_1.AD.SPEAKING_RATE_WPM) * 60000 + 2 * budget_1.AD.DUCK_RAMP_MS;
11
+ }
12
+ class CueScheduler {
13
+ constructor(events, options = {}) {
14
+ this.events = events;
15
+ this.cues = [];
16
+ this.cursor = 0;
17
+ this.firing = null;
18
+ this.enabled = true;
19
+ this.estimateMs = options.estimateMs ?? estimateCueMs;
20
+ }
21
+ /** `failed` cues carry no audio; they never enter the schedule. */
22
+ load(cues) {
23
+ this.cues = cues
24
+ .filter((c) => c.status === 'ok' && c.audio_uri !== '')
25
+ .sort((a, b) => a.start_ms - b.start_ms);
26
+ this.cursor = 0;
27
+ this.firing = null;
28
+ (0, log_1.log)(`scheduler.load cues=${this.cues.length}`);
29
+ }
30
+ setEnabled(on) {
31
+ this.enabled = on;
32
+ if (!on)
33
+ this.firing = null;
34
+ }
35
+ /** Called on every position update from the platform. */
36
+ tick(positionMs) {
37
+ if (!this.enabled)
38
+ return;
39
+ // Advance past every cue whose window closed while we were elsewhere.
40
+ while (this.cursor < this.cues.length && this.cues[this.cursor].end_ms <= positionMs) {
41
+ const skipped = this.cues[this.cursor];
42
+ if (skipped !== this.firing) {
43
+ (0, log_1.log)(`scheduler.skip id=${skipped.id} pos_ms=${positionMs}`);
44
+ }
45
+ this.cursor++;
46
+ }
47
+ const next = this.cues[this.cursor];
48
+ if (!next)
49
+ return;
50
+ // Fire only INSIDE the window, and only if the cue can FINISH there. A cue
51
+ // reached late — description switched back on, a seek into the middle of a
52
+ // gap — would otherwise still be speaking when dialogue resumes. Measured
53
+ // on the Vega Virtual Device: switched on 2.5 s before its window closed, a
54
+ // 2.6 s cue brought the film back up half a second into the next line.
55
+ if (positionMs >= next.start_ms && positionMs < next.end_ms && this.firing !== next) {
56
+ const remaining = next.end_ms - positionMs;
57
+ const needs = Math.round(this.estimateMs(next));
58
+ if (remaining < needs) {
59
+ this.cursor++;
60
+ (0, log_1.log)(`scheduler.skip id=${next.id} pos_ms=${positionMs} reason=late` +
61
+ ` remaining_ms=${remaining} needs_ms=${needs}`);
62
+ return;
63
+ }
64
+ this.firing = next;
65
+ this.cursor++;
66
+ (0, log_1.log)(`scheduler.fire id=${next.id} pos_ms=${positionMs}` +
67
+ ` window=[${next.start_ms},${next.end_ms})`);
68
+ this.events.onFire(next);
69
+ }
70
+ }
71
+ /**
72
+ * Called with the SETTLED position after a seek — not with each
73
+ * intermediate position a held direction produces. Resets the cursor to the
74
+ * first cue that has not closed yet, so no backlog fires.
75
+ */
76
+ resync(positionMs) {
77
+ let lo = 0;
78
+ let hi = this.cues.length;
79
+ while (lo < hi) {
80
+ const mid = (lo + hi) >> 1;
81
+ if (this.cues[mid].end_ms <= positionMs)
82
+ lo = mid + 1;
83
+ else
84
+ hi = mid;
85
+ }
86
+ this.cursor = lo;
87
+ this.firing = null;
88
+ (0, log_1.log)(`scheduler.resync pos_ms=${positionMs} cursor=${this.cursor}`);
89
+ }
90
+ }
91
+ exports.CueScheduler = CueScheduler;
92
+ /**
93
+ * The input half of seeking. A held D-pad direction emits a stream of key
94
+ * events; acting on each one produces a seek storm and a resync per event.
95
+ * This defers the action until the stream stops for `quietMs`.
96
+ */
97
+ function coalesce(fn, quietMs = 250) {
98
+ let timer = null;
99
+ let pending = 0;
100
+ let events = 0;
101
+ return (value) => {
102
+ pending = value;
103
+ events++;
104
+ if (timer)
105
+ clearTimeout(timer);
106
+ timer = setTimeout(() => {
107
+ (0, log_1.log)(`scheduler.coalesced events=${events} settled_ms=${pending}`);
108
+ events = 0;
109
+ timer = null;
110
+ fn(pending);
111
+ }, quietMs);
112
+ };
113
+ }
114
+ //# sourceMappingURL=CueScheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CueScheduler.js","sourceRoot":"","sources":["../src/CueScheduler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { MediaAdapter } from './MediaAdapter';
2
+ import type { DescriptionCue } from './track';
3
+ export interface DescriptionAudioOptions {
4
+ /** main track level while a cue speaks, % of full (default 25) */
5
+ duckTargetPct?: number;
6
+ /** fade length into and out of the duck (default 200) */
7
+ rampMs?: number;
8
+ }
9
+ /**
10
+ * Duck, speak, restore.
11
+ *
12
+ * The cue plays as a SECOND stream over the film rather than by switching to
13
+ * a pre-mixed track. On Vega that was measured to work: a video player and an
14
+ * audio player built for accessibility speech ran simultaneously with zero
15
+ * dropped frames (PLATFORM.md `concurrent_streams`).
16
+ */
17
+ export declare class DescriptionAudio {
18
+ private readonly media;
19
+ private active;
20
+ /**
21
+ * Bumped by `stop()`. A cue in flight when the screen unmounts keeps running
22
+ * — its `await` chain does not know the component is gone — and its
23
+ * `finally` would then ramp the volume on a player that has been destroyed,
24
+ * after `stop()` already restored it.
25
+ */
26
+ private generation;
27
+ private readonly duckPct;
28
+ private readonly rampMs;
29
+ constructor(media: MediaAdapter, options?: DescriptionAudioOptions);
30
+ speak(cue: DescriptionCue): Promise<void>;
31
+ /**
32
+ * Stop the cue that is speaking, if any, and bring the film back to full.
33
+ * Safe to call at any time — turning description off, leaving the screen.
34
+ */
35
+ stop(): Promise<void>;
36
+ }
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DescriptionAudio = void 0;
4
+ const budget_1 = require("./budget");
5
+ const duck_1 = require("./duck");
6
+ const log_1 = require("./log");
7
+ /**
8
+ * Duck, speak, restore.
9
+ *
10
+ * The cue plays as a SECOND stream over the film rather than by switching to
11
+ * a pre-mixed track. On Vega that was measured to work: a video player and an
12
+ * audio player built for accessibility speech ran simultaneously with zero
13
+ * dropped frames (PLATFORM.md `concurrent_streams`).
14
+ */
15
+ class DescriptionAudio {
16
+ constructor(media, options = {}) {
17
+ this.media = media;
18
+ this.active = false;
19
+ /**
20
+ * Bumped by `stop()`. A cue in flight when the screen unmounts keeps running
21
+ * — its `await` chain does not know the component is gone — and its
22
+ * `finally` would then ramp the volume on a player that has been destroyed,
23
+ * after `stop()` already restored it.
24
+ */
25
+ this.generation = 0;
26
+ this.duckPct = options.duckTargetPct ?? budget_1.AD.DUCK_TARGET_PCT;
27
+ this.rampMs = options.rampMs ?? budget_1.AD.DUCK_RAMP_MS;
28
+ // Backgrounding during a cue must stop the clip and restore the main
29
+ // level, or the app returns to the foreground ducked and silent.
30
+ this.media.lifecycle.onBackground(() => {
31
+ if (!this.active)
32
+ return;
33
+ (0, log_1.log)('audio.background active=true');
34
+ void this.stop();
35
+ });
36
+ }
37
+ async speak(cue) {
38
+ if (this.active) {
39
+ // a cue already speaking is never interrupted by another — but the one
40
+ // that lost is said out loud in the log, not dropped in silence
41
+ (0, log_1.log)(`audio.busy id=${cue.id}`);
42
+ return;
43
+ }
44
+ this.active = true;
45
+ const mine = this.generation;
46
+ const stale = () => mine !== this.generation;
47
+ try {
48
+ (0, log_1.log)(`audio.duck id=${cue.id} to_pct=${this.duckPct}`);
49
+ await (0, duck_1.rampVolumePct)(this.media.video, 100, this.duckPct, this.rampMs, stale);
50
+ await this.media.clips.play(cue.audio_uri);
51
+ (0, log_1.log)(stale() ? `audio.interrupted id=${cue.id}` : `audio.spoke id=${cue.id} words=${cue.words}`);
52
+ }
53
+ catch (err) {
54
+ (0, log_1.log)(`audio.failed id=${cue.id} err=${err.message}`);
55
+ }
56
+ finally {
57
+ // The main track ALWAYS returns to full, including on failure. A cue
58
+ // that fails must not leave the film at 25% for the rest of the runtime.
59
+ //
60
+ // Unless stop() already did it: then this cue is stale, the player may
61
+ // be torn down, and ramping again is work against a dead object.
62
+ if (!stale()) {
63
+ await (0, duck_1.rampVolumePct)(this.media.video, this.duckPct, 100, this.rampMs, stale);
64
+ (0, log_1.log)(`audio.restored id=${cue.id}`);
65
+ this.active = false;
66
+ }
67
+ }
68
+ }
69
+ /**
70
+ * Stop the cue that is speaking, if any, and bring the film back to full.
71
+ * Safe to call at any time — turning description off, leaving the screen.
72
+ */
73
+ async stop() {
74
+ this.generation++; // anything in flight is now stale and must not restore
75
+ this.media.clips.stop();
76
+ // Nothing speaking means the film is already at full. Ramping "back" from
77
+ // the duck level anyway would first SET it to the duck level: an audible
78
+ // dip every time description is switched off between cues.
79
+ if (!this.active)
80
+ return;
81
+ (0, log_1.log)('audio.stopped');
82
+ await (0, duck_1.rampVolumePct)(this.media.video, this.duckPct, 100, this.rampMs);
83
+ this.active = false;
84
+ (0, log_1.log)('audio.restored after=stop');
85
+ }
86
+ }
87
+ exports.DescriptionAudio = DescriptionAudio;
88
+ //# sourceMappingURL=DescriptionAudio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DescriptionAudio.js","sourceRoot":"","sources":["../src/DescriptionAudio.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,123 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
3
+ /**
4
+ * The only surface this library uses to reach a platform's media stack.
5
+ *
6
+ * Everything outside `src/vega/` is written against this interface and never
7
+ * against a platform package; a test fails if that stops being true. Bring
8
+ * your own implementation for another platform, or use the Vega one:
9
+ *
10
+ * import { createVegaAdapter } from 'react-native-tv-audio-description/vega';
11
+ *
12
+ * Each platform defect the Vega implementation works around is kept OUT of
13
+ * this interface on purpose — a caller that had to know about them would leak
14
+ * one platform's defects into every other. They are listed in PLATFORM.md.
15
+ */
16
+ export type Unsubscribe = () => void;
17
+ /** one piece of an asset, with times read from the emitted playlist */
18
+ export interface AssetSegment {
19
+ index: number;
20
+ start_ms: number;
21
+ end_ms: number;
22
+ uri: string;
23
+ }
24
+ /**
25
+ * What to play, and how it is cut up.
26
+ *
27
+ * ALWAYS A LIST, even for a single file — a whole-file asset is one segment
28
+ * with no init. One code path for both shapes means a short clip and a
29
+ * feature-length film exercise the same buffering logic, rather than the
30
+ * feature-length one taking a path nothing has ever run.
31
+ */
32
+ export interface AssetSource {
33
+ /** appended once, before any segment; absent for a whole-file asset */
34
+ initUri?: string;
35
+ segments: AssetSegment[];
36
+ }
37
+ export interface VideoPlayer {
38
+ /**
39
+ * Point the player at an asset and get it ready to play.
40
+ *
41
+ * Implementations must NOT assume the platform will fetch anything. On Vega
42
+ * it will not (PLATFORM.md `url_mode_broken`) and the implementation reads
43
+ * the bytes itself — nor can it read PART of a file (`no_range_requests`),
44
+ * which is why the asset arrives already cut into segments.
45
+ */
46
+ open(source: AssetSource): Promise<void>;
47
+ play(): Promise<void>;
48
+ pause(): void;
49
+ /** current playback position, ms */
50
+ positionMs(): number;
51
+ durationMs(): number;
52
+ isPlaying(): boolean;
53
+ /**
54
+ * Set the main track volume as a percentage of full, EFFECTIVE IMMEDIATELY.
55
+ *
56
+ * There is no ramp parameter, and its absence is measured rather than
57
+ * chosen: the W3C volume setter is instantaneous and Vega exposes no fade
58
+ * (PLATFORM.md `no_volume_ramp`). The fade is implemented once, in JS, in
59
+ * `duck.ts`. An interface that accepted `rampMs` would invite every
60
+ * implementation to reimplement the same loop and would imply a capability
61
+ * no platform here has.
62
+ */
63
+ setVolumePct(pct: number): void;
64
+ /** fires on every position update the platform emits ('timeupdate') */
65
+ onPosition(cb: (ms: number) => void): Unsubscribe;
66
+ /** fires after a seek settles, with the new position ('seeked') */
67
+ onSeek(cb: (ms: number) => void): Unsubscribe;
68
+ /**
69
+ * Fires when playback cannot continue because the buffer ran dry
70
+ * ('waiting' / 'stalled').
71
+ *
72
+ * With the app owning byte delivery this is a REACHABLE state and it is NOT
73
+ * an error — no `error` event follows it. A UI that only listens for
74
+ * `onError` shows a frozen picture and says nothing, which for a blind
75
+ * viewer is indistinguishable from a quiet scene.
76
+ */
77
+ onStalled(cb: () => void): Unsubscribe;
78
+ /**
79
+ * Fires when playback is actually running ('playing').
80
+ *
81
+ * The counterpart to `onStalled`, and not optional: MSE emits `waiting` at
82
+ * the START of normal playback while the first frames decode, so a screen
83
+ * that treats stalling as terminal announces "Buffering" over a film that is
84
+ * playing fine (PLATFORM.md `waiting_fires_at_start`).
85
+ */
86
+ onPlaying(cb: () => void): Unsubscribe;
87
+ onEnded(cb: () => void): Unsubscribe;
88
+ onError(cb: (err: Error) => void): Unsubscribe;
89
+ /** release everything; safe to call twice */
90
+ destroy(): Promise<void>;
91
+ }
92
+ export interface ClipPlayer {
93
+ /**
94
+ * Plays one description clip to completion; rejects if it cannot be played.
95
+ * Same rule as `VideoPlayer.open`: no implementation may assume the
96
+ * platform fetches the URI.
97
+ */
98
+ play(uri: string): Promise<void>;
99
+ stop(): void;
100
+ }
101
+ export interface AppLifecycle {
102
+ /** fires when the app leaves the foreground */
103
+ onBackground(cb: () => void): Unsubscribe;
104
+ }
105
+ export interface VideoSurfaceProps {
106
+ style?: StyleProp<ViewStyle>;
107
+ }
108
+ export interface MediaAdapter {
109
+ video: VideoPlayer;
110
+ clips: ClipPlayer;
111
+ lifecycle: AppLifecycle;
112
+ /**
113
+ * The platform's own view that decoded pixels render into, already wired to
114
+ * `video`.
115
+ *
116
+ * It is part of the adapter rather than something a screen imports, because
117
+ * mounting it IS platform code: on Vega it is `KeplerVideoSurfaceView` and
118
+ * the handle it hands back has to reach the player — and it has to be
119
+ * tracked against `initialize()`, which races it (PLATFORM.md
120
+ * `surface_races_init`).
121
+ */
122
+ VideoSurface: ComponentType<VideoSurfaceProps>;
123
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=MediaAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MediaAdapter.js","sourceRoot":"","sources":["../src/MediaAdapter.ts"],"names":[],"mappings":""}
@@ -0,0 +1,48 @@
1
+ import type { DescriptionTrack, Verbosity } from './track';
2
+ /**
3
+ * Load a track file and refuse to believe it.
4
+ *
5
+ * Validation here is not defensive decoration. The loader does not trust that
6
+ * the producer was well-behaved, and a malformed or missing track must end in
7
+ * a STATED visible and spoken state, never in silence. A silent app with
8
+ * nothing to say is indistinguishable from a working app in a quiet scene.
9
+ */
10
+ export type LoadResult = {
11
+ ok: true;
12
+ track: DescriptionTrack;
13
+ loaded_verbosity: Verbosity;
14
+ } | {
15
+ ok: false;
16
+ reason: 'missing' | 'malformed';
17
+ detail: string;
18
+ };
19
+ export declare const CUE_KEYS: readonly ["id", "start_ms", "end_ms", "words", "text", "audio_uri", "source_frames_ms", "status"];
20
+ export declare const TRACK_KEYS: readonly ["version", "asset_id", "generated_at", "source_subtitles", "verbosity", "model_id", "cues"];
21
+ /** Validates a parsed track field for field. */
22
+ export declare function validateTrack(raw: unknown): LoadResult;
23
+ /**
24
+ * Resolve a level by `<asset_id>.<verbosity>.track.json` beside the asset,
25
+ * FALLING BACK to `standard`. The fallback is what stops a missing `detailed`
26
+ * file from turning the feature off.
27
+ *
28
+ * `readJson` is yours: on Vega a packaged file is read with `fetch`, and the
29
+ * platform's own player cannot be pointed at it (PLATFORM.md
30
+ * `url_mode_broken`), so there is no platform loader to delegate to.
31
+ */
32
+ export declare function loadTrack(readJson: (path: string) => Promise<unknown>, assetDir: string, assetId: string, verbosity: Verbosity): Promise<LoadResult>;
33
+ /**
34
+ * A bounded clip cache, oldest out first.
35
+ *
36
+ * It holds BYTES, not URIs, and on Vega that is forced rather than chosen:
37
+ * there is no URI a player will fetch (PLATFORM.md `url_mode_broken`), so
38
+ * every clip is read by the app and appended through a MediaSource.
39
+ */
40
+ export declare class ClipCache {
41
+ private readonly max;
42
+ private order;
43
+ private held;
44
+ constructor(max?: number);
45
+ put(uri: string, bytes: ArrayBuffer): void;
46
+ get(uri: string): ArrayBuffer | undefined;
47
+ get size(): number;
48
+ }
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClipCache = exports.TRACK_KEYS = exports.CUE_KEYS = void 0;
4
+ exports.validateTrack = validateTrack;
5
+ exports.loadTrack = loadTrack;
6
+ const track_1 = require("./track");
7
+ const budget_1 = require("./budget");
8
+ const log_1 = require("./log");
9
+ exports.CUE_KEYS = [
10
+ 'id',
11
+ 'start_ms',
12
+ 'end_ms',
13
+ 'words',
14
+ 'text',
15
+ 'audio_uri',
16
+ 'source_frames_ms',
17
+ 'status',
18
+ ];
19
+ exports.TRACK_KEYS = [
20
+ 'version',
21
+ 'asset_id',
22
+ 'generated_at',
23
+ 'source_subtitles',
24
+ 'verbosity',
25
+ 'model_id',
26
+ 'cues',
27
+ ];
28
+ function validCue(v) {
29
+ if (typeof v !== 'object' || v === null)
30
+ return false;
31
+ const c = v;
32
+ if (exports.CUE_KEYS.some((k) => !(k in c)))
33
+ return false;
34
+ return (typeof c.id === 'string' &&
35
+ typeof c.start_ms === 'number' &&
36
+ typeof c.end_ms === 'number' &&
37
+ typeof c.words === 'number' &&
38
+ typeof c.text === 'string' &&
39
+ typeof c.audio_uri === 'string' &&
40
+ Array.isArray(c.source_frames_ms) &&
41
+ c.source_frames_ms.every((n) => typeof n === 'number') &&
42
+ (c.status === 'ok' || c.status === 'failed'));
43
+ }
44
+ /** Validates a parsed track field for field. */
45
+ function validateTrack(raw) {
46
+ if (typeof raw !== 'object' || raw === null) {
47
+ return { ok: false, reason: 'malformed', detail: 'not an object' };
48
+ }
49
+ const t = raw;
50
+ const missing = exports.TRACK_KEYS.filter((k) => !(k in t));
51
+ if (missing.length) {
52
+ return { ok: false, reason: 'malformed', detail: `missing ${missing.join(',')}` };
53
+ }
54
+ if (!track_1.VERBOSITY_LEVELS.includes(t.verbosity)) {
55
+ return { ok: false, reason: 'malformed', detail: `verbosity=${String(t.verbosity)}` };
56
+ }
57
+ if (!Array.isArray(t.cues) || !t.cues.every(validCue)) {
58
+ return { ok: false, reason: 'malformed', detail: 'cues' };
59
+ }
60
+ return {
61
+ ok: true,
62
+ track: raw,
63
+ loaded_verbosity: t.verbosity,
64
+ };
65
+ }
66
+ /**
67
+ * Resolve a level by `<asset_id>.<verbosity>.track.json` beside the asset,
68
+ * FALLING BACK to `standard`. The fallback is what stops a missing `detailed`
69
+ * file from turning the feature off.
70
+ *
71
+ * `readJson` is yours: on Vega a packaged file is read with `fetch`, and the
72
+ * platform's own player cannot be pointed at it (PLATFORM.md
73
+ * `url_mode_broken`), so there is no platform loader to delegate to.
74
+ */
75
+ async function loadTrack(readJson, assetDir, assetId, verbosity) {
76
+ const levels = verbosity === 'standard' ? [verbosity] : [verbosity, 'standard'];
77
+ for (const level of levels) {
78
+ const path = `${assetDir}/${(0, track_1.trackFileName)(assetId, level)}`;
79
+ let raw;
80
+ try {
81
+ raw = await readJson(path);
82
+ }
83
+ catch {
84
+ (0, log_1.log)(`loader.miss path=${path}`);
85
+ continue;
86
+ }
87
+ const result = validateTrack(raw);
88
+ (0, log_1.log)(`loader.load path=${path} ok=${result.ok}` +
89
+ (result.ok ? ` cues=${result.track.cues.length}` : ` reason=${result.reason}`));
90
+ // A malformed file is an error, not a reason to fall back: something
91
+ // produced a file that is not a track, and silently loading a different
92
+ // one would hide it.
93
+ return result;
94
+ }
95
+ return { ok: false, reason: 'missing', detail: (0, track_1.trackFileName)(assetId, verbosity) };
96
+ }
97
+ /**
98
+ * A bounded clip cache, oldest out first.
99
+ *
100
+ * It holds BYTES, not URIs, and on Vega that is forced rather than chosen:
101
+ * there is no URI a player will fetch (PLATFORM.md `url_mode_broken`), so
102
+ * every clip is read by the app and appended through a MediaSource.
103
+ */
104
+ class ClipCache {
105
+ constructor(max = budget_1.MAX_CLIPS_IN_MEMORY) {
106
+ this.max = max;
107
+ this.order = [];
108
+ this.held = new Map();
109
+ }
110
+ put(uri, bytes) {
111
+ if (this.held.has(uri))
112
+ this.order = this.order.filter((u) => u !== uri);
113
+ this.held.set(uri, bytes);
114
+ this.order.push(uri);
115
+ while (this.order.length > this.max) {
116
+ const evicted = this.order.shift();
117
+ this.held.delete(evicted);
118
+ (0, log_1.log)(`cache.evict uri=${evicted} size=${this.held.size}`);
119
+ }
120
+ }
121
+ get(uri) {
122
+ return this.held.get(uri);
123
+ }
124
+ get size() {
125
+ return this.held.size;
126
+ }
127
+ }
128
+ exports.ClipCache = ClipCache;
129
+ //# sourceMappingURL=TrackLoader.js.map