shaka-player 3.2.0 → 3.2.4

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 (162) hide show
  1. package/.github/workflows/build_and_test.yaml +73 -0
  2. package/.github/workflows/release.yaml +53 -0
  3. package/AUTHORS +3 -0
  4. package/CHANGELOG.md +182 -1
  5. package/CONTRIBUTORS +3 -0
  6. package/README.md +4 -4
  7. package/build/checkversion.py +1 -1
  8. package/build/generateExterns.js +1 -1
  9. package/build/misspellings.txt +1 -0
  10. package/build/shakaBuildHelpers.py +6 -5
  11. package/build/subprocessWindowsPatch.py +103 -0
  12. package/build/test.py +6 -0
  13. package/demo/common/assets.js +1 -1
  14. package/demo/index.html +1 -1
  15. package/dist/controls.css +2 -2
  16. package/dist/controls.css.map +1 -1
  17. package/dist/demo.compiled.debug.js +1 -1
  18. package/dist/demo.compiled.js +1 -1
  19. package/dist/deps.js +8 -8
  20. package/dist/locales.js +37 -37
  21. package/dist/receiver.compiled.debug.js +1 -1
  22. package/dist/receiver.compiled.js +1 -1
  23. package/dist/shaka-player.compiled.d.ts +195 -5
  24. package/dist/shaka-player.compiled.debug.d.ts +195 -5
  25. package/dist/shaka-player.compiled.debug.externs.js +226 -14
  26. package/dist/shaka-player.compiled.debug.js +424 -399
  27. package/dist/shaka-player.compiled.debug.map +4 -4
  28. package/dist/shaka-player.compiled.externs.js +226 -14
  29. package/dist/shaka-player.compiled.js +826 -812
  30. package/dist/shaka-player.compiled.map +4 -4
  31. package/dist/shaka-player.ui.d.ts +197 -16
  32. package/dist/shaka-player.ui.debug.d.ts +197 -16
  33. package/dist/shaka-player.ui.debug.externs.js +230 -27
  34. package/dist/shaka-player.ui.debug.js +466 -442
  35. package/dist/shaka-player.ui.debug.map +4 -4
  36. package/dist/shaka-player.ui.externs.js +230 -27
  37. package/dist/shaka-player.ui.js +946 -932
  38. package/dist/shaka-player.ui.map +4 -4
  39. package/docs/tutorials/fairplay.md +1 -1
  40. package/docs/tutorials/faq.md +14 -0
  41. package/docs/tutorials/ui.md +5 -5
  42. package/externs/ima.js +4 -0
  43. package/externs/shaka/ads.js +25 -0
  44. package/externs/shaka/player.js +8 -0
  45. package/externs/shaka/text.js +10 -0
  46. package/karma.conf.js +20 -15
  47. package/lib/ads/ad_manager.js +46 -26
  48. package/lib/ads/client_side_ad_manager.js +82 -88
  49. package/lib/ads/server_side_ad_manager.js +45 -13
  50. package/lib/cast/cast_proxy.js +6 -2
  51. package/lib/cast/cast_receiver.js +5 -1
  52. package/lib/cast/cast_sender.js +2 -2
  53. package/lib/cea/sei_processor.js +24 -17
  54. package/lib/dash/dash_parser.js +131 -24
  55. package/lib/dash/segment_list.js +1 -1
  56. package/lib/dash/segment_template.js +1 -1
  57. package/lib/hls/hls_parser.js +74 -17
  58. package/lib/hls/manifest_text_parser.js +1 -0
  59. package/lib/media/closed_caption_parser.js +3 -1
  60. package/lib/media/content_workarounds.js +12 -0
  61. package/lib/media/drm_engine.js +20 -1
  62. package/lib/media/gap_jumping_controller.js +8 -4
  63. package/lib/media/media_source_engine.js +1 -2
  64. package/lib/media/playhead.js +1 -1
  65. package/lib/media/region_observer.js +39 -33
  66. package/lib/media/region_timeline.js +17 -19
  67. package/lib/media/segment_index.js +2 -1
  68. package/lib/media/segment_reference.js +19 -1
  69. package/lib/media/streaming_engine.js +8 -4
  70. package/lib/net/networking_engine.js +6 -1
  71. package/lib/offline/download_manager.js +1 -2
  72. package/lib/offline/storage.js +8 -9
  73. package/lib/player.js +129 -59
  74. package/lib/polyfill/aria.js +12 -2
  75. package/lib/polyfill/encryption_scheme.js +2 -0
  76. package/lib/polyfill/fullscreen.js +2 -0
  77. package/lib/polyfill/mathround.js +2 -0
  78. package/lib/polyfill/media_capabilities.js +73 -21
  79. package/lib/polyfill/mediasource.js +8 -0
  80. package/lib/polyfill/orientation.js +3 -1
  81. package/lib/polyfill/patchedmediakeys_apple.js +6 -4
  82. package/lib/polyfill/patchedmediakeys_ms.js +6 -4
  83. package/lib/polyfill/patchedmediakeys_nop.js +2 -0
  84. package/lib/polyfill/patchedmediakeys_webkit.js +6 -4
  85. package/lib/polyfill/pip_webkit.js +2 -0
  86. package/lib/polyfill/storage_estimate.js +2 -0
  87. package/lib/polyfill/video_play_promise.js +2 -0
  88. package/lib/polyfill/videoplaybackquality.js +2 -0
  89. package/lib/polyfill/vttcue.js +2 -0
  90. package/lib/text/cue.js +6 -0
  91. package/lib/text/mp4_ttml_parser.js +3 -0
  92. package/lib/text/text_engine.js +14 -2
  93. package/lib/text/ttml_text_parser.js +135 -87
  94. package/lib/text/ui_text_displayer.js +213 -80
  95. package/lib/text/vtt_text_parser.js +4 -2
  96. package/lib/text/web_vtt_generator.js +21 -12
  97. package/lib/util/fake_event.js +53 -7
  98. package/lib/util/fake_event_target.js +21 -1
  99. package/lib/util/mp4_box_parsers.js +1 -1
  100. package/lib/util/periods.js +4 -1
  101. package/lib/util/platform.js +13 -1
  102. package/lib/util/stream_utils.js +105 -13
  103. package/lib/util/xml_utils.js +50 -12
  104. package/package.json +3 -6
  105. package/roadmap.md +24 -22
  106. package/test/cast/cast_proxy_unit.js +12 -6
  107. package/test/cast/cast_receiver_integration.js +1 -1
  108. package/test/cast/cast_utils_unit.js +1 -1
  109. package/test/cea/mp4_cea_parser_unit.js +26 -0
  110. package/test/dash/dash_parser_live_unit.js +43 -0
  111. package/test/dash/dash_parser_manifest_unit.js +222 -3
  112. package/test/hls/hls_parser_unit.js +79 -1
  113. package/test/media/closed_caption_parser_unit.js +20 -0
  114. package/test/media/drm_engine_integration.js +86 -2
  115. package/test/media/drm_engine_unit.js +57 -0
  116. package/test/media/playhead_unit.js +27 -1
  117. package/test/media/region_observer_unit.js +40 -5
  118. package/test/media/region_timeline_unit.js +40 -22
  119. package/test/media/streaming_engine_integration.js +2 -1
  120. package/test/media/streaming_engine_unit.js +145 -0
  121. package/test/player_integration.js +79 -8
  122. package/test/player_src_equals_integration.js +41 -8
  123. package/test/player_unit.js +14 -4
  124. package/test/test/assets/3675/chunk-stream0-00001.m4s +0 -0
  125. package/test/test/assets/3675/chunk-stream0-00002.m4s +0 -0
  126. package/test/test/assets/3675/chunk-stream0-00003.m4s +0 -0
  127. package/test/test/assets/3675/chunk-stream0-00004.m4s +0 -0
  128. package/test/test/assets/3675/chunk-stream0-00005.m4s +0 -0
  129. package/test/test/assets/3675/chunk-stream0-00006.m4s +0 -0
  130. package/test/test/assets/3675/chunk-stream0-00007.m4s +0 -0
  131. package/test/test/assets/3675/dash_0.mpd +30 -0
  132. package/test/test/assets/3675/dash_1.mpd +30 -0
  133. package/test/test/assets/3675/dash_2.mpd +30 -0
  134. package/test/test/assets/3675/dash_3.mpd +30 -0
  135. package/test/test/assets/3675/dash_4.mpd +30 -0
  136. package/test/test/assets/3675/dash_5.mpd +26 -0
  137. package/test/test/assets/3675/init-stream0.m4s +0 -0
  138. package/test/test/assets/chapters.vtt +2 -2
  139. package/test/test/assets/chapters2.vtt +10 -0
  140. package/test/test/assets/empty_caption_video_init.mp4 +0 -0
  141. package/test/test/assets/empty_caption_video_segment.mp4 +0 -0
  142. package/test/test/util/fake_ad_manager.js +11 -1
  143. package/test/test/util/ttml_utils.js +81 -0
  144. package/test/test/util/util.js +10 -7
  145. package/test/text/mp4_ttml_parser_unit.js +11 -18
  146. package/test/text/ttml_text_parser_unit.js +542 -220
  147. package/test/text/ui_text_displayer_unit.js +93 -20
  148. package/test/text/vtt_text_parser_unit.js +17 -1
  149. package/test/text/web_vtt_generator_unit.js +58 -3
  150. package/test/ui/localization_unit.js +15 -14
  151. package/test/ui/text_displayer_layout_unit.js +21 -8
  152. package/test/util/mp4_box_parsers_unit.js +28 -0
  153. package/test/util/stream_utils_unit.js +60 -3
  154. package/ui/cast_button.js +2 -3
  155. package/ui/controls.js +11 -6
  156. package/ui/less/buttons.less +0 -4
  157. package/ui/less/containers.less +6 -7
  158. package/ui/locales/ru.json +6 -6
  159. package/ui/localization.js +40 -87
  160. package/ui/pip_button.js +2 -3
  161. package/ui/seek_bar.js +1 -2
  162. package/ui/text_selection.js +1 -0
