shaka-player 3.2.2 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +1 -1
  3. package/.github/ISSUE_TEMPLATE/question.md +1 -1
  4. package/.github/workflows/build_and_test.yaml +73 -0
  5. package/.github/workflows/release.yaml +53 -0
  6. package/AUTHORS +6 -5
  7. package/CHANGELOG.md +314 -1
  8. package/CONTRIBUTORS +4 -2
  9. package/build/checkversion.py +1 -1
  10. package/build/conformance.textproto +183 -169
  11. package/build/test.py +6 -0
  12. package/build/types/core +3 -0
  13. package/build/types/offline +1 -0
  14. package/build/types/polyfill +1 -0
  15. package/build/types/ui +2 -0
  16. package/demo/common/assets.js +18 -0
  17. package/demo/common/message_ids.js +13 -1
  18. package/demo/config.js +31 -1
  19. package/demo/index.html +1 -1
  20. package/demo/locales/en.json +12 -0
  21. package/demo/locales/source.json +48 -0
  22. package/dist/controls.css +26 -2
  23. package/dist/controls.css.map +1 -1
  24. package/dist/demo.compiled.debug.js +19 -16
  25. package/dist/demo.compiled.debug.map +3 -3
  26. package/dist/demo.compiled.js +19 -16
  27. package/dist/demo.compiled.map +3 -3
  28. package/dist/deps.js +21 -14
  29. package/dist/locales.js +31 -29
  30. package/dist/receiver.compiled.debug.js +11 -9
  31. package/dist/receiver.compiled.debug.map +3 -3
  32. package/dist/receiver.compiled.js +11 -9
  33. package/dist/receiver.compiled.map +3 -3
  34. package/dist/shaka-player.compiled.d.ts +232 -18
  35. package/dist/shaka-player.compiled.debug.d.ts +232 -18
  36. package/dist/shaka-player.compiled.debug.externs.js +293 -17
  37. package/dist/shaka-player.compiled.debug.js +481 -422
  38. package/dist/shaka-player.compiled.debug.map +4 -4
  39. package/dist/shaka-player.compiled.externs.js +293 -17
  40. package/dist/shaka-player.compiled.js +846 -817
  41. package/dist/shaka-player.compiled.map +4 -4
  42. package/dist/shaka-player.ui.d.ts +250 -30
  43. package/dist/shaka-player.ui.debug.d.ts +250 -30
  44. package/dist/shaka-player.ui.debug.externs.js +322 -28
  45. package/dist/shaka-player.ui.debug.js +630 -553
  46. package/dist/shaka-player.ui.debug.map +4 -4
  47. package/dist/shaka-player.ui.externs.js +322 -28
  48. package/dist/shaka-player.ui.js +1048 -1006
  49. package/dist/shaka-player.ui.map +4 -4
  50. package/docs/design/bg-fetch-after.gv +29 -0
  51. package/docs/design/bg-fetch-after.gv.png +0 -0
  52. package/docs/design/bg-fetch-before.gv +18 -0
  53. package/docs/design/bg-fetch-before.gv.png +0 -0
  54. package/docs/design/bg-fetch.md +134 -0
  55. package/docs/tutorials/blob-url.md +15 -0
  56. package/docs/tutorials/index.json +1 -0
  57. package/docs/tutorials/ui-customization.md +66 -0
  58. package/docs/tutorials/ui.md +5 -5
  59. package/externs/cmcd.js +186 -0
  60. package/externs/ima.js +3 -0
  61. package/externs/shaka/ads.js +5 -0
  62. package/externs/shaka/manifest.js +12 -1
  63. package/externs/shaka/manifest_parser.js +10 -1
  64. package/externs/shaka/net.js +20 -4
  65. package/externs/shaka/offline.js +31 -2
  66. package/externs/shaka/player.js +138 -5
  67. package/externs/shaka/text.js +24 -0
  68. package/externs/webcrypto.js +18 -0
  69. package/karma.conf.js +19 -15
  70. package/lib/abr/ewma.js +14 -0
  71. package/lib/abr/ewma_bandwidth_estimator.js +16 -2
  72. package/lib/abr/simple_abr_manager.js +6 -0
  73. package/lib/ads/ad_manager.js +1 -0
  74. package/lib/ads/client_side_ad.js +8 -0
  75. package/lib/ads/client_side_ad_manager.js +8 -4
  76. package/lib/ads/server_side_ad.js +7 -0
  77. package/lib/cast/cast_proxy.js +3 -0
  78. package/lib/cast/cast_receiver.js +3 -0
  79. package/lib/cast/cast_utils.js +1 -0
  80. package/lib/dash/dash_parser.js +79 -33
  81. package/lib/dash/segment_base.js +25 -1
  82. package/lib/dash/segment_list.js +18 -9
  83. package/lib/dash/segment_template.js +23 -10
  84. package/lib/hls/hls_parser.js +116 -23
  85. package/lib/hls/manifest_text_parser.js +1 -0
  86. package/lib/media/quality_observer.js +274 -0
  87. package/lib/media/region_observer.js +39 -33
  88. package/lib/media/region_timeline.js +17 -19
  89. package/lib/media/segment_index.js +8 -1
  90. package/lib/media/segment_reference.js +37 -4
  91. package/lib/media/streaming_engine.js +47 -6
  92. package/lib/net/http_fetch_plugin.js +31 -11
  93. package/lib/net/http_plugin_utils.js +1 -0
  94. package/lib/net/http_xhr_plugin.js +40 -14
  95. package/lib/net/networking_engine.js +85 -18
  96. package/lib/offline/download_info.js +68 -0
  97. package/lib/offline/download_manager.js +13 -5
  98. package/lib/offline/indexeddb/base_storage_cell.js +35 -0
  99. package/lib/offline/indexeddb/storage_mechanism.js +1 -1
  100. package/lib/offline/indexeddb/v5_storage_cell.js +5 -0
  101. package/lib/offline/storage.js +401 -293
  102. package/lib/offline/stored_content_utils.js +2 -0
  103. package/lib/player.js +254 -27
  104. package/lib/polyfill/media_capabilities.js +30 -15
  105. package/lib/polyfill/random_uuid.js +52 -0
  106. package/lib/text/cue.js +18 -0
  107. package/lib/text/ttml_text_parser.js +109 -60
  108. package/lib/text/ui_text_displayer.js +19 -13
  109. package/lib/util/cmcd_manager.js +672 -0
  110. package/lib/util/error.js +6 -0
  111. package/lib/util/fake_event_target.js +21 -1
  112. package/lib/util/language_utils.js +12 -0
  113. package/lib/util/mutex.js +52 -0
  114. package/lib/util/periods.js +98 -64
  115. package/lib/util/player_configuration.js +18 -0
  116. package/lib/util/stream_utils.js +86 -10
  117. package/lib/util/xml_utils.js +19 -0
  118. package/package.json +3 -6
  119. package/shaka-player.uncompiled.js +3 -0
  120. package/test/dash/dash_parser_content_protection_unit.js +2 -0
  121. package/test/dash/dash_parser_live_unit.js +144 -1
  122. package/test/dash/dash_parser_manifest_unit.js +37 -2
  123. package/test/dash/dash_parser_segment_base_unit.js +2 -0
  124. package/test/dash/dash_parser_segment_list_unit.js +2 -1
  125. package/test/dash/dash_parser_segment_template_unit.js +2 -1
  126. package/test/hls/hls_live_unit.js +2 -0
  127. package/test/hls/hls_parser_unit.js +76 -0
  128. package/test/media/drm_engine_unit.js +3 -3
  129. package/test/media/quality_observer_unit.js +142 -0
  130. package/test/media/region_observer_unit.js +40 -5
  131. package/test/media/region_timeline_unit.js +40 -22
  132. package/test/media/segment_reference_unit.js +16 -1
  133. package/test/media/streaming_engine_integration.js +4 -2
  134. package/test/media/streaming_engine_unit.js +147 -0
  135. package/test/net/http_plugin_unit.js +20 -15
  136. package/test/offline/storage_integration.js +108 -4
  137. package/test/player_integration.js +140 -8
  138. package/test/player_src_equals_integration.js +41 -8
  139. package/test/player_unit.js +19 -9
  140. package/test/test/assets/chapters.vtt +2 -2
  141. package/test/test/assets/chapters2.vtt +10 -0
  142. package/test/test/util/dash_parser_util.js +4 -0
  143. package/test/test/util/fake_ad.js +7 -0
  144. package/test/test/util/fake_drm_engine.js +9 -2
  145. package/test/test/util/manifest_generator.js +3 -2
  146. package/test/test/util/test_scheme.js +14 -1
  147. package/test/test/util/ttml_utils.js +81 -0
  148. package/test/test/util/util.js +10 -7
  149. package/test/text/mp4_ttml_parser_unit.js +11 -18
  150. package/test/text/ttml_text_parser_unit.js +544 -228
  151. package/test/ui/text_displayer_layout_unit.js +8 -7
  152. package/test/ui/ui_unit.js +250 -0
  153. package/test/util/cmcd_manager_unit.js +291 -0
  154. package/test/util/stream_utils_unit.js +36 -30
  155. package/third_party/closure-uri/uri.js +21 -0
  156. package/ui/airplay_button.js +1 -0
  157. package/ui/audio_language_selection.js +7 -0
  158. package/ui/cast_button.js +1 -0
  159. package/ui/context_menu.js +109 -0
  160. package/ui/controls.js +44 -7
  161. package/ui/controls.less +1 -0
  162. package/ui/enums.js +4 -0
  163. package/ui/externs/ui.js +28 -1
  164. package/ui/fast_forward_button.js +14 -5
  165. package/ui/fullscreen_button.js +1 -0
  166. package/ui/less/buttons.less +0 -4
  167. package/ui/less/containers.less +74 -0
  168. package/ui/less/overflow_menu.less +0 -10
  169. package/ui/less/tooltip.less +97 -0
  170. package/ui/locales/en.json +1 -0
  171. package/ui/locales/source.json +4 -0
  172. package/ui/localization.js +12 -36
  173. package/ui/loop_button.js +5 -0
  174. package/ui/mute_button.js +2 -1
  175. package/ui/overflow_menu.js +5 -25
  176. package/ui/pip_button.js +5 -0
  177. package/ui/play_button.js +2 -2
  178. package/ui/playback_rate_selection.js +8 -9
  179. package/ui/resolution_selection.js +7 -0
  180. package/ui/rewind_button.js +15 -6
  181. package/ui/seek_bar.js +4 -2
  182. package/ui/settings_menu.js +36 -4
  183. package/ui/small_play_button.js +1 -0
  184. package/ui/statistics_button.js +253 -0
  185. package/ui/text_selection.js +12 -4
  186. package/ui/ui.js +30 -0
  187. package/ui/volume_bar.js +1 -1
  188. package/externs/media_capabilities.js +0 -16
