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.
- package/CHANGELOG.md +52 -0
- package/LICENSE +21 -0
- package/PLATFORM.md +262 -0
- package/README.md +324 -0
- package/lib/ADControls.d.ts +24 -0
- package/lib/ADControls.js +41 -0
- package/lib/ADControls.js.map +1 -0
- package/lib/CueScheduler.d.ts +49 -0
- package/lib/CueScheduler.js +114 -0
- package/lib/CueScheduler.js.map +1 -0
- package/lib/DescriptionAudio.d.ts +36 -0
- package/lib/DescriptionAudio.js +88 -0
- package/lib/DescriptionAudio.js.map +1 -0
- package/lib/MediaAdapter.d.ts +123 -0
- package/lib/MediaAdapter.js +3 -0
- package/lib/MediaAdapter.js.map +1 -0
- package/lib/TrackLoader.d.ts +48 -0
- package/lib/TrackLoader.js +129 -0
- package/lib/TrackLoader.js.map +1 -0
- package/lib/budget.d.ts +41 -0
- package/lib/budget.js +60 -0
- package/lib/budget.js.map +1 -0
- package/lib/duck.d.ts +21 -0
- package/lib/duck.js +40 -0
- package/lib/duck.js.map +1 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +33 -0
- package/lib/index.js.map +1 -0
- package/lib/log.d.ts +17 -0
- package/lib/log.js +18 -0
- package/lib/log.js.map +1 -0
- package/lib/messages.d.ts +21 -0
- package/lib/messages.js +17 -0
- package/lib/messages.js.map +1 -0
- package/lib/track.d.ts +44 -0
- package/lib/track.js +22 -0
- package/lib/track.js.map +1 -0
- package/lib/vega/SegmentBuffer.d.ts +87 -0
- package/lib/vega/SegmentBuffer.js +133 -0
- package/lib/vega/SegmentBuffer.js.map +1 -0
- package/lib/vega/index.d.ts +33 -0
- package/lib/vega/index.js +280 -0
- package/lib/vega/index.js.map +1 -0
- package/package.json +89 -0
- package/src/ADControls.tsx +92 -0
- package/src/CueScheduler.ts +145 -0
- package/src/DescriptionAudio.ts +97 -0
- package/src/MediaAdapter.ts +136 -0
- package/src/TrackLoader.ts +156 -0
- package/src/budget.ts +61 -0
- package/src/duck.ts +42 -0
- package/src/index.ts +42 -0
- package/src/log.ts +30 -0
- package/src/messages.ts +27 -0
- package/src/track.ts +52 -0
- package/src/vega/SegmentBuffer.ts +195 -0
- 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 @@
|
|
|
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
|