@amazon/vinyl 2.0.4 → 3.0.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/dist/index.cjs +624 -338
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +643 -350
- package/dist/index.js.map +4 -4
- package/dist/test/benchmark.es5.js +8 -5
- package/dist/test/benchmark.js +228 -0
- package/dist/test/benchmark.js.map +4 -4
- package/dist/test/integ.es5.js +183 -157
- package/dist/test/integ.js +1966 -817
- package/dist/test/integ.js.map +4 -4
- package/dist/types/current/index.d.ts +4 -0
- package/dist/types/current/index.d.ts.map +1 -1
- package/dist/types/current/index.js +4 -0
- package/dist/types/current/streaming/AudioOptions.d.ts +39 -0
- package/dist/types/current/streaming/AudioOptions.d.ts.map +1 -0
- package/dist/types/current/streaming/AudioOptions.js +15 -0
- package/dist/types/current/streaming/createDefaultMediaTimelineTransformer.d.ts +2 -2
- package/dist/types/current/streaming/createDefaultMediaTimelineTransformer.d.ts.map +1 -1
- package/dist/types/current/streaming/createDefaultMediaTimelineTransformer.js +5 -5
- package/dist/types/current/text/HtmlTextTrackRenderer.d.ts +60 -0
- package/dist/types/current/text/HtmlTextTrackRenderer.d.ts.map +1 -0
- package/dist/types/current/text/HtmlTextTrackRenderer.js +231 -0
- package/dist/types/current/text/SidecarTextTrackController.d.ts +65 -53
- package/dist/types/current/text/SidecarTextTrackController.d.ts.map +1 -1
- package/dist/types/current/text/SidecarTextTrackController.js +167 -154
- package/dist/types/current/text/SidecarTextTrackLoader.d.ts +5 -0
- package/dist/types/current/text/SidecarTextTrackLoader.d.ts.map +1 -1
- package/dist/types/current/text/SidecarTextTrackLoader.js +13 -7
- package/dist/types/current/text/TextTrack.d.ts +74 -20
- package/dist/types/current/text/TextTrack.d.ts.map +1 -1
- package/dist/types/current/text/TextTrack.js +20 -0
- package/dist/types/current/text/TextTrackRenderer.d.ts +24 -0
- package/dist/types/current/text/TextTrackRenderer.d.ts.map +1 -0
- package/dist/types/current/text/TextTrackRenderer.js +5 -0
- package/dist/types/current/text/mediaTextTrackProvider.d.ts +34 -0
- package/dist/types/current/text/mediaTextTrackProvider.d.ts.map +1 -0
- package/dist/types/current/text/mediaTextTrackProvider.js +42 -0
- package/dist/types/current/text/parseWebVtt.d.ts +16 -2
- package/dist/types/current/text/parseWebVtt.d.ts.map +1 -1
- package/dist/types/current/text/parseWebVtt.js +176 -48
- package/dist/types/current/text/pickPreferredTextTrack.d.ts +2 -1
- package/dist/types/current/text/pickPreferredTextTrack.d.ts.map +1 -1
- package/dist/types/current/text/pickPreferredTextTrack.js +6 -3
- package/dist/types/current/track/Track.d.ts +1 -1
- package/dist/types/current/track/TrackController.d.ts.map +1 -1
- package/dist/types/current/track/TrackController.js +30 -30
- package/dist/types/current/track/dash/createDashFactories.d.ts +21 -10
- package/dist/types/current/track/dash/createDashFactories.d.ts.map +1 -1
- package/dist/types/current/track/dash/createDashFactories.js +9 -21
- package/dist/types/current/track/hls/createHlsFactories.d.ts +21 -10
- package/dist/types/current/track/hls/createHlsFactories.d.ts.map +1 -1
- package/dist/types/current/track/hls/createHlsFactories.js +9 -21
- package/dist/types/current/track/mse/MseTrack.d.ts +1 -2
- package/dist/types/current/track/mse/MseTrack.d.ts.map +1 -1
- package/dist/types/current/track/mse/MseTrack.js +3 -7
- package/dist/types/current/tsconfig.tsbuildinfo +1 -1
- package/dist/types/current/vinyl/VinylOptions.d.ts +18 -40
- package/dist/types/current/vinyl/VinylOptions.d.ts.map +1 -1
- package/dist/types/current/vinyl/VinylOptions.js +12 -11
- package/dist/types/current/vinyl/VinylPlayer.d.ts +0 -6
- package/dist/types/current/vinyl/VinylPlayer.d.ts.map +1 -1
- package/dist/types/current/vinyl/VinylPlayer.js +2 -23
- package/dist/types/current/vinyl/createVinylFactories.d.ts +32 -18
- package/dist/types/current/vinyl/createVinylFactories.d.ts.map +1 -1
- package/dist/types/current/vinyl/createVinylFactories.js +6 -0
- package/dist/types/ts3.7/index.d.ts +4 -0
- package/dist/types/ts3.7/streaming/AudioOptions.d.ts +39 -0
- package/dist/types/ts3.7/streaming/createDefaultMediaTimelineTransformer.d.ts +2 -2
- package/dist/types/ts3.7/text/HtmlTextTrackRenderer.d.ts +60 -0
- package/dist/types/ts3.7/text/SidecarTextTrackController.d.ts +65 -53
- package/dist/types/ts3.7/text/SidecarTextTrackLoader.d.ts +5 -0
- package/dist/types/ts3.7/text/TextTrack.d.ts +74 -20
- package/dist/types/ts3.7/text/TextTrackRenderer.d.ts +24 -0
- package/dist/types/ts3.7/text/mediaTextTrackProvider.d.ts +34 -0
- package/dist/types/ts3.7/text/parseWebVtt.d.ts +16 -2
- package/dist/types/ts3.7/text/pickPreferredTextTrack.d.ts +2 -1
- package/dist/types/ts3.7/track/Track.d.ts +1 -1
- package/dist/types/ts3.7/track/dash/createDashFactories.d.ts +21 -10
- package/dist/types/ts3.7/track/hls/createHlsFactories.d.ts +21 -10
- package/dist/types/ts3.7/track/mse/MseTrack.d.ts +1 -2
- package/dist/types/ts3.7/vinyl/VinylOptions.d.ts +18 -40
- package/dist/types/ts3.7/vinyl/VinylPlayer.d.ts +0 -6
- package/dist/types/ts3.7/vinyl/createVinylFactories.d.ts +32 -18
- package/dist/types/ts4.1/index.d.ts +4 -0
- package/dist/types/ts4.1/streaming/AudioOptions.d.ts +39 -0
- package/dist/types/ts4.1/streaming/createDefaultMediaTimelineTransformer.d.ts +2 -2
- package/dist/types/ts4.1/text/HtmlTextTrackRenderer.d.ts +60 -0
- package/dist/types/ts4.1/text/SidecarTextTrackController.d.ts +65 -53
- package/dist/types/ts4.1/text/SidecarTextTrackLoader.d.ts +5 -0
- package/dist/types/ts4.1/text/TextTrack.d.ts +74 -20
- package/dist/types/ts4.1/text/TextTrackRenderer.d.ts +24 -0
- package/dist/types/ts4.1/text/mediaTextTrackProvider.d.ts +34 -0
- package/dist/types/ts4.1/text/parseWebVtt.d.ts +16 -2
- package/dist/types/ts4.1/text/pickPreferredTextTrack.d.ts +2 -1
- package/dist/types/ts4.1/track/Track.d.ts +1 -1
- package/dist/types/ts4.1/track/dash/createDashFactories.d.ts +21 -10
- package/dist/types/ts4.1/track/hls/createHlsFactories.d.ts +21 -10
- package/dist/types/ts4.1/track/mse/MseTrack.d.ts +1 -2
- package/dist/types/ts4.1/vinyl/VinylOptions.d.ts +18 -40
- package/dist/types/ts4.1/vinyl/VinylPlayer.d.ts +0 -6
- package/dist/types/ts4.1/vinyl/createVinylFactories.d.ts +32 -18
- package/dist/types/ts4.7/index.d.ts +4 -0
- package/dist/types/ts4.7/streaming/AudioOptions.d.ts +39 -0
- package/dist/types/ts4.7/streaming/createDefaultMediaTimelineTransformer.d.ts +2 -2
- package/dist/types/ts4.7/text/HtmlTextTrackRenderer.d.ts +60 -0
- package/dist/types/ts4.7/text/SidecarTextTrackController.d.ts +65 -53
- package/dist/types/ts4.7/text/SidecarTextTrackLoader.d.ts +5 -0
- package/dist/types/ts4.7/text/TextTrack.d.ts +74 -20
- package/dist/types/ts4.7/text/TextTrackRenderer.d.ts +24 -0
- package/dist/types/ts4.7/text/mediaTextTrackProvider.d.ts +34 -0
- package/dist/types/ts4.7/text/parseWebVtt.d.ts +16 -2
- package/dist/types/ts4.7/text/pickPreferredTextTrack.d.ts +2 -1
- package/dist/types/ts4.7/track/Track.d.ts +1 -1
- package/dist/types/ts4.7/track/dash/createDashFactories.d.ts +21 -10
- package/dist/types/ts4.7/track/hls/createHlsFactories.d.ts +21 -10
- package/dist/types/ts4.7/track/mse/MseTrack.d.ts +1 -2
- package/dist/types/ts4.7/vinyl/VinylOptions.d.ts +18 -40
- package/dist/types/ts4.7/vinyl/VinylPlayer.d.ts +0 -6
- package/dist/types/ts4.7/vinyl/createVinylFactories.d.ts +32 -18
- package/dist/unit/types/current/index.d.ts +2 -0
- package/dist/unit/types/current/index.d.ts.map +1 -1
- package/dist/unit/types/current/index.js +2 -0
- package/dist/unit/types/current/streaming/MseTrack.test.js +13 -7
- package/dist/unit/types/current/streaming/createDefaultMediaTimelineTransformer.test.js +8 -4
- package/dist/unit/types/current/text/HtmlTextTrackRenderer.test.d.ts +2 -0
- package/dist/unit/types/current/text/HtmlTextTrackRenderer.test.d.ts.map +1 -0
- package/dist/unit/types/current/text/HtmlTextTrackRenderer.test.js +231 -0
- package/dist/unit/types/current/text/SidecarTextTrackController.test.js +432 -333
- package/dist/unit/types/current/text/SidecarTextTrackLoader.test.js +29 -0
- package/dist/unit/types/current/text/mediaTextTrackProvider.test.d.ts +2 -0
- package/dist/unit/types/current/text/mediaTextTrackProvider.test.d.ts.map +1 -0
- package/dist/unit/types/current/text/mediaTextTrackProvider.test.js +86 -0
- package/dist/unit/types/current/text/parseWebVtt.test.js +187 -6
- package/dist/unit/types/current/text/pickPreferredTextTrack.test.js +12 -1
- package/dist/unit/types/current/track/TrackController.test.js +36 -0
- package/dist/unit/types/current/track/dash/createDashFactories.test.js +1 -2
- package/dist/unit/types/current/track/hls/buildHlsMediaTimeline.test.js +1 -2
- package/dist/unit/types/current/track/hls/createHlsFactories.test.js +1 -2
- package/dist/unit/types/current/tsconfig.tsbuildinfo +1 -1
- package/dist/unit/types/current/vinyl/VinylPlayerTextTracks.test.js +102 -177
- package/dist/unit/types/current/vinyl/createVinylFactories.test.js +2 -0
- package/dist/vinylTestUtil/index.js +14 -8
- package/dist/vinylTestUtil/index.js.map +2 -2
- package/dist/vinylTestUtil/types/current/mock/player/MockVinylPlayerDependencies.d.ts +3 -2
- package/dist/vinylTestUtil/types/current/mock/player/MockVinylPlayerDependencies.d.ts.map +1 -1
- package/dist/vinylTestUtil/types/current/mock/player/MockVinylPlayerDependencies.js +5 -2
- package/dist/vinylTestUtil/types/current/mock/text/MockTextTrackController.d.ts +3 -3
- package/dist/vinylTestUtil/types/current/mock/text/MockTextTrackController.d.ts.map +1 -1
- package/dist/vinylTestUtil/types/current/mock/text/MockTextTrackController.js +3 -3
- package/dist/vinylTestUtil/types/current/mock/track/dash/createMockDashDependencies.d.ts +1 -2
- package/dist/vinylTestUtil/types/current/mock/track/dash/createMockDashDependencies.d.ts.map +1 -1
- package/dist/vinylTestUtil/types/current/mock/track/dash/createMockDashDependencies.js +3 -2
- package/dist/vinylTestUtil/types/current/mock/track/hls/createMockHlsDependencies.d.ts +1 -2
- package/dist/vinylTestUtil/types/current/mock/track/hls/createMockHlsDependencies.d.ts.map +1 -1
- package/dist/vinylTestUtil/types/current/mock/track/hls/createMockHlsDependencies.js +3 -2
- package/dist/vinylTestUtil/types/current/tsconfig.tsbuildinfo +1 -1
- package/dist/vinylTestUtil/types/ts3.7/mock/player/MockVinylPlayerDependencies.d.ts +3 -2
- package/dist/vinylTestUtil/types/ts3.7/mock/text/MockTextTrackController.d.ts +3 -3
- package/dist/vinylTestUtil/types/ts3.7/mock/track/dash/createMockDashDependencies.d.ts +1 -2
- package/dist/vinylTestUtil/types/ts3.7/mock/track/hls/createMockHlsDependencies.d.ts +1 -2
- package/dist/vinylTestUtil/types/ts4.1/mock/player/MockVinylPlayerDependencies.d.ts +3 -2
- package/dist/vinylTestUtil/types/ts4.1/mock/text/MockTextTrackController.d.ts +3 -3
- package/dist/vinylTestUtil/types/ts4.1/mock/track/dash/createMockDashDependencies.d.ts +1 -2
- package/dist/vinylTestUtil/types/ts4.1/mock/track/hls/createMockHlsDependencies.d.ts +1 -2
- package/dist/vinylTestUtil/types/ts4.7/mock/player/MockVinylPlayerDependencies.d.ts +3 -2
- package/dist/vinylTestUtil/types/ts4.7/mock/text/MockTextTrackController.d.ts +3 -3
- package/dist/vinylTestUtil/types/ts4.7/mock/track/dash/createMockDashDependencies.d.ts +1 -2
- package/dist/vinylTestUtil/types/ts4.7/mock/track/hls/createMockHlsDependencies.d.ts +1 -2
- package/docs/USAGE.md +82 -31
- package/package.json +9 -9
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
|
-
import { SidecarTextTrackController } from '@amazon/vinyl';
|
|
5
|
+
import { resolveSelectedTrack, SidecarTextTrackController, } from '@amazon/vinyl';
|
|
6
6
|
import { requesterWithRetryRef } from '@amazon/vinyl-util';
|
|
7
7
|
import { flushPromises } from '@amazon/vinyl-util/browserTestUtil';
|
|
8
8
|
import { MockRequester } from '@amazon/vinyl-util/testUtil';
|
|
@@ -20,30 +20,64 @@ async function flushUntil(predicate) {
|
|
|
20
20
|
for (let i = 0; i < 100 && !predicate(); i++)
|
|
21
21
|
await flushPromises();
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
/**
|
|
24
|
+
* A memoized fake provider mirroring {@link MediaTextTrackProvider}: one track
|
|
25
|
+
* per `(kind,label,language)` key, so the number of distinct tracks reveals
|
|
26
|
+
* whether the controller accumulates tracks or reuses them.
|
|
27
|
+
*/
|
|
28
|
+
class FakeTextTrackProvider {
|
|
29
|
+
// Every distinct track created (one per rendition key).
|
|
30
|
+
created = [];
|
|
31
|
+
// The track handed out by the most recent getOrCreate (the active one).
|
|
32
|
+
lastTrack = null;
|
|
33
|
+
cache = new Map();
|
|
34
|
+
getOrCreate = jasmine
|
|
35
|
+
.createSpy('getOrCreate')
|
|
26
36
|
.and.callFake((kind, label, language) => {
|
|
37
|
+
const key = `${kind}-${label}-${language}`;
|
|
38
|
+
const existing = this.cache.get(key);
|
|
39
|
+
if (existing) {
|
|
40
|
+
this.lastTrack = existing.track;
|
|
41
|
+
return existing;
|
|
42
|
+
}
|
|
43
|
+
const storedCues = [];
|
|
27
44
|
const track = {
|
|
28
45
|
kind,
|
|
29
46
|
label: label ?? '',
|
|
30
47
|
language: language ?? '',
|
|
31
48
|
mode: 'disabled',
|
|
32
|
-
|
|
49
|
+
storedCues,
|
|
50
|
+
// Real TextTrack.cues returns null when mode is 'disabled'.
|
|
51
|
+
get cues() {
|
|
52
|
+
return this.mode === 'disabled' ? null : storedCues;
|
|
53
|
+
},
|
|
33
54
|
addCue(cue) {
|
|
34
|
-
|
|
55
|
+
storedCues.push(cue);
|
|
35
56
|
},
|
|
36
57
|
removeCue(cue) {
|
|
37
|
-
const i =
|
|
58
|
+
const i = storedCues.indexOf(cue);
|
|
38
59
|
if (i >= 0)
|
|
39
|
-
|
|
60
|
+
storedCues.splice(i, 1);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
const ref = {
|
|
64
|
+
track: track,
|
|
65
|
+
clear() {
|
|
66
|
+
// Mirrors the real ref: cues only clear while not disabled.
|
|
67
|
+
if (track.mode === 'disabled')
|
|
68
|
+
return;
|
|
69
|
+
storedCues.length = 0;
|
|
40
70
|
},
|
|
41
71
|
};
|
|
72
|
+
this.created.push(track);
|
|
73
|
+
this.cache.set(key, ref);
|
|
42
74
|
this.lastTrack = track;
|
|
43
|
-
return
|
|
75
|
+
return ref;
|
|
44
76
|
});
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
/** Tracks currently rendering captions (showing with at least one cue). */
|
|
78
|
+
showingWithCues() {
|
|
79
|
+
return this.created.filter((t) => t.mode === 'showing' && (t.cues?.length ?? 0) > 0);
|
|
80
|
+
}
|
|
47
81
|
}
|
|
48
82
|
function makeTrack(overrides = {}) {
|
|
49
83
|
return {
|
|
@@ -60,18 +94,45 @@ function makeTrack(overrides = {}) {
|
|
|
60
94
|
};
|
|
61
95
|
}
|
|
62
96
|
describe('SidecarTextTrackController', () => {
|
|
63
|
-
let media;
|
|
64
|
-
let controller;
|
|
65
97
|
let requester;
|
|
66
98
|
let originalCue;
|
|
99
|
+
const disposers = [];
|
|
100
|
+
function build(opts) {
|
|
101
|
+
const provider = new FakeTextTrackProvider();
|
|
102
|
+
const textTracks$ = data(Promise.resolve([]));
|
|
103
|
+
const options$ = data(opts?.options ?? { selection: {} });
|
|
104
|
+
const playback = { currentTime: 0 };
|
|
105
|
+
const controller = new SidecarTextTrackController({
|
|
106
|
+
textTrackProvider: provider,
|
|
107
|
+
textTracks: textTracks$,
|
|
108
|
+
options: options$,
|
|
109
|
+
playbackController: playback,
|
|
110
|
+
requestInit: opts?.requestInit,
|
|
111
|
+
textTrackRenderer: opts?.textTrackRenderer,
|
|
112
|
+
});
|
|
113
|
+
// The owning media track activates the controller once current; do the
|
|
114
|
+
// same so cues render (unless a test wants to control activation).
|
|
115
|
+
if (opts?.activate !== false)
|
|
116
|
+
controller.activate();
|
|
117
|
+
disposers.push(() => controller.dispose());
|
|
118
|
+
return {
|
|
119
|
+
controller,
|
|
120
|
+
provider,
|
|
121
|
+
options$,
|
|
122
|
+
textTracks$,
|
|
123
|
+
playback,
|
|
124
|
+
async setTracks(tracks) {
|
|
125
|
+
textTracks$.value = Promise.resolve(tracks);
|
|
126
|
+
await flushPromises();
|
|
127
|
+
},
|
|
128
|
+
select(selection) {
|
|
129
|
+
options$.value = { ...options$.value, selection };
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
67
133
|
beforeEach(() => {
|
|
68
134
|
requester = new MockRequester();
|
|
69
135
|
requesterWithRetryRef.set(() => requester);
|
|
70
|
-
media = new FakeMediaElement();
|
|
71
|
-
controller = new SidecarTextTrackController({
|
|
72
|
-
media: media,
|
|
73
|
-
cueStyle: data({ size: 90 }),
|
|
74
|
-
});
|
|
75
136
|
originalCue = globalThis.VTTCue;
|
|
76
137
|
globalThis.VTTCue = function VTTCue(startTime, endTime, text) {
|
|
77
138
|
this.startTime = startTime;
|
|
@@ -82,123 +143,123 @@ describe('SidecarTextTrackController', () => {
|
|
|
82
143
|
};
|
|
83
144
|
});
|
|
84
145
|
afterEach(() => {
|
|
85
|
-
|
|
146
|
+
for (const dispose of disposers.splice(0))
|
|
147
|
+
dispose();
|
|
86
148
|
globalThis.VTTCue = originalCue;
|
|
87
149
|
});
|
|
88
150
|
function respondVtt(body) {
|
|
89
151
|
requester.request.and.resolveTo(new Response(body));
|
|
90
152
|
}
|
|
91
153
|
it('starts with no tracks and no active selection', () => {
|
|
154
|
+
const { controller } = build();
|
|
92
155
|
expect(controller.textTracks).toEqual([]);
|
|
93
156
|
expect(controller.activeTextTrack).toBeNull();
|
|
94
157
|
});
|
|
95
|
-
describe('
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
158
|
+
describe('resolveSelectedTrack', () => {
|
|
159
|
+
const full = makeTrack({ id: 'full', language: 'en', forced: false });
|
|
160
|
+
const forced = makeTrack({ id: 'forced', language: 'en', forced: true });
|
|
161
|
+
const captions = makeTrack({
|
|
162
|
+
id: 'cc',
|
|
163
|
+
language: 'en',
|
|
164
|
+
forced: false,
|
|
165
|
+
kind: 'captions',
|
|
102
166
|
});
|
|
103
|
-
it(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
language: 'es',
|
|
109
|
-
forced: true,
|
|
110
|
-
default: true,
|
|
111
|
-
}),
|
|
112
|
-
]);
|
|
113
|
-
expect(controller.activeTextTrack?.id).toBe('es-f');
|
|
167
|
+
it("renders nothing when enabled is 'off' (even with an id)", () => {
|
|
168
|
+
expect(resolveSelectedTrack([full, forced], {
|
|
169
|
+
enabled: 'off',
|
|
170
|
+
selection: { id: 'full' },
|
|
171
|
+
})).toBeNull();
|
|
114
172
|
});
|
|
115
|
-
it('
|
|
116
|
-
|
|
117
|
-
|
|
173
|
+
it("enabled 'on' selects the full track", () => {
|
|
174
|
+
expect(resolveSelectedTrack([full, forced], {
|
|
175
|
+
enabled: 'on',
|
|
176
|
+
selection: { language: 'en' },
|
|
177
|
+
})?.id).toBe('full');
|
|
118
178
|
});
|
|
119
|
-
it('
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
]);
|
|
125
|
-
expect(controller.activeTextTrack).toBeNull();
|
|
179
|
+
it("enabled 'forced' selects the forced track", () => {
|
|
180
|
+
expect(resolveSelectedTrack([full, forced], {
|
|
181
|
+
enabled: 'forced',
|
|
182
|
+
selection: { language: 'en' },
|
|
183
|
+
})?.id).toBe('forced');
|
|
126
184
|
});
|
|
127
|
-
it('
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
]
|
|
135
|
-
|
|
185
|
+
it('selection.forced overrides the enabled mode', () => {
|
|
186
|
+
expect(resolveSelectedTrack([full, forced], {
|
|
187
|
+
enabled: 'on',
|
|
188
|
+
selection: { language: 'en', forced: true },
|
|
189
|
+
})?.id).toBe('forced');
|
|
190
|
+
});
|
|
191
|
+
it('an explicit id wins over other criteria', () => {
|
|
192
|
+
expect(resolveSelectedTrack([full, forced], {
|
|
193
|
+
selection: { id: 'forced', language: 'en', forced: false },
|
|
194
|
+
})?.id).toBe('forced');
|
|
195
|
+
});
|
|
196
|
+
it('unknown id selects nothing', () => {
|
|
197
|
+
expect(resolveSelectedTrack([full], { selection: { id: 'nope' } })).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
it('forced defaults to true (only forced tracks)', () => {
|
|
200
|
+
expect(resolveSelectedTrack([full, forced], {
|
|
201
|
+
selection: { language: 'en' },
|
|
202
|
+
})?.id).toBe('forced');
|
|
203
|
+
});
|
|
204
|
+
it('forced:false selects the full track', () => {
|
|
205
|
+
expect(resolveSelectedTrack([full, forced], {
|
|
206
|
+
selection: { language: 'en', forced: false },
|
|
207
|
+
})?.id).toBe('full');
|
|
208
|
+
});
|
|
209
|
+
it('kind restricts the candidate pool', () => {
|
|
210
|
+
expect(resolveSelectedTrack([full, captions], {
|
|
211
|
+
selection: {
|
|
212
|
+
language: 'en',
|
|
213
|
+
forced: false,
|
|
214
|
+
kind: 'captions',
|
|
215
|
+
},
|
|
216
|
+
})?.id).toBe('cc');
|
|
217
|
+
});
|
|
218
|
+
it('falls back to navigator.languages when no language is given', () => {
|
|
219
|
+
// Node exposes navigator.languages === ['en-US'], relating to 'en'.
|
|
220
|
+
expect(resolveSelectedTrack([full], { selection: { forced: false } })
|
|
221
|
+
?.id).toBe('full');
|
|
222
|
+
});
|
|
223
|
+
it('an ordered language list prefers earlier entries', () => {
|
|
224
|
+
const es = makeTrack({ id: 'es', language: 'es', forced: false });
|
|
225
|
+
expect(resolveSelectedTrack([full, es], {
|
|
226
|
+
selection: { language: ['es', 'en'], forced: false },
|
|
227
|
+
})?.id).toBe('es');
|
|
136
228
|
});
|
|
137
|
-
});
|
|
138
|
-
it('emits textTracksChange when discovered list changes', () => {
|
|
139
|
-
const handler = jasmine.createSpy('textTracksChange');
|
|
140
|
-
controller.on('textTracksChange', handler);
|
|
141
|
-
const t = makeTrack();
|
|
142
|
-
controller.setTextTracks([t]);
|
|
143
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
144
|
-
// Same list contents → no event.
|
|
145
|
-
controller.setTextTracks([t]);
|
|
146
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
147
|
-
});
|
|
148
|
-
it('detects changed track properties', () => {
|
|
149
|
-
const handler = jasmine.createSpy();
|
|
150
|
-
controller.on('textTracksChange', handler);
|
|
151
|
-
controller.setTextTracks([makeTrack({ label: 'A' })]);
|
|
152
|
-
controller.setTextTracks([makeTrack({ label: 'B' })]);
|
|
153
|
-
expect(handler).toHaveBeenCalledTimes(2);
|
|
154
229
|
});
|
|
155
230
|
it('selecting a known id loads cues and creates a TextTrack', async () => {
|
|
156
231
|
respondVtt(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nfirst`);
|
|
157
|
-
const
|
|
158
|
-
controller.setTextTracks([t]);
|
|
232
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
159
233
|
const activeChange = jasmine.createSpy();
|
|
160
|
-
controller.on('activeTextTrackChange', activeChange);
|
|
161
|
-
|
|
162
|
-
expect(controller.activeTextTrack).toBe(
|
|
234
|
+
h.controller.on('activeTextTrackChange', activeChange);
|
|
235
|
+
await h.setTracks([makeTrack()]);
|
|
236
|
+
expect(h.controller.activeTextTrack?.id).toBe('a');
|
|
163
237
|
expect(activeChange).toHaveBeenCalledTimes(1);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
expect(
|
|
167
|
-
expect(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
ctrl.setTextTracks([t]);
|
|
179
|
-
ctrl.setActiveTextTrack(t.id);
|
|
180
|
-
await flushUntil(() => (media.lastTrack?.cues.length ?? 0) > 0);
|
|
181
|
-
return media.lastTrack?.cues[0];
|
|
182
|
-
}
|
|
183
|
-
it('does not style cues when cueStyle is null', async () => {
|
|
184
|
-
const ctrl = new SidecarTextTrackController({
|
|
185
|
-
media: media,
|
|
186
|
-
cueStyle: data(null),
|
|
187
|
-
});
|
|
188
|
-
const cue = await loadFirstCue(ctrl);
|
|
189
|
-
expect(cue.size).toBe(100); // stub default, unchanged
|
|
190
|
-
ctrl.dispose();
|
|
238
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
239
|
+
expect(h.provider.getOrCreate).toHaveBeenCalledWith('subtitles', 'English', 'en');
|
|
240
|
+
expect(h.provider.lastTrack?.mode).toBe('showing');
|
|
241
|
+
expect(h.provider.lastTrack?.cues?.length).toBe(1);
|
|
242
|
+
});
|
|
243
|
+
describe('authored cue settings', () => {
|
|
244
|
+
it("applies the cue's own parsed settings to the VTTCue", async () => {
|
|
245
|
+
respondVtt(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000 align:left size:80%\nx`);
|
|
246
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
247
|
+
await h.setTracks([makeTrack()]);
|
|
248
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
249
|
+
const cue = h.provider.lastTrack?.cues?.[0];
|
|
250
|
+
expect(cue.align).toBe('left');
|
|
251
|
+
expect(cue.size).toBe(80);
|
|
191
252
|
});
|
|
192
|
-
it('
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
253
|
+
it('leaves the cue at defaults when it declares no settings', async () => {
|
|
254
|
+
respondVtt(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nx`);
|
|
255
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
256
|
+
await h.setTracks([makeTrack()]);
|
|
257
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
258
|
+
// The VTTCue stub defaults size to 100; unset settings don't touch it.
|
|
259
|
+
const cue = h.provider.lastTrack?.cues?.[0];
|
|
197
260
|
expect(cue.size).toBe(100);
|
|
198
|
-
ctrl.dispose();
|
|
199
261
|
});
|
|
200
|
-
it('does not
|
|
201
|
-
// VTTCue-less platform: cues are plain TextTrackCues, not styleable.
|
|
262
|
+
it('does not apply settings to non-VTTCue cues', async () => {
|
|
202
263
|
;
|
|
203
264
|
globalThis.VTTCue = undefined;
|
|
204
265
|
globalThis.TextTrackCue = function TextTrackCue(start, end, text) {
|
|
@@ -208,23 +269,106 @@ describe('SidecarTextTrackController', () => {
|
|
|
208
269
|
this.id = '';
|
|
209
270
|
this.size = 100;
|
|
210
271
|
};
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const cue =
|
|
216
|
-
expect(cue.size).toBe(100)
|
|
217
|
-
|
|
272
|
+
respondVtt(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000 size:80%\nignored`);
|
|
273
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
274
|
+
await h.setTracks([makeTrack()]);
|
|
275
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
276
|
+
const cue = h.provider.lastTrack?.cues?.[0];
|
|
277
|
+
expect(cue.size).toBe(100) // not a VTTCue → never styled
|
|
278
|
+
;
|
|
218
279
|
globalThis.TextTrackCue = undefined;
|
|
219
280
|
});
|
|
220
281
|
});
|
|
282
|
+
describe('text track renderer', () => {
|
|
283
|
+
function fakeRenderer() {
|
|
284
|
+
return {
|
|
285
|
+
setTextTrack: jasmine.createSpy('setTextTrack'),
|
|
286
|
+
setStyles: jasmine.createSpy('setStyles'),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
it("keeps the DOM track 'hidden' and hands it to the renderer", async () => {
|
|
290
|
+
respondVtt(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nx`);
|
|
291
|
+
const renderer = fakeRenderer();
|
|
292
|
+
const h = build({
|
|
293
|
+
options: { selection: { id: 'a' } },
|
|
294
|
+
textTrackRenderer: renderer,
|
|
295
|
+
});
|
|
296
|
+
await h.setTracks([makeTrack()]);
|
|
297
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
298
|
+
expect(h.provider.lastTrack?.mode).toBe('hidden');
|
|
299
|
+
expect(renderer.setTextTrack).toHaveBeenCalledWith(h.provider.lastTrack);
|
|
300
|
+
});
|
|
301
|
+
it('detaches the renderer when captions are turned off', async () => {
|
|
302
|
+
respondVtt(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nx`);
|
|
303
|
+
const renderer = fakeRenderer();
|
|
304
|
+
const h = build({
|
|
305
|
+
options: { selection: { id: 'a' } },
|
|
306
|
+
textTrackRenderer: renderer,
|
|
307
|
+
});
|
|
308
|
+
await h.setTracks([makeTrack()]);
|
|
309
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
310
|
+
renderer.setTextTrack.calls.reset();
|
|
311
|
+
h.options$.value = { enabled: 'off' };
|
|
312
|
+
expect(renderer.setTextTrack).toHaveBeenCalledWith(null);
|
|
313
|
+
});
|
|
314
|
+
it('forwards authored STYLE blocks to the renderer', async () => {
|
|
315
|
+
respondVtt(`WEBVTT\n\nSTYLE\n::cue { color: red }\n\n00:00:01.000 --> 00:00:02.000\nx`);
|
|
316
|
+
const renderer = fakeRenderer();
|
|
317
|
+
const h = build({
|
|
318
|
+
options: { selection: { id: 'a' } },
|
|
319
|
+
textTrackRenderer: renderer,
|
|
320
|
+
});
|
|
321
|
+
await h.setTracks([makeTrack()]);
|
|
322
|
+
await flushUntil(() => renderer.setStyles.calls.count() > 0);
|
|
323
|
+
expect(renderer.setStyles).toHaveBeenCalledWith([
|
|
324
|
+
'::cue { color: red }',
|
|
325
|
+
]);
|
|
326
|
+
});
|
|
327
|
+
it('ignores styles that arrive after the load is cancelled', async () => {
|
|
328
|
+
let resolveFetch = () => { };
|
|
329
|
+
requester.request.and.returnValue(new Promise((res) => {
|
|
330
|
+
resolveFetch = res;
|
|
331
|
+
}));
|
|
332
|
+
const renderer = fakeRenderer();
|
|
333
|
+
const h = build({
|
|
334
|
+
options: { selection: { id: 'a' } },
|
|
335
|
+
textTrackRenderer: renderer,
|
|
336
|
+
});
|
|
337
|
+
await h.setTracks([makeTrack()]);
|
|
338
|
+
h.options$.value = { enabled: 'off' }; // cancels the load
|
|
339
|
+
renderer.setStyles.calls.reset();
|
|
340
|
+
resolveFetch(new Response(`WEBVTT\n\nSTYLE\n::cue { color: red }\n\n00:00:01.000 --> 00:00:02.000\nx`));
|
|
341
|
+
await flushPromises();
|
|
342
|
+
expect(renderer.setStyles).not.toHaveBeenCalled();
|
|
343
|
+
});
|
|
344
|
+
it('works with a renderer that cannot consume styles', async () => {
|
|
345
|
+
respondVtt(`WEBVTT\n\nSTYLE\n::cue { color: red }\n\n00:00:01.000 --> 00:00:02.000\nx`);
|
|
346
|
+
// A renderer implementing only setTextTrack (setStyles is optional).
|
|
347
|
+
const renderer = {
|
|
348
|
+
setTextTrack: jasmine.createSpy('setTextTrack'),
|
|
349
|
+
};
|
|
350
|
+
const h = build({
|
|
351
|
+
options: { selection: { id: 'a' } },
|
|
352
|
+
textTrackRenderer: renderer,
|
|
353
|
+
});
|
|
354
|
+
await h.setTracks([makeTrack()]);
|
|
355
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
356
|
+
expect(renderer.setTextTrack).toHaveBeenCalled();
|
|
357
|
+
});
|
|
358
|
+
it("renders natively ('showing') when no renderer is injected", async () => {
|
|
359
|
+
respondVtt(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nx`);
|
|
360
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
361
|
+
await h.setTracks([makeTrack()]);
|
|
362
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
363
|
+
expect(h.provider.lastTrack?.mode).toBe('showing');
|
|
364
|
+
});
|
|
365
|
+
});
|
|
221
366
|
it('preserves cue id when present', async () => {
|
|
222
367
|
respondVtt(`WEBVTT\n\nidA\n00:00:01.000 --> 00:00:02.000\nfirst`);
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const cue = media.lastTrack?.cues[0];
|
|
368
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
369
|
+
await h.setTracks([makeTrack()]);
|
|
370
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
371
|
+
const cue = h.provider.lastTrack?.cues?.[0];
|
|
228
372
|
expect(cue.id).toBe('idA');
|
|
229
373
|
});
|
|
230
374
|
it('dedupes cues that appear in more than one segment', async () => {
|
|
@@ -251,55 +395,56 @@ b.vtt
|
|
|
251
395
|
return Promise.resolve(new Response(segA));
|
|
252
396
|
return Promise.resolve(new Response(segB));
|
|
253
397
|
});
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
await flushUntil(() => (media.lastTrack?.cues.length ?? 0) >= 3);
|
|
259
|
-
const texts = (media.lastTrack?.cues ?? []).map((c) => c.text);
|
|
398
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
399
|
+
await h.setTracks([makeTrack({ uri: 'https://x.test/sub.m3u8' })]);
|
|
400
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) >= 3);
|
|
401
|
+
const texts = (h.provider.lastTrack?.cues ?? []).map((c) => c.text);
|
|
260
402
|
expect(texts).toEqual(['only-a', 'shared', 'only-b']);
|
|
261
403
|
});
|
|
262
|
-
it('selecting an unknown id
|
|
404
|
+
it('selecting an unknown id renders nothing and stays inactive', async () => {
|
|
263
405
|
const handler = jasmine.createSpy();
|
|
264
|
-
|
|
265
|
-
controller.
|
|
266
|
-
|
|
406
|
+
const h = build({ options: { selection: { id: 'unknown' } } });
|
|
407
|
+
h.controller.on('activeTextTrackChange', handler);
|
|
408
|
+
await h.setTracks([makeTrack()]);
|
|
267
409
|
expect(handler).not.toHaveBeenCalled();
|
|
268
|
-
expect(controller.activeTextTrack).toBeNull();
|
|
410
|
+
expect(h.controller.activeTextTrack).toBeNull();
|
|
411
|
+
expect(h.provider.getOrCreate).not.toHaveBeenCalled();
|
|
269
412
|
});
|
|
270
|
-
it('
|
|
413
|
+
it('disabling clears the current active track', async () => {
|
|
271
414
|
respondVtt(`WEBVTT\n`);
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
controller.
|
|
275
|
-
|
|
276
|
-
expect(controller.activeTextTrack).toBeNull();
|
|
415
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
416
|
+
await h.setTracks([makeTrack()]);
|
|
417
|
+
expect(h.controller.activeTextTrack?.id).toBe('a');
|
|
418
|
+
h.options$.value = { enabled: 'off' };
|
|
419
|
+
expect(h.controller.activeTextTrack).toBeNull();
|
|
277
420
|
});
|
|
278
|
-
it('clears active selection when its track is removed', () => {
|
|
421
|
+
it('clears active selection when its track is removed', async () => {
|
|
279
422
|
respondVtt(`WEBVTT\n`);
|
|
423
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
280
424
|
const t1 = makeTrack({ id: 'a' });
|
|
281
425
|
const t2 = makeTrack({ id: 'b', uri: 'https://x.test/b.vtt' });
|
|
282
|
-
|
|
283
|
-
controller.
|
|
284
|
-
|
|
285
|
-
expect(controller.activeTextTrack).toBeNull();
|
|
426
|
+
await h.setTracks([t1, t2]);
|
|
427
|
+
expect(h.controller.activeTextTrack?.id).toBe('a');
|
|
428
|
+
await h.setTracks([t2]);
|
|
429
|
+
expect(h.controller.activeTextTrack).toBeNull();
|
|
286
430
|
});
|
|
287
|
-
it('keeps active selection when
|
|
431
|
+
it('keeps the active selection when the list updates with it still present', async () => {
|
|
288
432
|
respondVtt(`WEBVTT\n`);
|
|
433
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
289
434
|
const t1 = makeTrack({ id: 'a' });
|
|
290
435
|
const t2 = makeTrack({ id: 'b' });
|
|
291
|
-
|
|
292
|
-
controller.
|
|
293
|
-
|
|
294
|
-
expect(controller.activeTextTrack?.id).toBe('a');
|
|
436
|
+
await h.setTracks([t1]);
|
|
437
|
+
expect(h.controller.activeTextTrack?.id).toBe('a');
|
|
438
|
+
await h.setTracks([t1, t2]);
|
|
439
|
+
expect(h.controller.activeTextTrack?.id).toBe('a');
|
|
295
440
|
});
|
|
296
441
|
it('emits textTrackError on fetch failure', async () => {
|
|
297
442
|
requester.request.and.rejectWith(new Error('boom'));
|
|
443
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
298
444
|
const onError = jasmine.createSpy('textTrackError');
|
|
299
|
-
controller.on('textTrackError', onError);
|
|
445
|
+
h.controller.on('textTrackError', onError);
|
|
300
446
|
const t = makeTrack();
|
|
301
|
-
|
|
302
|
-
controller.setActiveTextTrack(t.id);
|
|
447
|
+
await h.setTracks([t]);
|
|
303
448
|
await flushUntil(() => onError.calls.count() > 0);
|
|
304
449
|
expect(onError).toHaveBeenCalledTimes(1);
|
|
305
450
|
const arg = onError.calls.mostRecent().args[0];
|
|
@@ -308,14 +453,12 @@ b.vtt
|
|
|
308
453
|
});
|
|
309
454
|
it('wraps non-Error rejections', async () => {
|
|
310
455
|
requester.request.and.rejectWith('plain');
|
|
456
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
311
457
|
const onError = jasmine.createSpy();
|
|
312
|
-
controller.on('textTrackError', onError);
|
|
313
|
-
|
|
314
|
-
controller.setTextTracks([t]);
|
|
315
|
-
controller.setActiveTextTrack(t.id);
|
|
458
|
+
h.controller.on('textTrackError', onError);
|
|
459
|
+
await h.setTracks([makeTrack()]);
|
|
316
460
|
await flushUntil(() => onError.calls.count() > 0);
|
|
317
|
-
|
|
318
|
-
expect(arg.error.message).toBe('plain');
|
|
461
|
+
expect(onError.calls.mostRecent().args[0].error.message).toBe('plain');
|
|
319
462
|
});
|
|
320
463
|
it('drops cues that arrive after the load has been cancelled', async () => {
|
|
321
464
|
// Delay the fetch by one macrotask so we can cancel between the
|
|
@@ -324,18 +467,16 @@ b.vtt
|
|
|
324
467
|
requester.request.and.returnValue(new Promise((res) => {
|
|
325
468
|
resolveFetch = res;
|
|
326
469
|
}));
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
470
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
471
|
+
await h.setTracks([makeTrack()]);
|
|
472
|
+
const track = h.provider.lastTrack;
|
|
473
|
+
// Turn captions off — aborts the in-flight load.
|
|
474
|
+
h.options$.value = { enabled: 'off' };
|
|
332
475
|
resolveFetch(new Response(`WEBVTT\n\n00:00:00.500 --> 00:00:01.500\npost`));
|
|
333
476
|
await flushPromises();
|
|
334
|
-
expect(
|
|
477
|
+
expect(track?.storedCues.length ?? 0).toBe(0);
|
|
335
478
|
});
|
|
336
|
-
it('does not emit textTrackError when load is cancelled by switching
|
|
337
|
-
// First request hangs until we resolve it. Deactivate the track,
|
|
338
|
-
// then resolve with a rejection — the abort should suppress the error.
|
|
479
|
+
it('does not emit textTrackError when a load is cancelled by switching', async () => {
|
|
339
480
|
let rejectFirst = () => { };
|
|
340
481
|
let callCount = 0;
|
|
341
482
|
requester.request.and.callFake(() => new Promise((_, rej) => {
|
|
@@ -345,18 +486,16 @@ b.vtt
|
|
|
345
486
|
else
|
|
346
487
|
rej(new Error('unused'));
|
|
347
488
|
}));
|
|
489
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
348
490
|
const onError = jasmine.createSpy('textTrackError');
|
|
349
|
-
controller.on('textTrackError', onError);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
controller.setActiveTextTrack(t.id);
|
|
353
|
-
// Now cancel by clearing.
|
|
354
|
-
controller.setActiveTextTrack(null);
|
|
491
|
+
h.controller.on('textTrackError', onError);
|
|
492
|
+
await h.setTracks([makeTrack()]);
|
|
493
|
+
h.options$.value = { enabled: 'off' };
|
|
355
494
|
rejectFirst(new Error('would-be error'));
|
|
356
495
|
await flushPromises();
|
|
357
496
|
expect(onError).not.toHaveBeenCalled();
|
|
358
497
|
});
|
|
359
|
-
it('switching
|
|
498
|
+
it('switching language reuses the per-rendition track and swaps its cues', async () => {
|
|
360
499
|
let call = 0;
|
|
361
500
|
requester.request.and.callFake(() => {
|
|
362
501
|
call++;
|
|
@@ -364,98 +503,114 @@ b.vtt
|
|
|
364
503
|
? `WEBVTT\n\n00:00:00.500 --> 00:00:01.500\none`
|
|
365
504
|
: `WEBVTT\n\n00:00:01.000 --> 00:00:02.000\ntwo`));
|
|
366
505
|
});
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
id: '
|
|
370
|
-
|
|
371
|
-
label: '
|
|
506
|
+
const en = makeTrack({ id: 'en', language: 'en', label: 'English' });
|
|
507
|
+
const es = makeTrack({
|
|
508
|
+
id: 'es',
|
|
509
|
+
language: 'es',
|
|
510
|
+
label: 'Espanol',
|
|
511
|
+
uri: 'https://x.test/es.vtt',
|
|
372
512
|
});
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
await flushUntil(() => (
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
//
|
|
380
|
-
expect(
|
|
381
|
-
expect(
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
expect(
|
|
513
|
+
const h = build({ options: { selection: { id: 'en' } } });
|
|
514
|
+
await h.setTracks([en, es]);
|
|
515
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
516
|
+
const enTrack = h.provider.lastTrack;
|
|
517
|
+
h.select({ id: 'es' });
|
|
518
|
+
// The outgoing English track is hidden and cleared; a distinct Spanish
|
|
519
|
+
// track (its own rendition) renders instead.
|
|
520
|
+
expect(enTrack?.storedCues.length).toBe(0);
|
|
521
|
+
expect(enTrack?.mode).toBe('disabled');
|
|
522
|
+
await flushUntil(() => h.provider.lastTrack !== enTrack &&
|
|
523
|
+
(h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
524
|
+
expect(h.provider.lastTrack).not.toBe(enTrack);
|
|
525
|
+
expect(h.provider.lastTrack?.language).toBe('es');
|
|
526
|
+
expect(h.provider.showingWithCues().length).toBe(1);
|
|
527
|
+
});
|
|
528
|
+
it('cycling languages never accumulates or double-shows cues', async () => {
|
|
529
|
+
requester.request.and.callFake(() => Promise.resolve(new Response(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000\ncue`)));
|
|
530
|
+
const langs = ['en', 'es', 'ja'].map((language) => makeTrack({
|
|
531
|
+
id: language,
|
|
532
|
+
language,
|
|
533
|
+
uri: `https://x.test/${language}.vtt`,
|
|
534
|
+
}));
|
|
535
|
+
const h = build({ options: { selection: { id: 'en' } } });
|
|
536
|
+
await h.setTracks(langs);
|
|
537
|
+
for (const id of ['en', 'es', 'ja', 'en']) {
|
|
538
|
+
h.select({ id });
|
|
539
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
540
|
+
expect(h.provider.showingWithCues().length)
|
|
541
|
+
.withContext(`after selecting ${id}`)
|
|
542
|
+
.toBe(1);
|
|
543
|
+
}
|
|
544
|
+
// One track per distinct rendition (3), reused on the repeat of 'en' —
|
|
545
|
+
// never one per activation (which would be 4).
|
|
546
|
+
expect(h.provider.created.length).toBe(3);
|
|
386
547
|
});
|
|
387
|
-
it('
|
|
388
|
-
// Fresh Response per call — resume() re-fetches and a Response body can
|
|
389
|
-
// only be read once.
|
|
548
|
+
it('deactivate hides the DOM track and activate rebuilds it, keeping the selection', async () => {
|
|
390
549
|
requester.request.and.callFake(() => Promise.resolve(new Response(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000\none`)));
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
expect(
|
|
397
|
-
|
|
398
|
-
controller.
|
|
399
|
-
expect(
|
|
400
|
-
expect(
|
|
401
|
-
expect(controller.activeTextTrack?.id).toBe(
|
|
402
|
-
|
|
403
|
-
controller.
|
|
404
|
-
await flushUntil(() =>
|
|
405
|
-
|
|
406
|
-
expect(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
controller.
|
|
411
|
-
expect(
|
|
412
|
-
});
|
|
413
|
-
it('
|
|
550
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
551
|
+
await h.setTracks([makeTrack()]);
|
|
552
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
553
|
+
const track = h.provider.lastTrack;
|
|
554
|
+
expect(track?.mode).toBe('showing');
|
|
555
|
+
expect(h.controller.active).toBeTrue();
|
|
556
|
+
h.controller.deactivate();
|
|
557
|
+
expect(h.controller.active).toBeFalse();
|
|
558
|
+
expect(track?.storedCues.length).toBe(0);
|
|
559
|
+
expect(track?.mode).toBe('disabled');
|
|
560
|
+
expect(h.controller.activeTextTrack?.id).toBe('a');
|
|
561
|
+
h.controller.activate();
|
|
562
|
+
expect(h.controller.active).toBeTrue();
|
|
563
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
564
|
+
expect(h.provider.lastTrack?.mode).toBe('showing');
|
|
565
|
+
expect(h.provider.lastTrack?.cues?.length).toBe(1);
|
|
566
|
+
});
|
|
567
|
+
it('activate is a no-op when nothing is selected', () => {
|
|
568
|
+
const h = build();
|
|
569
|
+
h.controller.activate();
|
|
570
|
+
expect(h.provider.getOrCreate).not.toHaveBeenCalled();
|
|
571
|
+
});
|
|
572
|
+
it('activate is a no-op when already rendering', async () => {
|
|
414
573
|
respondVtt(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000\none`);
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
574
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
575
|
+
await h.setTracks([makeTrack()]);
|
|
576
|
+
await flushUntil(() => (h.provider.lastTrack?.cues?.length ?? 0) > 0);
|
|
577
|
+
h.provider.getOrCreate.calls.reset();
|
|
578
|
+
h.controller.activate();
|
|
579
|
+
expect(h.provider.getOrCreate).not.toHaveBeenCalled();
|
|
580
|
+
});
|
|
581
|
+
it('does not render while deactivated even as the selection resolves', async () => {
|
|
582
|
+
respondVtt(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000\none`);
|
|
583
|
+
const h = build({
|
|
584
|
+
options: { selection: { id: 'a' } },
|
|
585
|
+
activate: false,
|
|
586
|
+
});
|
|
587
|
+
h.controller.deactivate();
|
|
588
|
+
await h.setTracks([makeTrack()]);
|
|
589
|
+
// Selection is tracked, but nothing is rendered until activate().
|
|
590
|
+
expect(h.controller.activeTextTrack?.id).toBe('a');
|
|
591
|
+
expect(h.provider.getOrCreate).not.toHaveBeenCalled();
|
|
422
592
|
});
|
|
423
593
|
it('skips DOM cue creation when no VTTCue ctor is available', async () => {
|
|
424
594
|
;
|
|
425
595
|
globalThis.VTTCue = undefined;
|
|
426
596
|
respondVtt(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nignored`);
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
controller.setActiveTextTrack(t.id);
|
|
430
|
-
await flushPromises();
|
|
431
|
-
expect(media.lastTrack?.cues.length).toBe(0);
|
|
432
|
-
});
|
|
433
|
-
it('can run without a DOM media element', async () => {
|
|
434
|
-
const headless = new SidecarTextTrackController({ media: null });
|
|
435
|
-
const t = makeTrack();
|
|
436
|
-
respondVtt(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nx`);
|
|
437
|
-
headless.setTextTracks([t]);
|
|
438
|
-
headless.setActiveTextTrack(t.id);
|
|
597
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
598
|
+
await h.setTracks([makeTrack()]);
|
|
439
599
|
await flushPromises();
|
|
440
|
-
expect(
|
|
441
|
-
headless.dispose();
|
|
600
|
+
expect(h.provider.lastTrack?.cues?.length ?? 0).toBe(0);
|
|
442
601
|
});
|
|
443
602
|
it('forwards requestInit when configured', async () => {
|
|
444
|
-
|
|
445
|
-
|
|
603
|
+
respondVtt(`WEBVTT\n`);
|
|
604
|
+
const h = build({
|
|
605
|
+
options: { selection: { id: 'a' } },
|
|
446
606
|
requestInit: { credentials: 'include' },
|
|
447
607
|
});
|
|
448
|
-
respondVtt(`WEBVTT\n`);
|
|
449
608
|
const t = makeTrack();
|
|
450
|
-
|
|
451
|
-
customController.setActiveTextTrack(t.id);
|
|
609
|
+
await h.setTracks([t]);
|
|
452
610
|
await flushPromises();
|
|
453
611
|
expect(requester.request).toHaveBeenCalledWith(t.uri, jasmine.objectContaining({ credentials: 'include' }), jasmine.any(Object));
|
|
454
|
-
customController.dispose();
|
|
455
612
|
});
|
|
456
613
|
it('drives HLS caption segment order from the media playhead', async () => {
|
|
457
|
-
// Media at t=12s should cause seg2 (10s..15s) to be fetched first.
|
|
458
|
-
media.currentTime = 12;
|
|
459
614
|
const playlistBody = `#EXTM3U
|
|
460
615
|
#EXT-X-TARGETDURATION:5
|
|
461
616
|
#EXTINF:5.0,
|
|
@@ -478,9 +633,10 @@ d.vtt
|
|
|
478
633
|
fetched.push(path);
|
|
479
634
|
return Promise.resolve(new Response(`WEBVTT\n\n00:00:00.000 --> 00:00:01.000\n${path}`));
|
|
480
635
|
});
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
636
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
637
|
+
// Media at t=12s should cause seg3 (10s..15s) to be fetched first.
|
|
638
|
+
h.playback.currentTime = 12;
|
|
639
|
+
await h.setTracks([makeTrack({ uri: 'https://x.test/sub.m3u8' })]);
|
|
484
640
|
await flushUntil(() => fetched.length > 0);
|
|
485
641
|
expect(fetched[0]).toBe('c.vtt');
|
|
486
642
|
});
|
|
@@ -489,99 +645,42 @@ d.vtt
|
|
|
489
645
|
requester.request.and.callFake(() => new Promise((res) => {
|
|
490
646
|
resolveResponse = res;
|
|
491
647
|
}));
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
expect(media.lastTrack).not.toBeNull();
|
|
498
|
-
controller.dispose();
|
|
648
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
649
|
+
await h.setTracks([makeTrack()]);
|
|
650
|
+
const track = h.provider.lastTrack;
|
|
651
|
+
expect(track).not.toBeNull();
|
|
652
|
+
h.controller.dispose();
|
|
499
653
|
// Resolving after dispose must not throw or push cues onto the track.
|
|
500
654
|
resolveResponse(new Response(`WEBVTT\n\n00:00:00.500 --> 00:00:01.500\npost`));
|
|
501
655
|
await flushPromises();
|
|
502
|
-
expect(
|
|
503
|
-
expect(
|
|
504
|
-
expect(controller.activeTextTrack).toBeNull();
|
|
505
|
-
expect(controller.textTracks).toEqual([]);
|
|
656
|
+
expect(track?.mode).toBe('disabled');
|
|
657
|
+
expect(track?.storedCues.length).toBe(0);
|
|
658
|
+
expect(h.controller.activeTextTrack).toBeNull();
|
|
659
|
+
expect(h.controller.textTracks).toEqual([]);
|
|
506
660
|
});
|
|
507
|
-
it('
|
|
661
|
+
it('creates the DOM track with an undefined language for null-language tracks', async () => {
|
|
508
662
|
respondVtt(`WEBVTT\n`);
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
controller.setActiveTextTrack(t.id);
|
|
663
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
664
|
+
await h.setTracks([makeTrack({ language: null })]);
|
|
512
665
|
await flushPromises();
|
|
513
|
-
expect(
|
|
666
|
+
expect(h.provider.getOrCreate).toHaveBeenCalledWith('subtitles', 'English', undefined);
|
|
514
667
|
});
|
|
515
|
-
it('
|
|
668
|
+
it('setting the same resolved track list twice emits textTracksChange once', async () => {
|
|
516
669
|
const handler = jasmine.createSpy();
|
|
517
|
-
|
|
670
|
+
const h = build();
|
|
671
|
+
h.controller.on('textTracksChange', handler);
|
|
518
672
|
const list = [makeTrack()];
|
|
519
|
-
|
|
520
|
-
|
|
673
|
+
await h.setTracks(list);
|
|
674
|
+
await h.setTracks([makeTrack()]); // deep-equal contents
|
|
521
675
|
expect(handler).toHaveBeenCalledTimes(1);
|
|
522
676
|
});
|
|
523
|
-
it('
|
|
524
|
-
const handler = jasmine.createSpy();
|
|
525
|
-
controller.on('activeTextTrackChange', handler);
|
|
526
|
-
controller.setActiveTextTrack(null);
|
|
527
|
-
expect(handler).not.toHaveBeenCalled();
|
|
528
|
-
});
|
|
529
|
-
it('selecting same id is a no-op', () => {
|
|
677
|
+
it('re-selecting the same id is a no-op', async () => {
|
|
530
678
|
respondVtt(`WEBVTT\n`);
|
|
531
|
-
const
|
|
679
|
+
const h = build({ options: { selection: { id: 'a' } } });
|
|
680
|
+
await h.setTracks([makeTrack()]);
|
|
532
681
|
const handler = jasmine.createSpy();
|
|
533
|
-
controller.
|
|
534
|
-
|
|
535
|
-
controller.on('activeTextTrackChange', handler);
|
|
536
|
-
controller.setActiveTextTrack(t.id);
|
|
682
|
+
h.controller.on('activeTextTrackChange', handler);
|
|
683
|
+
h.select({ id: 'a' });
|
|
537
684
|
expect(handler).not.toHaveBeenCalled();
|
|
538
685
|
});
|
|
539
|
-
describe('preferredTextLanguage', () => {
|
|
540
|
-
const en = makeTrack({ id: 'en', language: 'en', label: 'English' });
|
|
541
|
-
const es = makeTrack({ id: 'es', language: 'es', label: 'Espanol' });
|
|
542
|
-
let preferredTextLanguage;
|
|
543
|
-
let prefController;
|
|
544
|
-
beforeEach(() => {
|
|
545
|
-
preferredTextLanguage = data(null);
|
|
546
|
-
prefController = new SidecarTextTrackController({
|
|
547
|
-
media: null,
|
|
548
|
-
preferredTextLanguage,
|
|
549
|
-
});
|
|
550
|
-
});
|
|
551
|
-
afterEach(() => prefController.dispose());
|
|
552
|
-
it('selects the matching track when the preference changes', () => {
|
|
553
|
-
prefController.setTextTracks([en, es]);
|
|
554
|
-
expect(prefController.activeTextTrack).toBeNull();
|
|
555
|
-
preferredTextLanguage.value = 'es';
|
|
556
|
-
expect(prefController.activeTextTrack?.id).toBe('es');
|
|
557
|
-
});
|
|
558
|
-
it('clears the selection when the preference is set back to null', () => {
|
|
559
|
-
prefController.setTextTracks([en, es]);
|
|
560
|
-
preferredTextLanguage.value = 'es';
|
|
561
|
-
expect(prefController.activeTextTrack?.id).toBe('es');
|
|
562
|
-
preferredTextLanguage.value = null;
|
|
563
|
-
expect(prefController.activeTextTrack).toBeNull();
|
|
564
|
-
});
|
|
565
|
-
it('re-applies the preference when the track list changes', () => {
|
|
566
|
-
preferredTextLanguage.value = 'es';
|
|
567
|
-
// No tracks yet, so nothing matches.
|
|
568
|
-
expect(prefController.activeTextTrack).toBeNull();
|
|
569
|
-
prefController.setTextTracks([en, es]);
|
|
570
|
-
expect(prefController.activeTextTrack?.id).toBe('es');
|
|
571
|
-
});
|
|
572
|
-
it('a preferred language wins over forced auto-selection', () => {
|
|
573
|
-
preferredTextLanguage.value = 'es';
|
|
574
|
-
prefController.setTextTracks([
|
|
575
|
-
makeTrack({ id: 'en-forced', language: 'en', forced: true }),
|
|
576
|
-
es,
|
|
577
|
-
]);
|
|
578
|
-
expect(prefController.activeTextTrack?.id).toBe('es');
|
|
579
|
-
});
|
|
580
|
-
it('leaves forced auto-selection intact while no preference is set', () => {
|
|
581
|
-
prefController.setTextTracks([
|
|
582
|
-
makeTrack({ id: 'forced', forced: true }),
|
|
583
|
-
]);
|
|
584
|
-
expect(prefController.activeTextTrack?.id).toBe('forced');
|
|
585
|
-
});
|
|
586
|
-
});
|
|
587
686
|
});
|