shaka-player 4.2.1 → 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.
Files changed (80) hide show
  1. package/.github/workflows/build-and-test.yaml +31 -16
  2. package/.github/workflows/custom-actions/set-commit-status/action.yaml +1 -5
  3. package/.github/workflows/deflake.yaml +28 -0
  4. package/.github/workflows/selenium-lab-tests.yaml +13 -4
  5. package/AUTHORS +3 -0
  6. package/CHANGELOG.md +19 -0
  7. package/CONTRIBUTORS +3 -0
  8. package/demo/common/message_ids.js +1 -0
  9. package/demo/config.js +3 -1
  10. package/demo/locales/en.json +1 -0
  11. package/demo/locales/source.json +4 -0
  12. package/dist/demo.compiled.debug.js +7 -6
  13. package/dist/demo.compiled.debug.map +3 -3
  14. package/dist/demo.compiled.js +7 -6
  15. package/dist/demo.compiled.map +3 -3
  16. package/dist/deps.js +2 -2
  17. package/dist/locales.js +32 -32
  18. package/dist/receiver.compiled.debug.js +4 -3
  19. package/dist/receiver.compiled.debug.map +3 -3
  20. package/dist/receiver.compiled.js +4 -3
  21. package/dist/receiver.compiled.map +3 -3
  22. package/dist/shaka-player.compiled.d.ts +2 -2
  23. package/dist/shaka-player.compiled.debug.d.ts +2 -2
  24. package/dist/shaka-player.compiled.debug.externs.js +24 -24
  25. package/dist/shaka-player.compiled.debug.js +237 -229
  26. package/dist/shaka-player.compiled.debug.map +4 -4
  27. package/dist/shaka-player.compiled.externs.js +24 -24
  28. package/dist/shaka-player.compiled.js +744 -741
  29. package/dist/shaka-player.compiled.map +4 -4
  30. package/dist/shaka-player.ui.d.ts +2 -2
  31. package/dist/shaka-player.ui.debug.d.ts +2 -2
  32. package/dist/shaka-player.ui.debug.externs.js +24 -24
  33. package/dist/shaka-player.ui.debug.js +212 -203
  34. package/dist/shaka-player.ui.debug.map +4 -4
  35. package/dist/shaka-player.ui.externs.js +24 -24
  36. package/dist/shaka-player.ui.js +905 -901
  37. package/dist/shaka-player.ui.map +4 -4
  38. package/docs/jsdoc-template/publish.js +33 -0
  39. package/docs/jsdoc-template/tmpl/container.tmpl +4 -1
  40. package/docs/jsdoc.conf.json +1 -0
  41. package/externs/shaka/manifest_parser.js +4 -1
  42. package/externs/shaka/player.js +7 -1
  43. package/lib/ads/client_side_ad_manager.js +7 -0
  44. package/lib/cast/cast_sender.js +39 -14
  45. package/lib/cea/cea608_memory.js +4 -0
  46. package/lib/hls/hls_parser.js +30 -8
  47. package/lib/media/drm_engine.js +52 -1
  48. package/lib/media/media_source_engine.js +43 -15
  49. package/lib/media/segment_index.js +4 -1
  50. package/lib/media/streaming_engine.js +79 -14
  51. package/lib/offline/storage.js +1 -0
  52. package/lib/player.js +17 -5
  53. package/lib/text/ttml_text_parser.js +6 -0
  54. package/lib/util/platform.js +24 -0
  55. package/lib/util/player_configuration.js +4 -0
  56. package/lib/util/pssh.js +2 -1
  57. package/lib/util/stream_utils.js +2 -1
  58. package/package.json +1 -1
  59. package/test/cea/cea608_memory_unit.js +88 -0
  60. package/test/dash/dash_parser_content_protection_unit.js +1 -0
  61. package/test/dash/dash_parser_live_unit.js +1 -0
  62. package/test/dash/dash_parser_manifest_unit.js +1 -0
  63. package/test/dash/dash_parser_segment_base_unit.js +1 -0
  64. package/test/dash/dash_parser_segment_list_unit.js +1 -0
  65. package/test/dash/dash_parser_segment_template_unit.js +1 -0
  66. package/test/hls/hls_live_unit.js +169 -247
  67. package/test/hls/hls_parser_unit.js +60 -2
  68. package/test/media/drm_engine_unit.js +61 -0
  69. package/test/media/media_source_engine_integration.js +36 -0
  70. package/test/media/media_source_engine_unit.js +118 -1
  71. package/test/media/segment_index_unit.js +28 -2
  72. package/test/media/streaming_engine_integration.js +9 -7
  73. package/test/media/streaming_engine_unit.js +29 -0
  74. package/test/offline/offline_integration.js +3 -3
  75. package/test/player_integration.js +2 -2
  76. package/test/test/util/dash_parser_util.js +2 -0
  77. package/test/test/util/ttml_utils.js +0 -2
  78. package/test/text/ttml_text_parser_unit.js +46 -0
  79. package/test/ui/text_displayer_layout_unit.js +17 -2
  80. package/ui/controls.js +0 -7
