shaka-player 3.1.0 → 3.1.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 (181) hide show
  1. package/AUTHORS +8 -2
  2. package/CHANGELOG.md +209 -0
  3. package/CONTRIBUTORS +10 -3
  4. package/README.md +3 -0
  5. package/build/apps.py +2 -2
  6. package/build/check.py +2 -2
  7. package/build/conformance.textproto +0 -9
  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/demo/common/asset.js +11 -0
  13. package/demo/common/assets.js +1 -1
  14. package/demo/common/message_ids.js +5 -0
  15. package/demo/config.js +2 -1
  16. package/demo/custom.js +84 -3
  17. package/demo/demo.less +13 -1
  18. package/demo/demo_utils.js +1 -0
  19. package/demo/input.js +31 -12
  20. package/demo/input_container.js +24 -1
  21. package/demo/locales/en.json +6 -1
  22. package/demo/locales/source.json +20 -0
  23. package/demo/main.js +5 -1
  24. package/dist/controls.css +2 -2
  25. package/dist/controls.css.map +1 -1
  26. package/dist/demo.compiled.debug.js +99 -91
  27. package/dist/demo.compiled.debug.map +3 -3
  28. package/dist/demo.compiled.js +99 -91
  29. package/dist/demo.compiled.map +3 -3
  30. package/dist/demo.css +1 -1
  31. package/dist/demo.css.map +1 -1
  32. package/dist/deps.js +9 -9
  33. package/dist/locales.js +36 -34
  34. package/dist/receiver.compiled.debug.js +74 -70
  35. package/dist/receiver.compiled.debug.map +3 -3
  36. package/dist/receiver.compiled.js +74 -70
  37. package/dist/receiver.compiled.map +3 -3
  38. package/dist/shaka-player.compiled.d.ts +165 -9
  39. package/dist/shaka-player.compiled.debug.d.ts +165 -9
  40. package/dist/shaka-player.compiled.debug.externs.js +179 -11
  41. package/dist/shaka-player.compiled.debug.js +367 -339
  42. package/dist/shaka-player.compiled.debug.map +4 -4
  43. package/dist/shaka-player.compiled.externs.js +179 -11
  44. package/dist/shaka-player.compiled.js +818 -807
  45. package/dist/shaka-player.compiled.map +4 -4
  46. package/dist/shaka-player.ui.d.ts +165 -9
  47. package/dist/shaka-player.ui.debug.d.ts +165 -9
  48. package/dist/shaka-player.ui.debug.externs.js +181 -13
  49. package/dist/shaka-player.ui.debug.js +459 -429
  50. package/dist/shaka-player.ui.debug.map +4 -4
  51. package/dist/shaka-player.ui.externs.js +181 -13
  52. package/dist/shaka-player.ui.js +973 -961
  53. package/dist/shaka-player.ui.map +4 -4
  54. package/docs/tutorials/fairplay.md +1 -1
  55. package/docs/tutorials/faq.md +25 -0
  56. package/docs/tutorials/index.json +2 -2
  57. package/docs/tutorials/upgrade-v3.md +38 -0
  58. package/docs/tutorials/upgrade.md +3 -3
  59. package/externs/ima.js +4 -0
  60. package/externs/media_capabilities.js +16 -0
  61. package/externs/shaka/ads.js +2 -0
  62. package/externs/shaka/net.js +2 -2
  63. package/externs/shaka/player.js +6 -0
  64. package/karma.conf.js +1 -0
  65. package/lib/abr/simple_abr_manager.js +1 -1
  66. package/lib/ads/ad_manager.js +29 -11
  67. package/lib/ads/client_side_ad_manager.js +76 -86
  68. package/lib/ads/server_side_ad_manager.js +26 -11
  69. package/lib/cast/cast_proxy.js +3 -2
  70. package/lib/cast/cast_receiver.js +2 -1
  71. package/lib/cast/cast_sender.js +2 -2
  72. package/lib/cea/sei_processor.js +24 -17
  73. package/lib/dash/dash_parser.js +107 -4
  74. package/lib/dash/segment_list.js +1 -1
  75. package/lib/dash/segment_template.js +45 -15
  76. package/lib/dependencies/all.js +15 -15
  77. package/lib/hls/hls_parser.js +46 -18
  78. package/lib/media/closed_caption_parser.js +3 -1
  79. package/lib/media/content_workarounds.js +18 -0
  80. package/lib/media/drm_engine.js +20 -1
  81. package/lib/media/gap_jumping_controller.js +8 -4
  82. package/lib/media/media_source_engine.js +1 -2
  83. package/lib/media/playhead.js +1 -1
  84. package/lib/media/presentation_timeline.js +3 -1
  85. package/lib/media/segment_index.js +38 -12
  86. package/lib/media/segment_reference.js +8 -0
  87. package/lib/media/streaming_engine.js +30 -21
  88. package/lib/net/http_fetch_plugin.js +2 -2
  89. package/lib/net/networking_engine.js +3 -2
  90. package/lib/offline/storage.js +4 -0
  91. package/lib/player.js +113 -56
  92. package/lib/polyfill/encryption_scheme.js +2 -0
  93. package/lib/polyfill/fullscreen.js +2 -0
  94. package/lib/polyfill/mathround.js +2 -0
  95. package/lib/polyfill/media_capabilities.js +62 -23
  96. package/lib/polyfill/mediasource.js +8 -0
  97. package/lib/polyfill/orientation.js +3 -1
  98. package/lib/polyfill/patchedmediakeys_apple.js +6 -4
  99. package/lib/polyfill/patchedmediakeys_ms.js +6 -4
  100. package/lib/polyfill/patchedmediakeys_nop.js +2 -0
  101. package/lib/polyfill/patchedmediakeys_webkit.js +6 -4
  102. package/lib/polyfill/pip_webkit.js +2 -0
  103. package/lib/polyfill/storage_estimate.js +2 -0
  104. package/lib/polyfill/video_play_promise.js +2 -0
  105. package/lib/polyfill/videoplaybackquality.js +2 -0
  106. package/lib/polyfill/vttcue.js +2 -0
  107. package/lib/text/cue.js +11 -0
  108. package/lib/text/mp4_ttml_parser.js +3 -0
  109. package/lib/text/text_engine.js +14 -2
  110. package/lib/text/ttml_text_parser.js +52 -30
  111. package/lib/text/ui_text_displayer.js +255 -82
  112. package/lib/text/vtt_text_parser.js +4 -2
  113. package/lib/util/error.js +1 -4
  114. package/lib/util/fake_event.js +53 -7
  115. package/lib/util/mime_utils.js +33 -0
  116. package/lib/util/mp4_box_parsers.js +1 -1
  117. package/lib/util/mp4_parser.js +14 -0
  118. package/lib/util/networking.js +1 -1
  119. package/lib/util/periods.js +132 -37
  120. package/lib/util/platform.js +13 -1
  121. package/lib/util/stream_utils.js +46 -7
  122. package/package.json +3 -3
  123. package/test/cast/cast_proxy_unit.js +12 -6
  124. package/test/cast/cast_receiver_integration.js +1 -1
  125. package/test/cast/cast_utils_unit.js +1 -1
  126. package/test/cea/mp4_cea_parser_unit.js +26 -0
  127. package/test/dash/dash_parser_live_unit.js +1 -0
  128. package/test/dash/dash_parser_manifest_unit.js +187 -1
  129. package/test/dash/dash_parser_segment_template_unit.js +3 -3
  130. package/test/demo/demo_unit.js +0 -1
  131. package/test/hls/hls_parser_unit.js +45 -1
  132. package/test/media/closed_caption_parser_unit.js +20 -0
  133. package/test/media/drm_engine_unit.js +59 -3
  134. package/test/media/playhead_unit.js +27 -1
  135. package/test/media/presentation_timeline_unit.js +19 -0
  136. package/test/media/segment_index_unit.js +76 -3
  137. package/test/media/streaming_engine_unit.js +3 -3
  138. package/test/player_integration.js +38 -0
  139. package/test/player_unit.js +125 -6
  140. package/test/test/assets/3675/chunk-stream0-00001.m4s +0 -0
  141. package/test/test/assets/3675/chunk-stream0-00002.m4s +0 -0
  142. package/test/test/assets/3675/chunk-stream0-00003.m4s +0 -0
  143. package/test/test/assets/3675/chunk-stream0-00004.m4s +0 -0
  144. package/test/test/assets/3675/chunk-stream0-00005.m4s +0 -0
  145. package/test/test/assets/3675/chunk-stream0-00006.m4s +0 -0
  146. package/test/test/assets/3675/chunk-stream0-00007.m4s +0 -0
  147. package/test/test/assets/3675/dash_0.mpd +30 -0
  148. package/test/test/assets/3675/dash_1.mpd +30 -0
  149. package/test/test/assets/3675/dash_2.mpd +30 -0
  150. package/test/test/assets/3675/dash_3.mpd +30 -0
  151. package/test/test/assets/3675/dash_4.mpd +30 -0
  152. package/test/test/assets/3675/dash_5.mpd +26 -0
  153. package/test/test/assets/3675/init-stream0.m4s +0 -0
  154. package/test/test/assets/empty_caption_video_init.mp4 +0 -0
  155. package/test/test/assets/empty_caption_video_segment.mp4 +0 -0
  156. package/test/test/util/fake_ad_manager.js +4 -1
  157. package/test/test/util/simple_fakes.js +5 -3
  158. package/test/test/util/waiter.js +36 -2
  159. package/test/text/ttml_text_parser_unit.js +55 -0
  160. package/test/text/ui_text_displayer_unit.js +95 -18
  161. package/test/text/vtt_text_parser_unit.js +17 -1
  162. package/test/ui/localization_unit.js +15 -14
  163. package/test/ui/text_displayer_layout_unit.js +52 -1
  164. package/test/ui/ui_unit.js +4 -0
  165. package/test/util/mp4_box_parsers_unit.js +28 -0
  166. package/test/util/periods_unit.js +190 -2
  167. package/test/util/stream_utils_unit.js +56 -5
  168. package/ui/airplay_button.js +5 -0
  169. package/ui/big_play_button.js +10 -0
  170. package/ui/cast_button.js +2 -3
  171. package/ui/controls.js +9 -7
  172. package/ui/less/containers.less +10 -5
  173. package/ui/locales/en.json +1 -0
  174. package/ui/locales/ru.json +6 -6
  175. package/ui/locales/source.json +4 -0
  176. package/ui/localization.js +28 -51
  177. package/ui/pip_button.js +2 -3
  178. package/ui/play_button.js +10 -10
  179. package/ui/small_play_button.js +12 -0
  180. package/ui/ui.js +1 -1
  181. package/docs/tutorials/upgrade-v2-4.md +0 -481