@@ -25,6 +25,7 @@ goog.require('shaka.net.DataUriPlugin');
25
25
  goog.require('shaka.net.NetworkingEngine');
26
26
  goog.require('shaka.util.ArrayUtils');
27
27
  goog.require('shaka.util.BufferUtils');
28
+ goog.require('shaka.util.CmcdManager');
28
29
  goog.require('shaka.util.DataViewReader');
29
30
  goog.require('shaka.util.Error');
30
31
  goog.require('shaka.util.FakeEvent');
@@ -332,7 +333,8 @@ shaka.hls.HlsParser = class {
332
333
  const segments = await this.createSegments_(
333
334
  streamInfo.verbatimMediaPlaylistUri, playlist, stream.type,
334
335
  stream.mimeType, streamInfo.mediaSequenceToStartTime, mediaVariables,
335
- streamInfo.discontinuityToMediaSequence);
336
+ streamInfo.discontinuityToMediaSequence, stream.codecs,
337
+ stream.bandwidth);
336
338
 
337
339
  stream.segmentIndex.mergeAndEvict(
338
340
  segments, this.presentationTimeline_.getSegmentAvailabilityStart());
@@ -763,7 +765,7 @@ shaka.hls.HlsParser = class {
763
765
  const videoRange = tag.getAttributeValue('VIDEO-RANGE');
764
766
 
765
767
  const streamInfos = await this.createStreamInfosForVariantTag_(tag,
766
- resolution, frameRate);
768
+ resolution, frameRate, bandwidth);
767
769
 