@@ -38,6 +38,14 @@ describe('DashParser Manifest', () => {
38
38
 
39
39
  /** @type {string} */
40
40
  const thumbnailScheme = 'http://dashif.org/guidelines/thumbnail_tile';
41
+ /**
42
+ * CICP scheme. parameter must be one of the following: "ColourPrimaries",
43
+ * "TransferCharacteristics", or "MatrixCoefficients".
44
+ *
45
+ * @param {string} parameter
46
+ * @return {string}
47
+ */
48
+ const cicpScheme = (parameter) => `urn:mpeg:mpegB:cicp:${parameter}`;
41
49
 
42
50
  beforeAll(async () => {
43
51
  mp4Index = await shaka.test.Util.fetch(mp4IndexSegmentUri);
@@ -975,7 +983,8 @@ describe('DashParser Manifest', () => {
975
983
 
976
984
  it('duplicate Representation ids with live', async () => {
977
985
  const source = [
978
- '<MPD minBufferTime="PT75S" type="dynamic">',
986
+ '<MPD minBufferTime="PT75S" type="dynamic"',
987
+ ' availabilityStartTime="1970-01-01T00:00:00Z">',
979
988
  ' <Period id="1" duration="PT30S">',
980
989
  ' <AdaptationSet mimeType="video/mp4">',
981
990
  ' <Representation id="1" bandwidth="1">',
@@ -1864,7 +1873,7 @@ describe('DashParser Manifest', () => {
1864
1873
  expect(textStream.kind).toBe('caption');
1865
1874
  });
1866
1875
 
1867
- it('converts Roles element to "forced"', async () => {
1876
+ it('converts Roles element to "forced" (old role)', async () => {
1868
1877
  const manifestText = [
1869
1878
  '<MPD minBufferTime="PT75S">',
1870
1879
  ' <Period id="1" duration="PT30S">',
@@ -1896,7 +1905,40 @@ describe('DashParser Manifest', () => {
1896
1905
  expect(textStream.forced).toBe(true);
1897
1906
  });
1898
1907
 
1899
- it('supports HDR signaling', async () => {
1908
+ it('converts Roles element to "forced"', async () => {
1909
+ const manifestText = [
1910
+ '<MPD minBufferTime="PT75S">',
1911
+ ' <Period id="1" duration="PT30S">',
1912
+ ' <AdaptationSet id="1" contentType="text">',
1913
+ ' <Accessibility schemeIdUri="urn:mpeg:dash:role:2011" ',
1914
+ ' value="captions" />',
1915
+ ' <Accessibility schemeIdUri="urn:mpeg:dash:role:2011" ',
1916
+ ' value="forced-subtitle" />',
1917
+ ' <Accessibility schemeIdUri="foobar" value="bar" />',
1918
+ ' <Representation id="text-en" mimeType="text/webvtt">',
1919
+ ' <BaseURL>t-en.vtt</BaseURL>',
1920
+ ' </Representation>',
1921
+ ' </AdaptationSet>',
1922
+ ' <AdaptationSet id="1" mimeType="video/mp4">',
1923
+ ' <Representation id="video-sd" width="640" height="480">',
1924
+ ' <BaseURL>v-sd.mp4</BaseURL>',
1925
+ ' <SegmentBase indexRange="100-200" />',
1926
+ ' </Representation>',
1927
+ ' </AdaptationSet>',
1928
+ ' </Period>',
1929
+ '</MPD>',
1930
+ ].join('\n');
1931
+
1932
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
1933
+ /** @type {shaka.extern.Manifest} */
1934
+ const manifest = await parser.start('dummy://foo', playerInterface);
1935
+ const textStream = manifest.textStreams[0];
1936
+ expect(textStream.roles).toEqual(['captions', 'forced-subtitle']);
1937
+ expect(textStream.forced).toBe(true);
1938
+ });
1939
+
1940
+ it('supports HDR signaling via profiles', async () => {
1941
+ // (DASH-IF IOP v4.3 10.3.3.)
1900
1942
  const hdrProfile =
1901
1943
  'http://dashif.org/guidelines/dash-if-uhd#hevc-hdr-pq10';
1902
1944
  const manifestText = [
@@ -1927,6 +1969,110 @@ describe('DashParser Manifest', () => {
1927
1969
  expect(stream.hdr).toBe('PQ');
1928
1970
  });
1929
1971
 
1972
+ it('supports HDR signaling via SupplementalProperty', async () => {
1973
+ // (DASH-IF IOP v4.3 6.2.5.1.)
1974
+ const hdrScheme = cicpScheme('TransferCharacteristics');
1975
+ const pq = 16;
1976
+ const manifestText = [
1977
+ '<MPD minBufferTime="PT75S">',
1978
+ ' <Period id="1" duration="PT30S">',
1979
+ ' <AdaptationSet id="2" mimeType="video/mp4">',
1980
+ ` <SupplementalProperty schemeIdUri="${hdrScheme}" value="${pq}" />`,
1981
+ ' <Representation codecs="hvc1.2.4.L150.90">',
1982
+ ' <BaseURL>v-sd.mp4</BaseURL>',
1983
+ ' <SegmentBase indexRange="100-200" />',
1984
+ ' </Representation>',
1985
+ ' </AdaptationSet>',
1986
+ ' <AdaptationSet id="3" mimeType="audio/mp4">',
1987
+ ' <Representation id="audio-en">',
1988
+ ' <BaseURL>a-en.mp4</BaseURL>',
1989
+ ' <SegmentBase indexRange="100-200" />',
1990
+ ' </Representation>',
1991
+ ' </AdaptationSet>',
1992
+ ' </Period>',
1993
+ '</MPD>',
1994
+ ].join('\n');
1995
+
1996
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
1997
+
1998
+ /** @type {shaka.extern.Manifest} */
1999
+ const manifest = await parser.start('dummy://foo', playerInterface);
2000
+ expect(manifest.variants.length).toBe(1);
2001
+ const stream = manifest.variants[0].video;
2002
+ expect(stream.hdr).toBe('PQ');
2003
+ });
2004
+
2005
+ it('supports HDR signaling via EssentialProperty', async () => {
2006
+ // (DASH-IF IOP v4.3 6.2.5.1.)
2007
+ const hdrScheme = cicpScheme('TransferCharacteristics');
2008
+ const hlg = 18;
2009
+ const manifestText = [
2010
+ '<MPD minBufferTime="PT75S">',
2011
+ ' <Period id="1" duration="PT30S">',
2012
+ ' <AdaptationSet id="2" mimeType="video/mp4">',
2013
+ ` <EssentialProperty schemeIdUri="${hdrScheme}" value="${hlg}" />`,
2014
+ ' <Representation codecs="hvc1.2.4.L153.B0">',
2015
+ ' <BaseURL>v-sd.mp4</BaseURL>',
2016
+ ' <SegmentBase indexRange="100-200" />',
2017
+ ' </Representation>',
2018
+ ' </AdaptationSet>',
2019
+ ' <AdaptationSet id="3" mimeType="audio/mp4">',
2020
+ ' <Representation id="audio-en">',
2021
+ ' <BaseURL>a-en.mp4</BaseURL>',
2022
+ ' <SegmentBase indexRange="100-200" />',
2023
+ ' </Representation>',
2024
+ ' </AdaptationSet>',
2025
+ ' </Period>',
2026
+ '</MPD>',
2027
+ ].join('\n');
2028
+
2029
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
2030
+
2031
+ /** @type {shaka.extern.Manifest} */
2032
+ const manifest = await parser.start('dummy://foo', playerInterface);
2033
+ expect(manifest.variants.length).toBe(1);
2034
+ const stream = manifest.variants[0].video;
2035
+ expect(stream.hdr).toBe('HLG');
2036
+ });
2037
+
2038
+ it('supports SDR signalling via EssentialProperty', async () => {
2039
+ // (DASH-IF IOP v4.3 6.2.5.1.)
2040
+ const scheme = cicpScheme('TransferCharacteristics');
2041
+ const sdrValues = [1, 6, 13, 14, 15];
2042
+ const manifestPromises = [];
2043
+ for (const value of sdrValues) {
2044
+ const manifestText = [
2045
+ '<MPD minBufferTime="PT75S">',
2046
+ ' <Period id="1" duration="PT30S">',
2047
+ ' <AdaptationSet id="2" mimeType="video/mp4">',
2048
+ ` <EssentialProperty schemeIdUri="${scheme}" value="${value}" />`,
2049
+ ' <Representation codecs="avc1.640028">',
2050
+ ' <BaseURL>v-sd.mp4</BaseURL>',
2051
+ ' <SegmentBase indexRange="100-200" />',
2052
+ ' </Representation>',
2053
+ ' </AdaptationSet>',
2054
+ ' <AdaptationSet id="3" mimeType="audio/mp4">',
2055
+ ' <Representation id="audio-en">',
2056
+ ' <BaseURL>a-en.mp4</BaseURL>',
2057
+ ' <SegmentBase indexRange="100-200" />',
2058
+ ' </Representation>',
2059
+ ' </AdaptationSet>',
2060
+ ' </Period>',
2061
+ '</MPD>',
2062
+ ].join('\n');
2063
+
2064
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
2065
+
2066
+ manifestPromises.push(parser.start('dummy://foo', playerInterface));
2067
+ }
2068
+ const manifests = await Promise.all(manifestPromises);
2069
+ for (const manifest of manifests) {
2070
+ expect(manifest.variants.length).toBe(1);
2071
+ const stream = manifest.variants[0].video;
2072
+ expect(stream.hdr).toBe('SDR');
2073
+ }
2074
+ });
2075
+
1930
2076
  it('Does not error when image adaptation sets are present', async () => {
1931
2077
  const manifestText = [
1932
2078
  '<MPD minBufferTime="PT75S">',
@@ -2150,4 +2296,77 @@ describe('DashParser Manifest', () => {
2150
2296
  expect(uri).not.toContain('/p2/');
2151
2297
  }
2152
2298
  });
2299
+
2300
+ /**
2301
+ * @param {!Array.<number>} periods Start time of multiple periods
2302
+ * @return {string}
2303
+ */
2304
+ function buildManifestWithPeriodStartTime(periods) {
2305
+ const mpdTemplate = [
2306
+ `<MPD type="dynamic"`,
2307
+ 'availabilityStartTime="1970-01-01T00:00:00Z"',
2308
+ 'timeShiftBufferDepth="PT10H">',
2309
+ ' %(periods)s',
2310
+ '</MPD>',
2311
+ ].join('\n');
2312
+ const periodTemplate = (id, period, duration) => {
2313
+ return [
2314
+ ` <Period id="${id}" start="PT${period}S">`,
2315
+ ' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
2316
+ ' <SegmentTemplate startNumber="1" media="l-$Number$.mp4">',
2317
+ ' <SegmentTimeline>',
2318
+ ` <S t="0" d="${duration}" />`,
2319
+ ' </SegmentTimeline>',
2320
+ ' </SegmentTemplate>',
2321
+ ' <Representation id="1"/>',
2322
+ ' </AdaptationSet>',
2323
+ ' </Period>',
2324
+ ].join('\n');
2325
+ };
2326
+ const periodXmls = periods.map((period, i) => {
2327
+ const duration = i+1 === periods.length ? 10 : periods[i+1] - period;
2328
+ // Period start time as ID here. If we use index then there will be
2329
+ // periods with same period ID and different start time which are invalid.
2330
+ return periodTemplate(period, period, duration);
2331
+ });
2332
+ return sprintf(mpdTemplate, {
2333
+ periods: periodXmls.join('\n'),
2334
+ });
2335
+ }
2336
+
2337
+ // Bug description: Inconsistent period start time in the manifests due
2338
+ // to failover triggered in backend servers
2339
+
2340
+ // When one of the servers is down, the manifest will be served by other
2341
+ // redundant servers. The period start time might become out of sync
2342
+ // during the switch-over/recovery.
2343
+
2344
+ it('skip periods that are earlier than max period start time', async () => {
2345
+ const sources = [
2346
+ buildManifestWithPeriodStartTime([5, 15]),
2347
+ buildManifestWithPeriodStartTime([6, 15]), // simulate out-of-sync of +1s
2348
+ buildManifestWithPeriodStartTime([4, 15]), // simulate out-of-sync of -1s
2349
+ ];
2350
+ const segments = [];
2351
+
2352
+ for (const source of sources) {
2353
+ fakeNetEngine.setResponseText('dummy://foo', source);
2354
+ /** @type {shaka.extern.Manifest} */
2355
+ // eslint-disable-next-line no-await-in-loop
2356
+ const manifest = await parser.start('dummy://foo', playerInterface);
2357
+ const video = manifest.variants[0].video;
2358
+ // eslint-disable-next-line no-await-in-loop
2359
+ await video.createSegmentIndex();
2360
+ goog.asserts.assert(video.segmentIndex, 'Null segmentIndex!');
2361
+ segments.push(Array.from(video.segmentIndex));
2362
+ }
2363
+
2364
+ // Expect identical segments
2365
+ expect(segments[0].length).toBe(2);
2366
+ expect(segments[1].length).toBe(2);
2367
+ expect(segments[0][0].startTime).toBe(5);
2368
+ expect(segments[1][0].startTime).toBe(5);
2369
+ expect(segments[0][1].startTime).toBe(15);
2370
+ expect(segments[1][1].startTime).toBe(15);
2371
+ });
2153
2372
  });
@@ -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
+ });
@@ -162,7 +162,7 @@ describe('DrmEngine', () => {
162
162
  document.body.removeChild(video);
163
163
  });
164
164
 
165
- function checkSupport() {
165
+ function checkTrueDrmSupport() {
166
166
  if (shaka.util.Platform.isXboxOne()) {
167
167
  // Axinom won't issue a license for an Xbox One. The error message from
168
168
  // the license server says "Your DRM client's security level is 150, but
@@ -174,7 +174,11 @@ describe('DrmEngine', () => {
174
174
  return support['com.widevine.alpha'] || support['com.microsoft.playready'];
175
175
  }
176
176
 
177
- filterDescribe('basic flow', checkSupport, () => {
177
+ function checkClearKeySupport() {
178
+ return support['org.w3.clearkey'];
179
+ }
180
+
181
+ filterDescribe('basic flow', checkTrueDrmSupport, () => {
178
182
  drmIt('gets a license and can play encrypted segments', async () => {
179
183
  // The error callback should not be invoked.
180
184
  onErrorSpy.and.callFake(fail);
@@ -274,4 +278,84 @@ describe('DrmEngine', () => {
274
278
  expect(video.currentTime).toBeGreaterThan(0);
275
279
  });
276
280
  }); // describe('basic flow')
281
+
282
+ filterDescribe('ClearKey', checkClearKeySupport, () => {
283
+ drmIt('plays encrypted content with the ClearKey CDM', async () => {
284
+ // Configure DrmEngine for ClearKey playback.
285
+ const config = shaka.util.PlayerConfiguration.createDefault().drm;
286
+ config.clearKeys = {
287
+ // From https://github.com/Axinom/public-test-vectors/tree/conservative#v61-multidrm
288
+ '6e5a1d26275747d78046eaa5d1d34b5a': '197f26f572c864d2338b3ae5d114ea9c',
289
+ };
290
+ drmEngine.configure(config);
291
+
292
+ // The error callback should not be invoked.
293
+ onErrorSpy.and.callFake(fail);
294
+
295
+ /** @type {!shaka.util.PublicPromise} */
296
+ const encryptedEventSeen = new shaka.util.PublicPromise();
297
+ eventManager.listen(video, 'encrypted', () => {
298
+ encryptedEventSeen.resolve();
299
+ });
300
+ eventManager.listen(video, 'error', () => {
301
+ fail('MediaError code ' + video.error.code);
302
+ let extended = video.error.msExtendedCode;
303
+ if (extended) {
304
+ if (extended < 0) {
305
+ extended += Math.pow(2, 32);
306
+ }
307
+ fail('MediaError msExtendedCode ' + extended.toString(16));
308
+ }
309
+ });
310
+
311
+ /** @type {!shaka.util.PublicPromise} */
312
+ const keyStatusEventSeen = new shaka.util.PublicPromise();
313
+ onKeyStatusSpy.and.callFake(() => {
314
+ keyStatusEventSeen.resolve();
315
+ });
316
+
317
+ const variants = manifest.variants;
318
+
319
+ await drmEngine.initForPlayback(variants, manifest.offlineSessionIds);
320
+ await drmEngine.attach(video);
321
+ await mediaSourceEngine.appendBuffer(
322
+ ContentType.VIDEO, videoInitSegment, null, null,
323
+ /* hasClosedCaptions= */ false);
324
+ await mediaSourceEngine.appendBuffer(
325
+ ContentType.AUDIO, audioInitSegment, null, null,
326
+ /* hasClosedCaptions= */ false);
327
+ await encryptedEventSeen;
328
+
329
+ // Some platforms (notably 2017 Tizen TVs) do not fire key status
330
+ // events.
331
+ const keyStatusTimeout = shaka.test.Util.delay(5);
332
+ await Promise.race([keyStatusTimeout, keyStatusEventSeen]);
333
+
334
+ const call = onKeyStatusSpy.calls.mostRecent();
335
+ if (call) {
336
+ const map = /** @type {!Object} */ (call.args[0]);
337
+ expect(Object.keys(map).length).not.toBe(0);
338
+ for (const k in map) {
339
+ expect(map[k]).toBe('usable');
340
+ }
341
+ }
342
+
343
+ await mediaSourceEngine.appendBuffer(
344
+ ContentType.VIDEO, videoSegment, 0, 10,
345
+ /* hasClosedCaptions= */ false);
346
+ await mediaSourceEngine.appendBuffer(
347
+ ContentType.AUDIO, audioSegment, 0, 10,
348
+ /* hasClosedCaptions= */ false);
349
+
350
+ expect(video.buffered.end(0)).toBeGreaterThan(0);
351
+ video.play();
352
+
353
+ const waiter = new shaka.test.Waiter(eventManager).timeoutAfter(15);
354
+ await waiter.waitForMovement(video);
355
+
356
+ // Something should have played by now.
357
+ expect(video.readyState).toBeGreaterThan(1);
358
+ expect(video.currentTime).toBeGreaterThan(0);
359
+ });
360
+ }); // describe('ClearKey')
277
361
  });
@@ -2152,6 +2152,50 @@ describe('DrmEngine', () => {
2152
2152
  [drmInfoAudio]);
2153
2153
  expect(returned).toEqual([drmInfoDesired]);
2154
2154
  });
2155
+
2156
+ it('dedupes the merged init data based on keyId matching', () => {
2157
+ const serverCert = new Uint8Array(0);
2158
+ const drmInfoVideo = {
2159
+ keySystem: 'drm.abc',
2160
+ licenseServerUri: 'http://abc.drm/license',
2161
+ distinctiveIdentifierRequired: false,
2162
+ persistentStateRequired: true,
2163
+ videoRobustness: 'really_really_ridiculously_good',
2164
+ serverCertificate: serverCert,
2165
+ serverCertificateUri: '',
2166
+ initData: [{keyId: 'v-init'}],
2167
+ keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']),
2168
+ };
2169
+ const drmInfoAudio = {
2170
+ keySystem: 'drm.abc',
2171
+ licenseServerUri: undefined,
2172
+ distinctiveIdentifierRequired: true,
2173
+ persistentStateRequired: false,
2174
+ audioRobustness: 'good',
2175
+ serverCertificate: undefined,
2176
+ serverCertificateUri: '',
2177
+ initData: [{keyId: 'v-init'}, {keyId: 'a-init'}],
2178
+ keyIds: new Set(['eadbeefdeadbeefdeadbeefdeadbeefd']),
2179
+ };
2180
+ const drmInfoDesired = {
2181
+ keySystem: 'drm.abc',
2182
+ licenseServerUri: 'http://abc.drm/license',
2183
+ distinctiveIdentifierRequired: true,
2184
+ persistentStateRequired: true,
2185
+ audioRobustness: 'good',
2186
+ videoRobustness: 'really_really_ridiculously_good',
2187
+ serverCertificate: serverCert,
2188
+ serverCertificateUri: '',
2189
+ initData: [{keyId: 'v-init'}, {keyId: 'a-init'}],
2190
+ keyIds: new Set([
2191
+ 'deadbeefdeadbeefdeadbeefdeadbeef',
2192
+ 'eadbeefdeadbeefdeadbeefdeadbeefd',
2193
+ ]),
2194
+ };
2195
+ const returned = shaka.media.DrmEngine.getCommonDrmInfos([drmInfoVideo],
2196
+ [drmInfoAudio]);
2197
+ expect(returned).toEqual([drmInfoDesired]);
2198
+ });
2155
2199
  }); // describe('getCommonDrmInfos')
2156
2200
 
2157
2201
  describe('configure', () => {
@@ -2285,6 +2329,19 @@ describe('DrmEngine', () => {
2285
2329
  await expectAsync(drmEngine.removeSession('abc'))
2286
2330
  .toBeRejectedWith(expected);
2287
2331
  });
2332
+
2333
+ // Regression test for #3534
2334
+ it('does not remove the same session again on destroy', async () => {
2335
+ updatePromise.resolve();
2336
+ expect(session1.remove).not.toHaveBeenCalled();
2337
+ await drmEngine.removeSession('abc');
2338
+ expect(session1.remove).toHaveBeenCalled();
2339
+ session1.remove.calls.reset();
2340
+ await drmEngine.destroy();
2341
+ // The session should only be removed ONCE. If it's double-removed, it
2342
+ // will make a (non-fatal) DOMException.
2343
+ expect(session1.remove).not.toHaveBeenCalled();
2344
+ });
2288
2345
  });
2289
2346
 
2290
2347
  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