shaka-player 4.1.4 → 4.1.6

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 (95) hide show
  1. package/.github/workflows/build-and-test.yaml +42 -22
  2. package/.github/workflows/compute-incremental-coverage.py +14 -3
  3. package/.github/workflows/custom-actions/set-commit-status/action.yaml +1 -5
  4. package/.github/workflows/deflake.yaml +28 -0
  5. package/.github/workflows/demo-version-index.yaml +1 -1
  6. package/.github/workflows/nightly-demo.yaml +1 -1
  7. package/.github/workflows/release-please.yaml +5 -5
  8. package/.github/workflows/report-incremental-coverage.yaml +1 -1
  9. package/.github/workflows/selenium-lab-tests.yaml +21 -10
  10. package/AUTHORS +3 -0
  11. package/CHANGELOG.md +35 -0
  12. package/CONTRIBUTORS +3 -0
  13. package/demo/common/message_ids.js +1 -0
  14. package/demo/config.js +3 -1
  15. package/demo/locales/en.json +1 -0
  16. package/demo/locales/source.json +4 -0
  17. package/dist/demo.compiled.debug.js +6 -6
  18. package/dist/demo.compiled.debug.map +2 -2
  19. package/dist/demo.compiled.js +6 -6
  20. package/dist/demo.compiled.map +2 -2
  21. package/dist/deps.js +3 -3
  22. package/dist/locales.js +31 -31
  23. package/dist/receiver.compiled.debug.js +3 -3
  24. package/dist/receiver.compiled.debug.map +2 -2
  25. package/dist/receiver.compiled.js +3 -3
  26. package/dist/receiver.compiled.map +2 -2
  27. package/dist/shaka-player.compiled.d.ts +3 -2
  28. package/dist/shaka-player.compiled.debug.d.ts +3 -2
  29. package/dist/shaka-player.compiled.debug.externs.js +25 -24
  30. package/dist/shaka-player.compiled.debug.js +233 -224
  31. package/dist/shaka-player.compiled.debug.map +4 -4
  32. package/dist/shaka-player.compiled.externs.js +25 -24
  33. package/dist/shaka-player.compiled.js +800 -797
  34. package/dist/shaka-player.compiled.map +4 -4
  35. package/dist/shaka-player.ui.d.ts +3 -2
  36. package/dist/shaka-player.ui.debug.d.ts +3 -2
  37. package/dist/shaka-player.ui.debug.externs.js +25 -24
  38. package/dist/shaka-player.ui.debug.js +286 -278
  39. package/dist/shaka-player.ui.debug.map +4 -4
  40. package/dist/shaka-player.ui.externs.js +25 -24
  41. package/dist/shaka-player.ui.js +939 -935
  42. package/dist/shaka-player.ui.map +4 -4
  43. package/docs/jsdoc-template/publish.js +33 -0
  44. package/docs/jsdoc-template/tmpl/container.tmpl +4 -1
  45. package/docs/jsdoc.conf.json +1 -0
  46. package/docs/tutorials/welcome.md +2 -2
  47. package/externs/shaka/manifest_parser.js +4 -1
  48. package/externs/shaka/player.js +7 -1
  49. package/lib/ads/client_side_ad_manager.js +12 -2
  50. package/lib/cast/cast_sender.js +39 -14
  51. package/lib/cea/cea608_memory.js +4 -0
  52. package/lib/hls/hls_parser.js +48 -15
  53. package/lib/media/drm_engine.js +59 -7
  54. package/lib/media/media_source_engine.js +43 -15
  55. package/lib/media/playhead.js +1 -1
  56. package/lib/media/streaming_engine.js +79 -14
  57. package/lib/offline/indexeddb/storage_mechanism.js +24 -0
  58. package/lib/offline/storage.js +1 -0
  59. package/lib/player.js +17 -5
  60. package/lib/polyfill/media_capabilities.js +7 -6
  61. package/lib/text/ttml_text_parser.js +69 -87
  62. package/lib/text/vtt_text_parser.js +1 -1
  63. package/lib/util/error.js +7 -0
  64. package/lib/util/periods.js +9 -9
  65. package/lib/util/platform.js +19 -5
  66. package/lib/util/player_configuration.js +4 -0
  67. package/lib/util/pssh.js +2 -1
  68. package/lib/util/stream_utils.js +2 -1
  69. package/lib/util/xml_utils.js +45 -6
  70. package/package.json +1 -1
  71. package/test/cea/cea608_memory_unit.js +88 -0
  72. package/test/dash/dash_parser_content_protection_unit.js +1 -0
  73. package/test/dash/dash_parser_live_unit.js +1 -0
  74. package/test/dash/dash_parser_manifest_unit.js +1 -0
  75. package/test/dash/dash_parser_segment_base_unit.js +1 -0
  76. package/test/dash/dash_parser_segment_list_unit.js +1 -0
  77. package/test/dash/dash_parser_segment_template_unit.js +1 -0
  78. package/test/hls/hls_live_unit.js +169 -247
  79. package/test/hls/hls_parser_unit.js +60 -2
  80. package/test/media/drm_engine_unit.js +61 -0
  81. package/test/media/media_source_engine_integration.js +36 -0
  82. package/test/media/media_source_engine_unit.js +118 -1
  83. package/test/media/streaming_engine_integration.js +9 -7
  84. package/test/media/streaming_engine_unit.js +29 -0
  85. package/test/offline/offline_integration.js +3 -3
  86. package/test/offline/storage_integration.js +30 -0
  87. package/test/player_integration.js +2 -2
  88. package/test/test/util/dash_parser_util.js +2 -0
  89. package/test/test/util/ttml_utils.js +0 -2
  90. package/test/text/ttml_text_parser_unit.js +46 -0
  91. package/test/text/vtt_text_parser_unit.js +23 -1
  92. package/test/ui/text_displayer_layout_unit.js +17 -2
  93. package/test/util/periods_unit.js +149 -11
  94. package/test/util/xml_utils_unit.js +64 -0
  95. package/ui/controls.js +0 -7