@@ -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",URI="audio2"\n',
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',
@@ -1097,6 +1097,14 @@ describe('DrmEngine', () => {
1097
1097
  'encrypted', jasmine.any(Function), jasmine.anything());
1098
1098
  });
1099
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
+
1100
1108
  it('triggers the creation of a session', async () => {
1101
1109
  await initAndAttach();
1102
1110
  const initData1 = new Uint8Array(1);
@@ -2355,6 +2363,59 @@ describe('DrmEngine', () => {
2355
2363
  }
2356
2364
  });
2357
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
+
2358
2419
  async function initAndAttach() {
2359
2420
  const variants = manifest.variants;
2360
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: 60,
93
+ end: 40,
94
94
  };
95
95
 
96
96
  timeline = shaka.test.StreamingEngineUtil.createFakePresentationTimeline(
97
97
  segmentAvailability,
98
- /* presentationDuration= */ 60,
98
+ /* presentationDuration= */ 40,
99
99
  /* maxSegmentDuration= */ metadata.video.segmentDuration,
100
100
  /* isLive= */ false);
101
101
 
102
102
  setupNetworkingEngine(
103
- /* presentationDuration= */ 60,
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= */ 30,
112
- /* presentationDuration= */ 60);
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 60
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= */ 10);
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= */ 10);
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= */ 10);
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(20).waitForEnd(video);
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(20)
788
+ .timeoutAfter(40)
789
789
  .failOnTimeout(true);
790
790
  await waiter.waitForEnd(video);
791
791
 
@@ -43,6 +43,7 @@ shaka.test.Dash = class {
43
43
  isLowLatencyMode: () => false,
44
44
  isAutoLowLatencyMode: () => false,
45
45
  enableLowLatencyMode: () => {},
46
+ updateDuration: () => {},
46
47
  };
47
48
  const manifest = await dashParser.start('dummy://foo', playerInterface);
48
49
  const stream = manifest.variants[0].video;
@@ -77,6 +78,7 @@ shaka.test.Dash = class {
77
78
  isLowLatencyMode: () => false,
78
79
  isAutoLowLatencyMode: () => false,
79
80
  enableLowLatencyMode: () => {},
81
+ updateDuration: () => {},
80
82
  };
81
83
  const p = dashParser.start('dummy://foo', playerInterface);
82
84
  await expectAsync(p).toBeRejectedWith(
@@ -51,8 +51,6 @@ shaka.test.TtmlUtils = class {
51
51
  region,
52
52
  nestedCues: jasmine.any(Object),
53
53
  payload: '',
54
- startTime: 0,
55
- endTime: Infinity,
56
54
  isContainer: true,
57
55
  });
58
56
  Object.assign(containerCue, properties);
@@ -1535,6 +1535,52 @@ describe('TtmlTextParser', () => {
1535
1535
  {startTime: 1, endTime: 2});
1536
1536
  });
1537
1537
 
1538
+ // Regression test for #4468
1539
+ it('defaults the body background to transparent', () => {
1540
+ verifyHelper(
1541
+ // One cue, don't care about the details
1542
+ [{}],
1543
+ '<tt xmlns:tts="http://www.w3.org/ns/ttml#styling">' +
1544
+ '<head>' +
1545
+ '<styling>' +
1546
+ '<style xml:id="s1" tts:backgroundColor="black" />' +
1547
+ '</styling>' +
1548
+ '</head>' +
1549
+ '<body><div>' +
1550
+ '<p begin="00:01.00" end="00:02.00">' +
1551
+ '<span style="s1">Test</span>' +
1552
+ '</p>' +
1553
+ '</div></body></tt>',
1554
+ {periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0},
1555
+ // The body must have these properties:
1556
+ {backgroundColor: 'transparent'},
1557
+ // The div must have these properties:
1558
+ {}); // don't care
1559
+ });
1560
+
1561
+ // Regression test for #4468
1562
+ it('allows the body background color to be set', () => {
1563
+ verifyHelper(
1564
+ // One cue, don't care about the details
1565
+ [{}],
1566
+ '<tt xmlns:tts="http://www.w3.org/ns/ttml#styling">' +
1567
+ '<head>' +
1568
+ '<styling>' +
1569
+ '<style xml:id="s1" tts:backgroundColor="black" />' +
1570
+ '</styling>' +
1571
+ '</head>' +
1572
+ '<body style="s1"><div>' +
1573
+ '<p begin="00:01.00" end="00:02.00">' +
1574
+ '<span>Test</span>' +
1575
+ '</p>' +
1576
+ '</div></body></tt>',
1577
+ {periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0},
1578
+ // The body must have these properties:
1579
+ {backgroundColor: 'black'},
1580
+ // The div must have these properties:
1581
+ {}); // don't care
1582
+ });
1583
+
1538
1584
  it('parses wrapping option', () => {
1539
1585
  verifyHelper(
1540
1586
  [