shaka-player 3.1.2 → 3.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build_and_test.yaml +73 -0
- package/.github/workflows/release.yaml +53 -0
- package/AUTHORS +3 -0
- package/CHANGELOG.md +170 -1
- package/CONTRIBUTORS +3 -0
- package/build/checkversion.py +1 -1
- package/build/generateExterns.js +1 -1
- package/build/misspellings.txt +1 -0
- package/build/shakaBuildHelpers.py +6 -5
- package/build/subprocessWindowsPatch.py +103 -0
- package/build/test.py +6 -0
- package/demo/common/assets.js +1 -1
- package/dist/controls.css +2 -2
- package/dist/controls.css.map +1 -1
- package/dist/demo.compiled.debug.js +1 -1
- package/dist/demo.compiled.js +1 -1
- package/dist/deps.js +7 -7
- package/dist/locales.js +35 -35
- package/dist/receiver.compiled.debug.js +1 -1
- package/dist/receiver.compiled.js +1 -1
- package/dist/shaka-player.compiled.d.ts +172 -3
- package/dist/shaka-player.compiled.debug.d.ts +172 -3
- package/dist/shaka-player.compiled.debug.externs.js +184 -1
- package/dist/shaka-player.compiled.debug.js +390 -370
- package/dist/shaka-player.compiled.debug.map +3 -3
- package/dist/shaka-player.compiled.externs.js +184 -1
- package/dist/shaka-player.compiled.js +809 -805
- package/dist/shaka-player.compiled.map +3 -3
- package/dist/shaka-player.ui.d.ts +174 -14
- package/dist/shaka-player.ui.debug.d.ts +174 -14
- package/dist/shaka-player.ui.debug.externs.js +200 -26
- package/dist/shaka-player.ui.debug.js +431 -412
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +200 -26
- package/dist/shaka-player.ui.js +934 -929
- package/dist/shaka-player.ui.map +4 -4
- package/docs/tutorials/fairplay.md +1 -1
- package/docs/tutorials/faq.md +14 -0
- package/externs/ima.js +4 -0
- package/externs/shaka/ads.js +2 -0
- package/externs/shaka/text.js +10 -0
- package/karma.conf.js +20 -15
- package/lib/ads/ad_manager.js +30 -11
- package/lib/ads/client_side_ad_manager.js +76 -86
- package/lib/ads/server_side_ad_manager.js +26 -11
- package/lib/cast/cast_proxy.js +6 -2
- package/lib/cast/cast_receiver.js +5 -1
- package/lib/cast/cast_sender.js +2 -2
- package/lib/cea/sei_processor.js +24 -17
- package/lib/dash/dash_parser.js +131 -24
- package/lib/dash/segment_list.js +1 -1
- package/lib/dash/segment_template.js +1 -1
- package/lib/hls/hls_parser.js +44 -17
- package/lib/hls/manifest_text_parser.js +1 -0
- package/lib/media/closed_caption_parser.js +3 -1
- package/lib/media/content_workarounds.js +12 -0
- package/lib/media/drm_engine.js +20 -1
- package/lib/media/gap_jumping_controller.js +8 -4
- package/lib/media/media_source_engine.js +1 -2
- package/lib/media/playhead.js +1 -1
- package/lib/media/region_observer.js +39 -33
- package/lib/media/region_timeline.js +17 -19
- package/lib/media/streaming_engine.js +8 -4
- package/lib/net/networking_engine.js +6 -1
- package/lib/offline/storage.js +10 -11
- package/lib/player.js +66 -30
- package/lib/polyfill/encryption_scheme.js +2 -0
- package/lib/polyfill/fullscreen.js +2 -0
- package/lib/polyfill/mathround.js +2 -0
- package/lib/polyfill/media_capabilities.js +73 -21
- package/lib/polyfill/mediasource.js +8 -0
- package/lib/polyfill/orientation.js +3 -1
- package/lib/polyfill/patchedmediakeys_apple.js +6 -4
- package/lib/polyfill/patchedmediakeys_ms.js +6 -4
- package/lib/polyfill/patchedmediakeys_nop.js +2 -0
- package/lib/polyfill/patchedmediakeys_webkit.js +6 -4
- package/lib/polyfill/pip_webkit.js +2 -0
- package/lib/polyfill/storage_estimate.js +2 -0
- package/lib/polyfill/video_play_promise.js +2 -0
- package/lib/polyfill/videoplaybackquality.js +2 -0
- package/lib/polyfill/vttcue.js +2 -0
- package/lib/text/cue.js +6 -0
- package/lib/text/mp4_ttml_parser.js +3 -0
- package/lib/text/text_engine.js +14 -2
- package/lib/text/ttml_text_parser.js +135 -87
- package/lib/text/ui_text_displayer.js +213 -80
- package/lib/text/vtt_text_parser.js +4 -2
- package/lib/util/fake_event.js +53 -7
- package/lib/util/fake_event_target.js +21 -1
- package/lib/util/mp4_box_parsers.js +1 -1
- package/lib/util/periods.js +4 -1
- package/lib/util/platform.js +13 -1
- package/lib/util/stream_utils.js +77 -10
- package/lib/util/xml_utils.js +19 -0
- package/package.json +3 -6
- package/test/cast/cast_proxy_unit.js +12 -6
- package/test/cast/cast_receiver_integration.js +1 -1
- package/test/cast/cast_utils_unit.js +1 -1
- package/test/cea/mp4_cea_parser_unit.js +26 -0
- package/test/dash/dash_parser_live_unit.js +43 -0
- package/test/dash/dash_parser_manifest_unit.js +222 -3
- package/test/hls/hls_parser_unit.js +79 -1
- package/test/media/closed_caption_parser_unit.js +20 -0
- package/test/media/drm_engine_unit.js +56 -0
- package/test/media/playhead_unit.js +27 -1
- package/test/media/region_observer_unit.js +40 -5
- package/test/media/region_timeline_unit.js +40 -22
- package/test/media/streaming_engine_integration.js +2 -1
- package/test/media/streaming_engine_unit.js +145 -0
- package/test/player_integration.js +38 -0
- package/test/test/assets/3675/chunk-stream0-00001.m4s +0 -0
- package/test/test/assets/3675/chunk-stream0-00002.m4s +0 -0
- package/test/test/assets/3675/chunk-stream0-00003.m4s +0 -0
- package/test/test/assets/3675/chunk-stream0-00004.m4s +0 -0
- package/test/test/assets/3675/chunk-stream0-00005.m4s +0 -0
- package/test/test/assets/3675/chunk-stream0-00006.m4s +0 -0
- package/test/test/assets/3675/chunk-stream0-00007.m4s +0 -0
- package/test/test/assets/3675/dash_0.mpd +30 -0
- package/test/test/assets/3675/dash_1.mpd +30 -0
- package/test/test/assets/3675/dash_2.mpd +30 -0
- package/test/test/assets/3675/dash_3.mpd +30 -0
- package/test/test/assets/3675/dash_4.mpd +30 -0
- package/test/test/assets/3675/dash_5.mpd +26 -0
- package/test/test/assets/3675/init-stream0.m4s +0 -0
- package/test/test/assets/empty_caption_video_init.mp4 +0 -0
- package/test/test/assets/empty_caption_video_segment.mp4 +0 -0
- package/test/test/util/fake_ad_manager.js +4 -1
- package/test/test/util/ttml_utils.js +81 -0
- package/test/test/util/util.js +10 -7
- package/test/text/mp4_ttml_parser_unit.js +11 -18
- package/test/text/ttml_text_parser_unit.js +542 -220
- package/test/text/ui_text_displayer_unit.js +93 -20
- package/test/text/vtt_text_parser_unit.js +17 -1
- package/test/ui/localization_unit.js +15 -14
- package/test/ui/text_displayer_layout_unit.js +21 -8
- package/test/util/mp4_box_parsers_unit.js +28 -0
- package/test/util/stream_utils_unit.js +59 -2
- package/ui/cast_button.js +2 -3
- package/ui/controls.js +11 -6
- package/ui/less/buttons.less +0 -4
- package/ui/less/containers.less +6 -7
- package/ui/locales/ru.json +6 -6
- package/ui/localization.js +40 -87
- package/ui/pip_button.js +2 -3
- package/ui/text_selection.js +1 -0
|
@@ -209,6 +209,80 @@ describe('HlsParser', () => {
|
|
|
209
209
|
expect(actual).toEqual(manifest);
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
+
it('parses manifest attributes with space', async () => {
|
|
213
|
+
const master = [
|
|
214
|
+
'#EXTM3U\n',
|
|
215
|
+
'#EXT-X-MEDIA:TYPE=AUDIO, GROUP-ID="aud1", LANGUAGE="eng", ',
|
|
216
|
+
'CHANNELS="16/JOC", URI="audio"\n',
|
|
217
|
+
'#EXT-X-MEDIA:TYPE=SUBTITLES, GROUP-ID="sub1", LANGUAGE="eng", ',
|
|
218
|
+
'URI="text"\n',
|
|
219
|
+
'#EXT-X-MEDIA:TYPE=SUBTITLES, GROUP-ID="sub2", LANGUAGE="es", ',
|
|
220
|
+
'URI="text2"\n',
|
|
221
|
+
'#EXT-X-STREAM-INF:BANDWIDTH=200, CODECS="avc1,mp4a", ',
|
|
222
|
+
'RESOLUTION=960x540, FRAME-RATE=60, AUDIO="aud1"\n',
|
|
223
|
+
'video\n',
|
|
224
|
+
].join('');
|
|
225
|
+
|
|
226
|
+
const media = [
|
|
227
|
+
'#EXTM3U\n',
|
|
228
|
+
'#EXT-X-PLAYLIST-TYPE:VOD\n',
|
|
229
|
+
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
|
|
230
|
+
'#EXTINF:5,\n',
|
|
231
|
+
'#EXT-X-BYTERANGE:121090@616\n',
|
|
232
|
+
'main.mp4',
|
|
233
|
+
].join('');
|
|
234
|
+
|
|
235
|
+
const textMedia = [
|
|
236
|
+
'#EXTM3U\n',
|
|
237
|
+
'#EXT-X-PLAYLIST-TYPE:VOD\n',
|
|
238
|
+
'#EXTINF:5,\n',
|
|
239
|
+
'#EXT-X-BYTERANGE:121090@616\n',
|
|
240
|
+
'main.vtt',
|
|
241
|
+
].join('');
|
|
242
|
+
|
|
243
|
+
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
|
|
244
|
+
manifest.anyTimeline();
|
|
245
|
+
manifest.addPartialVariant((variant) => {
|
|
246
|
+
variant.language = 'en';
|
|
247
|
+
variant.bandwidth = 200;
|
|
248
|
+
variant.addPartialStream(ContentType.VIDEO, (stream) => {
|
|
249
|
+
stream.frameRate = 60;
|
|
250
|
+
stream.mime('video/mp4', 'avc1');
|
|
251
|
+
stream.size(960, 540);
|
|
252
|
+
});
|
|
253
|
+
variant.addPartialStream(ContentType.AUDIO, (stream) => {
|
|
254
|
+
stream.language = 'en';
|
|
255
|
+
stream.channelsCount = 16;
|
|
256
|
+
stream.spatialAudio = true;
|
|
257
|
+
stream.mime('audio/mp4', 'mp4a');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
manifest.addPartialTextStream((stream) => {
|
|
261
|
+
stream.language = 'en';
|
|
262
|
+
stream.kind = TextStreamKind.SUBTITLE;
|
|
263
|
+
stream.mime('text/vtt', '');
|
|
264
|
+
});
|
|
265
|
+
manifest.addPartialTextStream((stream) => {
|
|
266
|
+
stream.language = 'es';
|
|
267
|
+
stream.kind = TextStreamKind.SUBTITLE;
|
|
268
|
+
stream.mime('text/vtt', '');
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
fakeNetEngine
|
|
273
|
+
.setResponseText('test:/master', master)
|
|
274
|
+
.setResponseText('test:/audio', media)
|
|
275
|
+
.setResponseText('test:/video', media)
|
|
276
|
+
.setResponseText('test:/text', textMedia)
|
|
277
|
+
.setResponseText('test:/text2', textMedia)
|
|
278
|
+
.setResponseText('test:/main.vtt', vttText)
|
|
279
|
+
.setResponseValue('test:/init.mp4', initSegmentData)
|
|
280
|
+
.setResponseValue('test:/main.mp4', segmentData);
|
|
281
|
+
|
|
282
|
+
const actual = await parser.start('test:/master', playerInterface);
|
|
283
|
+
expect(actual).toEqual(manifest);
|
|
284
|
+
});
|
|
285
|
+
|
|
212
286
|
it('prioritize AVERAGE-BANDWIDTH to BANDWIDTH', async () => {
|
|
213
287
|
const master = [
|
|
214
288
|
'#EXTM3U\n',
|
|
@@ -783,7 +857,7 @@ describe('HlsParser', () => {
|
|
|
783
857
|
'RESOLUTION=960x540,FRAME-RATE=120,AUDIO="aud2"\n',
|
|
784
858
|
'video2\n',
|
|
785
859
|
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
|
|
786
|
-
'URI="audio"\n',
|
|
860
|
+
'DEFAULT=YES,URI="audio"\n',
|
|
787
861
|
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",LANGUAGE="fr",',
|
|
788
862
|
'URI="audio2"\n',
|
|
789
863
|
].join('');
|
|
@@ -801,6 +875,7 @@ describe('HlsParser', () => {
|
|
|
801
875
|
manifest.anyTimeline();
|
|
802
876
|
manifest.addPartialVariant((variant) => {
|
|
803
877
|
variant.bandwidth = 200;
|
|
878
|
+
variant.primary = true;
|
|
804
879
|
variant.addPartialStream(ContentType.VIDEO, (stream) => {
|
|
805
880
|
stream.size(960, 540);
|
|
806
881
|
});
|
|
@@ -810,6 +885,7 @@ describe('HlsParser', () => {
|
|
|
810
885
|
});
|
|
811
886
|
manifest.addPartialVariant((variant) => {
|
|
812
887
|
variant.bandwidth = 300;
|
|
888
|
+
variant.primary = false;
|
|
813
889
|
variant.addPartialStream(ContentType.VIDEO, (stream) => {
|
|
814
890
|
stream.size(960, 540);
|
|
815
891
|
});
|
|
@@ -1307,6 +1383,8 @@ describe('HlsParser', () => {
|
|
|
1307
1383
|
// stream.
|
|
1308
1384
|
const timeline = actual.presentationTimeline;
|
|
1309
1385
|
expect(timeline.getDuration()).toBe(10);
|
|
1386
|
+
expect(timeline.getSeekRangeStart()).toBe(0);
|
|
1387
|
+
expect(timeline.getSeekRangeEnd()).toBe(10);
|
|
1310
1388
|
|
|
1311
1389
|
expect(actual.textStreams.length).toBe(1);
|
|
1312
1390
|
expect(actual.variants.length).toBe(1);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*! @license
|
|
2
|
+
* Shaka Player
|
|
3
|
+
* Copyright 2016 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
goog.require('shaka.media.ClosedCaptionParser');
|
|
8
|
+
goog.require('shaka.test.Util');
|
|
9
|
+
|
|
10
|
+
describe('ClosedCaptionParser', () => {
|
|
11
|
+
it('can handle empty caption packets', async () => {
|
|
12
|
+
const initSegment = await shaka.test.Util.fetch(
|
|
13
|
+
'base/test/test/assets/empty_caption_video_init.mp4');
|
|
14
|
+
const videoSegment = await shaka.test.Util.fetch(
|
|
15
|
+
'base/test/test/assets/empty_caption_video_segment.mp4');
|
|
16
|
+
const parser = new shaka.media.ClosedCaptionParser();
|
|
17
|
+
parser.init(initSegment);
|
|
18
|
+
parser.parseFrom(videoSegment);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -2278,6 +2278,49 @@ function testDrmEngine(useMediaCapabilities) {
|
|
|
2278
2278
|
[drmInfoAudio]);
|
|
2279
2279
|
expect(returned).toEqual([drmInfoDesired]);
|
|
2280
2280
|
});
|
|
2281
|
+
|
|
2282
|
+
it('dedupes the merged init data based on keyId matching', () => {
|
|
2283
|
+
const serverCert = new Uint8Array(0);
|
|
2284
|
+
const drmInfoVideo = {
|
|
2285
|
+
keySystem: 'drm.abc',
|
|
2286
|
+
licenseServerUri: 'http://abc.drm/license',
|
|
2287
|
+
distinctiveIdentifierRequired: false,
|
|
2288
|
+
persistentStateRequired: true,
|
|
2289
|
+
videoRobustness: 'really_really_ridiculously_good',
|
|
2290
|
+
serverCertificate: serverCert,
|
|
2291
|
+
serverCertificateUri: '',
|
|
2292
|
+
initData: [{keyId: 'v-init'}],
|
|
2293
|
+
keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']),
|
|
2294
|
+
};
|
|
2295
|
+
const drmInfoAudio = {
|
|
2296
|
+
keySystem: 'drm.abc',
|
|
2297
|
+
licenseServerUri: undefined,
|
|
2298
|
+
distinctiveIdentifierRequired: true,
|
|
2299
|
+
persistentStateRequired: false,
|
|
2300
|
+
audioRobustness: 'good',
|
|
2301
|
+
serverCertificate: undefined,
|
|
2302
|
+
serverCertificateUri: '',
|
|
2303
|
+
initData: [{keyId: 'v-init'}, {keyId: 'a-init'}],
|
|
2304
|
+
keyIds: new Set(['eadbeefdeadbeefdeadbeefdeadbeefd']),
|
|
2305
|
+
};
|
|
2306
|
+
const drmInfoDesired = {
|
|
2307
|
+
keySystem: 'drm.abc',
|
|
2308
|
+
licenseServerUri: 'http://abc.drm/license',
|
|
2309
|
+
distinctiveIdentifierRequired: true,
|
|
2310
|
+
persistentStateRequired: true,
|
|
2311
|
+
audioRobustness: 'good',
|
|
2312
|
+
videoRobustness: 'really_really_ridiculously_good',
|
|
2313
|
+
serverCertificate: serverCert,
|
|
2314
|
+
initData: [{keyId: 'v-init'}, {keyId: 'a-init'}],
|
|
2315
|
+
keyIds: new Set([
|
|
2316
|
+
'deadbeefdeadbeefdeadbeefdeadbeef',
|
|
2317
|
+
'eadbeefdeadbeefdeadbeefdeadbeefd',
|
|
2318
|
+
]),
|
|
2319
|
+
};
|
|
2320
|
+
const returned = shaka.media.DrmEngine.getCommonDrmInfos([drmInfoVideo],
|
|
2321
|
+
[drmInfoAudio]);
|
|
2322
|
+
expect(returned).toEqual([drmInfoDesired]);
|
|
2323
|
+
});
|
|
2281
2324
|
}); // describe('getCommonDrmInfos')
|
|
2282
2325
|
|
|
2283
2326
|
describe('configure', () => {
|
|
@@ -2412,6 +2455,19 @@ function testDrmEngine(useMediaCapabilities) {
|
|
|
2412
2455
|
await expectAsync(drmEngine.removeSession('abc'))
|
|
2413
2456
|
.toBeRejectedWith(expected);
|
|
2414
2457
|
});
|
|
2458
|
+
|
|
2459
|
+
// Regression test for #3534
|
|
2460
|
+
it('does not remove the same session again on destroy', async () => {
|
|
2461
|
+
updatePromise.resolve();
|
|
2462
|
+
expect(session1.remove).not.toHaveBeenCalled();
|
|
2463
|
+
await drmEngine.removeSession('abc');
|
|
2464
|
+
expect(session1.remove).toHaveBeenCalled();
|
|
2465
|
+
session1.remove.calls.reset();
|
|
2466
|
+
await drmEngine.destroy();
|
|
2467
|
+
// The session should only be removed ONCE. If it's double-removed, it
|
|
2468
|
+
// will make a (non-fatal) DOMException.
|
|
2469
|
+
expect(session1.remove).not.toHaveBeenCalled();
|
|
2470
|
+
});
|
|
2415
2471
|
});
|
|
2416
2472
|
|
|
2417
2473
|
describe('expiration', () => {
|
|
@@ -1296,12 +1296,13 @@ describe('Playhead', () => {
|
|
|
1296
1296
|
});
|
|
1297
1297
|
|
|
1298
1298
|
// Regression test for https://github.com/google/shaka-player/issues/2987
|
|
1299
|
-
it('does gap jump if paused at 0', () => {
|
|
1299
|
+
it('does gap jump if paused at 0 and has autoplay', () => {
|
|
1300
1300
|
const buffered = [{start: 10, end: 20}];
|
|
1301
1301
|
video.buffered = createFakeBuffered(buffered);
|
|
1302
1302
|
video.currentTime = 0;
|
|
1303
1303
|
video.readyState = HTMLMediaElement.HAVE_ENOUGH_DATA;
|
|
1304
1304
|
video.paused = true;
|
|
1305
|
+
video.autoplay = true;
|
|
1305
1306
|
|
|
1306
1307
|
config.jumpLargeGaps = true;
|
|
1307
1308
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
@@ -1319,6 +1320,31 @@ describe('Playhead', () => {
|
|
|
1319
1320
|
expect(video.currentTime).toBe(10);
|
|
1320
1321
|
});
|
|
1321
1322
|
|
|
1323
|
+
// Regression test for https://github.com/google/shaka-player/issues/3451
|
|
1324
|
+
it('doesn\'t gap jump if paused at 0 and hasn\'t autoplay', () => {
|
|
1325
|
+
const buffered = [{start: 10, end: 20}];
|
|
1326
|
+
video.buffered = createFakeBuffered(buffered);
|
|
1327
|
+
video.currentTime = 0;
|
|
1328
|
+
video.readyState = HTMLMediaElement.HAVE_ENOUGH_DATA;
|
|
1329
|
+
video.paused = true;
|
|
1330
|
+
video.autoplay = false;
|
|
1331
|
+
|
|
1332
|
+
config.jumpLargeGaps = true;
|
|
1333
|
+
playhead = new shaka.media.MediaSourcePlayhead(
|
|
1334
|
+
video,
|
|
1335
|
+
manifest,
|
|
1336
|
+
config,
|
|
1337
|
+
/* startTime= */ 0,
|
|
1338
|
+
Util.spyFunc(onSeek),
|
|
1339
|
+
Util.spyFunc(onEvent));
|
|
1340
|
+
|
|
1341
|
+
playhead.notifyOfBufferingChange();
|
|
1342
|
+
jasmine.clock().tick(500);
|
|
1343
|
+
|
|
1344
|
+
// There should NOT have been a gap jump.
|
|
1345
|
+
expect(video.currentTime).toBe(0);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1322
1348
|
/**
|
|
1323
1349
|
* @param {string} name
|
|
1324
1350
|
* @param {SeekTestInfo} data
|
|
@@ -15,6 +15,9 @@ describe('RegionObserver', () => {
|
|
|
15
15
|
/** @type {!shaka.media.RegionObserver} */
|
|
16
16
|
let observer;
|
|
17
17
|
|
|
18
|
+
/** @type {!jasmine.Spy} */
|
|
19
|
+
let onSeekRange;
|
|
20
|
+
|
|
18
21
|
/** @type {!jasmine.Spy} */
|
|
19
22
|
let onEnterRegion;
|
|
20
23
|
/** @type {!jasmine.Spy} */
|
|
@@ -23,18 +26,26 @@ describe('RegionObserver', () => {
|
|
|
23
26
|
let onSkipRegion;
|
|
24
27
|
|
|
25
28
|
beforeEach(() => {
|
|
29
|
+
onSeekRange = jasmine.createSpy('onSeekRange');
|
|
30
|
+
onSeekRange.and.returnValue({start: 0, end: 100});
|
|
31
|
+
|
|
26
32
|
onEnterRegion = jasmine.createSpy('onEnterRegion');
|
|
27
33
|
onExitRegion = jasmine.createSpy('onExitRegion');
|
|
28
34
|
onSkipRegion = jasmine.createSpy('onSkipRegion');
|
|
29
35
|
|
|
30
36
|
timeline = new shaka.media.RegionTimeline(
|
|
31
|
-
()
|
|
37
|
+
shaka.test.Util.spyFunc(onSeekRange));
|
|
32
38
|
|
|
33
39
|
observer = new shaka.media.RegionObserver(timeline);
|
|
34
|
-
observer.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
observer.addEventListener('enter', (event) => {
|
|
41
|
+
shaka.test.Util.spyFunc(onEnterRegion)(event['region'], event['seeking']);
|
|
42
|
+
});
|
|
43
|
+
observer.addEventListener('exit', (event) => {
|
|
44
|
+
shaka.test.Util.spyFunc(onExitRegion)(event['region'], event['seeking']);
|
|
45
|
+
});
|
|
46
|
+
observer.addEventListener('skip', (event) => {
|
|
47
|
+
shaka.test.Util.spyFunc(onSkipRegion)(event['region'], event['seeking']);
|
|
48
|
+
});
|
|
38
49
|
});
|
|
39
50
|
|
|
40
51
|
it('fires enter event when adding a region the playhead is in', () => {
|
|
@@ -313,6 +324,30 @@ describe('RegionObserver', () => {
|
|
|
313
324
|
expect(onSkipRegion).not.toHaveBeenCalled();
|
|
314
325
|
});
|
|
315
326
|
|
|
327
|
+
// See https://github.com/google/shaka-player/issues/3949
|
|
328
|
+
it('cleans up references to regions', async () => {
|
|
329
|
+
const region = createRegion('my-region', 0, 10);
|
|
330
|
+
timeline.addRegion(region);
|
|
331
|
+
|
|
332
|
+
// Poll so that RegionObserver will store regions.
|
|
333
|
+
poll(observer,
|
|
334
|
+
/* timeInSeconds= */ 5,
|
|
335
|
+
/* seeking= */ false);
|
|
336
|
+
|
|
337
|
+
// Hack to get a private member without the compiler complaining:
|
|
338
|
+
/** @type {Map} */
|
|
339
|
+
const regionMap = (/** @type {?} */(observer))['oldPosition_'];
|
|
340
|
+
expect(regionMap.size).not.toBe(0);
|
|
341
|
+
|
|
342
|
+
// Move the seek range so that the region will be removed.
|
|
343
|
+
onSeekRange.and.returnValue({start: 20, end: 100});
|
|
344
|
+
// Give the timeline time to filter regions
|
|
345
|
+
await shaka.test.Util.delay(
|
|
346
|
+
shaka.media.RegionTimeline.REGION_FILTER_INTERVAL * 2);
|
|
347
|
+
|
|
348
|
+
expect(regionMap.size).toBe(0);
|
|
349
|
+
});
|
|
350
|
+
|
|
316
351
|
/**
|
|
317
352
|
* @param {string} id
|
|
318
353
|
* @param {number} startTimeSeconds
|
|
@@ -14,18 +14,27 @@ describe('RegionTimeline', () => {
|
|
|
14
14
|
/** @type {!jasmine.Spy} */
|
|
15
15
|
let onNewRegion;
|
|
16
16
|
|
|
17
|
+
/** @type {!jasmine.Spy} */
|
|
18
|
+
let onRemoveRegion;
|
|
19
|
+
|
|
17
20
|
/** @type {!jasmine.Spy} */
|
|
18
21
|
let onSeekRange;
|
|
19
22
|
|
|
20
23
|
beforeEach(() => {
|
|
21
|
-
onNewRegion = jasmine.createSpy('onNewRegion');
|
|
22
|
-
|
|
23
24
|
onSeekRange = jasmine.createSpy('onSeekRange');
|
|
24
25
|
onSeekRange.and.returnValue({start: 0, end: 100});
|
|
25
|
-
|
|
26
26
|
timeline = new shaka.media.RegionTimeline(
|
|
27
27
|
shaka.test.Util.spyFunc(onSeekRange));
|
|
28
|
-
|
|
28
|
+
|
|
29
|
+
onNewRegion = jasmine.createSpy('onNewRegion');
|
|
30
|
+
timeline.addEventListener('regionadd', (event) => {
|
|
31
|
+
shaka.test.Util.spyFunc(onNewRegion)(event['region']);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
onRemoveRegion = jasmine.createSpy('onRemoveRegion');
|
|
35
|
+
timeline.addEventListener('regionremove', (event) => {
|
|
36
|
+
shaka.test.Util.spyFunc(onRemoveRegion)(event['region']);
|
|
37
|
+
});
|
|
29
38
|
});
|
|
30
39
|
|
|
31
40
|
afterEach(() => {
|
|
@@ -85,24 +94,33 @@ describe('RegionTimeline', () => {
|
|
|
85
94
|
]);
|
|
86
95
|
});
|
|
87
96
|
|
|
88
|
-
it('filters
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
it('filters regions that end before the seek range', async () => {
|
|
98
|
+
onSeekRange.and.returnValue({start: 5, end: 100});
|
|
99
|
+
|
|
100
|
+
const region1 = createRegion('urn:foo', 'my-region', 0, 3);
|
|
101
|
+
const region2 = createRegion('urn:foo', 'my-region', 3, 10);
|
|
102
|
+
const region3 = createRegion('urn:foo', 'my-region', 5, 10);
|
|
103
|
+
|
|
104
|
+
timeline.addRegion(region1);
|
|
105
|
+
timeline.addRegion(region2);
|
|
106
|
+
timeline.addRegion(region3);
|
|
107
|
+
expect(onNewRegion).toHaveBeenCalledTimes(3);
|
|
108
|
+
expect(onNewRegion).toHaveBeenCalledWith(region1);
|
|
109
|
+
expect(onNewRegion).toHaveBeenCalledWith(region2);
|
|
110
|
+
expect(onNewRegion).toHaveBeenCalledWith(region3);
|
|
111
|
+
|
|
112
|
+
let regions = Array.from(timeline.regions());
|
|
113
|
+
expect(regions.length).toBe(3);
|
|
114
|
+
|
|
115
|
+
// Give the timeline time to filter regions
|
|
116
|
+
await shaka.test.Util.delay(
|
|
117
|
+
shaka.media.RegionTimeline.REGION_FILTER_INTERVAL * 2);
|
|
118
|
+
|
|
119
|
+
regions = Array.from(timeline.regions());
|
|
120
|
+
expect(regions).toEqual([region2, region3]);
|
|
121
|
+
|
|
122
|
+
expect(onRemoveRegion).toHaveBeenCalledWith(region1);
|
|
123
|
+
});
|
|
106
124
|
|
|
107
125
|
/**
|
|
108
126
|
* @param {string} schemeIdUri
|
|
@@ -419,8 +419,9 @@ describe('StreamingEngine', () => {
|
|
|
419
419
|
// 3. Playhead seeks to force us back inside the window
|
|
420
420
|
// 4. (maybe) seek if there is a gap at the period boundary
|
|
421
421
|
// 5. (maybe) seek to flush a pipeline stall
|
|
422
|
+
// 6. (maybe) on slower platforms (e.g. GitHub actions)
|
|
422
423
|
expect(seekCount).toBeGreaterThan(2);
|
|
423
|
-
expect(seekCount).toBeLessThan(
|
|
424
|
+
expect(seekCount).toBeLessThan(7);
|
|
424
425
|
});
|
|
425
426
|
});
|
|
426
427
|
|
|
@@ -97,6 +97,9 @@ describe('StreamingEngine', () => {
|
|
|
97
97
|
/** @type {!shaka.media.StreamingEngine} */
|
|
98
98
|
let streamingEngine;
|
|
99
99
|
|
|
100
|
+
/** @type {function(function(), number)} */
|
|
101
|
+
let realSetTimeout;
|
|
102
|
+
|
|
100
103
|
/**
|
|
101
104
|
* Runs the fake event loop.
|
|
102
105
|
* @param {function()=} callback An optional callback that is executed
|
|
@@ -116,6 +119,7 @@ describe('StreamingEngine', () => {
|
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
beforeAll(() => {
|
|
122
|
+
realSetTimeout = window.setTimeout;
|
|
119
123
|
jasmine.clock().install();
|
|
120
124
|
jasmine.clock().mockDate();
|
|
121
125
|
});
|
|
@@ -822,6 +826,53 @@ describe('StreamingEngine', () => {
|
|
|
822
826
|
.toHaveBeenCalledWith('video', 0, lt20, gt40);
|
|
823
827
|
});
|
|
824
828
|
|
|
829
|
+
// Regression test for https://github.com/google/shaka-player/issues/3717
|
|
830
|
+
it('applies fudge factors for the duration', async () => {
|
|
831
|
+
setupVod();
|
|
832
|
+
|
|
833
|
+
// In #3717, the duration was just barely large enough to encompass an
|
|
834
|
+
// additional segment, but that segment didn't exist, so playback never
|
|
835
|
+
// completed. Here, we set the duration to just beyond the 3rd segment, and
|
|
836
|
+
// we make the 4th segment fail when requested.
|
|
837
|
+
const duration = 30.000000005;
|
|
838
|
+
timeline.getDuration.and.returnValue(duration);
|
|
839
|
+
|
|
840
|
+
const targetUri = '1_video_3'; // The URI of the 4th video segment.
|
|
841
|
+
failRequestsForTarget(netEngine, targetUri);
|
|
842
|
+
|
|
843
|
+
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
844
|
+
createStreamingEngine();
|
|
845
|
+
|
|
846
|
+
// Here we go!
|
|
847
|
+
streamingEngine.switchVariant(variant);
|
|
848
|
+
streamingEngine.switchTextStream(textStream);
|
|
849
|
+
await streamingEngine.start();
|
|
850
|
+
playing = true;
|
|
851
|
+
await runTest();
|
|
852
|
+
|
|
853
|
+
// The end of the stream should have been reached, and the 4th segment from
|
|
854
|
+
// each type should never have been requested.
|
|
855
|
+
expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
|
|
856
|
+
|
|
857
|
+
const segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
858
|
+
|
|
859
|
+
netEngine.expectRequest('0_audio_0', segmentType);
|
|
860
|
+
netEngine.expectRequest('0_video_0', segmentType);
|
|
861
|
+
netEngine.expectRequest('0_text_0', segmentType);
|
|
862
|
+
|
|
863
|
+
netEngine.expectRequest('0_audio_1', segmentType);
|
|
864
|
+
netEngine.expectRequest('0_video_1', segmentType);
|
|
865
|
+
netEngine.expectRequest('0_text_1', segmentType);
|
|
866
|
+
|
|
867
|
+
netEngine.expectRequest('1_audio_2', segmentType);
|
|
868
|
+
netEngine.expectRequest('1_video_2', segmentType);
|
|
869
|
+
netEngine.expectRequest('1_text_2', segmentType);
|
|
870
|
+
|
|
871
|
+
netEngine.expectNoRequest('1_audio_3', segmentType);
|
|
872
|
+
netEngine.expectNoRequest('1_video_3', segmentType);
|
|
873
|
+
netEngine.expectNoRequest('1_text_3', segmentType);
|
|
874
|
+
});
|
|
875
|
+
|
|
825
876
|
it('does not buffer one media type ahead of another', async () => {
|
|
826
877
|
setupVod();
|
|
827
878
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
@@ -1389,6 +1440,66 @@ describe('StreamingEngine', () => {
|
|
|
1389
1440
|
});
|
|
1390
1441
|
});
|
|
1391
1442
|
|
|
1443
|
+
it('into unbuffered regions when nothing is buffered ' +
|
|
1444
|
+
'and mediaState is performing an update', async () => {
|
|
1445
|
+
// Here we go!
|
|
1446
|
+
streamingEngine.switchVariant(variant);
|
|
1447
|
+
streamingEngine.switchTextStream(textStream);
|
|
1448
|
+
// ensure init source buffer promise does not resolve before seeked()
|
|
1449
|
+
// so mediaState remains in "performingUpdate" state
|
|
1450
|
+
const initSourceBufferPromise = new shaka.util.PublicPromise();
|
|
1451
|
+
mediaSourceEngine.setStreamProperties.and
|
|
1452
|
+
.returnValue(initSourceBufferPromise);
|
|
1453
|
+
await streamingEngine.start();
|
|
1454
|
+
playing = true;
|
|
1455
|
+
|
|
1456
|
+
// tick to trigger mediaState updates
|
|
1457
|
+
jasmine.clock().tick(1);
|
|
1458
|
+
// give a chance for fetchAndAppend_ to be invoked
|
|
1459
|
+
// by async onUpdate_ callback
|
|
1460
|
+
await Util.shortDelay(realSetTimeout);
|
|
1461
|
+
|
|
1462
|
+
// Nothing is buffered yet.
|
|
1463
|
+
expect(mediaSourceEngine.segments).toEqual({
|
|
1464
|
+
audio: [false, false, false, false],
|
|
1465
|
+
video: [false, false, false, false],
|
|
1466
|
+
text: [false, false, false, false],
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
// Seek forward to an unbuffered region in the first Period.
|
|
1470
|
+
presentationTimeInSeconds = 15;
|
|
1471
|
+
streamingEngine.seeked();
|
|
1472
|
+
|
|
1473
|
+
// resolve initSourceBufferPromise after seeked(), waitingToClearBuffer
|
|
1474
|
+
// should have been set, so this will now trigger the actual flush of
|
|
1475
|
+
// the buffer
|
|
1476
|
+
initSourceBufferPromise.resolve();
|
|
1477
|
+
mediaSourceEngine.setStreamProperties.and
|
|
1478
|
+
.returnValue(Promise.resolve());
|
|
1479
|
+
|
|
1480
|
+
// allow mediaState update to resolve
|
|
1481
|
+
await Util.shortDelay(realSetTimeout);
|
|
1482
|
+
|
|
1483
|
+
onTick.and.callFake(() => {
|
|
1484
|
+
expect(mediaSourceEngine.clear).toHaveBeenCalled();
|
|
1485
|
+
onTick.and.stub();
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
await runTest(Util.spyFunc(onTick));
|
|
1489
|
+
|
|
1490
|
+
// Verify buffers.
|
|
1491
|
+
expect(mediaSourceEngine.initSegments).toEqual({
|
|
1492
|
+
audio: [false, true],
|
|
1493
|
+
video: [false, true],
|
|
1494
|
+
text: [],
|
|
1495
|
+
});
|
|
1496
|
+
expect(mediaSourceEngine.segments).toEqual({
|
|
1497
|
+
audio: [false, true, true, true],
|
|
1498
|
+
video: [false, true, true, true],
|
|
1499
|
+
text: [false, true, true, true],
|
|
1500
|
+
});
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1392
1503
|
it('into unbuffered regions near segment start', async () => {
|
|
1393
1504
|
// Here we go!
|
|
1394
1505
|
streamingEngine.switchVariant(variant);
|
|
@@ -3388,6 +3499,40 @@ describe('StreamingEngine', () => {
|
|
|
3388
3499
|
segmentAvailability.end++;
|
|
3389
3500
|
}
|
|
3390
3501
|
|
|
3502
|
+
/**
|
|
3503
|
+
* @param {!shaka.test.FakeNetworkingEngine} netEngine A NetworkingEngine
|
|
3504
|
+
* look-alike.
|
|
3505
|
+
* @param {string} targetUri
|
|
3506
|
+
* @param {shaka.util.Error.Code=} errorCode
|
|
3507
|
+
*/
|
|
3508
|
+
function failRequestsForTarget(
|
|
3509
|
+
netEngine, targetUri, errorCode=shaka.util.Error.Code.BAD_HTTP_STATUS) {
|
|
3510
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
3511
|
+
const originalNetEngineRequest = netEngine.request.bind(netEngine);
|
|
3512
|
+
|
|
3513
|
+
netEngine.request = jasmine.createSpy('request').and.callFake(
|
|
3514
|
+
(requestType, request) => {
|
|
3515
|
+
if (request.uris[0] == targetUri) {
|
|
3516
|
+
const data = [targetUri];
|
|
3517
|
+
|
|
3518
|
+
if (errorCode == shaka.util.Error.Code.BAD_HTTP_STATUS) {
|
|
3519
|
+
data.push(404);
|
|
3520
|
+
data.push('');
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
// The compiler still sees the error code parameter as potentially
|
|
3524
|
+
// undefined, even though we gave it a default value.
|
|
3525
|
+
goog.asserts.assert(errorCode != undefined, 'Undefined error code');
|
|
3526
|
+
|
|
3527
|
+
return shaka.util.AbortableOperation.failed(new shaka.util.Error(
|
|
3528
|
+
shaka.util.Error.Severity.CRITICAL,
|
|
3529
|
+
shaka.util.Error.Category.NETWORK,
|
|
3530
|
+
errorCode, data));
|
|
3531
|
+
}
|
|
3532
|
+
return originalNetEngineRequest(requestType, request);
|
|
3533
|
+
});
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3391
3536
|
/**
|
|
3392
3537
|
* @param {!shaka.test.FakeNetworkingEngine} netEngine A NetworkingEngine
|
|
3393
3538
|
* look-alike.
|
|
@@ -652,6 +652,44 @@ describe('Player', () => {
|
|
|
652
652
|
});
|
|
653
653
|
});
|
|
654
654
|
|
|
655
|
+
describe('seek range', () => {
|
|
656
|
+
// Regression test for Issue #3675.
|
|
657
|
+
it('has a reasonable seek range after going from live to VOD', async () => {
|
|
658
|
+
const netEngine = player.getNetworkingEngine();
|
|
659
|
+
const startTime = Date.now();
|
|
660
|
+
netEngine.registerRequestFilter((type, request) => {
|
|
661
|
+
if (type != shaka.net.NetworkingEngine.RequestType.MANIFEST) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
// Simulate a live stream by providing different manifests over time.
|
|
665
|
+
const time = (Date.now() - startTime) / 1000;
|
|
666
|
+
const manifestNumber = Math.min(5, Math.floor(0.5 + time / 2));
|
|
667
|
+
request.uris = [
|
|
668
|
+
'/base/test/test/assets/3675/dash_' + manifestNumber + '.mpd',
|
|
669
|
+
];
|
|
670
|
+
console.log('getting manifest', request.uris);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// Play the stream.
|
|
674
|
+
await player.load('/base/test/test/assets/3675/dash_0.mpd');
|
|
675
|
+
video.play();
|
|
676
|
+
|
|
677
|
+
// Wait for the stream to be over.
|
|
678
|
+
eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
|
|
679
|
+
/** @type {shaka.test.Waiter} */
|
|
680
|
+
const waiter = new shaka.test.Waiter(eventManager)
|
|
681
|
+
.timeoutAfter(20).failOnTimeout(true);
|
|
682
|
+
await waiter.waitForEnd(video);
|
|
683
|
+
|
|
684
|
+
// The stream should have transitioned to VOD by now.
|
|
685
|
+
expect(player.isLive()).toBe(false);
|
|
686
|
+
|
|
687
|
+
// Check that the final seek range is as expected.
|
|
688
|
+
const seekRange = player.seekRange();
|
|
689
|
+
expect(seekRange.end).toBe(14);
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
|
|
655
693
|
describe('buffering', () => {
|
|
656
694
|
const startBuffering = jasmine.objectContaining({buffering: true});
|
|
657
695
|
const endBuffering = jasmine.objectContaining({buffering: false});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|