@@ -440,6 +440,7 @@ function buildNav(members) {
440
440
  [members.externals, 'Externals', seen, linktoExternal],
441
441
  [members.namespaces, 'Namespaces', seen, linkto],
442
442
  [members.classes, 'Classes', seen, linkto],
443
+ [members.enums, 'Enums', seen, linkto],
443
444
  [members.interfaces, 'Interfaces', seen, linkto],
444
445
  [members.events, 'Events', seen, linkto],
445
446
  [members.mixins, 'Mixins', seen, linkto],
@@ -484,6 +485,7 @@ function fixUpType(type) {
484
485
  exports.publish = (taffyData, opts, tutorials) => {
485
486
  let classes;
486
487
  let conf;
488
+ let enums;
487
489
  let externals;
488
490
  let files;
489
491
  let fromDir;
@@ -733,6 +735,31 @@ exports.publish = (taffyData, opts, tutorials) => {
733
735
  });
734
736
 
735
737
  members = helper.getMembers(data);
738
+ members.enums = [];
739
+
740
+ data().each(doclet => {
741
+ if (doclet.isEnum) {
742
+ const hasParent = !!docletMap[doclet.memberof];
743
+
744
+ // Enums without parents (not attached to a class, but directly to a
745
+ // namespace) aren't recognized by jsdoc by default and need to be
746
+ // massaged into the docs separately.
747
+ if (!hasParent) {
748
+ // Temporarily call this a "class", so that we can fix the URL.
749
+ doclet.kind = 'class';
750
+ // Regenerate the URL, so that this enum gets its own page
751
+ // instead of a URL that links to an ID within a non-existent
752
+ // parent page.
753
+ delete helper.longnameToUrl[doclet.longname];
754
+ helper.createLink(doclet);
755
+ // Mark this as an enum (custom type), so that it doesn't get
756
+ // slotted with the classes.
757
+ doclet.kind = 'enum';
758
+ members.enums.push(doclet);
759
+ }
760
+ }
761
+ });
762
+
736
763
  members.tutorials = tutorials.children;
737
764
  if (conf.default.sortTutorialsByConfigOrder) {
738
765
  members.tutorials.sort((a, b) => {
@@ -778,6 +805,7 @@ exports.publish = (taffyData, opts, tutorials) => {
778
805
 
779
806
  // set up the lists that we'll use to generate pages
780
807
  classes = taffy(members.classes);
808
+ enums = taffy(members.enums);
781
809
  modules = taffy(members.modules);
782
810
  namespaces = taffy(members.namespaces);
783
811
  mixins = taffy(members.mixins);
@@ -786,6 +814,7 @@ exports.publish = (taffyData, opts, tutorials) => {
786
814
 
787
815
  Object.keys(helper.longnameToUrl).forEach(longname => {
788
816
  const myClasses = helper.find(classes, {longname: longname});
817
+ const myEnums = helper.find(enums, {longname: longname});
789
818
  const myExternals = helper.find(externals, {longname: longname});
790
819
  const myInterfaces = helper.find(interfaces, {longname: longname});
791
820
  const myMixins = helper.find(mixins, {longname: longname});
@@ -800,6 +829,10 @@ exports.publish = (taffyData, opts, tutorials) => {
800
829
  generate(`Class: ${myClasses[0].longname}`, myClasses, helper.longnameToUrl[longname]);
801
830
  }
802
831
 
832
+ if (myEnums.length) {
833
+ generate(`Enum: ${myEnums[0].longname}`, myEnums, helper.longnameToUrl[longname]);
834
+ }
835
+
803
836
  if (myNamespaces.length) {
804
837
  generate(`Namespace: ${myNamespaces[0].longname}`, myNamespaces, helper.longnameToUrl[longname]);
805
838
  }
@@ -144,7 +144,10 @@
144
144
  return m.longname && m.longname.indexOf('module:') !== 0;
145
145
  });
146
146
  }
147
- if (members && members.length && members.forEach) {
147
+
148
+ // Skip enums, since they already have their values listed under
149
+ // "Properties".
150
+ if (members && members.length && members.forEach && doc.kind != 'enum') {
148
151
  ?>
149
152
  <h3 class="subsection-title">Members</h3>
150
153
 
@@ -16,6 +16,7 @@
16
16
  "navOrder": [
17
17
  "Tutorials",
18
18
  "Classes",
19
+ "Enums",
19
20
  "Interfaces",
20
21
  "Events"
21
22
  ]
@@ -66,8 +66,8 @@ python build/all.py
66
66
 
67
67
  Alternatively you can use a docker container:
68
68
  ```sh
69
- cd build/docker
70
- docker-compose run --rm shaka-compiler
69
+ docker build -t shaka-player-build build/docker
70
+ docker run -v $(pwd):/usr/src --user $(id -u):$(id -g) shaka-player-build
71
71
  ```
72
72
 
73
73
  The output is:
@@ -111,7 +111,8 @@ shaka.extern.ManifestParser = class {
111
111
  * onError: function(!shaka.util.Error),
112
112
  * isLowLatencyMode: function():boolean,
113
113
  * isAutoLowLatencyMode: function():boolean,
114
- * enableLowLatencyMode: function()
114
+ * enableLowLatencyMode: function(),
115
+ * updateDuration: function()
115
116
  * }}
116
117
  *
117
118
  * @description
@@ -147,6 +148,8 @@ shaka.extern.ManifestParser = class {
147
148
  * Return true if auto low latency streaming mode is enabled.
148
149
  * @property {function()} enableLowLatencyMode
149
150
  * Enable low latency streaming mode.
151
+ * @property {function()} updateDuration
152
+ * Update the presentation duration based on PresentationTimeline.
150
153
  * @exportDoc
151
154
  */
152
155
  shaka.extern.ManifestParser.PlayerInterface;
@@ -632,7 +632,8 @@ shaka.extern.AdvancedDrmConfiguration;
632
632
  * logLicenseExchange: boolean,
633
633
  * updateExpirationTime: number,
634
634
  * preferredKeySystems: !Array.<string>,
635
- * keySystemsMapping: !Object.<string, string>
635
+ * keySystemsMapping: !Object.<string, string>,
636
+ * parseInbandPsshEnabled: boolean
636
637
  * }}
637
638
  *
638
639
  * @property {shaka.extern.RetryParameters} retryParameters
@@ -675,6 +676,11 @@ shaka.extern.AdvancedDrmConfiguration;
675
676
  * Specifies the priorties of available DRM key systems.
676
677
  * @property {Object.<string, string>} keySystemsMapping
677
678
  * A map of key system name to key system name.
679
+ * @property {boolean} parseInbandPsshEnabled
680
+ * <i>Defaults to true on Xbox One, and false for all other browsers.</i><br>
681
+ * When true parse DRM init data from pssh boxes in media and init segments
682
+ * and ignore 'encrypted' events.
683
+ * This is required when using in-band key rotation on Xbox One.
678
684
  *
679
685
  * @exportDoc
680
686
  */
@@ -15,7 +15,6 @@ goog.require('goog.asserts');
15
15
  goog.require('shaka.ads.ClientSideAd');
16
16
  goog.require('shaka.util.IReleasable');
17
17
 
18
-
19
18
  /**
20
19
  * A class responsible for client-side ad interactions.
21
20
  * @implements {shaka.util.IReleasable}
@@ -205,13 +204,24 @@ shaka.ads.ClientSideAdManager = class {
205
204
  this.video_.offsetHeight, viewMode);
206
205
  });
207
206
  this.resizeObserver_.observe(this.video_);
207
+ } else {
208
+ this.eventManager_.listen(document, 'fullscreenchange', () => {
209
+ const viewMode = this.isFullScreenEnabled_() ?
210
+ google.ima.ViewMode.FULLSCREEN : google.ima.ViewMode.NORMAL;
211
+ this.imaAdsManager_.resize(this.video_.offsetWidth,
212
+ this.video_.offsetHeight, viewMode);
213
+ });
208
214
  }
209
215
 
210
216
  // Single video and overlay ads will start at this time
211
217
  // TODO (ismena): Need a better inderstanding of what this does.
212
218
  // The docs say it's called to 'start playing the ads,' but I haven't
213
219
  // seen the ads actually play until requestAds() is called.
214
- this.imaAdsManager_.start();
220
+ // Note: We listen for a play event to avoid autoplay issues that might
221
+ // crash IMA.
222
+ this.video_.addEventListener('play', () => {
223
+ this.imaAdsManager_.start();
224
+ }, {once: true});
215
225
  } catch (adError) {
216
226
  // If there was a problem with the VAST response,
217
227
  // we we won't be getting an ad. Hide ad UI if we showed it already
@@ -192,21 +192,14 @@ shaka.cast.CastSender = class {
192
192
  // If the API becomes available before this instance dies, init will be
193
193
  // called again.
194
194
 
195
- // A note about this value: In our testing environment, we load both
196
- // uncompiled and compiled code. This global callback in uncompiled mode
197
- // can be overwritten by the same in compiled mode. The two versions will
198
- // each have their own instances_ map. Therefore the callback must have a
199
- // name, as opposed to being anonymous. This way, the CastSender tests
200
- // can invoke the named static method instead of using a global that could
201
- // be overwritten.
202
- if (!window.__onGCastApiAvailable) {
203
- window.__onGCastApiAvailable = shaka.cast.CastSender.onSdkLoaded_;
204
- }
205
- if (window.__onGCastApiAvailable != shaka.cast.CastSender.onSdkLoaded_) {
206
- shaka.log.alwaysWarn('A global Cast SDK hook is already installed! ' +
207
- 'Shaka Player will be unable to receive a notification when the ' +
208
- 'Cast SDK is ready.');
195
+ // Check if our callback is already installed.
196
+ if (window.__onGCastApiAvailable !== CastSender.onGCastApiAvailable_) {
197
+ // Save pre-existing __onGCastApiAvailable in order to restore later.
198
+ CastSender.__onGCastApiAvailable_ =
199
+ window.__onGCastApiAvailable || null;
200
+ window.__onGCastApiAvailable = CastSender.onGCastApiAvailable_;
209
201
  }
202
+
210
203
  return;
211
204
  }
212
205
 
@@ -757,6 +750,9 @@ shaka.cast.CastSender.hasReceivers_ = false;
757
750
  /** @private {chrome.cast.Session} */
758
751
  shaka.cast.CastSender.session_ = null;
759
752
 
753
+ /** @private {?function(boolean)} */
754
+ shaka.cast.CastSender.__onGCastApiAvailable_ = null;
755
+
760
756
  /**
761
757
  * A set of all living CastSender instances. The constructor and destroy
762
758
  * methods will add and remove instances from this set.
@@ -784,3 +780,32 @@ shaka.cast.CastSender.onSdkLoaded_ = (loaded) => {
784
780
  }
785
781
  }
786
782
  };
783
+
784
+ /**
785
+ * @param {boolean} available
786
+ * @private
787
+ */
788
+ shaka.cast.CastSender.onGCastApiAvailable_ = (available) => {
789
+ // Restore callback from saved.
790
+ if (shaka.cast.CastSender.__onGCastApiAvailable_) {
791
+ window.__onGCastApiAvailable =
792
+ shaka.cast.CastSender.__onGCastApiAvailable_;
793
+ } else {
794
+ delete window.__onGCastApiAvailable;
795
+ }
796
+ shaka.cast.CastSender.__onGCastApiAvailable_ = null;
797
+
798
+ // A note about this value: In our testing environment, we load both
799
+ // uncompiled and compiled code. This global callback in uncompiled mode
800
+ // can be overwritten by the same in compiled mode. The two versions will
801
+ // each have their own instances_ map. Therefore the callback must have a
802
+ // name, as opposed to being anonymous. This way, the CastSender tests
803
+ // can invoke the named static method instead of using a global that could
804
+ // be overwritten.
805
+ shaka.cast.CastSender.onSdkLoaded_(available);
806
+
807
+ // call restored callback (if any)
808
+ if (typeof window.__onGCastApiAvailable === 'function') {
809
+ window.__onGCastApiAvailable(available);
810
+ }
811
+ };
@@ -184,6 +184,10 @@ shaka.cea.Cea608Memory = class {
184
184
  * @param {number} count Count of rows to move.
185
185
  */
186
186
  moveRows(dst, src, count) {
187
+ if (src < 0 || dst < 0) {
188
+ return;
189
+ }
190
+
187
191
  if (dst >= src) {
188
192
  for (let i = count-1; i >= 0; i--) {
189
193
  this.rows_[dst + i] = this.rows_[src + i].map((e) => e);
@@ -287,6 +287,21 @@ shaka.hls.HlsParser = class {
287
287
  }
288
288
 
289
289
  await Promise.all(updates);
290
+
291
+ this.notifySegments_();
292
+
293
+ // If any hasEndList is false, the stream is still live.
294
+ const stillLive = streamInfos.some((s) => s.hasEndList == false);
295
+ if (!stillLive) {
296
+ // Convert the presentation to VOD and set the duration.
297
+ const PresentationType = shaka.hls.HlsParser.PresentationType_;
298
+ this.setPresentationType_(PresentationType.VOD);
299
+
300
+ const maxTimestamps = streamInfos.map((s) => s.maxTimestamp);
301
+ // The duration is the minimum of the end times of all streams.
302
+ this.presentationTimeline_.setDuration(Math.min(...maxTimestamps));
303
+ this.playerInterface_.updateDuration();
304
+ }
290
305
  }
291
306
 
292
307
  /**
@@ -297,7 +312,6 @@ shaka.hls.HlsParser = class {
297
312
  * @private
298
313
  */
299
314
  async updateStream_(streamInfo) {
300
- const PresentationType = shaka.hls.HlsParser.PresentationType_;
301
315
  const manifestUri = streamInfo.absoluteMediaPlaylistUri;
302
316
  const uriObj = new goog.Uri(manifestUri);
303
317
  if (this.lowLatencyMode_ && streamInfo.canSkipSegments) {
@@ -330,6 +344,7 @@ shaka.hls.HlsParser = class {
330
344
  streamInfo.verbatimMediaPlaylistUri, playlist, stream.type,
331
345
  stream.mimeType, streamInfo.mediaSequenceToStartTime, mediaVariables,
332
346
  stream.codecs);
347
+ this.segmentsToNotifyByStream_.push(segments);
333
348
 
334
349
  stream.segmentIndex.mergeAndEvict(
335
350
  segments, this.presentationTimeline_.getSegmentAvailabilityStart());
@@ -350,10 +365,10 @@ shaka.hls.HlsParser = class {
350
365
  shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-ENDLIST');
351
366
 
352
367
  if (endListTag) {
353
- // Convert the presentation to VOD and set the duration to the last
354
- // segment's end time.
355
- this.setPresentationType_(PresentationType.VOD);
356
- this.presentationTimeline_.setDuration(newestSegment.endTime);
368
+ // Flag this for later. We don't convert the whole presentation into VOD
369
+ // until we've seen the ENDLIST tag for all active playlists.
370
+ streamInfo.hasEndList = true;
371
+ streamInfo.maxTimestamp = newestSegment.endTime;
357
372
  }
358
373
  }
359
374
 
@@ -1761,6 +1776,7 @@ shaka.hls.HlsParser = class {
1761
1776
  maxTimestamp: lastEndTime,
1762
1777
  mediaSequenceToStartTime,
1763
1778
  canSkipSegments,
1779
+ hasEndList: false,
1764
1780
  };
1765
1781
  }
1766
1782
 
@@ -2392,7 +2408,12 @@ shaka.hls.HlsParser = class {
2392
2408
  const extension = parsedUri.getPath().split('.').pop();
2393
2409
  const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
2394
2410
 
2395
- const mimeType = map[extension];
2411
+ let mimeType = map[extension];
2412
+ if (mimeType) {
2413
+ return mimeType;
2414
+ }
2415
+
2416
+ mimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension];
2396
2417
  if (mimeType) {
2397
2418
  return mimeType;
2398
2419
  }
@@ -2523,8 +2544,11 @@ shaka.hls.HlsParser = class {
2523
2544
  try {
2524
2545
  await this.update();
2525
2546
 
2526
- const delay = this.updatePlaylistDelay_;
2527
- this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
2547
+ // This may have converted to VOD, in which case we stop updating.
2548
+ if (this.isLive_()) {
2549
+ const delay = this.updatePlaylistDelay_;
2550
+ this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
2551
+ }
2528
2552
  } catch (error) {
2529
2553
  // Detect a call to stop() during this.update()
2530
2554
  if (!this.playerInterface_) {
@@ -2707,7 +2731,8 @@ shaka.hls.HlsParser = class {
2707
2731
  * absoluteMediaPlaylistUri: string,
2708
2732
  * maxTimestamp: number,
2709
2733
  * mediaSequenceToStartTime: !Map.<number, number>,
2710
- * canSkipSegments: boolean
2734
+ * canSkipSegments: boolean,
2735
+ * hasEndList: boolean
2711
2736
  * }}
2712
2737
  *
2713
2738
  * @description
@@ -2730,6 +2755,8 @@ shaka.hls.HlsParser = class {
2730
2755
  * @property {boolean} canSkipSegments
2731
2756
  * True if the server supports delta playlist updates, and we can send a
2732
2757
  * request for a playlist that can skip older media segments.
2758
+ * @property {boolean} hasEndList
2759
+ * True if the stream has an EXT-X-ENDLIST tag.
2733
2760
  */
2734
2761
  shaka.hls.HlsParser.StreamInfo;
2735
2762
 
@@ -2747,6 +2774,18 @@ shaka.hls.HlsParser.StreamInfo;
2747
2774
  shaka.hls.HlsParser.StreamInfos;
2748
2775
 
2749
2776
 
2777
+ /**
2778
+ * @const {!Object.<string, string>}
2779
+ * @private
2780
+ */
2781
+ shaka.hls.HlsParser.RAW_FORMATS_TO_MIME_TYPES_ = {
2782
+ 'aac': 'audio/aac',
2783
+ 'ac3': 'audio/ac3',
2784
+ 'ec3': 'audio/ec3',
2785
+ 'mp3': 'audio/mpeg',
2786
+ };
2787
+
2788
+
2750
2789
  /**
2751
2790
  * @const {!Object.<string, string>}
2752
2791
  * @private
@@ -2761,12 +2800,6 @@ shaka.hls.HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_ = {
2761
2800
  'cmfa': 'audio/mp4',
2762
2801
  // MPEG2-TS also uses video/ for audio: https://bit.ly/TsMse
2763
2802
  'ts': 'video/mp2t',
2764
-
2765
- // Raw formats:
2766
- 'aac': 'audio/aac',
2767
- 'ac3': 'audio/ac3',
2768
- 'ec3': 'audio/ec3',
2769
- 'mp3': 'audio/mpeg',
2770
2803
  };
2771
2804
 
2772
2805
 
@@ -18,14 +18,17 @@ goog.require('shaka.util.FakeEvent');
18
18
  goog.require('shaka.util.IDestroyable');
19
19
  goog.require('shaka.util.Iterables');
20
20
  goog.require('shaka.util.Lazy');
21
+ goog.require('shaka.util.ManifestParserUtils');
21
22
  goog.require('shaka.util.MapUtils');
22
23
  goog.require('shaka.util.MimeUtils');
23
24
  goog.require('shaka.util.Platform');
25
+ goog.require('shaka.util.Pssh');
24
26
  goog.require('shaka.util.PublicPromise');
25
27
  goog.require('shaka.util.StreamUtils');
26
28
  goog.require('shaka.util.StringUtils');
27
29
  goog.require('shaka.util.Timer');
28
30
  goog.require('shaka.util.Uint8ArrayUtils');
31
+ goog.require('shaka.util.XmlUtils');
29
32
 
30
33
 
31
34
  /** @implements {shaka.util.IDestroyable} */
@@ -404,6 +407,7 @@ shaka.media.DrmEngine = class {
404
407
  if (this.mediaKeysAttached_) {
405
408
  await this.mediaKeysAttached_;
406
409
 
410
+ this.destroyer_.ensureNotDestroyed();
407
411
  return;
408
412
  }
409
413
 
@@ -481,14 +485,12 @@ shaka.media.DrmEngine = class {
481
485
 
482
486
  /**
483
487
  * We can attach media keys before the playback actually begins when:
484
- * - Using legacy implementations requires MediaKeys to be set before
485
- * having webkitneedkey / msneedkey event, which will be translated as
486
- * an encrypted event by the polyfills
488
+ * - If we are not using FairPlay Modern EME
487
489
  * - Some initData already has been generated (through the manifest)
488
490
  * - In case of an offline session
489
491
  */
490
492
  if (manifestInitData ||
491
- shaka.util.Platform.isMediaKeysPolyfilled() ||
493
+ this.currentDrmInfo_.keySystem !== 'com.apple.fps' ||
492
494
  this.offlineSessionIds_.length) {
493
495
  await this.attachMediaKeys_();
494
496
  }
@@ -497,7 +499,11 @@ shaka.media.DrmEngine = class {
497
499
 
498
500
  // Explicit init data for any one stream or an offline session is
499
501
  // sufficient to suppress 'encrypted' events for all streams.
500
- if (!manifestInitData && !this.offlineSessionIds_.length) {
502
+ // Also suppress 'encrypted' events when parsing in-band ppsh
503
+ // from media segments because that serves the same purpose as the
504
+ // 'encrypted' events.
505
+ if (!manifestInitData && !this.offlineSessionIds_.length &&
506
+ !this.config_.parseInbandPsshEnabled) {
501
507
  this.eventManager_.listen(
502
508
  this.video_, 'encrypted', (e) => this.onEncryptedEvent_(e));
503
509
  }
@@ -668,6 +674,14 @@ shaka.media.DrmEngine = class {
668
674
  }
669
675
  }
670
676
 
677
+ // If there are pre-existing sessions that have all been loaded
678
+ // then reset the allSessionsLoaded_ promise, which can now be
679
+ // used to wait for new sesssions to be loaded
680
+ if (this.activeSessions_.size > 0 && this.areAllSessionsLoaded_()) {
681
+ this.allSessionsLoaded_.resolve();
682
+ this.allSessionsLoaded_ = new shaka.util.PublicPromise();
683
+ this.allSessionsLoaded_.catch(() => {});
684
+ }
671
685
  this.createSession(initDataType, initData,
672
686
  this.currentDrmInfo_.sessionType);
673
687
  }
@@ -1492,7 +1506,8 @@ shaka.media.DrmEngine = class {
1492
1506
  return;
1493
1507
  }
1494
1508
  shaka.log.debug('Unwrapping PlayReady request.');
1495
- const dom = new DOMParser().parseFromString(xml, 'application/xml');
1509
+ const dom = shaka.util.XmlUtils.parseXmlString(xml, 'PlayReadyKeyMessage');
1510
+ goog.asserts.assert(dom, 'Failed to parse PlayReady XML!');
1496
1511
 
1497
1512
  // Set request headers.
1498
1513
  const headers = dom.getElementsByTagName('HttpHeader');
@@ -1576,7 +1591,7 @@ shaka.media.DrmEngine = class {
1576
1591
  hasExpiredKeys = true;
1577
1592
  }
1578
1593
 
1579
- const keyIdHex = shaka.util.Uint8ArrayUtils.toHex(keyId);
1594
+ const keyIdHex = shaka.util.Uint8ArrayUtils.toHex(keyId).slice(0, 32);
1580
1595
 
1581
1596
  this.keyStatusByKeyId_.set(keyIdHex, status);
1582
1597
  });
@@ -2329,6 +2344,43 @@ shaka.media.DrmEngine = class {
2329
2344
  }
2330
2345
  }
2331
2346
  }
2347
+
2348
+ /**
2349
+ * Parse pssh from a media segment and announce new initData
2350
+ *
2351
+ * @param {shaka.util.ManifestParserUtils.ContentType} contentType
2352
+ * @param {!BufferSource} mediaSegment
2353
+ * @return {!Promise<void>}
2354
+ */
2355
+ parseInbandPssh(contentType, mediaSegment) {
2356
+ if (!this.config_.parseInbandPsshEnabled) {
2357
+ return Promise.resolve();
2358
+ }
2359
+
2360
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
2361
+ if (![ContentType.AUDIO, ContentType.VIDEO].includes(contentType)) {
2362
+ return Promise.resolve();
2363
+ }
2364
+
2365
+ const pssh = new shaka.util.Pssh(
2366
+ shaka.util.BufferUtils.toUint8(mediaSegment));
2367
+
2368
+ let totalLength = 0;
2369
+ for (const data of pssh.data) {
2370
+ totalLength += data.length;
2371
+ }
2372
+ if (totalLength == 0) {
2373
+ return Promise.resolve();
2374
+ }
2375
+ const combinedData = new Uint8Array(totalLength);
2376
+ let pos = 0;
2377
+ for (const data of pssh.data) {
2378
+ combinedData.set(data, pos);
2379
+ pos += data.length;
2380
+ }
2381
+ this.newInitData('cenc', combinedData);
2382
+ return this.allSessionsLoaded_;
2383
+ }
2332
2384
  };
2333
2385
 
2334
2386
 
@@ -522,9 +522,13 @@ shaka.media.MediaSourceEngine = class {
522
522
  * @param {?boolean} hasClosedCaptions True if the buffer contains CEA closed
523
523
  * captions
524
524
  * @param {boolean=} seeked True if we just seeked
525
+ * @param {boolean=} adaptation True if we just automatically switched active
526
+ * variant(s).
525
527
  * @return {!Promise}
526
528
  */
527
- async appendBuffer(contentType, data, reference, hasClosedCaptions, seeked) {
529
+ async appendBuffer(
530
+ contentType, data, reference, hasClosedCaptions, seeked = false,
531
+ adaptation = false) {
528
532
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
529
533
 
530
534
  if (contentType == ContentType.TEXT) {
@@ -625,6 +629,14 @@ shaka.media.MediaSourceEngine = class {
625
629
 
626
630
  await this.enqueueOperation_(
627
631
  contentType, () => this.append_(contentType, data));
632
+ // If the input buffer passed to SourceBuffer#appendBuffer() does not
633
+ // contain a complete media segment, the call will exit while the
634
+ // SourceBuffer's append state is
635
+ // still PARSING_MEDIA_SEGMENT. Reset the parser state by calling
636
+ // abort() to safely reset timestampOffset to 'originalOffset'.
637
+ // https://www.w3.org/TR/media-source-2/#sourcebuffer-segment-parser-loop
638
+ await this.enqueueOperation_(
639
+ contentType, () => this.abort_(contentType));
628
640
 
629
641
  // Reset the offset and append window.
630
642
  sourceBuffer.timestampOffset = originalOffset;
@@ -638,10 +650,17 @@ shaka.media.MediaSourceEngine = class {
638
650
  const textOffset = (reference.startTime || 0) - (mediaStartTime || 0);
639
651
  this.textSequenceModeOffset_.resolve(textOffset);
640
652
 
641
- // Finally, clear the buffer.
653
+ // Clear the buffer.
642
654
  await this.enqueueOperation_(
643
655
  contentType,
644
656
  () => this.remove_(contentType, 0, this.mediaSource_.duration));
657
+
658
+ // Finally, flush the buffer in case of choppy video start on HLS fMP4.
659
+ if (contentType == ContentType.VIDEO) {
660
+ await this.enqueueOperation_(
661
+ contentType,
662
+ () => this.flush_(contentType));
663
+ }
645
664
  }
646
665
 
647
666
  // Now switch to sequence mode and fall through to our normal operations.
@@ -650,10 +669,15 @@ shaka.media.MediaSourceEngine = class {
650
669
 
651
670
  if (reference && this.sequenceMode_ && contentType != ContentType.TEXT) {
652
671
  // In sequence mode, for non-text streams, if we just cleared the buffer
653
- // and are performing an unbuffered seek, we need to set a new
654
- // timestampOffset on the sourceBuffer.
655
- if (seeked) {
672
+ // and are either performing an unbuffered seek or handling an automatic
673
+ // adaptation, we need to set a new timestampOffset on the sourceBuffer.
674
+ if (seeked || adaptation) {
656
675
  const timestampOffset = reference.startTime;
676
+ // The logic to call abort() before setting the timestampOffset is
677
+ // extended during unbuffered seeks or automatic adaptations; it is
678
+ // possible for the append state to be PARSING_MEDIA_SEGMENT from the
679
+ // previous SourceBuffer#appendBuffer() call.
680
+ this.enqueueOperation_(contentType, () => this.abort_(contentType));
657
681
  this.enqueueOperation_(
658
682
  contentType,
659
683
  () => this.setTimestampOffset_(contentType, timestampOffset));
@@ -849,21 +873,25 @@ shaka.media.MediaSourceEngine = class {
849
873
  }
850
874
 
851
875
  /**
852
- * We only support increasing duration at this time. Decreasing duration
853
- * causes the MSE removal algorithm to run, which results in an 'updateend'
854
- * event. Supporting this scenario would be complicated, and is not currently
855
- * needed.
856
- *
857
876
  * @param {number} duration
858
877
  * @return {!Promise}
859
878
  */
860
879
  async setDuration(duration) {
861
- goog.asserts.assert(
862
- isNaN(this.mediaSource_.duration) ||
863
- this.mediaSource_.duration <= duration,
864
- 'duration cannot decrease: ' + this.mediaSource_.duration + ' -> ' +
865
- duration);
866
880
  await this.enqueueBlockingOperation_(() => {
881
+ // Reducing the duration causes the MSE removal algorithm to run, which
882
+ // triggers an 'updateend' event to fire. To handle this scenario, we
883
+ // have to insert a dummy operation into the beginning of each queue,
884
+ // which the 'updateend' handler will remove.
885
+ if (duration < this.mediaSource_.duration) {
886
+ for (const contentType in this.sourceBuffers_) {
887
+ const dummyOperation = {
888
+ start: () => {},
889
+ p: new shaka.util.PublicPromise(),
890
+ };
891
+ this.queues_[contentType].unshift(dummyOperation);
892
+ }
893
+ }
894
+
867
895
  this.mediaSource_.duration = duration;
868
896
  });
869
897
  }
@@ -294,7 +294,7 @@ shaka.media.MediaSourcePlayhead = class {
294
294
 
295
295
  /** @override */
296
296
  getStallsDetected() {
297
- return this.stallDetector_.getStallsDetected();
297
+ return this.stallDetector_ ? this.stallDetector_.getStallsDetected() : 0;
298
298
  }
299
299
 
300
300
  /** @override */