shaka-player 4.2.0 → 4.2.2
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 +31 -16
- package/.github/workflows/custom-actions/set-commit-status/action.yaml +1 -5
- package/.github/workflows/deflake.yaml +28 -0
- package/.github/workflows/selenium-lab-tests.yaml +168 -54
- package/AUTHORS +3 -0
- package/CHANGELOG.md +30 -0
- package/CONTRIBUTORS +3 -0
- package/demo/common/message_ids.js +1 -0
- package/demo/config.js +3 -1
- package/demo/locales/en.json +1 -0
- package/demo/locales/source.json +4 -0
- package/dist/controls.css +1 -1
- package/dist/controls.css.map +1 -1
- package/dist/demo.compiled.debug.js +7 -6
- package/dist/demo.compiled.debug.map +3 -3
- package/dist/demo.compiled.js +7 -6
- package/dist/demo.compiled.map +3 -3
- package/dist/deps.js +2 -2
- package/dist/locales.js +29 -29
- package/dist/receiver.compiled.debug.js +4 -3
- package/dist/receiver.compiled.debug.map +3 -3
- package/dist/receiver.compiled.js +4 -3
- package/dist/receiver.compiled.map +3 -3
- package/dist/shaka-player.compiled.d.ts +2 -4
- package/dist/shaka-player.compiled.debug.d.ts +2 -4
- package/dist/shaka-player.compiled.debug.externs.js +24 -26
- package/dist/shaka-player.compiled.debug.js +267 -260
- package/dist/shaka-player.compiled.debug.map +4 -4
- package/dist/shaka-player.compiled.externs.js +24 -26
- package/dist/shaka-player.compiled.js +809 -807
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.d.ts +2 -4
- package/dist/shaka-player.ui.debug.d.ts +2 -4
- package/dist/shaka-player.ui.debug.externs.js +24 -26
- package/dist/shaka-player.ui.debug.js +244 -236
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +24 -26
- package/dist/shaka-player.ui.js +973 -971
- package/dist/shaka-player.ui.map +4 -4
- package/docs/jsdoc-template/publish.js +33 -0
- package/docs/jsdoc-template/tmpl/container.tmpl +4 -1
- package/docs/jsdoc.conf.json +1 -0
- package/externs/shaka/manifest_parser.js +4 -1
- package/externs/shaka/player.js +7 -1
- package/lib/ads/client_side_ad_manager.js +7 -0
- package/lib/cast/cast_sender.js +39 -14
- package/lib/cea/cea608_memory.js +4 -0
- package/lib/hls/hls_parser.js +54 -82
- package/lib/media/drm_engine.js +52 -1
- package/lib/media/media_source_engine.js +43 -15
- package/lib/media/segment_index.js +4 -1
- package/lib/media/streaming_engine.js +79 -14
- package/lib/offline/storage.js +1 -0
- package/lib/player.js +29 -5
- package/lib/text/ttml_text_parser.js +6 -0
- package/lib/text/ui_text_displayer.js +8 -0
- package/lib/util/error.js +2 -10
- package/lib/util/platform.js +24 -0
- package/lib/util/player_configuration.js +5 -0
- package/lib/util/pssh.js +2 -1
- package/lib/util/stream_utils.js +2 -1
- package/package.json +1 -1
- package/test/cea/cea608_memory_unit.js +88 -0
- package/test/dash/dash_parser_content_protection_unit.js +1 -0
- package/test/dash/dash_parser_live_unit.js +1 -0
- package/test/dash/dash_parser_manifest_unit.js +1 -0
- package/test/dash/dash_parser_segment_base_unit.js +1 -0
- package/test/dash/dash_parser_segment_list_unit.js +1 -0
- package/test/dash/dash_parser_segment_template_unit.js +1 -0
- package/test/hls/hls_live_unit.js +169 -247
- package/test/hls/hls_parser_unit.js +112 -3
- package/test/media/drm_engine_unit.js +90 -0
- package/test/media/media_source_engine_integration.js +36 -0
- package/test/media/media_source_engine_unit.js +118 -1
- package/test/media/segment_index_unit.js +28 -2
- package/test/media/streaming_engine_integration.js +9 -7
- package/test/media/streaming_engine_unit.js +29 -0
- package/test/offline/offline_integration.js +3 -3
- package/test/player_integration.js +2 -2
- package/test/player_src_equals_integration.js +2 -0
- package/test/player_unit.js +39 -1
- package/test/test/assets/small.mp4 +0 -0
- package/test/test/util/dash_parser_util.js +2 -0
- package/test/test/util/ttml_utils.js +0 -2
- package/test/text/ttml_text_parser_unit.js +46 -0
- package/test/ui/text_displayer_layout_unit.js +17 -2
- package/ui/controls.js +0 -7
- package/ui/less/ad_controls.less +4 -0
|
@@ -98,6 +98,7 @@ describe('HlsParser', () => {
|
|
|
98
98
|
isLowLatencyMode: () => false,
|
|
99
99
|
isAutoLowLatencyMode: () => false,
|
|
100
100
|
enableLowLatencyMode: () => {},
|
|
101
|
+
updateDuration: () => {},
|
|
101
102
|
};
|
|
102
103
|
|
|
103
104
|
parser = new shaka.hls.HlsParser();
|
|
@@ -117,6 +118,8 @@ describe('HlsParser', () => {
|
|
|
117
118
|
.setResponseText('test:/audio2', media)
|
|
118
119
|
.setResponseText('test:/video', media)
|
|
119
120
|
.setResponseText('test:/video2', media)
|
|
121
|
+
.setResponseText('test:/text', media)
|
|
122
|
+
.setResponseText('test:/text2', media)
|
|
120
123
|
.setResponseText('test:/main.vtt', vttText)
|
|
121
124
|
.setResponseValue('test:/init.mp4', initSegmentData)
|
|
122
125
|
.setResponseValue('test:/init2.mp4', initSegmentData)
|
|
@@ -934,11 +937,14 @@ describe('HlsParser', () => {
|
|
|
934
937
|
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
|
|
935
938
|
'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n',
|
|
936
939
|
'video\n',
|
|
940
|
+
|
|
937
941
|
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="en",',
|
|
938
|
-
'URI="audio"\n',
|
|
942
|
+
'NAME="English",URI="audio"\n',
|
|
943
|
+
|
|
939
944
|
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="en",',
|
|
940
945
|
'CHARACTERISTICS="public.accessibility.describes-video,',
|
|
941
|
-
'public.accessibility.describes-music-and-sound",
|
|
946
|
+
'public.accessibility.describes-music-and-sound",',
|
|
947
|
+
'NAME="English (describes-video)",URI="audio2"\n',
|
|
942
948
|
].join('');
|
|
943
949
|
|
|
944
950
|
const media = [
|
|
@@ -976,6 +982,58 @@ describe('HlsParser', () => {
|
|
|
976
982
|
await testHlsParser(master, media, manifest);
|
|
977
983
|
});
|
|
978
984
|
|
|
985
|
+
it('parses characteristics from text tags', async () => {
|
|
986
|
+
const master = [
|
|
987
|
+
'#EXTM3U\n',
|
|
988
|
+
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
|
|
989
|
+
'RESOLUTION=960x540,FRAME-RATE=60,SUBTITLES="sub1"\n',
|
|
990
|
+
'video\n',
|
|
991
|
+
|
|
992
|
+
'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="en",',
|
|
993
|
+
'NAME="English (caption)",DEFAULT=YES,AUTOSELECT=YES,',
|
|
994
|
+
'URI="text"\n',
|
|
995
|
+
|
|
996
|
+
'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="en",',
|
|
997
|
+
'NAME="English (caption)",DEFAULT=YES,AUTOSELECT=YES,',
|
|
998
|
+
'CHARACTERISTICS="public.accessibility.describes-spoken-dialog,',
|
|
999
|
+
'public.accessibility.describes-music-and-sound",',
|
|
1000
|
+
'URI="text2"\n',
|
|
1001
|
+
].join('');
|
|
1002
|
+
|
|
1003
|
+
const media = [
|
|
1004
|
+
'#EXTM3U\n',
|
|
1005
|
+
'#EXT-X-PLAYLIST-TYPE:VOD\n',
|
|
1006
|
+
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
|
|
1007
|
+
'#EXTINF:5,\n',
|
|
1008
|
+
'#EXT-X-BYTERANGE:121090@616\n',
|
|
1009
|
+
'main.mp4',
|
|
1010
|
+
].join('');
|
|
1011
|
+
|
|
1012
|
+
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
|
|
1013
|
+
manifest.anyTimeline();
|
|
1014
|
+
manifest.addPartialVariant((variant) => {
|
|
1015
|
+
variant.addPartialStream(ContentType.VIDEO);
|
|
1016
|
+
});
|
|
1017
|
+
manifest.addPartialTextStream((stream) => {
|
|
1018
|
+
stream.language = 'en';
|
|
1019
|
+
stream.kind = TextStreamKind.SUBTITLE;
|
|
1020
|
+
stream.mime('application/mp4', '');
|
|
1021
|
+
});
|
|
1022
|
+
manifest.addPartialTextStream((stream) => {
|
|
1023
|
+
stream.language = 'en';
|
|
1024
|
+
stream.kind = TextStreamKind.SUBTITLE;
|
|
1025
|
+
stream.mime('application/mp4', '');
|
|
1026
|
+
stream.roles = [
|
|
1027
|
+
'public.accessibility.describes-spoken-dialog',
|
|
1028
|
+
'public.accessibility.describes-music-and-sound',
|
|
1029
|
+
];
|
|
1030
|
+
});
|
|
1031
|
+
manifest.sequenceMode = true;
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
await testHlsParser(master, media, manifest);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
979
1037
|
it('gets mime type from header request', async () => {
|
|
980
1038
|
const master = [
|
|
981
1039
|
'#EXTM3U\n',
|
|
@@ -2537,9 +2595,60 @@ describe('HlsParser', () => {
|
|
|
2537
2595
|
|
|
2538
2596
|
const actual = await parser.start('test:/master', playerInterface);
|
|
2539
2597
|
expect(actual).toEqual(manifest);
|
|
2540
|
-
return actual;
|
|
2541
2598
|
});
|
|
2542
2599
|
|
|
2600
|
+
it('fails on AES-128 if WebCrypto APIs are not available', async () => {
|
|
2601
|
+
const master = [
|
|
2602
|
+
'#EXTM3U\n',
|
|
2603
|
+
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
|
|
2604
|
+
'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n',
|
|
2605
|
+
'video\n',
|
|
2606
|
+
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
|
|
2607
|
+
'URI="audio"\n',
|
|
2608
|
+
].join('');
|
|
2609
|
+
|
|
2610
|
+
const mediaWithMp4AesEncryption = [
|
|
2611
|
+
'#EXTM3U\n',
|
|
2612
|
+
'#EXT-X-PLAYLIST-TYPE:VOD\n',
|
|
2613
|
+
'#EXT-X-KEY:METHOD=AES-128,',
|
|
2614
|
+
'URI="800k.key"\n',
|
|
2615
|
+
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
|
|
2616
|
+
'#EXTINF:5,\n',
|
|
2617
|
+
'#EXT-X-BYTERANGE:121090@616\n',
|
|
2618
|
+
'main.mp4',
|
|
2619
|
+
].join('');
|
|
2620
|
+
|
|
2621
|
+
fakeNetEngine
|
|
2622
|
+
.setResponseText('test:/master', master)
|
|
2623
|
+
.setResponseText('test:/audio', mediaWithMp4AesEncryption)
|
|
2624
|
+
.setResponseText('test:/video', mediaWithMp4AesEncryption)
|
|
2625
|
+
.setResponseValue('test:/init.mp4', initSegmentData)
|
|
2626
|
+
.setResponseValue('test:/main.mp4', segmentData)
|
|
2627
|
+
.setResponseValue('test:/800k.key', aes128Key);
|
|
2628
|
+
|
|
2629
|
+
let originalWebCrypto = null;
|
|
2630
|
+
try {
|
|
2631
|
+
originalWebCrypto = window.crypto;
|
|
2632
|
+
Object.defineProperty(window, 'crypto', {
|
|
2633
|
+
configurable: true,
|
|
2634
|
+
value: null,
|
|
2635
|
+
});
|
|
2636
|
+
|
|
2637
|
+
const expectedError = shaka.test.Util.jasmineError(new shaka.util.Error(
|
|
2638
|
+
shaka.util.Error.Severity.CRITICAL,
|
|
2639
|
+
shaka.util.Error.Category.MANIFEST,
|
|
2640
|
+
shaka.util.Error.Code.NO_WEB_CRYPTO_API));
|
|
2641
|
+
await expectAsync(parser.start('test:/master', playerInterface))
|
|
2642
|
+
.toBeRejectedWith(expectedError);
|
|
2643
|
+
} finally {
|
|
2644
|
+
Object.defineProperty(window, 'crypto', {
|
|
2645
|
+
configurable: true,
|
|
2646
|
+
value: originalWebCrypto,
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
});
|
|
2650
|
+
|
|
2651
|
+
|
|
2543
2652
|
it('constructs DrmInfo for Widevine', async () => {
|
|
2544
2653
|
const master = [
|
|
2545
2654
|
'#EXTM3U\n',
|
|
@@ -677,6 +677,35 @@ describe('DrmEngine', () => {
|
|
|
677
677
|
.toBeRejectedWith(expected);
|
|
678
678
|
});
|
|
679
679
|
|
|
680
|
+
it('uses key system IDs from keySystemsMapping config', async () => {
|
|
681
|
+
manifest = shaka.test.ManifestGenerator.generate((manifest) => {
|
|
682
|
+
manifest.addVariant(0, (variant) => {
|
|
683
|
+
variant.addVideo(1, (stream) => {
|
|
684
|
+
stream.encrypted = true;
|
|
685
|
+
stream.addDrmInfo('drm.abc');
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
setDecodingInfoSpy([]);
|
|
691
|
+
|
|
692
|
+
config.keySystemsMapping['drm.abc'] = 'drm.def';
|
|
693
|
+
drmEngine.configure(config);
|
|
694
|
+
|
|
695
|
+
const variants = manifest.variants;
|
|
696
|
+
await expectAsync(
|
|
697
|
+
drmEngine.initForPlayback(variants, manifest.offlineSessionIds))
|
|
698
|
+
.toBeRejected();
|
|
699
|
+
|
|
700
|
+
expect(drmEngine.initialized()).toBe(false);
|
|
701
|
+
expect(decodingInfoSpy).toHaveBeenCalledTimes(1);
|
|
702
|
+
expect(decodingInfoSpy).toHaveBeenCalledWith(containing({
|
|
703
|
+
keySystemConfiguration: containing({
|
|
704
|
+
keySystem: 'drm.def',
|
|
705
|
+
}),
|
|
706
|
+
}));
|
|
707
|
+
});
|
|
708
|
+
|
|
680
709
|
it('maps TS MIME types through the transmuxer', async () => {
|
|
681
710
|
const originalIsSupported = shaka.media.Transmuxer.isSupported;
|
|
682
711
|
|
|
@@ -1068,6 +1097,14 @@ describe('DrmEngine', () => {
|
|
|
1068
1097
|
'encrypted', jasmine.any(Function), jasmine.anything());
|
|
1069
1098
|
});
|
|
1070
1099
|
|
|
1100
|
+
it('is not listened for if parseInbandPsshEnabled is true', async () => {
|
|
1101
|
+
config.parseInbandPsshEnabled = true;
|
|
1102
|
+
drmEngine.configure(config);
|
|
1103
|
+
await initAndAttach();
|
|
1104
|
+
expect(mockVideo.addEventListener).not.toHaveBeenCalledWith(
|
|
1105
|
+
'encrypted', jasmine.any(Function), jasmine.anything());
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1071
1108
|
it('triggers the creation of a session', async () => {
|
|
1072
1109
|
await initAndAttach();
|
|
1073
1110
|
const initData1 = new Uint8Array(1);
|
|
@@ -2326,6 +2363,59 @@ describe('DrmEngine', () => {
|
|
|
2326
2363
|
}
|
|
2327
2364
|
});
|
|
2328
2365
|
|
|
2366
|
+
describe('parseInbandPssh', () => {
|
|
2367
|
+
const WIDEVINE_PSSH =
|
|
2368
|
+
'00000028' + // atom size
|
|
2369
|
+
'70737368' + // atom type='pssh'
|
|
2370
|
+
'00000000' + // v0, flags=0
|
|
2371
|
+
'edef8ba979d64acea3c827dcd51d21ed' + // system id (Widevine)
|
|
2372
|
+
'00000008' + // data size
|
|
2373
|
+
'0102030405060708'; // data
|
|
2374
|
+
|
|
2375
|
+
const PLAYREADY_PSSH =
|
|
2376
|
+
'00000028' + // atom size
|
|
2377
|
+
'70737368' + // atom type 'pssh'
|
|
2378
|
+
'00000000' + // v0, flags=0
|
|
2379
|
+
'9a04f07998404286ab92e65be0885f95' + // system id (PlayReady)
|
|
2380
|
+
'00000008' + // data size
|
|
2381
|
+
'0102030405060708'; // data
|
|
2382
|
+
|
|
2383
|
+
const SEGMENT =
|
|
2384
|
+
'00000058' + // atom size = 28x + 28x + 8x
|
|
2385
|
+
'6d6f6f66' + // atom type 'moof'
|
|
2386
|
+
WIDEVINE_PSSH +
|
|
2387
|
+
PLAYREADY_PSSH;
|
|
2388
|
+
|
|
2389
|
+
const binarySegment = shaka.util.Uint8ArrayUtils.fromHex(SEGMENT);
|
|
2390
|
+
|
|
2391
|
+
it('calls newInitData when enabled', async () => {
|
|
2392
|
+
config.parseInbandPsshEnabled = true;
|
|
2393
|
+
await initAndAttach();
|
|
2394
|
+
|
|
2395
|
+
/** @type {!jasmine.Spy} */
|
|
2396
|
+
const newInitDataSpy = jasmine.createSpy('newInitData');
|
|
2397
|
+
drmEngine.newInitData = shaka.test.Util.spyFunc(newInitDataSpy);
|
|
2398
|
+
|
|
2399
|
+
await drmEngine.parseInbandPssh(
|
|
2400
|
+
shaka.util.ManifestParserUtils.ContentType.VIDEO, binarySegment);
|
|
2401
|
+
const expectedInitData = shaka.util.Uint8ArrayUtils.fromHex(
|
|
2402
|
+
WIDEVINE_PSSH + PLAYREADY_PSSH);
|
|
2403
|
+
expect(newInitDataSpy).toHaveBeenCalledWith('cenc', expectedInitData);
|
|
2404
|
+
});
|
|
2405
|
+
|
|
2406
|
+
it('does not call newInitData when disabled', async () => {
|
|
2407
|
+
config.parseInbandPsshEnabled = false;
|
|
2408
|
+
await initAndAttach();
|
|
2409
|
+
|
|
2410
|
+
/** @type {!jasmine.Spy} */
|
|
2411
|
+
const newInitDataSpy = jasmine.createSpy('newInitData');
|
|
2412
|
+
drmEngine.newInitData = shaka.test.Util.spyFunc(newInitDataSpy);
|
|
2413
|
+
await drmEngine.parseInbandPssh(
|
|
2414
|
+
shaka.util.ManifestParserUtils.ContentType.VIDEO, binarySegment);
|
|
2415
|
+
expect(newInitDataSpy).not.toHaveBeenCalled();
|
|
2416
|
+
});
|
|
2417
|
+
});
|
|
2418
|
+
|
|
2329
2419
|
async function initAndAttach() {
|
|
2330
2420
|
const variants = manifest.variants;
|
|
2331
2421
|
await drmEngine.initForPlayback(variants, manifest.offlineSessionIds);
|
|
@@ -380,6 +380,42 @@ describe('MediaSourceEngine', () => {
|
|
|
380
380
|
expect(textDisplayer.appendSpy).toHaveBeenCalledTimes(3);
|
|
381
381
|
});
|
|
382
382
|
|
|
383
|
+
it('buffers partial TS video segments in sequence mode', async () => {
|
|
384
|
+
metadata = shaka.test.TestScheme.DATA['cea-708_ts'];
|
|
385
|
+
generators = shaka.test.TestScheme.GENERATORS['cea-708_ts'];
|
|
386
|
+
|
|
387
|
+
const videoType = ContentType.VIDEO;
|
|
388
|
+
const initObject = new Map();
|
|
389
|
+
initObject.set(videoType, getFakeStream(metadata.video));
|
|
390
|
+
|
|
391
|
+
await mediaSourceEngine.init(
|
|
392
|
+
initObject, /* forceTransmuxTS= */ false, /* sequenceMode= */ true);
|
|
393
|
+
await mediaSourceEngine.setDuration(presentationDuration);
|
|
394
|
+
await mediaSourceEngine.setStreamProperties(
|
|
395
|
+
videoType,
|
|
396
|
+
/* timestampOffset= */ 0,
|
|
397
|
+
/* appendWindowStart= */ 0,
|
|
398
|
+
/* appendWindowEnd= */ Infinity,
|
|
399
|
+
/* sequenceMode= */ true);
|
|
400
|
+
|
|
401
|
+
const segment = generators[videoType].getSegment(0, Date.now() / 1000);
|
|
402
|
+
const partialSegmentLength = Math.floor(segment.byteLength / 3);
|
|
403
|
+
|
|
404
|
+
let partialSegment = shaka.util.BufferUtils.toUint8(
|
|
405
|
+
segment, /* offset= */ 0, /* length= */ partialSegmentLength);
|
|
406
|
+
let reference = dummyReference(videoType, 0);
|
|
407
|
+
await mediaSourceEngine.appendBuffer(
|
|
408
|
+
videoType, partialSegment, reference, /* hasClosedCaptions= */ false);
|
|
409
|
+
|
|
410
|
+
partialSegment = shaka.util.BufferUtils.toUint8(
|
|
411
|
+
segment,
|
|
412
|
+
/* offset= */ partialSegmentLength);
|
|
413
|
+
reference = dummyReference(videoType, 1);
|
|
414
|
+
await mediaSourceEngine.appendBuffer(
|
|
415
|
+
videoType, partialSegment, reference, /* hasClosedCaptions= */ false,
|
|
416
|
+
/* seeked= */ true);
|
|
417
|
+
});
|
|
418
|
+
|
|
383
419
|
it('extracts CEA-708 captions from dash', async () => {
|
|
384
420
|
// Load MP4 file with CEA-708 closed captions.
|
|
385
421
|
metadata = shaka.test.TestScheme.DATA['cea-708_mp4'];
|
|
@@ -26,7 +26,8 @@ let MockTimeRanges;
|
|
|
26
26
|
* timestampOffset: number,
|
|
27
27
|
* appendWindowEnd: number,
|
|
28
28
|
* updateend: function(),
|
|
29
|
-
* error: function()
|
|
29
|
+
* error: function(),
|
|
30
|
+
* mode: string
|
|
30
31
|
* }}
|
|
31
32
|
*/
|
|
32
33
|
let MockSourceBuffer;
|
|
@@ -57,6 +58,7 @@ describe('MediaSourceEngine', () => {
|
|
|
57
58
|
let audioSourceBuffer;
|
|
58
59
|
let videoSourceBuffer;
|
|
59
60
|
let mockVideo;
|
|
61
|
+
|
|
60
62
|
/** @type {HTMLMediaElement} */
|
|
61
63
|
let video;
|
|
62
64
|
let mockMediaSource;
|
|
@@ -622,6 +624,83 @@ describe('MediaSourceEngine', () => {
|
|
|
622
624
|
|
|
623
625
|
expect(mockTextEngine.storeAndAppendClosedCaptions).toHaveBeenCalled();
|
|
624
626
|
});
|
|
627
|
+
|
|
628
|
+
it('sets timestampOffset on adaptations in sequence mode', async () => {
|
|
629
|
+
const initObject = new Map();
|
|
630
|
+
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
631
|
+
videoSourceBuffer.mode = 'sequence';
|
|
632
|
+
|
|
633
|
+
await mediaSourceEngine.init(
|
|
634
|
+
initObject, /* forceTransmuxTS= */ false, /* sequenceMode= */ true);
|
|
635
|
+
|
|
636
|
+
expect(videoSourceBuffer.timestampOffset).toBe(0);
|
|
637
|
+
|
|
638
|
+
// Mocks appending a segment from a newly adapted variant with a 0.50
|
|
639
|
+
// second misalignment from the old variant.
|
|
640
|
+
const reference = dummyReference(0, 1000);
|
|
641
|
+
reference.startTime = 0.50;
|
|
642
|
+
const appendVideo = mediaSourceEngine.appendBuffer(
|
|
643
|
+
ContentType.VIDEO, buffer, reference, /* hasClosedCaptions= */ false,
|
|
644
|
+
/* seeked= */ false, /* adaptation= */ true);
|
|
645
|
+
videoSourceBuffer.updateend();
|
|
646
|
+
await appendVideo;
|
|
647
|
+
|
|
648
|
+
expect(videoSourceBuffer.timestampOffset).toBe(0.50);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('calls abort before setting timestampOffset', async () => {
|
|
652
|
+
const simulateUpdate = async () => {
|
|
653
|
+
await Util.shortDelay();
|
|
654
|
+
videoSourceBuffer.updateend();
|
|
655
|
+
};
|
|
656
|
+
const initObject = new Map();
|
|
657
|
+
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
658
|
+
|
|
659
|
+
await mediaSourceEngine.init(
|
|
660
|
+
initObject, /* forceTransmuxTS= */ false, /* sequenceMode= */ true);
|
|
661
|
+
|
|
662
|
+
// First, mock the scenario where timestampOffset is set to help align
|
|
663
|
+
// text segments. In this case, SourceBuffer mode is still 'segments'.
|
|
664
|
+
let reference = dummyReference(0, 1000);
|
|
665
|
+
let appendVideo = mediaSourceEngine.appendBuffer(
|
|
666
|
+
ContentType.VIDEO, buffer, reference, /* hasClosedCaptions= */ false);
|
|
667
|
+
// Wait for the first appendBuffer(), in segments mode.
|
|
668
|
+
await simulateUpdate();
|
|
669
|
+
// Next, wait for abort(), used to reset the parser state for a safe
|
|
670
|
+
// setting of timestampOffset. Shaka fakes an updateend event on abort(),
|
|
671
|
+
// so simulateUpdate() isn't needed.
|
|
672
|
+
await Util.shortDelay();
|
|
673
|
+
// Next, wait for remove(), used to clear the SourceBuffer from the
|
|
674
|
+
// initial append.
|
|
675
|
+
await simulateUpdate();
|
|
676
|
+
// Next, wait for the second appendBuffer(), falling through to normal
|
|
677
|
+
// operations.
|
|
678
|
+
await simulateUpdate();
|
|
679
|
+
// Lastly, wait for the function-scoped MediaSourceEngine#appendBuffer()
|
|
680
|
+
// promise to resolve.
|
|
681
|
+
await appendVideo;
|
|
682
|
+
expect(videoSourceBuffer.abort).toHaveBeenCalledTimes(1);
|
|
683
|
+
|
|
684
|
+
// Second, mock the scenario where timestampOffset is set during an
|
|
685
|
+
// unbuffered seek or adaptation. SourceBuffer mode is 'sequence' now.
|
|
686
|
+
reference = dummyReference(0, 1000);
|
|
687
|
+
appendVideo = mediaSourceEngine.appendBuffer(
|
|
688
|
+
ContentType.VIDEO, buffer, reference, /* hasClosedCaptions= */ false,
|
|
689
|
+
/* seeked= */ true);
|
|
690
|
+
// First, wait for abort(), used to reset the parser state for a safe
|
|
691
|
+
// setting of timestampOffset.
|
|
692
|
+
await Util.shortDelay();
|
|
693
|
+
// The subsequent setTimestampOffset() fakes an updateend event for us, so
|
|
694
|
+
// simulateUpdate() isn't needed.
|
|
695
|
+
await Util.shortDelay();
|
|
696
|
+
// Next, wait for the second appendBuffer(), falling through to normal
|
|
697
|
+
// operations.
|
|
698
|
+
await simulateUpdate();
|
|
699
|
+
// Lastly, wait for the function-scoped MediaSourceEngine#appendBuffer()
|
|
700
|
+
// promise to resolve.
|
|
701
|
+
await appendVideo;
|
|
702
|
+
expect(videoSourceBuffer.abort).toHaveBeenCalledTimes(2);
|
|
703
|
+
});
|
|
625
704
|
});
|
|
626
705
|
|
|
627
706
|
describe('remove', () => {
|
|
@@ -1003,6 +1082,43 @@ describe('MediaSourceEngine', () => {
|
|
|
1003
1082
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
1004
1083
|
audioSourceBuffer.updateend();
|
|
1005
1084
|
});
|
|
1085
|
+
|
|
1086
|
+
it('allows duration to be shrunk', async () => {
|
|
1087
|
+
// Pretend the initial duration was 100.
|
|
1088
|
+
mockMediaSource.durationGetter.and.returnValue(100);
|
|
1089
|
+
|
|
1090
|
+
// When duration is shrunk, 'updateend' events are generated. This is
|
|
1091
|
+
// because reducing the duration triggers the MSE removal algorithm to
|
|
1092
|
+
// run.
|
|
1093
|
+
mockMediaSource.durationSetter.and.callFake((duration) => {
|
|
1094
|
+
expect(duration).toBe(50);
|
|
1095
|
+
videoSourceBuffer.updateend();
|
|
1096
|
+
audioSourceBuffer.updateend();
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
audioSourceBuffer.appendBuffer.and.callFake(() => {
|
|
1100
|
+
audioSourceBuffer.updateend();
|
|
1101
|
+
});
|
|
1102
|
+
videoSourceBuffer.appendBuffer.and.callFake(() => {
|
|
1103
|
+
videoSourceBuffer.updateend();
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
/** @type {!Promise} */
|
|
1107
|
+
const p1 = mediaSourceEngine.setDuration(50);
|
|
1108
|
+
expect(mockMediaSource.durationSetter).not.toHaveBeenCalled();
|
|
1109
|
+
|
|
1110
|
+
// These operations should be blocked until after duration is shrunk.
|
|
1111
|
+
// This is tested because shrinking duration generates 'updateend'
|
|
1112
|
+
// events, and we want to show that the queue still operates correctly.
|
|
1113
|
+
const a1 = mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null,
|
|
1114
|
+
/* hasClosedCaptions= */ false);
|
|
1115
|
+
const a2 = mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null,
|
|
1116
|
+
/* hasClosedCaptions= */ false);
|
|
1117
|
+
|
|
1118
|
+
await p1;
|
|
1119
|
+
await a1;
|
|
1120
|
+
await a2;
|
|
1121
|
+
});
|
|
1006
1122
|
});
|
|
1007
1123
|
|
|
1008
1124
|
describe('destroy', () => {
|
|
@@ -1160,6 +1276,7 @@ describe('MediaSourceEngine', () => {
|
|
|
1160
1276
|
appendWindowEnd: Infinity,
|
|
1161
1277
|
updateend: () => {},
|
|
1162
1278
|
error: () => {},
|
|
1279
|
+
mode: 'segments',
|
|
1163
1280
|
};
|
|
1164
1281
|
}
|
|
1165
1282
|
|
|
@@ -207,6 +207,25 @@ describe('SegmentIndex', /** @suppress {accessControls} */ () => {
|
|
|
207
207
|
];
|
|
208
208
|
expect(index.references).toEqual(newReferences);
|
|
209
209
|
});
|
|
210
|
+
|
|
211
|
+
it('preserves hls key of the last reference', () => {
|
|
212
|
+
// The hls key of the last segment should be preserved.
|
|
213
|
+
const references = [
|
|
214
|
+
makeReference(uri(0), 0, 5, [],
|
|
215
|
+
{method: 'AES-128', firstMediaSequenceNumber: 0}),
|
|
216
|
+
makeReference(uri(1), 5, 10, [],
|
|
217
|
+
{method: 'AES-128', firstMediaSequenceNumber: 0}),
|
|
218
|
+
makeReference(uri(2), 10, 15, [],
|
|
219
|
+
{method: 'AES-128', firstMediaSequenceNumber: 0}),
|
|
220
|
+
];
|
|
221
|
+
const index = new shaka.media.SegmentIndex(references);
|
|
222
|
+
expect(index.references).toEqual(references);
|
|
223
|
+
|
|
224
|
+
index.fit(/* windowStart= */ 0, /* windowEnd= */ 10);
|
|
225
|
+
expect(
|
|
226
|
+
index.references[index.references.length - 1].hlsAes128Key,
|
|
227
|
+
).toEqual({method: 'AES-128', firstMediaSequenceNumber: 0});
|
|
228
|
+
});
|
|
210
229
|
});
|
|
211
230
|
|
|
212
231
|
describe('merge', () => {
|
|
@@ -955,9 +974,11 @@ describe('SegmentIndex', /** @suppress {accessControls} */ () => {
|
|
|
955
974
|
* @param {number} startTime
|
|
956
975
|
* @param {number} endTime
|
|
957
976
|
* @param {!Array.<!shaka.media.SegmentReference>=} partialReferences
|
|
977
|
+
* @param {?shaka.extern.HlsAes128Key=} hlsAes128Key
|
|
958
978
|
* @return {shaka.media.SegmentReference}
|
|
959
979
|
*/
|
|
960
|
-
function makeReference(uri, startTime, endTime, partialReferences = []
|
|
980
|
+
function makeReference(uri, startTime, endTime, partialReferences = [],
|
|
981
|
+
hlsAes128Key = null) {
|
|
961
982
|
return new shaka.media.SegmentReference(
|
|
962
983
|
startTime,
|
|
963
984
|
endTime,
|
|
@@ -968,6 +989,11 @@ describe('SegmentIndex', /** @suppress {accessControls} */ () => {
|
|
|
968
989
|
/* timestampOffset= */ 0,
|
|
969
990
|
/* appendWindowStart= */ 0,
|
|
970
991
|
/* appendWindowEnd= */ Infinity,
|
|
971
|
-
/* partialReferences= */ partialReferences
|
|
992
|
+
/* partialReferences= */ partialReferences,
|
|
993
|
+
/* tilesLayout= */ undefined,
|
|
994
|
+
/* tileDuration= */ undefined,
|
|
995
|
+
/* syncTime= */ undefined,
|
|
996
|
+
/* status= */ undefined,
|
|
997
|
+
/* hlsAes128Key= */ hlsAes128Key);
|
|
972
998
|
}
|
|
973
999
|
});
|
|
@@ -90,17 +90,17 @@ describe('StreamingEngine', () => {
|
|
|
90
90
|
|
|
91
91
|
segmentAvailability = {
|
|
92
92
|
start: 0,
|
|
93
|
-
end:
|
|
93
|
+
end: 40,
|
|
94
94
|
};
|
|
95
95
|
|
|
96
96
|
timeline = shaka.test.StreamingEngineUtil.createFakePresentationTimeline(
|
|
97
97
|
segmentAvailability,
|
|
98
|
-
/* presentationDuration= */
|
|
98
|
+
/* presentationDuration= */ 40,
|
|
99
99
|
/* maxSegmentDuration= */ metadata.video.segmentDuration,
|
|
100
100
|
/* isLive= */ false);
|
|
101
101
|
|
|
102
102
|
setupNetworkingEngine(
|
|
103
|
-
/* presentationDuration= */
|
|
103
|
+
/* presentationDuration= */ 40,
|
|
104
104
|
{
|
|
105
105
|
audio: metadata.audio.segmentDuration,
|
|
106
106
|
video: metadata.video.segmentDuration,
|
|
@@ -108,8 +108,8 @@ describe('StreamingEngine', () => {
|
|
|
108
108
|
|
|
109
109
|
setupManifest(
|
|
110
110
|
/* firstPeriodStartTime= */ 0,
|
|
111
|
-
/* secondPeriodStartTime= */
|
|
112
|
-
/* presentationDuration= */
|
|
111
|
+
/* secondPeriodStartTime= */ 20,
|
|
112
|
+
/* presentationDuration= */ 40);
|
|
113
113
|
|
|
114
114
|
setupPlayhead();
|
|
115
115
|
|
|
@@ -247,6 +247,7 @@ describe('StreamingEngine', () => {
|
|
|
247
247
|
onManifestUpdate: () => {},
|
|
248
248
|
onSegmentAppended: () => playhead.notifyOfBufferingChange(),
|
|
249
249
|
onInitSegmentAppended: () => {},
|
|
250
|
+
beforeAppendSegment: () => Promise.resolve(),
|
|
250
251
|
};
|
|
251
252
|
streamingEngine = new shaka.media.StreamingEngine(
|
|
252
253
|
/** @type {shaka.extern.Manifest} */(manifest), playerInterface);
|
|
@@ -263,10 +264,11 @@ describe('StreamingEngine', () => {
|
|
|
263
264
|
streamingEngine.switchVariant(variant);
|
|
264
265
|
await streamingEngine.start();
|
|
265
266
|
video.play();
|
|
266
|
-
// The overall test timeout is 120 seconds, and the content is
|
|
267
|
+
// The overall test timeout is 120 seconds, and the content is 40
|
|
267
268
|
// seconds. It should be possible to complete this test in 100 seconds,
|
|
268
269
|
// and if not, we want the error thrown to be within the overall test's
|
|
269
|
-
// timeout window.
|
|
270
|
+
// timeout window. Note that we have seen some devices fail to play at
|
|
271
|
+
// full speed for reasons beyond our control, so we plan for >= 0.5x.
|
|
270
272
|
await waiter.timeoutAfter(100).waitForEnd(video);
|
|
271
273
|
});
|
|
272
274
|
|
|
@@ -72,6 +72,8 @@ describe('StreamingEngine', () => {
|
|
|
72
72
|
let getBandwidthEstimate;
|
|
73
73
|
/** @type {!shaka.media.StreamingEngine} */
|
|
74
74
|
let streamingEngine;
|
|
75
|
+
/** @type {!jasmine.Spy} */
|
|
76
|
+
let beforeAppendSegment;
|
|
75
77
|
|
|
76
78
|
/** @type {function(function(), number)} */
|
|
77
79
|
let realSetTimeout;
|
|
@@ -429,9 +431,14 @@ describe('StreamingEngine', () => {
|
|
|
429
431
|
onEvent = jasmine.createSpy('onEvent');
|
|
430
432
|
onManifestUpdate = jasmine.createSpy('onManifestUpdate');
|
|
431
433
|
onSegmentAppended = jasmine.createSpy('onSegmentAppended');
|
|
434
|
+
beforeAppendSegment = jasmine.createSpy('beforeAppendSegment');
|
|
432
435
|
getBandwidthEstimate = jasmine.createSpy('getBandwidthEstimate');
|
|
433
436
|
getBandwidthEstimate.and.returnValue(1e3);
|
|
434
437
|
|
|
438
|
+
beforeAppendSegment.and.callFake((segment) => {
|
|
439
|
+
return Promise.resolve();
|
|
440
|
+
});
|
|
441
|
+
|
|
435
442
|
if (!config) {
|
|
436
443
|
config = shaka.util.PlayerConfiguration.createDefault().streaming;
|
|
437
444
|
config.rebufferingGoal = 2;
|
|
@@ -453,6 +460,7 @@ describe('StreamingEngine', () => {
|
|
|
453
460
|
onManifestUpdate: Util.spyFunc(onManifestUpdate),
|
|
454
461
|
onSegmentAppended: Util.spyFunc(onSegmentAppended),
|
|
455
462
|
onInitSegmentAppended: () => {},
|
|
463
|
+
beforeAppendSegment: Util.spyFunc(beforeAppendSegment),
|
|
456
464
|
};
|
|
457
465
|
streamingEngine = new shaka.media.StreamingEngine(
|
|
458
466
|
/** @type {shaka.extern.Manifest} */(manifest), playerInterface);
|
|
@@ -3546,6 +3554,27 @@ describe('StreamingEngine', () => {
|
|
|
3546
3554
|
});
|
|
3547
3555
|
});
|
|
3548
3556
|
|
|
3557
|
+
describe('beforeAppendSegment', () => {
|
|
3558
|
+
it('is called before appending media segment', async () => {
|
|
3559
|
+
setupVod();
|
|
3560
|
+
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
3561
|
+
createStreamingEngine();
|
|
3562
|
+
beforeAppendSegment.and.callFake((segment) => {
|
|
3563
|
+
return shaka.test.Util.shortDelay();
|
|
3564
|
+
});
|
|
3565
|
+
streamingEngine.switchVariant(variant);
|
|
3566
|
+
streamingEngine.switchTextStream(textStream);
|
|
3567
|
+
await streamingEngine.start();
|
|
3568
|
+
// Simulate time passing.
|
|
3569
|
+
playing = true;
|
|
3570
|
+
await Util.fakeEventLoop(10);
|
|
3571
|
+
expect(beforeAppendSegment).toHaveBeenCalledWith(
|
|
3572
|
+
ContentType.AUDIO, segmentData[ContentType.AUDIO].initSegments[0]);
|
|
3573
|
+
expect(beforeAppendSegment).toHaveBeenCalledWith(
|
|
3574
|
+
ContentType.AUDIO, segmentData[ContentType.AUDIO].segments[0]);
|
|
3575
|
+
});
|
|
3576
|
+
});
|
|
3577
|
+
|
|
3549
3578
|
/**
|
|
3550
3579
|
* Slides the segment availability window forward by 1 second.
|
|
3551
3580
|
*/
|
|
@@ -68,7 +68,7 @@ filterDescribe('Offline', supportsStorage, () => {
|
|
|
68
68
|
await player.load(contentUri);
|
|
69
69
|
|
|
70
70
|
video.play();
|
|
71
|
-
await playTo(/* end= */ 3, /* timeout= */
|
|
71
|
+
await playTo(/* end= */ 3, /* timeout= */ 20);
|
|
72
72
|
await player.unload();
|
|
73
73
|
await storage.remove(contentUri);
|
|
74
74
|
});
|
|
@@ -104,7 +104,7 @@ filterDescribe('Offline', supportsStorage, () => {
|
|
|
104
104
|
await player.load(contentUri);
|
|
105
105
|
|
|
106
106
|
video.play();
|
|
107
|
-
await playTo(/* end= */ 3, /* timeout= */
|
|
107
|
+
await playTo(/* end= */ 3, /* timeout= */ 20);
|
|
108
108
|
await player.unload();
|
|
109
109
|
await storage.remove(contentUri);
|
|
110
110
|
});
|
|
@@ -147,7 +147,7 @@ filterDescribe('Offline', supportsStorage, () => {
|
|
|
147
147
|
await player.load(contentUri);
|
|
148
148
|
|
|
149
149
|
video.play();
|
|
150
|
-
await playTo(/* end= */ 3, /* timeout= */
|
|
150
|
+
await playTo(/* end= */ 3, /* timeout= */ 20);
|
|
151
151
|
await player.unload();
|
|
152
152
|
await storage.remove(contentUri);
|
|
153
153
|
});
|
|
@@ -434,7 +434,7 @@ describe('Player', () => {
|
|
|
434
434
|
// Seek the video, and see if it can continue playing from that point.
|
|
435
435
|
video.currentTime = 20;
|
|
436
436
|
// Expect that we can then reach the end of the video.
|
|
437
|
-
await waiter.timeoutAfter(
|
|
437
|
+
await waiter.timeoutAfter(40).waitForEnd(video);
|
|
438
438
|
});
|
|
439
439
|
|
|
440
440
|
// Regression test for #2326.
|
|
@@ -785,7 +785,7 @@ describe('Player', () => {
|
|
|
785
785
|
/** @type {shaka.test.Waiter} */
|
|
786
786
|
const waiter = new shaka.test.Waiter(eventManager)
|
|
787
787
|
.setPlayer(player)
|
|
788
|
-
.timeoutAfter(
|
|
788
|
+
.timeoutAfter(40)
|
|
789
789
|
.failOnTimeout(true);
|
|
790
790
|
await waiter.waitForEnd(video);
|
|
791
791
|
|
|
@@ -305,6 +305,8 @@ describe('Player Src Equals', () => {
|
|
|
305
305
|
expect(stats.loadLatency).toBeGreaterThan(0);
|
|
306
306
|
expect(stats.manifestTimeSeconds).toBeNaN(); // There's no manifest.
|
|
307
307
|
expect(stats.drmTimeSeconds).toBeNaN(); // There's no DRM.
|
|
308
|
+
expect(stats.height).toBe(110);
|
|
309
|
+
expect(stats.width).toBe(256);
|
|
308
310
|
});
|
|
309
311
|
|
|
310
312
|
it('plays with external text tracks', async () => {
|