shaka-player 4.1.1 → 4.2.0

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 (179) hide show
  1. package/.github/workflows/build-and-test.yaml +67 -15
  2. package/.github/workflows/compute-incremental-coverage.py +251 -0
  3. package/.github/workflows/report-incremental-coverage.yaml +39 -0
  4. package/.github/workflows/selenium-lab-tests.yaml +44 -1
  5. package/AUTHORS +1 -0
  6. package/CHANGELOG.md +36 -8
  7. package/CONTRIBUTORS +3 -0
  8. package/README.md +20 -6
  9. package/demo/common/assets.js +25 -0
  10. package/demo/common/message_ids.js +2 -0
  11. package/demo/config.js +5 -1
  12. package/demo/index.html +1 -3
  13. package/demo/locales/en.json +2 -0
  14. package/demo/locales/source.json +8 -0
  15. package/dist/controls.css +1 -1
  16. package/dist/demo.compiled.debug.js +25 -23
  17. package/dist/demo.compiled.debug.map +3 -3
  18. package/dist/demo.compiled.js +25 -23
  19. package/dist/demo.compiled.map +3 -3
  20. package/dist/deps.js +6 -5
  21. package/dist/locales.js +30 -30
  22. package/dist/receiver.compiled.debug.js +24 -22
  23. package/dist/receiver.compiled.debug.map +3 -3
  24. package/dist/receiver.compiled.js +24 -22
  25. package/dist/receiver.compiled.map +3 -3
  26. package/dist/shaka-player.compiled.d.ts +69 -5
  27. package/dist/shaka-player.compiled.debug.d.ts +69 -5
  28. package/dist/shaka-player.compiled.debug.externs.js +111 -42
  29. package/dist/shaka-player.compiled.debug.js +352 -301
  30. package/dist/shaka-player.compiled.debug.map +4 -4
  31. package/dist/shaka-player.compiled.externs.js +111 -42
  32. package/dist/shaka-player.compiled.js +823 -802
  33. package/dist/shaka-player.compiled.map +4 -4
  34. package/dist/shaka-player.ui.d.ts +69 -5
  35. package/dist/shaka-player.ui.debug.d.ts +69 -5
  36. package/dist/shaka-player.ui.debug.externs.js +111 -42
  37. package/dist/shaka-player.ui.debug.js +360 -310
  38. package/dist/shaka-player.ui.debug.map +4 -4
  39. package/dist/shaka-player.ui.externs.js +111 -42
  40. package/dist/shaka-player.ui.js +980 -957
  41. package/dist/shaka-player.ui.map +4 -4
  42. package/docs/tutorials/fairplay.md +149 -0
  43. package/docs/tutorials/ui.md +6 -6
  44. package/docs/tutorials/upgrade.md +1 -0
  45. package/docs/tutorials/welcome.md +6 -3
  46. package/externs/shaka/abr_manager.js +8 -0
  47. package/externs/shaka/manifest.js +43 -0
  48. package/externs/shaka/player.js +13 -2
  49. package/karma.conf.js +3 -11
  50. package/lib/abr/simple_abr_manager.js +72 -4
  51. package/lib/ads/client_side_ad_manager.js +1 -1
  52. package/lib/dash/dash_parser.js +5 -0
  53. package/lib/hls/hls_parser.js +123 -27
  54. package/lib/media/drm_engine.js +1 -3
  55. package/lib/media/media_source_engine.js +57 -24
  56. package/lib/media/presentation_timeline.js +24 -1
  57. package/lib/media/segment_index.js +8 -0
  58. package/lib/media/segment_reference.js +7 -1
  59. package/lib/media/streaming_engine.js +42 -6
  60. package/lib/media/time_ranges_utils.js +46 -0
  61. package/lib/net/networking_engine.js +1 -0
  62. package/lib/player.js +8 -6
  63. package/lib/polyfill/media_capabilities.js +9 -0
  64. package/lib/polyfill/patchedmediakeys_apple.js +729 -0
  65. package/lib/polyfill/videoplaybackquality.js +3 -1
  66. package/lib/text/cue.js +0 -5
  67. package/lib/text/simple_text_displayer.js +21 -9
  68. package/lib/text/ui_text_displayer.js +83 -19
  69. package/lib/text/vtt_text_parser.js +61 -4
  70. package/lib/util/buffer_utils.js +3 -0
  71. package/lib/util/error.js +21 -1
  72. package/lib/util/fairplay_utils.js +100 -2
  73. package/lib/util/object_utils.js +1 -0
  74. package/lib/util/platform.js +30 -1
  75. package/lib/util/player_configuration.js +3 -10
  76. package/lib/util/stream_utils.js +23 -3
  77. package/lib/util/string_utils.js +102 -13
  78. package/maintained-branches.md +15 -0
  79. package/package.json +3 -5
  80. package/shaka-player.uncompiled.js +1 -0
  81. package/test/hls/hls_parser_unit.js +49 -33
  82. package/test/media/drm_engine_integration.js +24 -8
  83. package/test/media/media_source_engine_integration.js +24 -10
  84. package/test/media/media_source_engine_unit.js +53 -38
  85. package/test/media/streaming_engine_unit.js +107 -32
  86. package/test/test/util/fake_media_source_engine.js +10 -9
  87. package/test/test/util/simple_fakes.js +4 -0
  88. package/test/test/util/streaming_engine_util.js +10 -2
  89. package/test/text/simple_text_displayer_unit.js +27 -0
  90. package/test/text/ui_text_displayer_unit.js +65 -0
  91. package/test/text/vtt_text_parser_unit.js +26 -0
  92. package/test/util/string_utils_unit.js +46 -8
  93. package/ui/text_selection.js +2 -2
  94. package/app-engine/README.md +0 -9
  95. package/app-engine/demo-version-index/README.md +0 -14
  96. package/app-engine/demo-version-index/app.yaml +0 -28
  97. package/app-engine/demo-version-index/generate.py +0 -164
  98. package/app-engine/demo-version-index/main.py +0 -11
  99. package/app-engine/demo-version-index/requirements.txt +0 -5
  100. package/app-engine/demo-version-index/templates/index.html +0 -114
  101. package/app-engine/shaka-player-demo/README.md +0 -18
  102. package/app-engine/shaka-player-demo/app.yaml +0 -61
  103. package/app-engine/shaka-player-demo/main.py +0 -95
  104. package/app-engine/shaka-player-demo/posters/10yo.png +0 -0
  105. package/app-engine/shaka-player-demo/posters/10yo.xcf +0 -0
  106. package/app-engine/shaka-player-demo/posters/1yo.jpg +0 -0
  107. package/app-engine/shaka-player-demo/posters/2yo.png +0 -0
  108. package/app-engine/shaka-player-demo/posters/2yo.xcf +0 -0
  109. package/app-engine/shaka-player-demo/posters/3yo.png +0 -0
  110. package/app-engine/shaka-player-demo/posters/3yo.xcf +0 -0
  111. package/app-engine/shaka-player-demo/posters/4yo.png +0 -0
  112. package/app-engine/shaka-player-demo/posters/4yo.xcf +0 -0
  113. package/app-engine/shaka-player-demo/posters/5yo.png +0 -0
  114. package/app-engine/shaka-player-demo/posters/5yo.xcf +0 -0
  115. package/app-engine/shaka-player-demo/posters/6yo.png +0 -0
  116. package/app-engine/shaka-player-demo/posters/6yo.xcf +0 -0
  117. package/app-engine/shaka-player-demo/posters/7yo.png +0 -0
  118. package/app-engine/shaka-player-demo/posters/7yo.xcf +0 -0
  119. package/app-engine/shaka-player-demo/posters/8yo.png +0 -0
  120. package/app-engine/shaka-player-demo/posters/8yo.xcf +0 -0
  121. package/app-engine/shaka-player-demo/posters/9yo.png +0 -0
  122. package/app-engine/shaka-player-demo/posters/9yo.xcf +0 -0
  123. package/app-engine/shaka-player-demo/posters/audioOnly.gif +0 -0
  124. package/app-engine/shaka-player-demo/posters/audioRickRoll.gif +0 -0
  125. package/app-engine/shaka-player-demo/posters/colorbars.gif +0 -0
  126. package/app-engine/shaka-player-demo/posters/fireworks.jpg +0 -0
  127. package/app-engine/shaka-player-demo/posters/party-hat.png +0 -0
  128. package/app-engine/shaka-player-demo/posters/sources.txt +0 -46
  129. package/app-engine/shaka-player-demo/posters/standard.png +0 -0
  130. package/app-engine/shaka-player-demo/posters/trek.jpg +0 -0
  131. package/app-engine/shaka-player-demo/posters/valentines.png +0 -0
  132. package/app-engine/shaka-player-demo/posters/zombie.jpg +0 -0
  133. package/app-engine/shaka-player-demo/posters.py +0 -110
  134. package/app-engine/shaka-player-demo/requirements.txt +0 -5
  135. package/app-engine/shaka-player-demo/time.txt +0 -1
  136. package/build/README.md +0 -127
  137. package/build/__init__.py +0 -0
  138. package/build/all.py +0 -150
  139. package/build/apps.py +0 -186
  140. package/build/build.py +0 -392
  141. package/build/check.py +0 -354
  142. package/build/checkversion.py +0 -84
  143. package/build/compiler.py +0 -513
  144. package/build/conformance.textproto +0 -335
  145. package/build/docker/Dockerfile +0 -20
  146. package/build/docker/docker-compose.yml +0 -10
  147. package/build/docs.py +0 -54
  148. package/build/eslint-plugin-shaka-rules/arg-comment-spacing.js +0 -72
  149. package/build/eslint-plugin-shaka-rules/index.js +0 -24
  150. package/build/eslint-plugin-shaka-rules/package.json +0 -7
  151. package/build/eslint-plugin-shaka-rules/private.js +0 -109
  152. package/build/gendeps.py +0 -71
  153. package/build/generateExterns.js +0 -867
  154. package/build/generateLocalizations.py +0 -262
  155. package/build/generateTsDefs.py +0 -137
  156. package/build/install-linux-prereqs.sh +0 -136
  157. package/build/license-header +0 -5
  158. package/build/misspellings.txt +0 -39
  159. package/build/pixelsChanged.js +0 -35
  160. package/build/shaka-lab.yaml +0 -161
  161. package/build/shakaBuildHelpers.py +0 -356
  162. package/build/stats.py +0 -816
  163. package/build/subprocessWindowsPatch.py +0 -103
  164. package/build/test.py +0 -541
  165. package/build/types/ads +0 -8
  166. package/build/types/cast +0 -6
  167. package/build/types/cea +0 -13
  168. package/build/types/complete +0 -9
  169. package/build/types/core +0 -108
  170. package/build/types/dash +0 -8
  171. package/build/types/hls +0 -9
  172. package/build/types/manifests +0 -5
  173. package/build/types/networking +0 -6
  174. package/build/types/offline +0 -22
  175. package/build/types/polyfill +0 -18
  176. package/build/types/text +0 -11
  177. package/build/types/ui +0 -39
  178. package/build/updateScreenshots.py +0 -85
  179. package/build/wrapper.template.js +0 -52
