shaka-player 4.9.13 → 4.10.1

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 (91) hide show
  1. package/README.md +4 -0
  2. package/dist/controls.css +2 -2
  3. package/dist/controls.css.map +1 -1
  4. package/dist/deps.js +24 -19
  5. package/dist/locales.js +46 -35
  6. package/dist/shaka-player.compiled.d.ts +291 -12
  7. package/dist/shaka-player.compiled.debug.d.ts +291 -12
  8. package/dist/shaka-player.compiled.debug.externs.js +443 -6
  9. package/dist/shaka-player.compiled.debug.js +651 -556
  10. package/dist/shaka-player.compiled.debug.map +4 -4
  11. package/dist/shaka-player.compiled.externs.js +443 -6
  12. package/dist/shaka-player.compiled.js +1325 -1270
  13. package/dist/shaka-player.compiled.map +4 -4
  14. package/dist/shaka-player.ui.d.ts +306 -13
  15. package/dist/shaka-player.ui.debug.d.ts +306 -13
  16. package/dist/shaka-player.ui.debug.externs.js +477 -18
  17. package/dist/shaka-player.ui.debug.js +799 -681
  18. package/dist/shaka-player.ui.debug.map +4 -4
  19. package/dist/shaka-player.ui.externs.js +477 -18
  20. package/dist/shaka-player.ui.js +1600 -1533
  21. package/dist/shaka-player.ui.map +4 -4
  22. package/externs/shaka/ads.js +21 -1
  23. package/externs/shaka/manifest.js +20 -1
  24. package/externs/shaka/manifest_parser.js +18 -1
  25. package/externs/shaka/player.js +164 -50
  26. package/lib/ads/ad_manager.js +225 -10
  27. package/lib/ads/ad_utils.js +323 -0
  28. package/lib/ads/ads_stats.js +24 -0
  29. package/lib/ads/client_side_ad_manager.js +42 -34
  30. package/lib/ads/interstitial_ad.js +292 -0
  31. package/lib/ads/interstitial_ad_manager.js +772 -0
  32. package/lib/ads/media_tailor_ad_manager.js +19 -22
  33. package/lib/ads/server_side_ad_manager.js +12 -15
  34. package/lib/cast/cast_utils.js +1 -0
  35. package/lib/cea/cea_utils.js +12 -0
  36. package/lib/cea/mp4_cea_parser.js +17 -0
  37. package/lib/dash/dash_parser.js +81 -28
  38. package/lib/dash/segment_base.js +3 -0
  39. package/lib/hls/hls_parser.js +429 -121
  40. package/lib/hls/manifest_text_parser.js +1 -0
  41. package/lib/media/adaptation_set_criteria.js +31 -1
  42. package/lib/media/drm_engine.js +16 -2
  43. package/lib/media/manifest_filterer.js +7 -5
  44. package/lib/media/preload_manager.js +8 -10
  45. package/lib/media/presentation_timeline.js +37 -5
  46. package/lib/media/quality_observer.js +68 -2
  47. package/lib/media/segment_reference.js +1 -1
  48. package/lib/media/segment_utils.js +33 -1
  49. package/lib/media/streaming_engine.js +120 -43
  50. package/lib/mss/mss_parser.js +13 -2
  51. package/lib/net/networking_engine.js +1 -0
  52. package/lib/offline/manifest_converter.js +3 -0
  53. package/lib/offline/offline_manifest_parser.js +5 -0
  54. package/lib/offline/storage.js +2 -0
  55. package/lib/player.js +498 -165
  56. package/lib/util/fake_event.js +1 -0
  57. package/lib/util/manifest_parser_utils.js +9 -2
  58. package/lib/util/mp4_box_parsers.js +32 -0
  59. package/lib/util/player_configuration.js +21 -9
  60. package/lib/util/stats.js +43 -0
  61. package/package.json +1 -1
  62. package/shaka-player.uncompiled.js +2 -0
  63. package/ui/ad_counter.js +3 -3
  64. package/ui/ad_position.js +3 -3
  65. package/ui/ad_statistics_button.js +237 -0
  66. package/ui/audio_language_selection.js +8 -4
  67. package/ui/controls.js +6 -3
  68. package/ui/element.js +3 -3
  69. package/ui/enums.js +1 -0
  70. package/ui/externs/ui.js +3 -0
  71. package/ui/language_utils.js +48 -3
  72. package/ui/less/containers.less +32 -0
  73. package/ui/less/thumbnails.less +13 -0
  74. package/ui/locales/en.json +2 -0
  75. package/ui/locales/es-419.json +2 -0
  76. package/ui/locales/es.json +3 -0
  77. package/ui/locales/fr.json +1 -0
  78. package/ui/locales/pl.json +1 -0
  79. package/ui/locales/pt-BR.json +1 -0
  80. package/ui/locales/pt-PT.json +1 -0
  81. package/ui/locales/source.json +8 -0
  82. package/ui/mute_button.js +4 -4
  83. package/ui/overflow_menu.js +3 -3
  84. package/ui/play_button.js +5 -7
  85. package/ui/presentation_time.js +3 -3
  86. package/ui/save_video_frame_button.js +196 -0
  87. package/ui/seek_bar.js +75 -20
  88. package/ui/skip_ad_button.js +4 -4
  89. package/ui/statistics_button.js +14 -2
  90. package/ui/ui.js +15 -0
  91. package/ui/volume_bar.js +4 -4
@@ -19,6 +19,7 @@ goog.require('shaka.log');
19
19
  goog.require('shaka.media.InitSegmentReference');
20
20
  goog.require('shaka.media.ManifestParser');
21
21
  goog.require('shaka.media.PresentationTimeline');
22
+ goog.require('shaka.media.QualityObserver');
22
23
  goog.require('shaka.media.SegmentIndex');
23
24
  goog.require('shaka.media.SegmentReference');
24
25
  goog.require('shaka.net.DataUriPlugin');