768
770
  if (streamInfos) {
769
771
  goog.asserts.assert(streamInfos.audio.length ||
@@ -797,10 +799,11 @@ shaka.hls.HlsParser = class {
797
799
  * @param {!shaka.hls.Tag} tag
798
800
  * @param {?string} resolution
799
801
  * @param {?string} frameRate
802
+ * @param {number} bandwidth
800
803
  * @return {!Promise.<?shaka.hls.HlsParser.StreamInfos>}
801
804
  * @private
802
805
  */
803
- async createStreamInfosForVariantTag_(tag, resolution, frameRate) {
806
+ async createStreamInfosForVariantTag_(tag, resolution, frameRate, bandwidth) {
804
807
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
805
808
  /** @type {!Array.<string>} */
806
809
  let allCodecs = this.getCodecsForVariantTag_(tag);
@@ -870,7 +873,8 @@ shaka.hls.HlsParser = class {
870
873
  let streamInfo;
871
874
  if (!ignoreStream) {
872
875
  streamInfo =
873
- await this.createStreamInfoFromVariantTag_(tag, allCodecs, type);
876
+ await this.createStreamInfoFromVariantTag_(tag, allCodecs, type,
877
+ bandwidth);
874
878
  }
875
879
  if (streamInfo) {
876
880
  res[streamInfo.stream.type] = [streamInfo];
@@ -893,15 +897,12 @@ shaka.hls.HlsParser = class {
893
897
  */
894
898
  getCodecsForVariantTag_(tag) {
895
899
  // These are the default codecs to assume if none are specified.
896
- // The video codec is H.264, with baseline profile and level 3.0.
897
- // http://blog.pearce.org.nz/2013/11/what-does-h264avc1-codecs-parameters.html
898
- // The audio codec is "low-complexity" AAC.
899
900
  const defaultCodecsArray = [];
900
901
  if (!this.config_.disableVideo) {
901
- defaultCodecsArray.push('avc1.42E01E');
902
+ defaultCodecsArray.push(this.config_.hls.defaultVideoCodec);
902
903
  }
903
904
  if (!this.config_.disableAudio) {
904
- defaultCodecsArray.push('mp4a.40.2');
905
+ defaultCodecsArray.push(this.config_.hls.defaultAudioCodec);
905
906
  }
906
907
  const defaultCodecs = defaultCodecsArray.join(',');
907
908
 
@@ -1280,6 +1281,30 @@ shaka.hls.HlsParser = class {
1280
1281
  if (this.uriToStreamInfosMap_.has(verbatimImagePlaylistUri)) {
1281
1282
  return this.uriToStreamInfosMap_.get(verbatimImagePlaylistUri);
1282
1283
  }
1284
+
1285
+ // Parse misc attributes.
1286
+ const resolution = tag.getAttributeValue('RESOLUTION');
1287
+ if (resolution) {
1288
+ // The RESOLUTION tag represents the resolution of a single thumbnail, not
1289
+ // of the entire sheet at once (like we expect in the output).
1290
+ // So multiply by the layout size.
1291
+
1292
+ const reference = streamInfo.stream.segmentIndex.get(0);
1293
+ const layout = reference.getTilesLayout();
1294
+ if (layout) {
1295
+ streamInfo.stream.width =
1296
+ Number(resolution.split('x')[0]) * Number(layout.split('x')[0]);
1297
+ streamInfo.stream.height =
1298
+ Number(resolution.split('x')[1]) * Number(layout.split('x')[1]);
1299
+ // TODO: What happens if there are multiple grids, with different
1300
+ // layout sizes, inside this image stream?
1301
+ }
1302
+ }
1303
+ const bandwidth = tag.getAttributeValue('BANDWIDTH');
1304
+ if (bandwidth) {
1305
+ streamInfo.stream.bandwidth = Number(bandwidth);
1306
+ }
1307
+
1283
1308
  this.uriToStreamInfosMap_.set(verbatimImagePlaylistUri, streamInfo);
1284
1309
  return streamInfo;
1285
1310
  }
@@ -1290,10 +1315,11 @@ shaka.hls.HlsParser = class {
1290
1315
  * @param {!shaka.hls.Tag} tag
1291
1316
  * @param {!Array.<string>} allCodecs
1292
1317
  * @param {string} type
1318
+ * @param {number} bandwidth
1293
1319
  * @return {!Promise.<?shaka.hls.HlsParser.StreamInfo>}
1294
1320
  * @private
1295
1321
  */
1296
- async createStreamInfoFromVariantTag_(tag, allCodecs, type) {
1322
+ async createStreamInfoFromVariantTag_(tag, allCodecs, type, bandwidth) {
1297
1323
  goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
1298
1324
  'Should only be called on variant tags!');
1299
1325
  const verbatimMediaPlaylistUri = this.variableSubstitution_(
@@ -1309,7 +1335,7 @@ shaka.hls.HlsParser = class {
1309
1335
  codecs, type, /* language= */ 'und', /* primary= */ false,
1310
1336
  /* name= */ null, /* channelcount= */ null, closedCaptions,
1311
1337
  /* characteristics= */ null, /* forced= */ false,
1312
- /* spatialAudio= */ false);
1338
+ /* spatialAudio= */ false, bandwidth);
1313
1339
  if (streamInfo == null) {
1314
1340
  return null;
1315
1341
  }
@@ -1336,12 +1362,13 @@ shaka.hls.HlsParser = class {
1336
1362
  * @param {?string} characteristics
1337
1363
  * @param {boolean} forced
1338
1364
  * @param {boolean} spatialAudio
1365
+ * @param {(number|undefined)} bandwidth
1339
1366
  * @return {!Promise.<?shaka.hls.HlsParser.StreamInfo>}
1340
1367
  * @private
1341
1368
  */
1342
1369
  async createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language,
1343
1370
  primary, name, channelsCount, closedCaptions, characteristics, forced,
1344
- spatialAudio) {
1371
+ spatialAudio, bandwidth = undefined) {
1345
1372
  // TODO: Refactor, too many parameters
1346
1373
  let absoluteMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
1347
1374
  this.masterPlaylistUri_, verbatimMediaPlaylistUri);
@@ -1459,7 +1486,7 @@ shaka.hls.HlsParser = class {
1459
1486
  try {
1460
1487
  segments = await this.createSegments_(verbatimMediaPlaylistUri,
1461
1488
  playlist, type, mimeType, mediaSequenceToStartTime, mediaVariables,
1462
- discontinuityToMediaSequence);
1489
+ discontinuityToMediaSequence, codecs, bandwidth);
1463
1490
  } catch (error) {
1464
1491
  if (error.code == shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM) {
1465
1492
  shaka.log.alwaysWarn('Skipping unsupported HLS stream',
@@ -1811,6 +1838,7 @@ shaka.hls.HlsParser = class {
1811
1838
  }
1812
1839
 
1813
1840
  let tilesLayout = '';
1841
+ let tileDuration = null;
1814
1842
  if (type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
1815
1843
  // By default in HLS the tilesLayout is 1x1
1816
1844
  tilesLayout = '1x1';
@@ -1818,6 +1846,10 @@ shaka.hls.HlsParser = class {
1818
1846
  shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-TILES');
1819
1847
  if (tilesTag) {
1820
1848
  tilesLayout = tilesTag.getRequiredAttrValue('LAYOUT');
1849
+ const duration = tilesTag.getAttributeValue('DURATION');
1850
+ if (duration) {
1851
+ tileDuration = Number(duration);
1852
+ }
1821
1853
  }
1822
1854
  }
1823
1855
 
@@ -1833,6 +1865,7 @@ shaka.hls.HlsParser = class {
1833
1865
  /* appendWindowEnd= */ Infinity,
1834
1866
  partialSegmentRefs,
1835
1867
  tilesLayout,
1868
+ tileDuration,
1836
1869
  );
1837
1870
  }
1838
1871
 
@@ -1887,11 +1920,14 @@ shaka.hls.HlsParser = class {
1887
1920
  * @param {!Map.<number, number>} mediaSequenceToStartTime
1888
1921
  * @param {!Map.<string, string>} variables
1889
1922
  * @param {!Map.<number, number>} discontinuityToMediaSequence
1923
+ * @param {string} codecs
1924
+ * @param {(number|undefined)} bandwidth
1890
1925
  * @return {!Promise<!Array.<!shaka.media.SegmentReference>>}
1891
1926
  * @private
1892
1927
  */
1893
1928
  async createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType,
1894
- mediaSequenceToStartTime, variables, discontinuityToMediaSequence) {
1929
+ mediaSequenceToStartTime, variables, discontinuityToMediaSequence,
1930
+ codecs, bandwidth) {
1895
1931
  /** @type {Array.<!shaka.hls.Segment>} */
1896
1932
  const hlsSegments = playlist.segments;
1897
1933
  goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
@@ -1934,7 +1970,7 @@ shaka.hls.HlsParser = class {
1934
1970
  this.playlistStartTime_ = await this.getStartTime_(
1935
1971
  verbatimMediaPlaylistUri, initSegmentRef, mimeType,
1936
1972
  position, /* isDiscontinuity= */ false,
1937
- hlsSegments[0], variables, type);
1973
+ hlsSegments[0], variables, type, codecs, bandwidth);
1938
1974
  }
1939
1975
  firstStartTime = this.playlistStartTime_;
1940
1976
  }
@@ -1987,7 +2023,8 @@ shaka.hls.HlsParser = class {
1987
2023
  // eslint-disable-next-line no-await-in-loop
1988
2024
  timestampOffset = await this.getTimestampOffset_(
1989
2025
  discontintuitySequenceNum, verbatimMediaPlaylistUri, initSegmentRef,
1990
- mimeType, position, item, variables, startTime, type);
2026
+ mimeType, position, item, variables, startTime, type, codecs,
2027
+ bandwidth);
1991
2028
  }
1992
2029
 
1993
2030
  // If the stream is low latency and the user has not configured the
@@ -2041,13 +2078,16 @@ shaka.hls.HlsParser = class {
2041
2078
  * @param {!Map.<string, string>} variables
2042
2079
  * @param {number} startTime
2043
2080
  * @param {string} type
2081
+ * @param {string} codecs
2082
+ * @param {number|undefined} bandwidth
2044
2083
  * @return {!Promise.<number>}
2045
2084
  * @throws {shaka.util.Error}
2046
2085
  * @private
2047
2086
  */
2048
2087
  async getTimestampOffset_(discontintuitySequenceNum,
2049
2088
  verbatimMediaPlaylistUri, initSegmentRef,
2050
- mimeType, mediaSequenceNumber, segment, variables, startTime, type) {
2089
+ mimeType, mediaSequenceNumber, segment, variables, startTime, type,
2090
+ codecs, bandwidth) {
2051
2091
  let timestampOffset = 0;
2052
2092
  if (this.discontinuityToTso_.has(discontintuitySequenceNum)) {
2053
2093
  timestampOffset =
@@ -2056,7 +2096,7 @@ shaka.hls.HlsParser = class {
2056
2096
  const mediaStartTime = await this.getStartTime_(
2057
2097
  verbatimMediaPlaylistUri, initSegmentRef, mimeType,
2058
2098
  mediaSequenceNumber, /* isDiscontinuity= */ true, segment,
2059
- variables, type);
2099
+ variables, type, codecs, bandwidth);
2060
2100
  timestampOffset = startTime - mediaStartTime;
2061
2101
  shaka.log.v1('Segment timestampOffset =', timestampOffset);
2062
2102
  this.discontinuityToTso_.set(
@@ -2070,10 +2110,14 @@ shaka.hls.HlsParser = class {
2070
2110
  * segment if we have to.
2071
2111
  *
2072
2112
  * @param {!shaka.media.AnySegmentReference} reference
2113
+ * @param {string} type
2114
+ * @param {string} mimeType
2115
+ * @param {string} codecs
2116
+ * @param {number|undefined} bandwidth
2073
2117
  * @return {!Promise.<shaka.extern.Response>}
2074
2118
  * @private
2075
2119
  */
2076
- async fetchStartOfSegment_(reference) {
2120
+ async fetchStartOfSegment_(reference, type, mimeType, codecs, bandwidth) {
2077
2121
  const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
2078
2122
 
2079
2123
  // Create two requests:
@@ -2087,6 +2131,20 @@ shaka.hls.HlsParser = class {
2087
2131
  reference.endByte,
2088
2132
  this.config_.retryParameters);
2089
2133
 
2134
+ // We can only add partial CMCD data here because the stream
2135
+ // and manifest objects are still being created
2136
+ this.playerInterface_.modifySegmentRequest(
2137
+ fullRequest,
2138
+ {
2139
+ type: type,
2140
+ init: reference instanceof shaka.media.InitSegmentReference,
2141
+ duration: reference.endTime - reference.startTime,
2142
+ mimeType: mimeType,
2143
+ codecs: codecs,
2144
+ bandwidth: bandwidth,
2145
+ },
2146
+ );
2147
+
2090
2148
  if (this.config_.hls.useFullSegmentsForStartTime) {
2091
2149
  return this.makeNetworkRequest_(fullRequest, requestType);
2092
2150
  }
@@ -2097,6 +2155,18 @@ shaka.hls.HlsParser = class {
2097
2155
  reference.startByte + shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_ - 1,
2098
2156
  this.config_.retryParameters);
2099
2157
 
2158
+ this.playerInterface_.modifySegmentRequest(
2159
+ partialRequest,
2160
+ {
2161
+ type: type,
2162
+ init: reference instanceof shaka.media.InitSegmentReference,
2163
+ duration: reference.endTime - reference.startTime,
2164
+ mimeType: mimeType,
2165
+ codecs: codecs,
2166
+ bandwidth: bandwidth,
2167
+ },
2168
+ );
2169
+
2100
2170
  // TODO(vaage): The need to do fall back requests is not likely to be unique
2101
2171
  // to here. It would be nice if the fallback(s) could be included into
2102
2172
  // the same abortable operation as the original request.
@@ -2144,12 +2214,14 @@ shaka.hls.HlsParser = class {
2144
2214
  * @param {!shaka.hls.Segment} segment
2145
2215
  * @param {!Map.<string, string>} variables
2146
2216
  * @param {string} type
2217
+ * @param {string} codecs
2218
+ * @param {number|undefined} bandwidth
2147
2219
  * @return {!Promise.<number>}
2148
2220
  * @private
2149
2221
  */
2150
2222
  async getStartTime_(
2151
2223
  verbatimMediaPlaylistUri, initSegmentRef, mimeType, mediaSequenceNumber,
2152
- isDiscontinuity, segment, variables, type) {
2224
+ isDiscontinuity, segment, variables, type, codecs, bandwidth) {
2153
2225
  const segmentRef = this.createSegmentReference_(
2154
2226
  initSegmentRef,
2155
2227
  /* previousReference= */ null,
@@ -2217,10 +2289,22 @@ shaka.hls.HlsParser = class {
2217
2289
  if (mimeType == 'video/mp4' || mimeType == 'audio/mp4') {
2218
2290
  // We also need the init segment to get the correct timescale. But if the
2219
2291
  // stream is self-initializing, use the same response for both.
2220
- const fetches = [this.fetchStartOfSegment_(segmentRef)];
2292
+ const fetches = [this.fetchStartOfSegment_(
2293
+ segmentRef,
2294
+ type,
2295
+ mimeType,
2296
+ codecs,
2297
+ bandwidth,
2298
+ )];
2221
2299
 
2222
2300
  if (initSegmentRef) {
2223
- fetches.push(this.fetchStartOfSegment_(initSegmentRef));
2301
+ fetches.push(this.fetchStartOfSegment_(
2302
+ initSegmentRef,
2303
+ type,
2304
+ mimeType,
2305
+ codecs,
2306
+ bandwidth,
2307
+ ));
2224
2308
  }
2225
2309
 
2226
2310
  const responses = await Promise.all(fetches);
@@ -2236,7 +2320,13 @@ shaka.hls.HlsParser = class {
2236
2320
  }
2237
2321
 
2238
2322
  if (mimeType == 'video/mp2t') {
2239
- const response = await this.fetchStartOfSegment_(segmentRef);
2323
+ const response = await this.fetchStartOfSegment_(
2324
+ segmentRef,
2325
+ type,
2326
+ mimeType,
2327
+ codecs,
2328
+ bandwidth,
2329
+ );
2240
2330
  goog.asserts.assert(response.data, 'Should have a response body!');
2241
2331
  return this.getStartTimeFromTsSegment_(
2242
2332
  verbatimMediaPlaylistUri, response.uri, response.data);
@@ -2614,6 +2704,9 @@ shaka.hls.HlsParser = class {
2614
2704
  const request = shaka.net.NetworkingEngine.makeRequest(
2615
2705
  [absoluteUri], this.config_.retryParameters);
2616
2706
 
2707
+ const format = shaka.util.CmcdManager.StreamingFormat.HLS;
2708
+ this.playerInterface_.modifyManifestRequest(request, {format: format});
2709
+
2617
2710
  return this.makeNetworkRequest_(request, requestType);
2618
2711
  }
2619
2712
 
@@ -277,6 +277,7 @@ shaka.hls.ManifestTextParser = class {
277
277
  const attrValue = blockAttrs[2] || blockAttrs[3];
278
278
  const attribute = new shaka.hls.Attribute(attrName, attrValue);
279
279
  attributes.push(attribute);
280
+ parser.skipWhitespace();
280
281
  }
281
282
  }
282
283
 
@@ -0,0 +1,274 @@
1
+ /*! @license
2
+ * Shaka Player
3
+ * Copyright 2016 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ goog.provide('shaka.media.QualityObserver');
8
+
9
+ goog.require('shaka.media.IPlayheadObserver');
10
+ goog.require('shaka.log');
11
+ goog.require('shaka.util.FakeEvent');
12
+ goog.require('shaka.util.FakeEventTarget');
13
+
14
+ /**
15
+ * Monitors the quality of content being appended to the source buffers and
16
+ * fires 'qualitychange' events when the media quality at the playhead changes.
17
+ *
18
+ * @implements {shaka.media.IPlayheadObserver}
19
+ * @final
20
+ */
21
+ shaka.media.QualityObserver = class extends shaka.util.FakeEventTarget {
22
+ /**
23
+ * Creates a new QualityObserver.
24
+ *
25
+ * @param {!function():!shaka.extern.BufferedInfo} getBufferedInfo
26
+ * Buffered info is needed to purge QualityChanges that are no
27
+ * longer relevant.
28
+ */
29
+ constructor(getBufferedInfo) {
30
+ super();
31
+
32
+ /**
33
+ * @private {!Map.<string,!shaka.media.QualityObserver.ContentTypeState>}
34
+ */
35
+ this.contentTypeStates_ = new Map();
36
+
37
+ /** @private function():!shaka.extern.BufferedInfo */
38
+ this.getBufferedInfo_ = getBufferedInfo;
39
+ }
40
+
41
+ /** @override */
42
+ release() {
43
+ this.contentTypeStates_.clear();
44
+ super.release();
45
+ }
46
+
47
+ /**
48
+ * Get the ContenTypeState for a contentType, creating a new
49
+ * one if necessary.
50
+ *
51
+ * @param {!string} contentType
52
+ * The contend type e.g. "video" or "audio".
53
+ * @return {!shaka.media.QualityObserver.ContentTypeState}
54
+ * @private
55
+ */
56
+ getContentTypeState_(contentType) {
57
+ let contentTypeState = this.contentTypeStates_.get(contentType);
58
+ if (!contentTypeState) {
59
+ contentTypeState = {
60
+ qualityChangePositions: [],
61
+ currentQuality: null,
62
+ contentType: contentType,
63
+ };
64
+ this.contentTypeStates_.set(contentType, contentTypeState);
65
+ }
66
+ return contentTypeState;
67
+ }
68
+
69
+ /**
70
+ * Adds a QualityChangePosition for the contentType identified by
71
+ * the mediaQuality.contentType.
72
+ *
73
+ * @param {!shaka.extern.MediaQualityInfo} mediaQuality
74
+ * @param {!number} position
75
+ * Position in seconds of the quality change.
76
+ */
77
+ addMediaQualityChange(mediaQuality, position) {
78
+ const contentTypeState =
79
+ this.getContentTypeState_(mediaQuality.contentType);
80
+
81
+ // Remove unneeded QualityChangePosition(s) before adding the new one
82
+ this.purgeQualityChangePositions_(contentTypeState);
83
+
84
+ const newChangePosition = {
85
+ mediaQuality: mediaQuality,
86
+ position: position,
87
+ };
88
+
89
+ const changePositions = contentTypeState.qualityChangePositions;
90
+ const insertBeforeIndex = changePositions.findIndex(
91
+ (qualityChange) => (qualityChange.position >= position));
92
+
93
+ if (insertBeforeIndex >= 0) {
94
+ const duplicatePositions =
95
+ (changePositions[insertBeforeIndex].position == position) ? 1 : 0;
96
+ changePositions.splice(
97
+ insertBeforeIndex, duplicatePositions, newChangePosition);
98
+ } else {
99
+ changePositions.push(newChangePosition);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Determines the media quality at a specific position in the source buffer.
105
+ *
106
+ * @param {!number} position
107
+ * Position in seconds
108
+ * @param {!shaka.media.QualityObserver.ContentTypeState} contentTypeState
109
+ * @return {?shaka.extern.MediaQualityInfo}
110
+ * @private
111
+ */
112
+ static getMediaQualityAtPosition_(position, contentTypeState) {
113
+ // The qualityChangePositions must be ordered by position ascending
114
+ // Find the last QualityChangePosition prior to the position
115
+ const changePositions = contentTypeState.qualityChangePositions;
116
+ for (let i = changePositions.length - 1; i >= 0; i--) {
117
+ const qualityChange = changePositions[i];
118
+ if (qualityChange.position <= position) {
119
+ return qualityChange.mediaQuality;
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+
125
+ /**
126
+ * Determines if two MediaQualityInfo objects are the same or not.
127
+ *
128
+ * @param {?shaka.extern.MediaQualityInfo} mq1
129
+ * @param {?shaka.extern.MediaQualityInfo} mq2
130
+ * @return {boolean}
131
+ * @private
132
+ */
133
+ static mediaQualitiesAreTheSame_(mq1, mq2) {
134
+ if (mq1 === mq2) {
135
+ return true;
136
+ }
137
+ if (!mq1 || !mq2) {
138
+ return false;
139
+ }
140
+ return (mq1.bandwidth == mq2.bandwidth) &&
141
+ (mq1.audioSamplingRate == mq2.audioSamplingRate) &&
142
+ (mq1.codecs == mq2.codecs) &&
143
+ (mq1.contentType == mq2.contentType) &&
144
+ (mq1.frameRate == mq2.frameRate) &&
145
+ (mq1.height == mq2.height) &&
146
+ (mq1.mimeType == mq2.mimeType) &&
147
+ (mq1.channelsCount == mq2.channelsCount) &&
148
+ (mq1.pixelAspectRatio == mq2.pixelAspectRatio) &&
149
+ (mq1.width == mq2.width);
150
+ }
151
+
152
+ /** @override */
153
+ poll(positionInSeconds, wasSeeking) {
154
+ for (const contentTypeState of this.contentTypeStates_.values()) {
155
+ const qualityAtPosition =
156
+ shaka.media.QualityObserver.getMediaQualityAtPosition_(
157
+ positionInSeconds, contentTypeState);
158
+ if (qualityAtPosition &&
159
+ !shaka.media.QualityObserver.mediaQualitiesAreTheSame_(
160
+ contentTypeState.currentQuality, qualityAtPosition)) {
161
+ if (this.positionIsBuffered_(
162
+ positionInSeconds, qualityAtPosition.contentType)) {
163
+ contentTypeState.currentQuality = qualityAtPosition;
164
+
165
+ shaka.log.debug('Media quality changed at position ' +
166
+ positionInSeconds + ' ' + JSON.stringify(qualityAtPosition));
167
+
168
+ const event = new shaka.util.FakeEvent('qualitychange', new Map([
169
+ ['quality', qualityAtPosition],
170
+ ['position', positionInSeconds],
171
+ ]));
172
+ this.dispatchEvent(event);
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Determine if a position is buffered for a given content type.
180
+ *
181
+ * @param {!number} position
182
+ * @param {!string} contentType
183
+ * @private
184
+ */
185
+ positionIsBuffered_(position, contentType) {
186
+ const bufferedInfo = this.getBufferedInfo_();
187
+ const bufferedRanges = bufferedInfo[contentType];
188
+ if (bufferedRanges && bufferedRanges.length > 0) {
189
+ const bufferStart = bufferedRanges[0].start;
190
+ const bufferEnd = bufferedRanges[bufferedRanges.length - 1].end;
191
+ if (position >= bufferStart && position < bufferEnd) {
192
+ return true;
193
+ }
194
+ }
195
+ return false;
196
+ }
197
+
198
+ /**
199
+ * Removes the QualityChangePosition(s) that are not relevant to the buffered
200
+ * content of the specified contentType. Note that this function is
201
+ * invoked just before adding the quality change info associated with
202
+ * the next media segment to be appended.
203
+ *
204
+ * @param {!shaka.media.QualityObserver.ContentTypeState} contentTypeState
205
+ * @private
206
+ */
207
+ purgeQualityChangePositions_(contentTypeState) {
208
+ const bufferedInfo = this.getBufferedInfo_();
209
+ const bufferedRanges = bufferedInfo[contentTypeState.contentType];
210
+
211
+ if (bufferedRanges && bufferedRanges.length > 0) {
212
+ const bufferStart = bufferedRanges[0].start;
213
+ const bufferEnd = bufferedRanges[bufferedRanges.length - 1].end;
214
+ const oldChangePositions = contentTypeState.qualityChangePositions;
215
+ contentTypeState.qualityChangePositions =
216
+ oldChangePositions.filter(
217
+ (qualityChange, index) => {
218
+ // Remove all but last quality change before bufferStart.
219
+ if ((qualityChange.position <= bufferStart) &&
220
+ (index + 1 < oldChangePositions.length) &&
221
+ (oldChangePositions[index + 1].position <= bufferStart)) {
222
+ return false;
223
+ }
224
+ // Remove all quality changes after bufferEnd.
225
+ if (qualityChange.position >= bufferEnd) {
226
+ return false;
227
+ }
228
+ return true;
229
+ });
230
+ } else {
231
+ // Nothing is buffered; so remove all quality changes.
232
+ contentTypeState.qualityChangePositions = [];
233
+ }
234
+ }
235
+ };
236
+
237
+ /**
238
+ * @typedef {{
239
+ * mediaQuality: !shaka.extern.MediaQualityInfo,
240
+ * position: !number
241
+ * }}
242
+ *
243
+ * @description
244
+ * Identifies the position of a media quality change in the
245
+ * source buffer.
246
+ *
247
+ * @property {shaka.extern.MediaQualityInfo} !mediaQuality
248
+ * The new media quality for content after position in the source buffer.
249
+ * @property {number} !position
250
+ * A position in seconds in the source buffer
251
+ */
252
+ shaka.media.QualityObserver.QualityChangePosition;
253
+
254
+ /**
255
+ * @typedef {{
256
+ * qualityChangePositions:
257
+ * !Array.<shaka.media.QualityObserver.QualityChangePosition>,
258
+ * currentQuality: ?shaka.extern.MediaQualityInfo,
259
+ * contentType: !string
260
+ * }}
261
+ *
262
+ * @description
263
+ * Contains media quality information for a specific content type
264
+ * e.g video or audio.
265
+ *
266
+ * @property {!Array.<shaka.media.QualityObserver.QualityChangePosition>}
267
+ * qualityChangePositions
268
+ * Quality changes ordered by position ascending.
269
+ * @property {?shaka.media.MediaQualityInfo} currentMediaQuality
270
+ * The media quality at the playhead position.
271
+ * @property {string} contentType
272
+ * The contentType e.g. 'video' or 'audio'
273
+ */
274
+ shaka.media.QualityObserver.ContentTypeState;