@@ -37,6 +37,7 @@ goog.require('shaka.util.OperationManager');
37
37
  goog.require('shaka.util.Pssh');
38
38
  goog.require('shaka.util.Timer');
39
39
  goog.require('shaka.util.Platform');
40
+ goog.require('shaka.util.Uint8ArrayUtils');
40
41
  goog.require('shaka.util.XmlUtils');
41
42
  goog.requireType('shaka.hls.Segment');
42
43
 
@@ -740,8 +741,14 @@ shaka.hls.HlsParser = class {
740
741
  for (const stream of streamsToNotify) {
741
742
  this.segmentsToNotifyByStream_.push(stream.segmentIndex.references);
742
743
  }
744
+
743
745
  this.notifySegments_();
744
746
 
747
+ // This is the first point where we have a meaningful presentation start
748
+ // time, and we need to tell PresentationTimeline that so that it can
749
+ // maintain consistency from here on.
750
+ this.presentationTimeline_.lockStartTime();
751
+
745
752
  // This asserts that the live edge is being calculated from segment times.
746
753
  // For VOD and event streams, this check should still pass.
747
754
  goog.asserts.assert(
@@ -1638,34 +1645,30 @@ shaka.hls.HlsParser = class {
1638
1645
  if (method != 'NONE') {
1639
1646
  encrypted = true;
1640
1647
 
1641
- // We do not support AES-128 encryption with HLS yet. So, do not create
1642
- // StreamInfo for the playlist encrypted with AES-128.
1643
- // TODO: Remove the error message once we add support for AES-128.
1644
1648
  if (method == 'AES-128') {
1645
- shaka.log.warning('Unsupported HLS Encryption', method);
1649
+ // These keys are handled separately.
1646
1650
  this.aesEncrypted_ = true;
1647
- return null;
1648
- }
1649
-
1650
- const keyFormat = drmTag.getRequiredAttrValue('KEYFORMAT');
1651
- const drmParser =
1652
- shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
1653
-
1654
- const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
1655
- if (drmInfo) {
1656
- if (drmInfo.keyIds) {
1657
- for (const keyId of drmInfo.keyIds) {
1658
- keyIds.add(keyId);
1651
+ } else {
1652
+ const keyFormat = drmTag.getRequiredAttrValue('KEYFORMAT');
1653
+ const drmParser =
1654
+ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
1655
+
1656
+ const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
1657
+ if (drmInfo) {
1658
+ if (drmInfo.keyIds) {
1659
+ for (const keyId of drmInfo.keyIds) {
1660
+ keyIds.add(keyId);
1661
+ }
1659
1662
  }
1663
+ drmInfos.push(drmInfo);
1664
+ } else {
1665
+ shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
1660
1666
  }
1661
- drmInfos.push(drmInfo);
1662
- } else {
1663
- shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
1664
1667
  }
1665
1668
  }
1666
1669
  }
1667
1670
 
1668
- if (encrypted && !drmInfos.length) {
1671
+ if (encrypted && !drmInfos.length && !this.aesEncrypted_) {
1669
1672
  throw new shaka.util.Error(
1670
1673
  shaka.util.Error.Severity.CRITICAL,
1671
1674
  shaka.util.Error.Category.MANIFEST,
@@ -1682,9 +1685,8 @@ shaka.hls.HlsParser = class {
1682
1685
 
1683
1686
  let segments;
1684
1687
  try {
1685
- segments = this.createSegments_(verbatimMediaPlaylistUri,
1686
- playlist, type, mimeType, mediaSequenceToStartTime, mediaVariables,
1687
- codecs);
1688
+ segments = this.createSegments_(verbatimMediaPlaylistUri, playlist, type,
1689
+ mimeType, mediaSequenceToStartTime, mediaVariables, codecs);
1688
1690
  } catch (error) {
1689
1691
  if (error.code == shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM) {
1690
1692
  shaka.log.alwaysWarn('Skipping unsupported HLS stream',
@@ -1759,6 +1761,78 @@ shaka.hls.HlsParser = class {
1759
1761
  }
1760
1762
 
1761
1763
 
1764
+ /**
1765
+ * @param {!shaka.hls.Tag} drmTag
1766
+ * @param {!shaka.hls.Playlist} playlist
1767
+ * @return {!shaka.extern.HlsAes128Key}
1768
+ * @private
1769
+ */
1770
+ parseAES128DrmTag_(drmTag, playlist) {
1771
+ // Check if the Web Crypto API is available.
1772
+ if (!window.crypto || !window.crypto.subtle) {
1773
+ shaka.log.alwaysWarn('Web Crypto API is not available to decrypt ' +
1774
+ 'AES-128. (Web Crypto only exists in secure origins like https)');
1775
+ throw new shaka.util.Error(
1776
+ shaka.util.Error.Severity.CRITICAL,
1777
+ shaka.util.Error.Category.MANIFEST,
1778
+ shaka.util.Error.Code.NO_WEB_CRYPTO_API);
1779
+ }
1780
+
1781
+ // HLS RFC 8216 Section 5.2:
1782
+ // An EXT-X-KEY tag with a KEYFORMAT of "identity" that does not have an IV
1783
+ // attribute indicates that the Media Sequence Number is to be used as the
1784
+ // IV when decrypting a Media Segment, by putting its big-endian binary
1785
+ // representation into a 16-octet (128-bit) buffer and padding (on the left)
1786
+ // with zeros.
1787
+ let firstMediaSequenceNumber = 0;
1788
+ let iv;
1789
+ const ivHex = drmTag.getAttributeValue('IV', '');
1790
+ if (!ivHex) {
1791
+ // Media Sequence Number will be used as IV.
1792
+ firstMediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
1793
+ playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
1794
+ } else {
1795
+ // Exclude 0x at the start of string.
1796
+ iv = shaka.util.Uint8ArrayUtils.fromHex(ivHex.substr(2));
1797
+ if (iv.byteLength != 16) {
1798
+ throw new shaka.util.Error(
1799
+ shaka.util.Error.Severity.CRITICAL,
1800
+ shaka.util.Error.Category.MANIFEST,
1801
+ shaka.util.Error.Code.HLS_AES_128_INVALID_IV_LENGTH);
1802
+ }
1803
+ }
1804
+
1805
+ const keyUri = shaka.hls.Utils.constructAbsoluteUri(
1806
+ playlist.absoluteUri, drmTag.getRequiredAttrValue('URI'));
1807
+
1808
+ const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
1809
+ const request = shaka.net.NetworkingEngine.makeRequest(
1810
+ [keyUri], this.config_.retryParameters);
1811
+
1812
+ const keyInfo = {method: 'AES-128', iv, firstMediaSequenceNumber};
1813
+
1814
+ // Don't download the key object until the segment is parsed, to avoid a
1815
+ // startup delay for long manifests with lots of keys.
1816
+ keyInfo.fetchKey = async () => {
1817
+ const keyResponse = await this.makeNetworkRequest_(request, requestType);
1818
+
1819
+ // keyResponse.status is undefined when URI is "data:text/plain;base64,"
1820
+ if (!keyResponse.data || keyResponse.data.byteLength != 16) {
1821
+ throw new shaka.util.Error(
1822
+ shaka.util.Error.Severity.CRITICAL,
1823
+ shaka.util.Error.Category.MANIFEST,
1824
+ shaka.util.Error.Code.HLS_AES_128_INVALID_KEY_LENGTH);
1825
+ }
1826
+
1827
+ keyInfo.cryptoKey = await window.crypto.subtle.importKey(
1828
+ 'raw', keyResponse.data, 'AES-CBC', true, ['decrypt']);
1829
+ keyInfo.fetchKey = undefined; // No longer needed.
1830
+ };
1831
+
1832
+ return keyInfo;
1833
+ }
1834
+
1835
+
1762
1836
  /**
1763
1837
  * @param {!shaka.hls.Playlist} playlist
1764
1838
  * @private
@@ -1859,8 +1933,6 @@ shaka.hls.HlsParser = class {
1859
1933
  /* presentationStartTime= */ null, /* delay= */ 0);
1860
1934
  this.presentationTimeline_.setStatic(true);
1861
1935
  }
1862
-
1863
- this.notifySegments_();
1864
1936
  }
1865
1937
 
1866
1938
  /**
@@ -1935,12 +2007,13 @@ shaka.hls.HlsParser = class {
1935
2007
  * @param {!Map.<string, string>} variables
1936
2008
  * @param {string} absoluteMediaPlaylistUri
1937
2009
  * @param {string} type
2010
+ * @param {shaka.extern.HlsAes128Key=} hlsAes128Key
1938
2011
  * @return {shaka.media.SegmentReference}
1939
2012
  * @private
1940
2013
  */
1941
2014
  createSegmentReference_(
1942
2015
  initSegmentReference, previousReference, hlsSegment, startTime,
1943
- variables, absoluteMediaPlaylistUri, type) {
2016
+ variables, absoluteMediaPlaylistUri, type, hlsAes128Key) {
1944
2017
  const tags = hlsSegment.tags;
1945
2018
  const absoluteSegmentUri = this.variableSubstitution_(
1946
2019
  hlsSegment.absoluteUri, variables);
@@ -2035,6 +2108,8 @@ shaka.hls.HlsParser = class {
2035
2108
  partialStatus = shaka.media.SegmentReference.Status.MISSING;
2036
2109
  }
2037
2110
 
2111
+ // We do not set the AES-128 key information for partial segments, as we
2112
+ // do not support AES-128 and low-latency at the same time.
2038
2113
  const partial = new shaka.media.SegmentReference(
2039
2114
  pStartTime,
2040
2115
  pEndTime,
@@ -2116,6 +2191,7 @@ shaka.hls.HlsParser = class {
2116
2191
  tileDuration,
2117
2192
  syncTime,
2118
2193
  status,
2194
+ hlsAes128Key,
2119
2195
  );
2120
2196
  }
2121
2197
 
@@ -2182,6 +2258,9 @@ shaka.hls.HlsParser = class {
2182
2258
  /** @type {shaka.media.InitSegmentReference} */
2183
2259
  let initSegmentRef;
2184
2260
 
2261
+ /** @type {shaka.extern.HlsAes128Key|undefined} */
2262
+ let hlsAes128Key = undefined;
2263
+
2185
2264
  // We may need to look at the media itself to determine a segment start
2186
2265
  // time.
2187
2266
  const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
@@ -2210,6 +2289,14 @@ shaka.hls.HlsParser = class {
2210
2289
  (i == 0) ? firstStartTime : previousReference.endTime;
2211
2290
  position = mediaSequenceNumber + skippedSegments + i;
2212
2291
 
2292
+ // Apply new AES-128 tags as you see them, keeping a running total.
2293
+ for (const drmTag of item.tags) {
2294
+ if (drmTag.name == 'EXT-X-KEY' &&
2295
+ drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
2296
+ hlsAes128Key = this.parseAES128DrmTag_(drmTag, playlist);
2297
+ }
2298
+ }
2299
+
2213
2300
  mediaSequenceToStartTime.set(position, startTime);
2214
2301
 
2215
2302
  initSegmentRef = this.getInitSegmentReference_(playlist.absoluteUri,
@@ -2234,7 +2321,8 @@ shaka.hls.HlsParser = class {
2234
2321
  startTime,
2235
2322
  variables,
2236
2323
  playlist.absoluteUri,
2237
- type);
2324
+ type,
2325
+ hlsAes128Key);
2238
2326
  previousReference = reference;
2239
2327
 
2240
2328
  if (reference) {
@@ -2607,6 +2695,14 @@ shaka.hls.HlsParser = class {
2607
2695
  shaka.util.Error.Code.HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED);
2608
2696
  }
2609
2697
 
2698
+ if (shaka.util.Platform.isMediaKeysPolyfilled()) {
2699
+ throw new shaka.util.Error(
2700
+ shaka.util.Error.Severity.CRITICAL,
2701
+ shaka.util.Error.Category.MANIFEST,
2702
+ shaka.util.Error.Code
2703
+ .HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED);
2704
+ }
2705
+
2610
2706
  /*
2611
2707
  * Even if we're not able to construct initData through the HLS tag, adding
2612
2708
  * a DRMInfo will allow DRM Engine to request a media key system access
@@ -1548,7 +1548,7 @@ shaka.media.DrmEngine = class {
1548
1548
  if (shaka.media.DrmEngine.isPlayReadyKeySystem(
1549
1549
  this.currentDrmInfo_.keySystem) &&
1550
1550
  keyId.byteLength == 16 &&
1551
- shaka.util.Platform.isEdge()) {
1551
+ (shaka.util.Platform.isEdge() || shaka.util.Platform.isPS4())) {
1552
1552
  // Read out some fields in little-endian:
1553
1553
  const dataView = shaka.util.BufferUtils.toDataView(keyId);
1554
1554
  const part0 = dataView.getUint32(0, /* LE= */ true);
@@ -1668,8 +1668,6 @@ shaka.media.DrmEngine = class {
1668
1668
  'com.widevine.alpha',
1669
1669
  'com.microsoft.playready',
1670
1670
  'com.microsoft.playready.recommendation',
1671
- 'com.apple.fps.3_0',
1672
- 'com.apple.fps.2_0',
1673
1671
  'com.apple.fps.1_0',
1674
1672
  'com.apple.fps',
1675
1673
  'com.adobe.primetime',
@@ -10,6 +10,7 @@ goog.require('goog.asserts');
10
10
  goog.require('shaka.log');
11
11
  goog.require('shaka.media.ContentWorkarounds');
12
12
  goog.require('shaka.media.IClosedCaptionParser');
13
+ goog.require('shaka.media.SegmentReference');
13
14
  goog.require('shaka.media.TimeRangesUtils');
14
15
  goog.require('shaka.media.Transmuxer');
15
16
  goog.require('shaka.text.TextEngine');
@@ -466,13 +467,13 @@ shaka.media.MediaSourceEngine = class {
466
467
  */
467
468
  getBufferedInfo() {
468
469
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
469
- const TimeRangeUtils = shaka.media.TimeRangesUtils;
470
+ const TimeRangesUtils = shaka.media.TimeRangesUtils;
470
471
 
471
472
  const info = {
472
- total: TimeRangeUtils.getBufferedInfo(this.video_.buffered),
473
- audio: TimeRangeUtils.getBufferedInfo(
473
+ total: TimeRangesUtils.getBufferedInfo(this.video_.buffered),
474
+ audio: TimeRangesUtils.getBufferedInfo(
474
475
  this.getBuffered_(ContentType.AUDIO)),
475
- video: TimeRangeUtils.getBufferedInfo(
476
+ video: TimeRangesUtils.getBufferedInfo(
476
477
  this.getBuffered_(ContentType.VIDEO)),
477
478
  text: [],
478
479
  };
@@ -516,15 +517,14 @@ shaka.media.MediaSourceEngine = class {
516
517
  *
517
518
  * @param {shaka.util.ManifestParserUtils.ContentType} contentType
518
519
  * @param {!BufferSource} data
519
- * @param {?number} startTime relative to the start of the presentation
520
- * @param {?number} endTime relative to the start of the presentation
520
+ * @param {?shaka.media.SegmentReference} reference The segment reference
521
+ * we are appending, or null for init segments
521
522
  * @param {?boolean} hasClosedCaptions True if the buffer contains CEA closed
522
- * captions
523
+ * captions
523
524
  * @param {boolean=} seeked True if we just seeked
524
525
  * @return {!Promise}
525
526
  */
526
- async appendBuffer(
527
- contentType, data, startTime, endTime, hasClosedCaptions, seeked) {
527
+ async appendBuffer(contentType, data, reference, hasClosedCaptions, seeked) {
528
528
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
529
529
 
530
530
  if (contentType == ContentType.TEXT) {
@@ -533,7 +533,10 @@ shaka.media.MediaSourceEngine = class {
533
533
  const offset = await this.textSequenceModeOffset_;
534
534
  this.textEngine_.setTimestampOffset(offset);
535
535
  }
536
- await this.textEngine_.appendBuffer(data, startTime, endTime);
536
+ await this.textEngine_.appendBuffer(
537
+ data,
538
+ reference ? reference.startTime : null,
539
+ reference ? reference.endTime : null);
537
540
  return;
538
541
  }
539
542
 
@@ -549,7 +552,10 @@ shaka.media.MediaSourceEngine = class {
549
552
  if (transmuxedData.metadata) {
550
553
  const timestampOffset =
551
554
  this.sourceBuffers_[contentType].timestampOffset;
552
- this.onMetadata_(transmuxedData.metadata, timestampOffset, endTime);
555
+ this.onMetadata_(
556
+ transmuxedData.metadata,
557
+ timestampOffset,
558
+ reference ? reference.endTime : null);
553
559
  }
554
560
  // This doesn't work for native TS support (ex. Edge/Chromecast),
555
561
  // since no transmuxing is needed for native TS.
@@ -559,7 +565,10 @@ shaka.media.MediaSourceEngine = class {
559
565
  const closedCaptions = this.textEngine_
560
566
  .convertMuxjsCaptionsToShakaCaptions(transmuxedData.captions);
561
567
  this.textEngine_.storeAndAppendClosedCaptions(
562
- closedCaptions, startTime, endTime, videoOffset);
568
+ closedCaptions,
569
+ reference ? reference.startTime : null,
570
+ reference ? reference.endTime : null,
571
+ videoOffset);
563
572
  }
564
573
 
565
574
  data = transmuxedData.data;
@@ -569,7 +578,7 @@ shaka.media.MediaSourceEngine = class {
569
578
  }
570
579
  // If it is the init segment for closed captions, initialize the closed
571
580
  // caption parser.
572
- if (startTime == null && endTime == null) {
581
+ if (!reference) {
573
582
  this.captionParser_.init(data);
574
583
  } else {
575
584
  const closedCaptions = this.captionParser_.parseFrom(data);
@@ -577,18 +586,21 @@ shaka.media.MediaSourceEngine = class {
577
586
  const videoOffset =
578
587
  this.sourceBuffers_[ContentType.VIDEO].timestampOffset;
579
588
  this.textEngine_.storeAndAppendClosedCaptions(
580
- closedCaptions, startTime, endTime, videoOffset);
589
+ closedCaptions,
590
+ reference.startTime,
591
+ reference.endTime,
592
+ videoOffset);
581
593
  }
582
594
  }
583
595
  }
584
596
 
585
- data = this.workAroundBrokenPlatforms_(data, startTime, contentType);
597
+ data = this.workAroundBrokenPlatforms_(
598
+ data, reference ? reference.startTime : null, contentType);
586
599
 
587
600
  const sourceBuffer = this.sourceBuffers_[contentType];
588
601
  const SEQUENCE = shaka.media.MediaSourceEngine.SourceBufferMode_.SEQUENCE;
589
602
 
590
- if (this.sequenceMode_ && sourceBuffer.mode != SEQUENCE &&
591
- startTime != null) {
603
+ if (this.sequenceMode_ && sourceBuffer.mode != SEQUENCE && reference) {
592
604
  // This is the first media segment to be appended to a SourceBuffer in
593
605
  // sequence mode. We set the mode late so that we can trick MediaSource
594
606
  // into extracting a timestamp for us to align text segments in sequence
@@ -623,7 +635,7 @@ shaka.media.MediaSourceEngine = class {
623
635
  // segments.
624
636
  const mediaStartTime = shaka.media.TimeRangesUtils.bufferStart(
625
637
  this.getBuffered_(contentType));
626
- const textOffset = (startTime || 0) - (mediaStartTime || 0);
638
+ const textOffset = (reference.startTime || 0) - (mediaStartTime || 0);
627
639
  this.textSequenceModeOffset_.resolve(textOffset);
628
640
 
629
641
  // Finally, clear the buffer.
@@ -636,22 +648,43 @@ shaka.media.MediaSourceEngine = class {
636
648
  sourceBuffer.mode = SEQUENCE;
637
649
  }
638
650
 
639
- if (startTime != null && this.sequenceMode_ &&
640
- contentType != ContentType.TEXT) {
651
+ if (reference && this.sequenceMode_ && contentType != ContentType.TEXT) {
641
652
  // In sequence mode, for non-text streams, if we just cleared the buffer
642
653
  // and are performing an unbuffered seek, we need to set a new
643
654
  // timestampOffset on the sourceBuffer.
644
655
  if (seeked) {
645
- const timestampOffset = /** @type {number} */ (startTime);
656
+ const timestampOffset = reference.startTime;
646
657
  this.enqueueOperation_(
647
658
  contentType,
648
659
  () => this.setTimestampOffset_(contentType, timestampOffset));
649
660
  }
650
661
  }
651
662
 
652
- await this.enqueueOperation_(
653
- contentType,
654
- () => this.append_(contentType, data));
663
+ let bufferedBefore = null;
664
+
665
+ await this.enqueueOperation_(contentType, () => {
666
+ if (goog.DEBUG && reference) {
667
+ bufferedBefore = this.getBuffered_(contentType);
668
+ }
669
+ this.append_(contentType, data);
670
+ });
671
+
672
+ if (goog.DEBUG && reference) {
673
+ const bufferedAfter = this.getBuffered_(contentType);
674
+ const newBuffered = shaka.media.TimeRangesUtils.computeAddedRange(
675
+ bufferedBefore, bufferedAfter);
676
+ if (newBuffered) {
677
+ const segmentDuration = reference.endTime - reference.startTime;
678
+ if (Math.abs(newBuffered.start - reference.startTime) >
679
+ segmentDuration / 2) {
680
+ shaka.log.error('Possible encoding problem detected!',
681
+ 'Unexpected buffered range for reference', reference,
682
+ 'from URIs', reference.getUris(),
683
+ 'should be', {start: reference.startTime, end: reference.endTime},
684
+ 'but got', newBuffered);
685
+ }
686
+ }
687
+ }
655
688
  }
656
689
 
657
690
  /**
@@ -93,6 +93,9 @@ shaka.media.PresentationTimeline = class {
93
93
  * @private {number}
94
94
  */
95
95
  this.availabilityTimeOffset_ = 0;
96
+
97
+ /** @private {boolean} */
98
+ this.startTimeLocked_ = false;
96
99
  }
97
100
 
98
101
 
@@ -229,7 +232,8 @@ shaka.media.PresentationTimeline = class {
229
232
  this.maxSegmentEndTime_ =
230
233
  Math.max(this.maxSegmentEndTime_, lastReferenceEndTime);
231
234
 
232
- if (this.presentationStartTime_ != null && this.autoCorrectDrift_) {
235
+ if (this.presentationStartTime_ != null && this.autoCorrectDrift_ &&
236
+ !this.startTimeLocked_) {
233
237
  // Since we have explicit segment end times, calculate a presentation
234
238
  // start based on them. This start time accounts for drift.
235
239
  // Date.now() is in milliseconds, from which we compute "now" in seconds.
@@ -243,6 +247,25 @@ shaka.media.PresentationTimeline = class {
243
247
  }
244
248
 
245
249
 
250
+ /**
251
+ * Lock the presentation timeline's start time. After this is called, no
252
+ * further adjustments to presentationStartTime_ will be permitted.
253
+ *
254
+ * This should be called after all Periods have been parsed, and all calls to
255
+ * notifySegments() from the initial manifest parse have been made.
256
+ *
257
+ * Without this, we can get assertion failures in SegmentIndex for certain
258
+ * DAI content. If DAI adds ad segments to the manifest faster than
259
+ * real-time, adjustments to presentationStartTime_ can cause availability
260
+ * windows to jump around on updates.
261
+ *
262
+ * @export
263
+ */
264
+ lockStartTime() {
265
+ this.startTimeLocked_ = true;
266
+ }
267
+
268
+
246
269
  /**
247
270
  * Gives PresentationTimeline a Stream's minimum segment start time.
248
271
  *
@@ -529,6 +529,14 @@ shaka.media.SegmentIterator = class {
529
529
  this.currentPartialPosition_ = partialSegmentIndex;
530
530
  }
531
531
 
532
+ /**
533
+ * @return {number}
534
+ * @export
535
+ */
536
+ currentPosition() {
537
+ return this.currentPosition_;
538
+ }
539
+
532
540
  /**
533
541
  * @return {shaka.media.SegmentReference}
534
542
  * @export
@@ -166,12 +166,15 @@ shaka.media.SegmentReference = class {
166
166
  * @param {shaka.media.SegmentReference.Status=} status
167
167
  * The segment status is used to indicate that a segment does not exist or is
168
168
  * not available.
169
+ * @param {?shaka.extern.HlsAes128Key=} hlsAes128Key
170
+ * The segment's AES-128-CBC full segment encryption key and iv.
169
171
  */
170
172
  constructor(
171
173
  startTime, endTime, uris, startByte, endByte, initSegmentReference,
172
174
  timestampOffset, appendWindowStart, appendWindowEnd,
173
175
  partialReferences = [], tilesLayout = '', tileDuration = null,
174
- syncTime = null, status = shaka.media.SegmentReference.Status.AVAILABLE) {
176
+ syncTime = null, status = shaka.media.SegmentReference.Status.AVAILABLE,
177
+ hlsAes128Key = null) {
175
178
  // A preload hinted Partial Segment has the same startTime and endTime.
176
179
  goog.asserts.assert(startTime <= endTime,
177
180
  'startTime must be less than or equal to endTime');
@@ -233,6 +236,9 @@ shaka.media.SegmentReference = class {
233
236
 
234
237
  /** @type {shaka.media.SegmentReference.Status} */
235
238
  this.status = status;
239
+
240
+ /** @type {?shaka.extern.HlsAes128Key} */
241
+ this.hlsAes128Key = hlsAes128Key;
236
242
  }
237
243
 
238
244
  /**
@@ -1233,7 +1233,10 @@ shaka.media.StreamingEngine = class {
1233
1233
  stream.mimeType == 'audio/mp4';
1234
1234
  const isReadableStreamSupported = window.ReadableStream;
1235
1235
  // Enable MP4 low latency streaming with ReadableStream chunked data.
1236
- if (this.config_.lowLatencyMode && isReadableStreamSupported && isMP4) {
1236
+ // Disabled when AES-128 is present, as we cannot decrypt part of a
1237
+ // segment.
1238
+ if (this.config_.lowLatencyMode && isReadableStreamSupported && isMP4 &&
1239
+ !reference.hlsAes128Key) {
1237
1240
  let remaining = new Uint8Array(0);
1238
1241
  const streamDataCallback = async (data) => {
1239
1242
  this.destroyer_.ensureNotDestroyed();
@@ -1273,11 +1276,16 @@ shaka.media.StreamingEngine = class {
1273
1276
  'ReadableStream is not supported by the browser.');
1274
1277
  }
1275
1278
  const fetchSegment = this.fetch_(mediaState, reference);
1276
- const result = await fetchSegment;
1279
+ let result = await fetchSegment;
1277
1280
  this.destroyer_.ensureNotDestroyed();
1278
1281
  if (this.fatalError_) {
1279
1282
  return;
1280
1283
  }
1284
+ if (reference.hlsAes128Key) {
1285
+ goog.asserts.assert(iter, 'mediaState.segmentIterator should exist');
1286
+ result = await this.aes128Decrypt_(result, reference, iter);
1287
+ }
1288
+ this.destroyer_.ensureNotDestroyed();
1281
1289
 
1282
1290
  // If the text stream gets switched between fetch_() and append_(), the
1283
1291
  // new text parser is initialized, but the new init segment is not
@@ -1368,6 +1376,34 @@ shaka.media.StreamingEngine = class {
1368
1376
  }
1369
1377
  }
1370
1378
 
1379
+ /**
1380
+ * @param {!BufferSource} rawResult
1381
+ * @param {!shaka.media.SegmentReference} reference
1382
+ * @param {!shaka.media.SegmentIterator} iter
1383
+ * @return {!Promise.<!BufferSource>} finalResult
1384
+ * @private
1385
+ */
1386
+ async aes128Decrypt_(rawResult, reference, iter) {
1387
+ const key = reference.hlsAes128Key;
1388
+ if (!key.cryptoKey) {
1389
+ goog.asserts.assert(key.fetchKey, 'If AES-128 cryptoKey was not ' +
1390
+ 'preloaded, fetchKey function should be provided');
1391
+ await key.fetchKey();
1392
+ goog.asserts.assert(key.cryptoKey, 'AES-128 cryptoKey should now be set');
1393
+ }
1394
+ let iv = key.iv;
1395
+ if (!iv) {
1396
+ iv = shaka.util.BufferUtils.toUint8(new ArrayBuffer(16));
1397
+ let sequence = key.firstMediaSequenceNumber + iter.currentPosition();
1398
+ for (let i = iv.byteLength - 1; i >= 0; i--) {
1399
+ iv[i] = sequence & 0xff;
1400
+ sequence >>= 8;
1401
+ }
1402
+ }
1403
+ return window.crypto.subtle.decrypt(
1404
+ {name: 'AES-CBC', iv}, key.cryptoKey, rawResult);
1405
+ }
1406
+
1371
1407
 
1372
1408
  /**
1373
1409
  * Clear per-stream error states and retry any failed streams.
@@ -1555,8 +1591,8 @@ shaka.media.StreamingEngine = class {
1555
1591
  const hasClosedCaptions = mediaState.stream.closedCaptions &&
1556
1592
  mediaState.stream.closedCaptions.size > 0;
1557
1593
  await this.playerInterface_.mediaSourceEngine.appendBuffer(
1558
- mediaState.type, initSegment, /* startTime= */ null,
1559
- /* endTime= */ null, hasClosedCaptions);
1594
+ mediaState.type, initSegment, /* reference= */ null,
1595
+ hasClosedCaptions);
1560
1596
  } catch (error) {
1561
1597
  mediaState.lastInitSegmentReference = null;
1562
1598
  throw error;
@@ -1609,8 +1645,7 @@ shaka.media.StreamingEngine = class {
1609
1645
  await this.playerInterface_.mediaSourceEngine.appendBuffer(
1610
1646
  mediaState.type,
1611
1647
  segment,
1612
- reference.startTime,
1613
- reference.endTime,
1648
+ reference,
1614
1649
  hasClosedCaptions,
1615
1650
  seeked);
1616
1651
  this.destroyer_.ensureNotDestroyed();
@@ -1791,6 +1826,7 @@ shaka.media.StreamingEngine = class {
1791
1826
  *
1792
1827
  * @return {!Promise.<BufferSource>}
1793
1828
  * @private
1829
+ * @suppress {strictMissingProperties}
1794
1830
  */
1795
1831
  async fetch_(mediaState, reference, streamDataCallback) {
1796
1832
  const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
@@ -155,4 +155,50 @@ shaka.media.TimeRangesUtils = class {
155
155
  }
156
156
  return ret;
157
157
  }
158
+
159
+ /**
160
+ * This operation can be potentially EXPENSIVE and should only be done in
161
+ * debug builds for debugging purposes.
162
+ *
163
+ * @param {TimeRanges} oldRanges
164
+ * @param {TimeRanges} newRanges
165
+ * @return {?shaka.extern.BufferedRange} The last added range,
166
+ * chronologically by presentation time.
167
+ */
168
+ static computeAddedRange(oldRanges, newRanges) {
169
+ const TimeRangesUtils = shaka.media.TimeRangesUtils;
170
+
171
+ if (!oldRanges || !oldRanges.length) {
172
+ return null;
173
+ }
174
+ if (!newRanges || !newRanges.length) {
175
+ return TimeRangesUtils.getBufferedInfo(newRanges).pop();
176
+ }
177
+
178
+ const newRangesReversed =
179
+ TimeRangesUtils.getBufferedInfo(newRanges).reverse();
180
+ const oldRangesReversed =
181
+ TimeRangesUtils.getBufferedInfo(oldRanges).reverse();
182
+ for (const newRange of newRangesReversed) {
183
+ let foundOverlap = false;
184
+
185
+ for (const oldRange of oldRangesReversed) {
186
+ if (oldRange.end >= newRange.start && oldRange.end <= newRange.end) {
187
+ foundOverlap = true;
188
+
189
+ // If the new range goes beyond the corresponding old one, the
190
+ // difference is newly-added.
191
+ if (newRange.end > oldRange.end) {
192
+ return {start: oldRange.end, end: newRange.end};
193
+ }
194
+ }
195
+ }
196
+
197
+ if (!foundOverlap) {
198
+ return newRange;
199
+ }
200
+ }
201
+
202
+ return null;
203
+ }
158
204
  };