@@ -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);
@@ -1854,7 +1862,8 @@ describe('DashParser Manifest', () => {
1854
1862
  expect(textStream.forced).toBe(true);
1855
1863
  });
1856
1864
 
1857
- it('supports HDR signaling', async () => {
1865
+ it('supports HDR signaling via profiles', async () => {
1866
+ // (DASH-IF IOP v4.3 10.3.3.)
1858
1867
  const hdrProfile =
1859
1868
  'http://dashif.org/guidelines/dash-if-uhd#hevc-hdr-pq10';
1860
1869
  const manifestText = [
@@ -1885,6 +1894,110 @@ describe('DashParser Manifest', () => {
1885
1894
  expect(stream.hdr).toBe('PQ');
1886
1895
  });
1887
1896
 
1897
+ it('supports HDR signaling via SupplementalProperty', async () => {
1898
+ // (DASH-IF IOP v4.3 6.2.5.1.)
1899
+ const hdrScheme = cicpScheme('TransferCharacteristics');
1900
+ const pq = 16;
1901
+ const manifestText = [
1902
+ '<MPD minBufferTime="PT75S">',
1903
+ ' <Period id="1" duration="PT30S">',
1904
+ ' <AdaptationSet id="2" mimeType="video/mp4">',
1905
+ ` <SupplementalProperty schemeIdUri="${hdrScheme}" value="${pq}" />`,
1906
+ ' <Representation codecs="hvc1.2.4.L150.90">',
1907
+ ' <BaseURL>v-sd.mp4</BaseURL>',
1908
+ ' <SegmentBase indexRange="100-200" />',
1909
+ ' </Representation>',
1910
+ ' </AdaptationSet>',
1911
+ ' <AdaptationSet id="3" mimeType="audio/mp4">',
1912
+ ' <Representation id="audio-en">',
1913
+ ' <BaseURL>a-en.mp4</BaseURL>',
1914
+ ' <SegmentBase indexRange="100-200" />',
1915
+ ' </Representation>',
1916
+ ' </AdaptationSet>',
1917
+ ' </Period>',
1918
+ '</MPD>',
1919
+ ].join('\n');
1920
+
1921
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
1922
+
1923
+ /** @type {shaka.extern.Manifest} */
1924
+ const manifest = await parser.start('dummy://foo', playerInterface);
1925
+ expect(manifest.variants.length).toBe(1);
1926
+ const stream = manifest.variants[0].video;
1927
+ expect(stream.hdr).toBe('PQ');
1928
+ });
1929
+
1930
+ it('supports HDR signaling via EssentialProperty', async () => {
1931
+ // (DASH-IF IOP v4.3 6.2.5.1.)
1932
+ const hdrScheme = cicpScheme('TransferCharacteristics');
1933
+ const hlg = 18;
1934
+ const manifestText = [
1935
+ '<MPD minBufferTime="PT75S">',
1936
+ ' <Period id="1" duration="PT30S">',
1937
+ ' <AdaptationSet id="2" mimeType="video/mp4">',
1938
+ ` <EssentialProperty schemeIdUri="${hdrScheme}" value="${hlg}" />`,
1939
+ ' <Representation codecs="hvc1.2.4.L153.B0">',
1940
+ ' <BaseURL>v-sd.mp4</BaseURL>',
1941
+ ' <SegmentBase indexRange="100-200" />',
1942
+ ' </Representation>',
1943
+ ' </AdaptationSet>',
1944
+ ' <AdaptationSet id="3" mimeType="audio/mp4">',
1945
+ ' <Representation id="audio-en">',
1946
+ ' <BaseURL>a-en.mp4</BaseURL>',
1947
+ ' <SegmentBase indexRange="100-200" />',
1948
+ ' </Representation>',
1949
+ ' </AdaptationSet>',
1950
+ ' </Period>',
1951
+ '</MPD>',
1952
+ ].join('\n');
1953
+
1954
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
1955
+
1956
+ /** @type {shaka.extern.Manifest} */
1957
+ const manifest = await parser.start('dummy://foo', playerInterface);
1958
+ expect(manifest.variants.length).toBe(1);
1959
+ const stream = manifest.variants[0].video;
1960
+ expect(stream.hdr).toBe('HLG');
1961
+ });
1962
+
1963
+ it('supports SDR signalling via EssentialProperty', async () => {
1964
+ // (DASH-IF IOP v4.3 6.2.5.1.)
1965
+ const scheme = cicpScheme('TransferCharacteristics');
1966
+ const sdrValues = [1, 6, 13, 14, 15];
1967
+ const manifestPromises = [];
1968
+ for (const value of sdrValues) {
1969
+ const manifestText = [
1970
+ '<MPD minBufferTime="PT75S">',
1971
+ ' <Period id="1" duration="PT30S">',
1972
+ ' <AdaptationSet id="2" mimeType="video/mp4">',
1973
+ ` <EssentialProperty schemeIdUri="${scheme}" value="${value}" />`,
1974
+ ' <Representation codecs="avc1.640028">',
1975
+ ' <BaseURL>v-sd.mp4</BaseURL>',
1976
+ ' <SegmentBase indexRange="100-200" />',
1977
+ ' </Representation>',
1978
+ ' </AdaptationSet>',
1979
+ ' <AdaptationSet id="3" mimeType="audio/mp4">',
1980
+ ' <Representation id="audio-en">',
1981
+ ' <BaseURL>a-en.mp4</BaseURL>',
1982
+ ' <SegmentBase indexRange="100-200" />',
1983
+ ' </Representation>',
1984
+ ' </AdaptationSet>',
1985
+ ' </Period>',
1986
+ '</MPD>',
1987
+ ].join('\n');
1988
+
1989
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
1990
+
1991
+ manifestPromises.push(parser.start('dummy://foo', playerInterface));
1992
+ }
1993
+ const manifests = await Promise.all(manifestPromises);
1994
+ for (const manifest of manifests) {
1995
+ expect(manifest.variants.length).toBe(1);
1996
+ const stream = manifest.variants[0].video;
1997
+ expect(stream.hdr).toBe('SDR');
1998
+ }
1999
+ });
2000
+
1888
2001
  it('Does not error when image adaptation sets are present', async () => {
1889
2002
  const manifestText = [
1890
2003
  '<MPD minBufferTime="PT75S">',
@@ -2108,4 +2221,77 @@ describe('DashParser Manifest', () => {
2108
2221
  expect(uri).not.toContain('/p2/');
2109
2222
  }
2110
2223
  });
2224
+
2225
+ /**
2226
+ * @param {!Array.<number>} periods Start time of multiple periods
2227
+ * @return {string}
2228
+ */
2229
+ function buildManifestWithPeriodStartTime(periods) {
2230
+ const mpdTemplate = [
2231
+ `<MPD type="dynamic"`,
2232
+ 'availabilityStartTime="1970-01-01T00:00:00Z"',
2233
+ 'timeShiftBufferDepth="PT10H">',
2234
+ ' %(periods)s',
2235
+ '</MPD>',
2236
+ ].join('\n');
2237
+ const periodTemplate = (id, period, duration) => {
2238
+ return [
2239
+ ` <Period id="${id}" start="PT${period}S">`,
2240
+ ' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
2241
+ ' <SegmentTemplate startNumber="1" media="l-$Number$.mp4">',
2242
+ ' <SegmentTimeline>',
2243
+ ` <S t="0" d="${duration}" />`,
2244
+ ' </SegmentTimeline>',
2245
+ ' </SegmentTemplate>',
2246
+ ' <Representation id="1"/>',
2247
+ ' </AdaptationSet>',
2248
+ ' </Period>',
2249
+ ].join('\n');
2250
+ };
2251
+ const periodXmls = periods.map((period, i) => {
2252
+ const duration = i+1 === periods.length ? 10 : periods[i+1] - period;
2253
+ // Period start time as ID here. If we use index then there will be
2254
+ // periods with same period ID and different start time which are invalid.
2255
+ return periodTemplate(period, period, duration);
2256
+ });
2257
+ return sprintf(mpdTemplate, {
2258
+ periods: periodXmls.join('\n'),
2259
+ });
2260
+ }
2261
+
2262
+ // Bug description: Inconsistent period start time in the manifests due
2263
+ // to failover triggered in backend servers
2264
+
2265
+ // When one of the servers is down, the manifest will be served by other
2266
+ // redundant servers. The period start time might become out of sync
2267
+ // during the switch-over/recovery.
2268
+
2269
+ it('skip periods that are earlier than max period start time', async () => {
2270
+ const sources = [
2271
+ buildManifestWithPeriodStartTime([5, 15]),
2272
+ buildManifestWithPeriodStartTime([6, 15]), // simulate out-of-sync of +1s
2273
+ buildManifestWithPeriodStartTime([4, 15]), // simulate out-of-sync of -1s
2274
+ ];
2275
+ const segments = [];
2276
+
2277
+ for (const source of sources) {
2278
+ fakeNetEngine.setResponseText('dummy://foo', source);
2279
+ /** @type {shaka.extern.Manifest} */
2280
+ // eslint-disable-next-line no-await-in-loop
2281
+ const manifest = await parser.start('dummy://foo', playerInterface);
2282
+ const video = manifest.variants[0].video;
2283
+ // eslint-disable-next-line no-await-in-loop
2284
+ await video.createSegmentIndex();
2285
+ goog.asserts.assert(video.segmentIndex, 'Null segmentIndex!');
2286
+ segments.push(Array.from(video.segmentIndex));
2287
+ }
2288
+
2289
+ // Expect identical segments
2290
+ expect(segments[0].length).toBe(2);
2291
+ expect(segments[1].length).toBe(2);
2292
+ expect(segments[0][0].startTime).toBe(5);
2293
+ expect(segments[1][0].startTime).toBe(5);
2294
+ expect(segments[0][1].startTime).toBe(15);
2295
+ expect(segments[1][1].startTime).toBe(15);
2296
+ });
2111
2297
  });
@@ -127,9 +127,9 @@ describe('DashParser SegmentTemplate', () => {
127
127
  // The first segment is number 1 and position 0.
128
128
  // Although the segment is 60 seconds long, it is clipped to the period
129
129
  // duration of 30 seconds.
130
- const references = [
131
- ManifestParser.makeReference('s1.mp4', 0, 30, baseUri),
132
- ];
130
+ const ref = ManifestParser.makeReference('s1.mp4', 0, 30, baseUri);
131
+ ref.trueEndTime = 60;
132
+ const references = [ref];
133
133
  await Dash.testSegmentIndex(source, references);
134
134
  });
135
135
 
@@ -96,7 +96,6 @@ describe('Demo', () => {
96
96
  const configPrimitives = new Set(['number', 'string', 'boolean']);
97
97
  const exceptions = new Set()
98
98
  .add('preferredVariantRole')
99
- .add('preferredTextRole')
100
99
  .add('playRangeStart')
101
100
  .add('playRangeEnd')
102
101
  .add('manifest.dash.keySystemsByURI');
@@ -209,6 +209,46 @@ describe('HlsParser', () => {
209
209
  expect(actual).toEqual(manifest);
210
210
  });
211
211
 
212
+ it('prioritize AVERAGE-BANDWIDTH to BANDWIDTH', async () => {
213
+ const master = [
214
+ '#EXTM3U\n',
215
+ '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
216
+ 'RESOLUTION=960x540,FRAME-RATE=60,',
217
+ 'AVERAGE-BANDWIDTH=100\n',
218
+ 'video\n',
219
+ ].join('');
220
+
221
+ const media = [
222
+ '#EXTM3U\n',
223
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
224
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
225
+ '#EXTINF:5,\n',
226
+ '#EXT-X-BYTERANGE:121090@616\n',
227
+ 'main.mp4',
228
+ ].join('');
229
+
230
+ const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
231
+ manifest.anyTimeline();
232
+ manifest.addPartialVariant((variant) => {
233
+ variant.bandwidth = 100;
234
+ variant.addPartialStream(ContentType.VIDEO, (stream) => {
235
+ stream.frameRate = 60;
236
+ stream.mime('video/mp4', 'avc1');
237
+ stream.size(960, 540);
238
+ });
239
+ });
240
+ });
241
+
242
+ fakeNetEngine
243
+ .setResponseText('test:/master', master)
244
+ .setResponseText('test:/video', media)
245
+ .setResponseValue('test:/init.mp4', initSegmentData)
246
+ .setResponseValue('test:/main.mp4', segmentData);
247
+
248
+ const actual = await parser.start('test:/master', playerInterface);
249
+ expect(actual).toEqual(manifest);
250
+ });
251
+
212
252
  it('ignores duplicate CODECS', async () => {
213
253
  const master = [
214
254
  '#EXTM3U\n',
@@ -743,7 +783,7 @@ describe('HlsParser', () => {
743
783
  'RESOLUTION=960x540,FRAME-RATE=120,AUDIO="aud2"\n',
744
784
  'video2\n',
745
785
  '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
746
- 'URI="audio"\n',
786
+ 'DEFAULT=YES,URI="audio"\n',
747
787
  '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",LANGUAGE="fr",',
748
788
  'URI="audio2"\n',
749
789
  ].join('');
@@ -761,6 +801,7 @@ describe('HlsParser', () => {
761
801
  manifest.anyTimeline();
762
802
  manifest.addPartialVariant((variant) => {
763
803
  variant.bandwidth = 200;
804
+ variant.primary = true;
764
805
  variant.addPartialStream(ContentType.VIDEO, (stream) => {
765
806
  stream.size(960, 540);
766
807
  });
@@ -770,6 +811,7 @@ describe('HlsParser', () => {
770
811
  });
771
812
  manifest.addPartialVariant((variant) => {
772
813
  variant.bandwidth = 300;
814
+ variant.primary = false;
773
815
  variant.addPartialStream(ContentType.VIDEO, (stream) => {
774
816
  stream.size(960, 540);
775
817
  });
@@ -1267,6 +1309,8 @@ describe('HlsParser', () => {
1267
1309
  // stream.
1268
1310
  const timeline = actual.presentationTimeline;
1269
1311
  expect(timeline.getDuration()).toBe(10);
1312
+ expect(timeline.getSeekRangeStart()).toBe(0);
1313
+ expect(timeline.getSeekRangeEnd()).toBe(10);
1270
1314
 
1271
1315
  expect(actual.textStreams.length).toBe(1);
1272
1316
  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
+ });
@@ -37,7 +37,7 @@ function testDrmEngine(useMediaCapabilities) {
37
37
  navigator.requestMediaKeySystemAccess;
38
38
  const originalLogError = shaka.log.error;
39
39
  const originalBatchTime = shaka.media.DrmEngine.KEY_STATUS_BATCH_TIME;
40
- const originalDecodingInfo = navigator.mediaCapabilities.decodingInfo;
40
+ const originalDecodingInfo = window.shakaMediaCapabilities.decodingInfo;
41
41
 
42
42
  /** @type {!jasmine.Spy} */
43
43
  let requestMediaKeySystemAccessSpy;
@@ -88,7 +88,7 @@ function testDrmEngine(useMediaCapabilities) {
88
88
  navigator.requestMediaKeySystemAccess =
89
89
  shaka.test.Util.spyFunc(requestMediaKeySystemAccessSpy);
90
90
  decodingInfoSpy = jasmine.createSpy('decodingInfo');
91
- navigator.mediaCapabilities.decodingInfo =
91
+ window.shakaMediaCapabilities.decodingInfo =
92
92
  shaka.test.Util.spyFunc(decodingInfoSpy);
93
93
 
94
94
  logErrorSpy = jasmine.createSpy('shaka.log.error');
@@ -165,7 +165,7 @@ function testDrmEngine(useMediaCapabilities) {
165
165
 
166
166
  navigator.requestMediaKeySystemAccess =
167
167
  originalRequestMediaKeySystemAccess;
168
- navigator.mediaCapabilities.decodingInfo = originalDecodingInfo;
168
+ window.shakaMediaCapabilities.decodingInfo = originalDecodingInfo;
169
169
  shaka.log.error = originalLogError;
170
170
  });
171
171
 
@@ -2278,6 +2278,49 @@ function testDrmEngine(useMediaCapabilities) {
2278
2278
  [drmInfoAudio]);
2279
2279
  expect(returned).toEqual([drmInfoDesired]);
2280
2280
  });
2281
+
2282
+ it('dedupes the merged init data based on keyId matching', () => {
2283
+ const serverCert = new Uint8Array(0);
2284
+ const drmInfoVideo = {
2285
+ keySystem: 'drm.abc',
2286
+ licenseServerUri: 'http://abc.drm/license',
2287
+ distinctiveIdentifierRequired: false,
2288
+ persistentStateRequired: true,
2289
+ videoRobustness: 'really_really_ridiculously_good',
2290
+ serverCertificate: serverCert,
2291
+ serverCertificateUri: '',
2292
+ initData: [{keyId: 'v-init'}],
2293
+ keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']),
2294
+ };
2295
+ const drmInfoAudio = {
2296
+ keySystem: 'drm.abc',
2297
+ licenseServerUri: undefined,
2298
+ distinctiveIdentifierRequired: true,
2299
+ persistentStateRequired: false,
2300
+ audioRobustness: 'good',
2301
+ serverCertificate: undefined,
2302
+ serverCertificateUri: '',
2303
+ initData: [{keyId: 'v-init'}, {keyId: 'a-init'}],
2304
+ keyIds: new Set(['eadbeefdeadbeefdeadbeefdeadbeefd']),
2305
+ };
2306
+ const drmInfoDesired = {
2307
+ keySystem: 'drm.abc',
2308
+ licenseServerUri: 'http://abc.drm/license',
2309
+ distinctiveIdentifierRequired: true,
2310
+ persistentStateRequired: true,
2311
+ audioRobustness: 'good',
2312
+ videoRobustness: 'really_really_ridiculously_good',
2313
+ serverCertificate: serverCert,
2314
+ initData: [{keyId: 'v-init'}, {keyId: 'a-init'}],
2315
+ keyIds: new Set([
2316
+ 'deadbeefdeadbeefdeadbeefdeadbeef',
2317
+ 'eadbeefdeadbeefdeadbeefdeadbeefd',
2318
+ ]),
2319
+ };
2320
+ const returned = shaka.media.DrmEngine.getCommonDrmInfos([drmInfoVideo],
2321
+ [drmInfoAudio]);
2322
+ expect(returned).toEqual([drmInfoDesired]);
2323
+ });
2281
2324
  }); // describe('getCommonDrmInfos')
2282
2325
 
2283
2326
  describe('configure', () => {
@@ -2412,6 +2455,19 @@ function testDrmEngine(useMediaCapabilities) {
2412
2455
  await expectAsync(drmEngine.removeSession('abc'))
2413
2456
  .toBeRejectedWith(expected);
2414
2457
  });
2458
+
2459
+ // Regression test for #3534
2460
+ it('does not remove the same session again on destroy', async () => {
2461
+ updatePromise.resolve();
2462
+ expect(session1.remove).not.toHaveBeenCalled();
2463
+ await drmEngine.removeSession('abc');
2464
+ expect(session1.remove).toHaveBeenCalled();
2465
+ session1.remove.calls.reset();
2466
+ await drmEngine.destroy();
2467
+ // The session should only be removed ONCE. If it's double-removed, it
2468
+ // will make a (non-fatal) DOMException.
2469
+ expect(session1.remove).not.toHaveBeenCalled();
2470
+ });
2415
2471
  });
2416
2472
 
2417
2473
  describe('expiration', () => {
@@ -1296,12 +1296,13 @@ describe('Playhead', () => {
1296
1296
  });
1297
1297
 
1298
1298
  // Regression test for https://github.com/google/shaka-player/issues/2987
1299
- it('does gap jump if paused at 0', () => {
1299
+ it('does gap jump if paused at 0 and has autoplay', () => {
1300
1300
  const buffered = [{start: 10, end: 20}];
1301
1301
  video.buffered = createFakeBuffered(buffered);
1302
1302
  video.currentTime = 0;
1303
1303
  video.readyState = HTMLMediaElement.HAVE_ENOUGH_DATA;
1304
1304
  video.paused = true;
1305
+ video.autoplay = true;
1305
1306
 
1306
1307
  config.jumpLargeGaps = true;
1307
1308
  playhead = new shaka.media.MediaSourcePlayhead(
@@ -1319,6 +1320,31 @@ describe('Playhead', () => {
1319
1320
  expect(video.currentTime).toBe(10);
1320
1321
  });
1321
1322
 
1323
+ // Regression test for https://github.com/google/shaka-player/issues/3451
1324
+ it('doesn\'t gap jump if paused at 0 and hasn\'t autoplay', () => {
1325
+ const buffered = [{start: 10, end: 20}];
1326
+ video.buffered = createFakeBuffered(buffered);
1327
+ video.currentTime = 0;
1328
+ video.readyState = HTMLMediaElement.HAVE_ENOUGH_DATA;
1329
+ video.paused = true;
1330
+ video.autoplay = false;
1331
+
1332
+ config.jumpLargeGaps = true;
1333
+ playhead = new shaka.media.MediaSourcePlayhead(
1334
+ video,
1335
+ manifest,
1336
+ config,
1337
+ /* startTime= */ 0,
1338
+ Util.spyFunc(onSeek),
1339
+ Util.spyFunc(onEvent));
1340
+
1341
+ playhead.notifyOfBufferingChange();
1342
+ jasmine.clock().tick(500);
1343
+
1344
+ // There should NOT have been a gap jump.
1345
+ expect(video.currentTime).toBe(0);
1346
+ });
1347
+
1322
1348
  /**
1323
1349
  * @param {string} name
1324
1350
  * @param {SeekTestInfo} data
@@ -290,6 +290,25 @@ describe('PresentationTimeline', () => {
290
290
  // last segment time (50)
291
291
  expect(timeline.getSegmentAvailabilityEnd()).toBe(50);
292
292
  });
293
+
294
+ it('calculates time when there a transition of live to static', () => {
295
+ const timeline = makeLiveTimeline(/* availability= */ 20);
296
+
297
+ const ref1 = makeSegmentReference(0, 10);
298
+ const ref2 = makeSegmentReference(10, 20);
299
+ const ref3 = makeSegmentReference(20, 30);
300
+ const ref4 = makeSegmentReference(30, 40);
301
+ const ref5 = makeSegmentReference(40, 50);
302
+
303
+ setElapsed(50);
304
+ timeline.notifySegments([ref1, ref2, ref3, ref4, ref5]);
305
+
306
+ expect(timeline.getSegmentAvailabilityEnd()).toBe(50);
307
+
308
+ timeline.setStatic(true);
309
+
310
+ expect(timeline.getSegmentAvailabilityEnd()).toBe(50);
311
+ });
293
312
  });
294
313
 
295
314
  describe('getDuration', () => {
@@ -484,12 +484,40 @@ describe('SegmentIndex', /** @suppress {accessControls} */ () => {
484
484
  makeReference(uri(20), 20, 30),
485
485
  ];
486
486
 
487
- // The first reference ends before the availabilityWindowStart, so it
488
- // should be discarded.
489
- index1.mergeAndEvict(references2, 19);
487
+ // The first two references end before the availabilityWindowStart, so
488
+ // they should be discarded.
489
+ index1.mergeAndEvict(references2, 21);
490
+ expect(index1.references.length).toBe(1);
491
+ expect(index1.references[0]).toEqual(references2[2]);
492
+ });
493
+
494
+ it('discards segments that end before the first old segment', () => {
495
+ /** @type {!Array.<!shaka.media.SegmentReference>} */
496
+ const references1 = [
497
+ // Assuming ref(0, 10) has been already evicted
498
+ makeReference(uri(10), 10, 20),
499
+ ];
500
+ const index1 = new shaka.media.SegmentIndex(references1);
501
+ const position1 = index1.find(10);
502
+
503
+ /** @type {!Array.<!shaka.media.SegmentReference>} */
504
+ const references2 = [
505
+ makeReference(uri(0), 0, 10),
506
+ makeReference(uri(10), 10, 20),
507
+ makeReference(uri(20), 20, 30),
508
+ ];
509
+
510
+ // The new first reference ends before the first old reference starts, so
511
+ // it should be discarded. We will never grow the list at the beginning.
512
+ index1.mergeAndEvict(references2, 0);
490
513
  expect(index1.references.length).toBe(2);
491
514
  expect(index1.references[0]).toEqual(references2[1]);
492
515
  expect(index1.references[1]).toEqual(references2[2]);
516
+
517
+ // The positions should be the same, as well.
518
+ expect(index1.find(10)).toBe(position1);
519
+ goog.asserts.assert(position1 != null, 'Position should not be null!');
520
+ expect(index1.get(position1)).toBe(references2[1]);
493
521
  });
494
522
  });
495
523
 
@@ -567,6 +595,12 @@ describe('SegmentIndex', /** @suppress {accessControls} */ () => {
567
595
  makeReference(uri(20.30), 20, 30),
568
596
  ];
569
597
 
598
+ const additionalRefs = [
599
+ makeReference(uri(30.40), 30, 40),
600
+ makeReference(uri(40.50), 40, 50),
601
+ makeReference(uri(50.60), 50, 60),
602
+ ];
603
+
570
604
  const partialRefs1 = [
571
605
  makeReference(uri(10.15), 10, 15),
572
606
  makeReference(uri(15.20), 15, 20),
@@ -626,6 +660,45 @@ describe('SegmentIndex', /** @suppress {accessControls} */ () => {
626
660
  expect(Array.from(index)).toEqual(inputRefs.slice(1));
627
661
  });
628
662
 
663
+ it('resumes iteration after new references are added', () => {
664
+ const refs = inputRefs.slice();
665
+ const index = new shaka.media.SegmentIndex(refs);
666
+
667
+ // This simulates the pattern of calls in StreamingEngine when we buffer
668
+ // to the edge of a live stream.
669
+ const iterator = index[Symbol.iterator]();
670
+ iterator.next();
671
+ expect(iterator.current()).toBe(inputRefs[0]);
672
+
673
+ iterator.next();
674
+ expect(iterator.current()).toBe(inputRefs[1]);
675
+
676
+ iterator.next();
677
+ expect(iterator.current()).toBe(inputRefs[2]);
678
+
679
+ iterator.next();
680
+ expect(iterator.current()).toBe(null);
681
+ // After we reach the end of the iteration, we should still get null back
682
+ // on subsequent requests.
683
+ expect(iterator.current()).toBe(null);
684
+ expect(iterator.current()).toBe(null);
685
+
686
+ // Although we have reached the end, we should still be able to add new
687
+ // references and iterate to them without starting over the iteration.
688
+ refs.push(...additionalRefs);
689
+
690
+ expect(iterator.current()).toBe(additionalRefs[0]);
691
+
692
+ iterator.next();
693
+ expect(iterator.current()).toBe(additionalRefs[1]);
694
+
695
+ iterator.next();
696
+ expect(iterator.current()).toBe(additionalRefs[2]);
697
+
698
+ iterator.next();
699
+ expect(iterator.current()).toBe(null);
700
+ });
701
+
629
702
  describe('seek', () => {
630
703
  it('returns the matching segment', () => {
631
704
  const index = new shaka.media.SegmentIndex(inputRefs);
@@ -835,7 +835,7 @@ describe('StreamingEngine', () => {
835
835
  // This would let some media types buffer faster than others if unchecked.
836
836
  netEngineDelays.text = 0.1;
837
837
  netEngineDelays.audio = 1.0;
838
- netEngineDelays.video = 10.0;
838
+ netEngineDelays.video = 5.0; // Need init segment and media segment
839
839
 
840
840
  mediaSourceEngine.appendBuffer.and.callFake((type, data, start, end) => {
841
841
  // Call to the underlying implementation.
@@ -2170,13 +2170,13 @@ describe('StreamingEngine', () => {
2170
2170
  .bind(mediaSourceEngine);
2171
2171
 
2172
2172
  mediaSourceEngine.remove.and.callFake((type, start, end) => {
2173
- expect(presentationTimeInSeconds).toBe(20);
2173
+ expect(presentationTimeInSeconds).toBeGreaterThanOrEqual(20);
2174
2174
  expect(start).toBe(0);
2175
2175
  expect(end).toBe(10);
2176
2176
 
2177
2177
  if (mediaSourceEngine.remove.calls.count() == 3) {
2178
2178
  mediaSourceEngine.remove.and.callFake((type, start, end) => {
2179
- expect(presentationTimeInSeconds).toBe(30);
2179
+ expect(presentationTimeInSeconds).toBeGreaterThanOrEqual(30);
2180
2180
  expect(start).toBe(10);
2181
2181
  expect(end).toBe(20);
2182
2182
  return originalRemove(type, start, end);
@@ -652,6 +652,44 @@ describe('Player', () => {
652
652
  });
653
653
  });
654
654
 
655
+ describe('seek range', () => {
656
+ // Regression test for Issue #3675.
657
+ it('has a reasonable seek range after going from live to VOD', async () => {
658
+ const netEngine = player.getNetworkingEngine();
659
+ const startTime = Date.now();
660
+ netEngine.registerRequestFilter((type, request) => {
661
+ if (type != shaka.net.NetworkingEngine.RequestType.MANIFEST) {
662
+ return;
663
+ }
664
+ // Simulate a live stream by providing different manifests over time.
665
+ const time = (Date.now() - startTime) / 1000;
666
+ const manifestNumber = Math.min(5, Math.floor(0.5 + time / 2));
667
+ request.uris = [
668
+ '/base/test/test/assets/3675/dash_' + manifestNumber + '.mpd',
669
+ ];
670
+ console.log('getting manifest', request.uris);
671
+ });
672
+
673
+ // Play the stream.
674
+ await player.load('/base/test/test/assets/3675/dash_0.mpd');
675
+ video.play();
676
+
677
+ // Wait for the stream to be over.
678
+ eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
679
+ /** @type {shaka.test.Waiter} */
680
+ const waiter = new shaka.test.Waiter(eventManager)
681
+ .timeoutAfter(20).failOnTimeout(true);
682
+ await waiter.waitForEnd(video);
683
+
684
+ // The stream should have transitioned to VOD by now.
685
+ expect(player.isLive()).toBe(false);
686
+
687
+ // Check that the final seek range is as expected.
688
+ const seekRange = player.seekRange();
689
+ expect(seekRange.end).toBe(14);
690
+ });
691
+ });
692
+
655
693
  describe('buffering', () => {
656
694
  const startBuffering = jasmine.objectContaining({buffering: true});
657
695
  const endBuffering = jasmine.objectContaining({buffering: false});