@@ -28,6 +29,7 @@ goog.require('shaka.util.BufferUtils');
28
29
  goog.require('shaka.util.DrmUtils');
29
30
  goog.require('shaka.util.ContentSteeringManager');
30
31
  goog.require('shaka.util.Error');
32
+ goog.require('shaka.util.EventManager');
31
33
  goog.require('shaka.util.FakeEvent');
32
34
  goog.require('shaka.util.LanguageUtils');
33
35
  goog.require('shaka.util.ManifestParserUtils');
@@ -37,6 +39,7 @@ goog.require('shaka.util.OperationManager');
37
39
  goog.require('shaka.util.Pssh');
38
40
  goog.require('shaka.media.SegmentUtils');
39
41
  goog.require('shaka.util.Timer');
42
+ goog.require('shaka.util.TsParser');
40
43
  goog.require('shaka.util.TXml');
41
44
  goog.require('shaka.util.Platform');
42
45
  goog.require('shaka.util.Uint8ArrayUtils');
@@ -172,6 +175,14 @@ shaka.hls.HlsParser = class {
172
175
  * @private {shaka.util.Timer}
173
176
  */
174
177
  this.updatePlaylistTimer_ = new shaka.util.Timer(() => {
178
+ if (this.mediaElement_ && !this.config_.continueLoadingWhenPaused) {
179
+ this.eventManager_.unlisten(this.mediaElement_, 'timeupdate');
180
+ if (this.mediaElement_.paused) {
181
+ this.eventManager_.listenOnce(
182
+ this.mediaElement_, 'timeupdate', () => this.onUpdate_());
183
+ return;
184
+ }
185
+ }
175
186
  this.onUpdate_();
176
187
  });
177
188
 
@@ -247,6 +258,15 @@ shaka.hls.HlsParser = class {
247
258
 
248
259
  /** @private {boolean} */
249
260
  this.needsClosedCaptionsDetection_ = true;
261
+
262
+ /** @private {Set.<string>} */
263
+ this.dateRangeIdsEmitted_ = new Set();
264
+
265
+ /** @private {shaka.util.EventManager} */
266
+ this.eventManager_ = new shaka.util.EventManager();
267
+
268
+ /** @private {HTMLMediaElement} */
269
+ this.mediaElement_ = null;
250
270
  }
251
271
 
252
272
 
@@ -316,11 +336,17 @@ shaka.hls.HlsParser = class {
316
336
  this.aesKeyMap_.clear();
317
337
  this.identityKeyMap_.clear();
318
338
  this.identityKidMap_.clear();
339
+ this.dateRangeIdsEmitted_.clear();
319
340
 
320
341
  if (this.contentSteeringManager_) {
321
342
  this.contentSteeringManager_.destroy();
322
343
  }
323
344
 
345
+ if (this.eventManager_) {
346
+ this.eventManager_.release();
347
+ this.eventManager_ = null;
348
+ }
349
+
324
350
  return Promise.all(pending);
325
351
  }
326
352
 
@@ -339,6 +365,7 @@ shaka.hls.HlsParser = class {
339
365
 
340
366
  // This is necessary to calculate correctly the update time.
341
367
  this.lastTargetDuration_ = Infinity;
368
+ this.manifest_.gapCount = 0;
342
369
 
343
370
  // Only update active streams.
344
371
  const activeStreamInfos = streamInfos.filter((s) => s.stream.segmentIndex);
@@ -368,6 +395,15 @@ shaka.hls.HlsParser = class {
368
395
  if (stillLive) {
369
396
  this.determineDuration_();
370
397
  }
398
+ // Check if any playlist does not have the first reference (due to a
399
+ // problem in the live encoder for example), and disable the stream if
400
+ // necessary.
401
+ for (const streamInfo of activeStreamInfos) {
402
+ if (streamInfo.stream.segmentIndex &&
403
+ !streamInfo.stream.segmentIndex.earliestReference()) {
404
+ this.playerInterface_.disableStream(streamInfo.stream);
405
+ }
406
+ }
371
407
  }
372
408
 
373
409
  /**
@@ -418,8 +454,16 @@ shaka.hls.HlsParser = class {
418
454
  }
419
455
  manifestUris.push(uriObj.toString());
420
456
  }
421
- const response =
422
- await this.requestManifest_(manifestUris, /* isPlaylist= */ true);
457
+ let response;
458
+ try {
459
+ response =
460
+ await this.requestManifest_(manifestUris, /* isPlaylist= */ true);
461
+ } catch (e) {
462
+ if (this.playerInterface_) {
463
+ this.playerInterface_.disableStream(streamInfo.stream);
464
+ }
465
+ throw e;
466
+ }
423
467
  if (!streamInfo.stream.segmentIndex) {
424
468
  // The stream was closed since the update was first requested.
425
469
  return;
@@ -472,6 +516,14 @@ shaka.hls.HlsParser = class {
472
516
  stream.bandwidth = bandwidth;
473
517
  }
474
518
 
519
+ const qualityInfo =
520
+ shaka.media.QualityObserver.createQualityInfo(stream);
521
+ for (const segment of segments) {
522
+ if (segment.initSegmentReference) {
523
+ segment.initSegmentReference.mediaQuality = qualityInfo;
524
+ }
525
+ }
526
+
475
527
  stream.segmentIndex.mergeAndEvict(
476
528
  segments, this.presentationTimeline_.getSegmentAvailabilityStart());
477
529
  if (segments.length) {
@@ -490,14 +542,14 @@ shaka.hls.HlsParser = class {
490
542
  stream.segmentIndex.evict(playlistStartTime);
491
543
  }
492
544
  const oldSegment = stream.segmentIndex.earliestReference();
493
- goog.asserts.assert(oldSegment, 'Should have segments!');
494
-
495
- streamInfo.minTimestamp = oldSegment.startTime;
545
+ if (oldSegment) {
546
+ streamInfo.minTimestamp = oldSegment.startTime;
496
547
 
497
- const newestSegment = segments[segments.length - 1];
498
- goog.asserts.assert(newestSegment, 'Should have segments!');
548
+ const newestSegment = segments[segments.length - 1];
549
+ goog.asserts.assert(newestSegment, 'Should have segments!');
499
550
 
500
- streamInfo.maxTimestamp = newestSegment.endTime;
551
+ streamInfo.maxTimestamp = newestSegment.endTime;
552
+ }
501
553
 
502
554
  // Once the last segment has been added to the playlist,
503
555
  // #EXT-X-ENDLIST tag will be appended.
@@ -512,6 +564,9 @@ shaka.hls.HlsParser = class {
512
564
  }
513
565
 
514
566
  this.determineLastTargetDuration_(playlist);
567
+
568
+ this.processDateRangeTags_(
569
+ playlist.tags, stream.type, mediaVariables, streamInfo.getUris);
515
570
  }
516
571
 
517
572
 
@@ -541,6 +596,14 @@ shaka.hls.HlsParser = class {
541
596
  }
542
597
  }
543
598
 
599
+ /**
600
+ * @override
601
+ * @exportInterface
602
+ */
603
+ setMediaElement(mediaElement) {
604
+ this.mediaElement_ = mediaElement;
605
+ }
606
+
544
607
  /**
545
608
  * Align the streams by sequence number by dropping early segments. Then
546
609
  * offset the streams to begin at presentation time 0.
@@ -728,6 +791,7 @@ shaka.hls.HlsParser = class {
728
791
  */
729
792
  async parseManifest_(data, uri) {
730
793
  const Utils = shaka.hls.Utils;
794
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
731
795
 
732
796
  goog.asserts.assert(this.masterPlaylistUri_,
733
797
  'Master playlist URI must be set before calling parseManifest_!');
@@ -760,6 +824,12 @@ shaka.hls.HlsParser = class {
760
824
  return [uri];
761
825
  };
762
826
 
827
+ /** @type {?string} */
828
+ let mediaPlaylistType = null;
829
+
830
+ /** @type {!Map.<string, string>} */
831
+ let mediaVariables = new Map();
832
+
763
833
  // Parsing a media playlist results in a single-variant stream.
764
834
  if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
765
835
  this.needsClosedCaptionsDetection_ = false;
@@ -768,29 +838,19 @@ shaka.hls.HlsParser = class {
768
838
  const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
769
839
  'EXT-X-DEFINE');
770
840
 
771
- const mediaVariables =
772
- this.parseMediaVariables_(variablesTags, this.masterPlaylistUri_);
773
-
774
- // Get necessary info for this stream. These are things we would normally
775
- // find from the master playlist (e.g. from values on EXT-X-MEDIA tags).
776
- const basicInfo = await this.getMediaPlaylistBasicInfo_(
777
- playlist, getUris, mediaVariables);
778
- const type = basicInfo.type;
779
- const mimeType = basicInfo.mimeType;
780
- const codecs = basicInfo.codecs;
781
- const languageValue = basicInfo.language;
782
- const height = basicInfo.height;
783
- const width = basicInfo.width;
784
- const channelsCount = basicInfo.channelCount;
785
- const sampleRate = basicInfo.sampleRate;
786
- const closedCaptions = basicInfo.closedCaptions;
787
- const videoRange = basicInfo.videoRange;
788
- const colorGamut = basicInfo.colorGamut;
789
-
790
- // Some values we cannot figure out, and aren't important enough to ask
791
- // the user to provide through config values. A lot of these are only
792
- // relevant to ABR, which isn't necessary if there's only one variant.
793
- // So these unknowns should be set to false or null, largely.
841
+ mediaVariables = this.parseMediaVariables_(
842
+ variablesTags, this.masterPlaylistUri_);
843
+
844
+ // By default we assume it is video, but in a later step the correct type
845
+ // is obtained.
846
+ mediaPlaylistType = ContentType.VIDEO;
847
+
848
+ // These values can be obtained later so these default values are good.
849
+ const codecs = '';
850
+ const languageValue = '';
851
+ const channelsCount = null;
852
+ const sampleRate = null;
853
+ const closedCaptions = new Map();
794
854
  const spatialAudio = false;
795
855
  const characteristics = null;
796
856
  const forced = false; // Only relevant for text.
@@ -800,24 +860,32 @@ shaka.hls.HlsParser = class {
800
860
  // Make the stream info, with those values.
801
861
  const streamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
802
862
  this.globalId_++, mediaVariables, playlist, getUris, uri, codecs,
803
- type, languageValue, primary, name, channelsCount, closedCaptions,
804
- characteristics, forced, sampleRate, spatialAudio, mimeType);
863
+ mediaPlaylistType, languageValue, primary, name, channelsCount,
864
+ closedCaptions, characteristics, forced, sampleRate, spatialAudio);
805
865
  this.uriToStreamInfosMap_.set(uri, streamInfo);
806
866
 
807
- if (type == 'video') {
808
- this.addVideoAttributes_(streamInfo.stream, width, height,
809
- /* frameRate= */ null, videoRange, /* videoLayout= */ null,
810
- colorGamut);
867
+
868
+ if (streamInfo.stream) {
869
+ const qualityInfo =
870
+ shaka.media.QualityObserver.createQualityInfo(streamInfo.stream);
871
+ streamInfo.stream.segmentIndex.forEachTopLevelReference(
872
+ (reference) => {
873
+ if (reference.initSegmentReference) {
874
+ reference.initSegmentReference.mediaQuality = qualityInfo;
875
+ }
876
+ });
811
877
  }
812
878
 
879
+ mediaPlaylistType = streamInfo.stream.type;
880
+
813
881
  // Wrap the stream from that stream info with a variant.
814
882
  variants.push({
815
883
  id: 0,
816
884
  language: this.getLanguage_(languageValue),
817
885
  disabledUntilTime: 0,
818
886
  primary: true,
819
- audio: type == 'audio' ? streamInfo.stream : null,
820
- video: type == 'video' ? streamInfo.stream : null,
887
+ audio: mediaPlaylistType == 'audio' ? streamInfo.stream : null,
888
+ video: mediaPlaylistType == 'video' ? streamInfo.stream : null,
821
889
  bandwidth: streamInfo.stream.bandwidth || 0,
822
890
  allowedByApplication: true,
823
891
  allowedByKeySystem: true,
@@ -880,6 +948,11 @@ shaka.hls.HlsParser = class {
880
948
  const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
881
949
  this.finalizeStreams_(streamInfos);
882
950
  this.determineDuration_();
951
+
952
+ goog.asserts.assert(mediaPlaylistType,
953
+ 'mediaPlaylistType should be non-null');
954
+ this.processDateRangeTags_(
955
+ playlist.tags, mediaPlaylistType, mediaVariables, getUris);
883
956
  }
884
957
 
885
958
  this.manifest_ = {
@@ -895,6 +968,9 @@ shaka.hls.HlsParser = class {
895
968
  type: shaka.media.ManifestParser.HLS,
896
969
  serviceDescription: null,
897
970
  nextUrl: null,
971
+ periodCount: 1,
972
+ gapCount: 0,
973
+ isLowLatency: false,
898
974
  };
899
975
 
900
976
  // If there is no 'CODECS' attribute in the manifest and codec guessing is
@@ -931,26 +1007,21 @@ shaka.hls.HlsParser = class {
931
1007
  }
932
1008
 
933
1009
  /**
934
- * @param {shaka.hls.Playlist} playlist
935
- * @param {function():!Array.<string>} getUris
936
- * @param {?Map.<string, string>=} variables
1010
+ * @param {!Array.<!shaka.media.SegmentReference>} segments
937
1011
  * @return {!Promise.<shaka.media.SegmentUtils.BasicInfo>}
938
1012
  * @private
939
1013
  */
940
- async getMediaPlaylistBasicInfo_(playlist, getUris, variables) {
1014
+ async getBasicInfoFromSegments_(segments) {
941
1015
  const HlsParser = shaka.hls.HlsParser;
942
1016
  const defaultBasicInfo = shaka.media.SegmentUtils.getBasicInfoFromMimeType(
943
1017
  this.config_.hls.mediaPlaylistFullMimeType);
944
- if (!playlist.segments.length) {
1018
+ if (!segments.length) {
945
1019
  return defaultBasicInfo;
946
1020
  }
947
- const firstSegment = playlist.segments[0];
948
- const firstSegmentUris = shaka.hls.Utils.constructSegmentUris(
949
- getUris(),
950
- firstSegment.verbatimSegmentUri,
951
- variables);
952
- const firstSegmentUri = firstSegmentUris[0];
953
- const parsedUri = new goog.Uri(firstSegmentUri);
1021
+ const segment = this.getAvailableSegment_(segments);
1022
+ const segmentUris = segment.getUris();
1023
+ const segmentUri = segmentUris[0];
1024
+ const parsedUri = new goog.Uri(segmentUri);
954
1025
  const extension = parsedUri.getPath().split('.').pop();
955
1026
  const rawMimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension];
956
1027
  if (rawMimeType) {
@@ -961,33 +1032,28 @@ shaka.hls.HlsParser = class {
961
1032
  const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
962
1033
 
963
1034
  let initData = null;
964
- const initSegmentRef = this.getInitSegmentReference_(
965
- playlist, firstSegment.tags, getUris, variables);
966
- this.mapTagToInitSegmentRefMap_.clear();
1035
+ let initMimeType = null;
1036
+ const initSegmentRef = segment.initSegmentReference;
967
1037
  if (initSegmentRef) {
968
1038
  const initSegmentRequest = shaka.util.Networking.createSegmentRequest(
969
- initSegmentRef.getUris(),
970
- initSegmentRef.getStartByte(),
971
- initSegmentRef.getEndByte(),
972
- this.config_.retryParameters);
1039
+ initSegmentRef.getUris(), initSegmentRef.getStartByte(),
1040
+ initSegmentRef.getEndByte(), this.config_.retryParameters);
973
1041
  const initType =
974
1042
  shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT;
975
1043
  const initResponse = await this.makeNetworkRequest_(
976
1044
  initSegmentRequest, requestType, {type: initType});
977
1045
  initData = initResponse.data;
978
- }
979
1046
 
980
- let startByte = 0;
981
- let endByte = null;
982
- const byterangeTag = shaka.hls.Utils.getFirstTagWithName(
983
- firstSegment.tags, 'EXT-X-BYTERANGE');
984
- if (byterangeTag) {
985
- [startByte, endByte] = this.parseByteRange_(
986
- /* previousReference= */ null, byterangeTag.value);
1047
+ initMimeType = initResponse.headers['content-type'];
1048
+ if (initMimeType) {
1049
+ // Split the MIME type in case the server sent additional parameters.
1050
+ initMimeType = initMimeType.split(';')[0].toLowerCase();
1051
+ }
987
1052
  }
988
1053
 
989
1054
  const segmentRequest = shaka.util.Networking.createSegmentRequest(
990
- firstSegmentUris, startByte, endByte, this.config_.retryParameters);
1055
+ segment.getUris(), segment.getStartByte(), segment.getEndByte(),
1056
+ this.config_.retryParameters);
991
1057
  const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
992
1058
  const response = await this.makeNetworkRequest_(
993
1059
  segmentRequest, requestType, {type});
@@ -998,23 +1064,49 @@ shaka.hls.HlsParser = class {
998
1064
  contentMimeType = contentMimeType.split(';')[0].toLowerCase();
999
1065
  }
1000
1066
 
1001
- if (extension == 'ts' || contentMimeType == 'video/mp2t') {
1067
+ const validMp4Extensions = [
1068
+ 'mp4',
1069
+ 'mp4a',
1070
+ 'm4s',
1071
+ 'm4i',
1072
+ 'm4a',
1073
+ 'm4f',
1074
+ 'cmfa',
1075
+ 'mp4v',
1076
+ 'm4v',
1077
+ 'cmfv',
1078
+ 'fmp4',
1079
+ ];
1080
+ const validMp4MimeType = [
1081
+ 'audio/mp4',
1082
+ 'video/mp4',
1083
+ 'video/iso.segment',
1084
+ ];
1085
+
1086
+ if (shaka.util.TsParser.probe(
1087
+ shaka.util.BufferUtils.toUint8(response.data))) {
1002
1088
  const basicInfo =
1003
1089
  shaka.media.SegmentUtils.getBasicInfoFromTs(response.data);
1004
1090
  if (basicInfo) {
1005
1091
  return basicInfo;
1006
1092
  }
1007
- } else if (extension == 'mp4' || extension == 'cmfv' ||
1008
- extension == 'm4s' || extension == 'fmp4' ||
1009
- contentMimeType == 'video/mp4' ||
1010
- contentMimeType == 'audio/mp4' ||
1011
- contentMimeType == 'video/iso.segment') {
1093
+ } else if (validMp4Extensions.includes(extension) ||
1094
+ validMp4MimeType.includes(contentMimeType) ||
1095
+ (initMimeType && validMp4MimeType.includes(initMimeType))) {
1012
1096
  const basicInfo = shaka.media.SegmentUtils.getBasicInfoFromMp4(
1013
1097
  initData, response.data);
1014
1098
  if (basicInfo) {
1015
1099
  return basicInfo;
1016
1100
  }
1017
1101
  }
1102
+ if (contentMimeType) {
1103
+ return shaka.media.SegmentUtils.getBasicInfoFromMimeType(
1104
+ contentMimeType);
1105
+ }
1106
+ if (initMimeType) {
1107
+ return shaka.media.SegmentUtils.getBasicInfoFromMimeType(
1108
+ initMimeType);
1109
+ }
1018
1110
  return defaultBasicInfo;
1019
1111
  }
1020
1112
 
@@ -2285,8 +2377,19 @@ shaka.hls.HlsParser = class {
2285
2377
 
2286
2378
  const uris = streamInfo.getUris();
2287
2379
  // Download the actual manifest.
2288
- const response = await this.requestManifest_(
2289
- streamInfo.getUris(), /* isPlaylist= */ true);
2380
+ let response;
2381
+ try {
2382
+ response = await this.requestManifest_(
2383
+ streamInfo.getUris(), /* isPlaylist= */ true);
2384
+ } catch (e) {
2385
+ stream.closeSegmentIndex();
2386
+ const handled = this.playerInterface_.disableStream(stream);
2387
+ if (handled) {
2388
+ return;
2389
+ } else {
2390
+ throw e;
2391
+ }
2392
+ }
2290
2393
  if (abortSignal.aborted) {
2291
2394
  return;
2292
2395
  }
@@ -2316,9 +2419,9 @@ shaka.hls.HlsParser = class {
2316
2419
  const mediaVariables =
2317
2420
  this.parseMediaVariables_(variablesTags, responseUri);
2318
2421
 
2319
- let mimeType = undefined;
2422
+ const mimeType = undefined;
2320
2423
 
2321
- let closedCaptionsUpdated = false;
2424
+ let requestBasicInfo = false;
2322
2425
 
2323
2426
  // If no codec info was provided in the manifest and codec guessing is
2324
2427
  // disabled we try to get necessary info from the media data.
@@ -2326,40 +2429,21 @@ shaka.hls.HlsParser = class {
2326
2429
  this.config_.hls.disableCodecGuessing) ||
2327
2430
  (this.needsClosedCaptionsDetection_ && type == ContentType.VIDEO &&
2328
2431
  !this.config_.hls.disableClosedCaptionsDetection)) {
2329
- let canRequestBasicInfo = playlist.segments.length > 0;
2330
- if (canRequestBasicInfo) {
2331
- const segment = playlist.segments[0];
2332
- if (shaka.hls.Utils.getFirstTagWithName(segment.tags, 'EXT-X-GAP')) {
2333
- canRequestBasicInfo = false;
2334
- }
2335
- }
2336
- if (canRequestBasicInfo) {
2432
+ if (playlist.segments.length > 0) {
2337
2433
  this.needsClosedCaptionsDetection_ = false;
2338
- const basicInfo = await this.getMediaPlaylistBasicInfo_(
2339
- playlist, getUris, mediaVariables);
2340
-
2341
- goog.asserts.assert(
2342
- type === basicInfo.type, 'Media types should match!');
2343
-
2344
- if (basicInfo.closedCaptions.size && (!closedCaptions ||
2345
- closedCaptions.size != basicInfo.closedCaptions.size)) {
2346
- closedCaptions = basicInfo.closedCaptions;
2347
- closedCaptionsUpdated = true;
2348
- }
2349
-
2350
- if (!this.codecInfoInManifest_ &&
2351
- this.config_.hls.disableCodecGuessing) {
2352
- mimeType = basicInfo.mimeType;
2353
- codecs = basicInfo.codecs;
2354
- }
2434
+ requestBasicInfo = true;
2355
2435
  }
2356
2436
  }
2357
2437
 
2438
+ const allowOverrideMimeType = !this.codecInfoInManifest_ &&
2439
+ this.config_.hls.disableCodecGuessing;
2440
+
2358
2441
  const wasLive = this.isLive_();
2359
2442
  const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
2360
2443
  streamId, mediaVariables, playlist, getUris, responseUri, codecs,
2361
2444
  type, languageValue, primary, name, channelsCount, closedCaptions,
2362
- characteristics, forced, sampleRate, spatialAudio, mimeType);
2445
+ characteristics, forced, sampleRate, spatialAudio, mimeType,
2446
+ requestBasicInfo, allowOverrideMimeType);
2363
2447
  if (abortSignal.aborted) {
2364
2448
  return;
2365
2449
  }
@@ -2392,6 +2476,17 @@ shaka.hls.HlsParser = class {
2392
2476
  stream.codecs = stream.codecs || realStream.codecs;
2393
2477
  stream.closedCaptions =
2394
2478
  stream.closedCaptions || realStream.closedCaptions;
2479
+ stream.width = stream.width || realStream.width;
2480
+ stream.height = stream.height || realStream.height;
2481
+ stream.hdr = stream.hdr || realStream.hdr;
2482
+ stream.colorGamut = stream.colorGamut || realStream.colorGamut;
2483
+ if (stream.language == 'und' && realStream.language != 'und') {
2484
+ stream.language = realStream.language;
2485
+ }
2486
+ stream.language = stream.language || realStream.language;
2487
+ stream.channelsCount = stream.channelsCount || realStream.channelsCount;
2488
+ stream.audioSamplingRate =
2489
+ stream.audioSamplingRate || realStream.audioSamplingRate;
2395
2490
  this.setFullTypeForStream_(stream);
2396
2491
 
2397
2492
  // Since we lazy-loaded this content, the player may need to create new
@@ -2400,6 +2495,13 @@ shaka.hls.HlsParser = class {
2400
2495
  this.playerInterface_.newDrmInfo(stream);
2401
2496
  }
2402
2497
 
2498
+ let closedCaptionsUpdated = false;
2499
+ if ((!closedCaptions && stream.closedCaptions) ||
2500
+ (closedCaptions && stream.closedCaptions &&
2501
+ closedCaptions.size != stream.closedCaptions.size)) {
2502
+ closedCaptionsUpdated = true;
2503
+ }
2504
+
2403
2505
  if (this.manifest_ && closedCaptionsUpdated) {
2404
2506
  this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
2405
2507
  }
@@ -2423,6 +2525,14 @@ shaka.hls.HlsParser = class {
2423
2525
  }
2424
2526
  }
2425
2527
 
2528
+ const qualityInfo =
2529
+ shaka.media.QualityObserver.createQualityInfo(stream);
2530
+ stream.segmentIndex.forEachTopLevelReference((reference) => {
2531
+ if (reference.initSegmentReference) {
2532
+ reference.initSegmentReference.mediaQuality = qualityInfo;
2533
+ }
2534
+ });
2535
+
2426
2536
  // Add finishing touches to the stream that can only be done once we have
2427
2537
  // more full context on the media as a whole.
2428
2538
  if (this.hasEnoughInfoToFinalizeStreams_()) {
@@ -2451,6 +2561,9 @@ shaka.hls.HlsParser = class {
2451
2561
  this.finalizeStreams_([streamInfo]);
2452
2562
  }
2453
2563
  }
2564
+
2565
+ this.processDateRangeTags_(
2566
+ playlist.tags, stream.type, mediaVariables, getUris);
2454
2567
  };
2455
2568
 
2456
2569
  /** @type {Promise} */
@@ -2631,13 +2744,16 @@ shaka.hls.HlsParser = class {
2631
2744
  * @param {?number} sampleRate
2632
2745
  * @param {boolean} spatialAudio
2633
2746
  * @param {(string|undefined)} mimeType
2747
+ * @param {boolean=} requestBasicInfo
2748
+ * @param {boolean=} allowOverrideMimeType
2634
2749
  * @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
2635
2750
  * @private
2636
2751
  */
2637
2752
  async convertParsedPlaylistIntoStreamInfo_(streamId, variables, playlist,
2638
2753
  getUris, responseUri, codecs, type, languageValue, primary, name,
2639
2754
  channelsCount, closedCaptions, characteristics, forced, sampleRate,
2640
- spatialAudio, mimeType = undefined) {
2755
+ spatialAudio, mimeType = undefined, requestBasicInfo = true,
2756
+ allowOverrideMimeType = true) {
2641
2757
  goog.asserts.assert(playlist.segments != null,
2642
2758
  'Media playlist should have segments!');
2643
2759
 
@@ -2653,6 +2769,30 @@ shaka.hls.HlsParser = class {
2653
2769
  const {segments, bandwidth} = this.createSegments_(
2654
2770
  playlist, mediaSequenceToStartTime, variables, getUris, type);
2655
2771
 
2772
+ let width = null;
2773
+ let height = null;
2774
+ let videoRange = null;
2775
+ let colorGamut = null;
2776
+ if (segments.length > 0 && requestBasicInfo) {
2777
+ const basicInfo = await this.getBasicInfoFromSegments_(segments);
2778
+
2779
+ type = basicInfo.type;
2780
+ languageValue = basicInfo.language;
2781
+ channelsCount = basicInfo.channelCount;
2782
+ sampleRate = basicInfo.sampleRate;
2783
+ closedCaptions = basicInfo.closedCaptions;
2784
+
2785
+ height = basicInfo.height;
2786
+ width = basicInfo.width;
2787
+ videoRange = basicInfo.videoRange;
2788
+ colorGamut = basicInfo.colorGamut;
2789
+
2790
+ if (allowOverrideMimeType) {
2791
+ mimeType = basicInfo.mimeType;
2792
+ codecs = basicInfo.codecs;
2793
+ }
2794
+ }
2795
+
2656
2796
  if (!mimeType) {
2657
2797
  mimeType = await this.guessMimeType_(type, codecs, segments);
2658
2798
  }
@@ -2679,6 +2819,13 @@ shaka.hls.HlsParser = class {
2679
2819
  }
2680
2820
  this.setFullTypeForStream_(stream);
2681
2821
 
2822
+ if (type == shaka.util.ManifestParserUtils.ContentType.VIDEO &&
2823
+ (width || height || videoRange || colorGamut)) {
2824
+ this.addVideoAttributes_(stream, width, height,
2825
+ /* frameRate= */ null, videoRange, /* videoLayout= */ null,
2826
+ colorGamut);
2827
+ }
2828
+
2682
2829
  // This new calculation is necessary for Low Latency streams.
2683
2830
  if (this.isLive_()) {
2684
2831
  this.determineLastTargetDuration_(playlist);
@@ -3325,11 +3472,8 @@ shaka.hls.HlsParser = class {
3325
3472
  let startByte = 0;
3326
3473
  let endByte = null;
3327
3474
 
3328
- if (hlsSegment.partialSegments.length && !this.lowLatencyMode_) {
3329
- shaka.log.alwaysWarn('Low-latency HLS live stream detected, but ' +
3330
- 'low-latency streaming mode is not enabled in Shaka ' +
3331
- 'Player. Set streaming.lowLatencyMode configuration to ' +
3332
- 'true, and see https://bit.ly/3clctcj for details.');
3475
+ if (hlsSegment.partialSegments.length) {
3476
+ this.manifest_.isLowLatency = true;
3333
3477
  }
3334
3478
 
3335
3479
  let syncTime = null;
@@ -3345,6 +3489,7 @@ shaka.hls.HlsParser = class {
3345
3489
 
3346
3490
  let status = shaka.media.SegmentReference.Status.AVAILABLE;
3347
3491
  if (shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-GAP')) {
3492
+ this.manifest_.gapCount++;
3348
3493
  status = shaka.media.SegmentReference.Status.MISSING;
3349
3494
  }
3350
3495
 
@@ -3427,6 +3572,7 @@ shaka.hls.HlsParser = class {
3427
3572
 
3428
3573
  let partialStatus = shaka.media.SegmentReference.Status.AVAILABLE;
3429
3574
  if (item.getAttributeValue('GAP') == 'YES') {
3575
+ this.manifest_.gapCount++;
3430
3576
  partialStatus = shaka.media.SegmentReference.Status.MISSING;
3431
3577
  somePartialSegmentWithGap = true;
3432
3578
  }
@@ -3621,6 +3767,156 @@ shaka.hls.HlsParser = class {
3621
3767
  return [startByte, endByte];
3622
3768
  }
3623
3769
 
3770
+ /**
3771
+ * @param {!Array.<!shaka.hls.Tag>} tags
3772
+ * @param {string} contentType
3773
+ * @param {!Map.<string, string>} variables
3774
+ * @param {function():!Array.<string>} getUris
3775
+ * @private
3776
+ */
3777
+ processDateRangeTags_(tags, contentType, variables, getUris) {
3778
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
3779
+ if (contentType != ContentType.VIDEO && contentType != ContentType.AUDIO) {
3780
+ // DATE-RANGE should only appear in AUDIO or VIDEO playlists.
3781
+ // We ignore those that appear in other playlists.
3782
+ return;
3783
+ }
3784
+ const Utils = shaka.hls.Utils;
3785
+ const initialProgramDateTime =
3786
+ this.presentationTimeline_.getInitialProgramDateTime();
3787
+ if (!initialProgramDateTime ||
3788
+ this.ignoreManifestProgramDateTimeFor_(contentType)) {
3789
+ return;
3790
+ }
3791
+ let dateRangeTags =
3792
+ shaka.hls.Utils.filterTagsByName(tags, 'EXT-X-DATERANGE');
3793
+ dateRangeTags = dateRangeTags.sort((a, b) => {
3794
+ const aStartDateValue = a.getRequiredAttrValue('START-DATE');
3795
+ const bStartDateValue = b.getRequiredAttrValue('START-DATE');
3796
+ if (aStartDateValue < bStartDateValue) {
3797
+ return -1;
3798
+ }
3799
+ if (aStartDateValue > bStartDateValue) {
3800
+ return 1;
3801
+ }
3802
+ return 0;
3803
+ });
3804
+ for (let i = 0; i < dateRangeTags.length; i++) {
3805
+ const tag = dateRangeTags[i];
3806
+ const id = tag.getRequiredAttrValue('ID');
3807
+ if (this.dateRangeIdsEmitted_.has(id)) {
3808
+ continue;
3809
+ }
3810
+ const startDateValue = tag.getRequiredAttrValue('START-DATE');
3811
+ const startDate = shaka.util.TXml.parseDate(startDateValue);
3812
+ if (isNaN(startDate)) {
3813
+ // Invalid START-DATE
3814
+ continue;
3815
+ }
3816
+ goog.asserts.assert(startDate != null, 'Start date should not be null!');
3817
+ const startTime = Math.max(0, startDate - initialProgramDateTime);
3818
+
3819
+ let endTime = null;
3820
+ const endDateValue = tag.getAttributeValue('END-DATE');
3821
+ if (endDateValue) {
3822
+ const endDate = shaka.util.TXml.parseDate(endDateValue);
3823
+ if (!isNaN(endDate)) {
3824
+ goog.asserts.assert(endDate != null, 'End date should not be null!');
3825
+ endTime = endDate - initialProgramDateTime;
3826
+ if (endTime < 0) {
3827
+ // Date range in the past
3828
+ continue;
3829
+ }
3830
+ }
3831
+ }
3832
+ if (endTime == null) {
3833
+ const durationValue = tag.getAttributeValue('DURATION') ||
3834
+ tag.getAttributeValue('PLANNED-DURATION');
3835
+ if (durationValue) {
3836
+ const duration = parseFloat(durationValue);
3837
+ if (!isNaN(duration)) {
3838
+ endTime = startTime + duration;
3839
+ }
3840
+ const realEndTime = startDate - initialProgramDateTime + duration;
3841
+ if (realEndTime < 0) {
3842
+ // Date range in the past
3843
+ continue;
3844
+ }
3845
+ }
3846
+ }
3847
+ const type = tag.getAttributeValue('CLASS') || 'com.apple.quicktime.HLS';
3848
+
3849
+ const endOnNext = tag.getAttributeValue('END-ON-NEXT') == 'YES';
3850
+ if (endTime == null && endOnNext) {
3851
+ for (let j = i + 1; j < dateRangeTags.length; j++) {
3852
+ const otherDateRangeType =
3853
+ dateRangeTags[j].getAttributeValue('CLASS') ||
3854
+ 'com.apple.quicktime.HLS';
3855
+ if (type != otherDateRangeType) {
3856
+ continue;
3857
+ }
3858
+ const otherDateRangeStartDateValue =
3859
+ dateRangeTags[j].getRequiredAttrValue('START-DATE');
3860
+ const otherDateRangeStartDate =
3861
+ shaka.util.TXml.parseDate(otherDateRangeStartDateValue);
3862
+ if (isNaN(otherDateRangeStartDate)) {
3863
+ // Invalid START-DATE
3864
+ continue;
3865
+ }
3866
+ if (otherDateRangeStartDate && otherDateRangeStartDate > startDate) {
3867
+ endTime = Math.max(0,
3868
+ otherDateRangeStartDate - initialProgramDateTime);
3869
+ break;
3870
+ }
3871
+ }
3872
+ if (endTime == null) {
3873
+ // Since we cannot know when it ends, we omit it for now and in the
3874
+ // future with an update we will be able to have more information.
3875
+ continue;
3876
+ }
3877
+ }
3878
+
3879
+ // Exclude these attributes from the metadata since they already go into
3880
+ // other fields (eg: startTime or endTime) or are not necessary..
3881
+ const excludedAttributes = [
3882
+ 'ID',
3883
+ 'CLASS',
3884
+ 'START-DATE',
3885
+ 'END-DATE',
3886
+ 'DURATION',
3887
+ 'END-ON-NEXT',
3888
+ ];
3889
+
3890
+ /* @type {!Array.<shaka.extern.MetadataFrame>} */
3891
+ const values = [];
3892
+ for (const attribute of tag.attributes) {
3893
+ if (excludedAttributes.includes(attribute.name)) {
3894
+ continue;
3895
+ }
3896
+ let data = Utils.variableSubstitution(attribute.value, variables);
3897
+ if (attribute.name == 'X-ASSET-URI' ||
3898
+ attribute.name == 'X-ASSET-LIST') {
3899
+ data = Utils.constructSegmentUris(
3900
+ getUris(), attribute.value, variables)[0];
3901
+ }
3902
+ const metadataFrame = {
3903
+ key: attribute.name,
3904
+ description: '',
3905
+ data,
3906
+ mimeType: null,
3907
+ pictureType: null,
3908
+ };
3909
+ values.push(metadataFrame);
3910
+ }
3911
+
3912
+ if (values.length) {
3913
+ this.playerInterface_.onMetadata(type, startTime, endTime, values);
3914
+ }
3915
+
3916
+ this.dateRangeIdsEmitted_.add(id);
3917
+ }
3918
+ }
3919
+
3624
3920
  /**
3625
3921
  * Parses shaka.hls.Segment objects into shaka.media.SegmentReferences and
3626
3922
  * get the bandwidth necessary for this segments If it's defined in the
@@ -3925,6 +4221,28 @@ shaka.hls.HlsParser = class {
3925
4221
  return map['mp4'];
3926
4222
  }
3927
4223
 
4224
+ /**
4225
+ * @param {!Array.<!shaka.media.SegmentReference>} segments
4226
+ * @return {!shaka.media.SegmentReference}
4227
+ * @private
4228
+ */
4229
+ getAvailableSegment_(segments) {
4230
+ goog.asserts.assert(segments.length, 'Should have segments!');
4231
+
4232
+ // If you wait long enough, requesting the first segment can fail
4233
+ // because it has fallen off the left edge of DVR, so to be safer,
4234
+ // let's request the middle segment.
4235
+ let segmentIndex = this.isLive_() ?
4236
+ Math.trunc((segments.length - 1) / 2) : 0;
4237
+ let segment = segments[segmentIndex];
4238
+ while (segment.getStatus() == shaka.media.SegmentReference.Status.MISSING &&
4239
+ (segmentIndex + 1) < segments.length) {
4240
+ segmentIndex ++;
4241
+ segment = segments[segmentIndex];
4242
+ }
4243
+ return segment;
4244
+ }
4245
+
3928
4246
  /**
3929
4247
  * Attempts to guess stream's mime type.
3930
4248
  *
@@ -3938,17 +4256,7 @@ shaka.hls.HlsParser = class {
3938
4256
  const HlsParser = shaka.hls.HlsParser;
3939
4257
  const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
3940
4258
 
3941
- // If you wait long enough, requesting the first segment can fail
3942
- // because it has fallen off the left edge of DVR, so to be safer,
3943
- // let's request the middle segment.
3944
- goog.asserts.assert(segments.length, 'Should have segments!');
3945
- let segmentIndex = Math.trunc((segments.length - 1) / 2);
3946
- let segment = segments[segmentIndex];
3947
- while (segment.status == shaka.media.SegmentReference.Status.MISSING &&
3948
- segmentIndex < segments.length) {
3949
- segmentIndex ++;
3950
- segment = segments[segmentIndex];
3951
- }
4259
+ const segment = this.getAvailableSegment_(segments);
3952
4260
 
3953
4261
  if (segment.status == shaka.media.SegmentReference.Status.MISSING) {
3954
4262
  return this.guessMimeTypeFallback_(contentType);
@@ -4264,7 +4572,7 @@ shaka.hls.HlsParser = class {
4264
4572
  const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
4265
4573
  'com.apple.fps', encryptionScheme, [
4266
4574
  {initDataType: 'sinf', initData: new Uint8Array(0), keyId: null},
4267
- ]);
4575
+ ], drmTag.getRequiredAttrValue('URI'));
4268
4576
 
4269
4577
  return drmInfo;
4270
4578
  }