shaka-player 4.0.0 → 4.0.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 (57) hide show
  1. package/.github/config.yml +1 -0
  2. package/.github/workflows/release-please.yaml +6 -11
  3. package/CHANGELOG.md +15 -0
  4. package/build/misspellings.txt +1 -0
  5. package/dist/controls.css +1 -1
  6. package/dist/locales.js +32 -32
  7. package/dist/shaka-player.compiled.d.ts +2 -1
  8. package/dist/shaka-player.compiled.debug.d.ts +2 -1
  9. package/dist/shaka-player.compiled.debug.externs.js +1 -0
  10. package/dist/shaka-player.compiled.debug.js +170 -165
  11. package/dist/shaka-player.compiled.debug.map +3 -3
  12. package/dist/shaka-player.compiled.externs.js +1 -0
  13. package/dist/shaka-player.compiled.js +783 -779
  14. package/dist/shaka-player.compiled.map +3 -3
  15. package/dist/shaka-player.ui.d.ts +2 -1
  16. package/dist/shaka-player.ui.debug.d.ts +2 -1
  17. package/dist/shaka-player.ui.debug.externs.js +1 -0
  18. package/dist/shaka-player.ui.debug.js +182 -177
  19. package/dist/shaka-player.ui.debug.map +3 -3
  20. package/dist/shaka-player.ui.externs.js +1 -0
  21. package/dist/shaka-player.ui.js +924 -920
  22. package/dist/shaka-player.ui.map +3 -3
  23. package/docs/tutorials/upgrade.md +4 -2
  24. package/externs/shaka/player.js +11 -0
  25. package/karma.conf.js +4 -0
  26. package/lib/dash/dash_parser.js +2 -2
  27. package/lib/hls/hls_parser.js +20 -11
  28. package/lib/media/drm_engine.js +16 -0
  29. package/lib/media/media_source_engine.js +91 -37
  30. package/lib/media/streaming_engine.js +1 -2
  31. package/lib/offline/indexeddb/storage_mechanism.js +4 -2
  32. package/lib/player.js +165 -107
  33. package/lib/text/vtt_text_parser.js +7 -7
  34. package/lib/util/config_utils.js +25 -0
  35. package/lib/util/error.js +7 -0
  36. package/lib/util/manifest_parser_utils.js +1 -1
  37. package/lib/util/mime_utils.js +48 -1
  38. package/lib/util/periods.js +5 -4
  39. package/lib/util/player_configuration.js +14 -14
  40. package/lib/util/stream_utils.js +4 -2
  41. package/package.json +4 -1
  42. package/test/hls/hls_live_unit.js +22 -0
  43. package/test/hls/manifest_text_parser_unit.js +19 -0
  44. package/test/offline/indexeddb_storage_unit.js +1 -1
  45. package/test/offline/storage_compatibility_unit.js +1 -1
  46. package/test/player_external.js +7 -1
  47. package/test/player_integration.js +6 -10
  48. package/test/player_src_equals_integration.js +81 -83
  49. package/test/player_unit.js +98 -1
  50. package/test/test/boot.js +9 -0
  51. package/test/test/externs/filters.js +4 -0
  52. package/test/test/util/simple_fakes.js +31 -2
  53. package/test/text/vtt_text_parser_unit.js +64 -5
  54. package/test/util/mime_utils_unit.js +54 -0
  55. package/test/util/periods_unit.js +79 -0
  56. package/ui/resolution_selection.js +1 -1
  57. package/docs/tutorials/gsoc-ideas-list.md +0 -3
@@ -42,10 +42,12 @@ application:
42
42
  `manifest.defaultPresentationDelay` (deprecated in v3.0.0)
43
43
  - Configuration of factories should be plain factory functions, not
44
44
  constructors; these will not be invoked with `new` (deprecated in v3.1.0)
45
- - `drm.initDataTransform` has been removed (no longer needed since the
46
- minimum supported version of iOS is now 13)
45
+ - `drm.initDataTransform` now defaults to a no-op
47
46
  - `streaming.smallGapLimit` and `streaming.jumpLargeGaps` have been removed;
48
47
  all gaps will now be jumped
48
+ - `manifest.hls.useFullSegmentsForStartTime` has been removed; this setting
49
+ is no longer necessary, as we no longer fetch segments for start times in
50
+ the HLS parser
49
51
 
50
52
  - Player API changes:
51
53
  - `shaka.Player.prototype.addTextTrack()` has been replaced by
@@ -618,6 +618,9 @@ shaka.extern.AdvancedDrmConfiguration;
618
618
  * clearKeys: !Object.<string, string>,
619
619
  * delayLicenseRequestUntilPlayed: boolean,
620
620
  * advanced: Object.<string, shaka.extern.AdvancedDrmConfiguration>,
621
+ * initDataTransform:
622
+ * ((function(!Uint8Array, string, ?shaka.extern.DrmInfo):!Uint8Array)|
623
+ * undefined),
621
624
  * logLicenseExchange: boolean,
622
625
  * updateExpirationTime: number,
623
626
  * preferredKeySystems: !Array.<string>
@@ -641,6 +644,14 @@ shaka.extern.AdvancedDrmConfiguration;
641
644
  * <i>Optional.</i> <br>
642
645
  * A dictionary which maps key system IDs to advanced DRM configuration for
643
646
  * those key systems.
647
+ * @property
648
+ * {((function(!Uint8Array, string, ?shaka.extern.DrmInfo):!Uint8Array)|
649
+ * undefined)}
650
+ * initDataTransform
651
+ * <i>Optional.</i><br>
652
+ * If given, this function is called with the init data from the
653
+ * manifest/media and should return the (possibly transformed) init data to
654
+ * pass to the browser.
644
655
  * @property {boolean} logLicenseExchange
645
656
  * <i>Optional.</i><br>
646
657
  * If set to <code>true</code>, prints logs containing the license exchange.
package/karma.conf.js CHANGED
@@ -363,6 +363,8 @@ module.exports = (config) => {
363
363
  // If testing custom assets, we don't serve other unit or integration tests.
364
364
  // External asset tests are the basis for custom asset testing, so this file
365
365
  // is automatically included.
366
+ clientArgs.testFiles.push('demo/common/asset.js');
367
+ clientArgs.testFiles.push('demo/common/assets.js');
366
368
  clientArgs.testFiles.push('test/player_external.js');
367
369
  } else {
368
370
  // In a normal test run, we serve unit tests.
@@ -374,6 +376,8 @@ module.exports = (config) => {
374
376
  }
375
377
  if (settings.external) {
376
378
  // If --external is present, we serve external asset tests.
379
+ clientArgs.testFiles.push('demo/common/asset.js');
380
+ clientArgs.testFiles.push('demo/common/assets.js');
377
381
  clientArgs.testFiles.push('test/**/*_external.js');
378
382
  }
379
383
  }
@@ -765,8 +765,8 @@ shaka.dash.DashParser = class {
765
765
  // currently support that. Just choose one.
766
766
  // TODO: https://github.com/shaka-project/shaka-player/issues/1528
767
767
  stream.trickModeVideo = trickModeSet.streams.find((trickStream) =>
768
- shaka.util.MimeUtils.getCodecBase(stream.codecs) ==
769
- shaka.util.MimeUtils.getCodecBase(trickStream.codecs));
768
+ shaka.util.MimeUtils.getNormalizedCodec(stream.codecs) ==
769
+ shaka.util.MimeUtils.getNormalizedCodec(trickStream.codecs));
770
770
  }
771
771
  }
772
772
  }
@@ -1825,7 +1825,7 @@ shaka.hls.HlsParser = class {
1825
1825
  * @param {!Map.<string, string>} variables
1826
1826
  * @param {string} absoluteMediaPlaylistUri
1827
1827
  * @param {string} type
1828
- * @return {!shaka.media.SegmentReference}
1828
+ * @return {shaka.media.SegmentReference}
1829
1829
  * @private
1830
1830
  */
1831
1831
  createSegmentReference_(
@@ -1870,9 +1870,23 @@ shaka.hls.HlsParser = class {
1870
1870
  status = shaka.media.SegmentReference.Status.MISSING;
1871
1871
  }
1872
1872
 
1873
+ if (!extinfTag) {
1874
+ if (hlsSegment.partialSegments.length == 0) {
1875
+ // EXTINF tag must be available if the segment has no partial segments.
1876
+ throw new shaka.util.Error(
1877
+ shaka.util.Error.Severity.CRITICAL,
1878
+ shaka.util.Error.Category.MANIFEST,
1879
+ shaka.util.Error.Code.HLS_REQUIRED_TAG_MISSING, 'EXTINF');
1880
+ } else if (!this.lowLatencyMode_) {
1881
+ // Without EXTINF and without low-latency mode, partial segments get
1882
+ // ignored.
1883
+ return null;
1884
+ }
1885
+ }
1886
+
1873
1887
  // Create SegmentReferences for the partial segments.
1874
1888
  const partialSegmentRefs = [];
1875
- if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) {
1889
+ if (this.lowLatencyMode_) {
1876
1890
  for (let i = 0; i < hlsSegment.partialSegments.length; i++) {
1877
1891
  const item = hlsSegment.partialSegments[i];
1878
1892
  const pPreviousReference = i == 0 ?
@@ -1913,14 +1927,6 @@ shaka.hls.HlsParser = class {
1913
1927
  /* appendWindowEnd= */ Infinity);
1914
1928
  partialSegmentRefs.push(partial);
1915
1929
  } // for-loop of hlsSegment.partialSegments
1916
- } else {
1917
- // EXTINF tag must be available if the segment has no partial segments.
1918
- if (!extinfTag) {
1919
- throw new shaka.util.Error(
1920
- shaka.util.Error.Severity.CRITICAL,
1921
- shaka.util.Error.Category.MANIFEST,
1922
- shaka.util.Error.Code.HLS_REQUIRED_TAG_MISSING, 'EXTINF');
1923
- }
1924
1930
  }
1925
1931
 
1926
1932
  // If the segment has EXTINF tag, set the segment's end time, start byte
@@ -1939,6 +1945,7 @@ shaka.hls.HlsParser = class {
1939
1945
  } else {
1940
1946
  endTime = partialSegmentRefs[partialSegmentRefs.length - 1].endTime;
1941
1947
  }
1948
+
1942
1949
  // If the segment has EXT-X-BYTERANGE tag, set the start byte and end byte
1943
1950
  // base on the byterange information. If segment has no EXT-X-BYTERANGE tag
1944
1951
  // and has partial segments, set the start byte and end byte base on the
@@ -2107,7 +2114,9 @@ shaka.hls.HlsParser = class {
2107
2114
  playlist.absoluteUri,
2108
2115
  type);
2109
2116
 
2110
- references.push(reference);
2117
+ if (reference) {
2118
+ references.push(reference);
2119
+ }
2111
2120
  }
2112
2121
 
2113
2122
  // If some segments have sync times, but not all, extrapolate the sync
@@ -1253,6 +1253,22 @@ shaka.media.DrmEngine = class {
1253
1253
  };
1254
1254
  this.activeSessions_.set(session, metadata);
1255
1255
 
1256
+ try {
1257
+ initData = this.config_.initDataTransform(
1258
+ initData, initDataType, this.currentDrmInfo_);
1259
+ } catch (error) {
1260
+ let shakaError = error;
1261
+ if (!(error instanceof shaka.util.Error)) {
1262
+ shakaError = new shaka.util.Error(
1263
+ shaka.util.Error.Severity.CRITICAL,
1264
+ shaka.util.Error.Category.DRM,
1265
+ shaka.util.Error.Code.INIT_DATA_TRANSFORM_ERROR,
1266
+ error);
1267
+ }
1268
+ this.onError_(shakaError);
1269
+ return;
1270
+ }
1271
+
1256
1272
  if (this.config_.logLicenseExchange) {
1257
1273
  const str = shaka.util.Uint8ArrayUtils.toBase64(initData);
1258
1274
  shaka.log.info('EME init data: type=', initDataType, 'data=', str);
@@ -106,6 +106,12 @@ shaka.media.MediaSourceEngine = class {
106
106
 
107
107
  /** @private {string} */
108
108
  this.url_ = '';
109
+
110
+ /** @private {boolean} */
111
+ this.sequenceMode_ = false;
112
+
113
+ /** @private {!shaka.util.PublicPromise.<number>} */
114
+ this.textSequenceModeOffset_ = new shaka.util.PublicPromise();
109
115
  }
110
116
 
111
117
  /**
@@ -331,6 +337,8 @@ shaka.media.MediaSourceEngine = class {
331
337
 
332
338
  await this.mediaSourceOpen_;
333
339
 
340
+ this.sequenceMode_ = sequenceMode;
341
+
334
342
  for (const contentType of streamsByType.keys()) {
335
343
  const stream = streamsByType.get(contentType);
336
344
  goog.asserts.assert(
@@ -348,11 +356,9 @@ shaka.media.MediaSourceEngine = class {
348
356
  mimeType =
349
357
  shaka.media.Transmuxer.convertTsCodecs(contentType, mimeType);
350
358
  }
359
+
351
360
  const sourceBuffer = this.mediaSource_.addSourceBuffer(mimeType);
352
- if (sequenceMode) {
353
- sourceBuffer.mode =
354
- shaka.media.MediaSourceEngine.SourceBufferMode_.SEQUENCE;
355
- }
361
+
356
362
  this.eventManager_.listen(
357
363
  sourceBuffer, 'error',
358
364
  () => this.onError_(contentType));
@@ -515,35 +521,29 @@ shaka.media.MediaSourceEngine = class {
515
521
  * @param {?boolean} hasClosedCaptions True if the buffer contains CEA closed
516
522
  * captions
517
523
  * @param {boolean=} seeked True if we just seeked
518
- * @param {boolean=} sequenceMode True if sequence mode
519
524
  * @return {!Promise}
520
525
  */
521
- async appendBuffer(contentType, data, startTime, endTime, hasClosedCaptions,
522
- seeked, sequenceMode) {
526
+ async appendBuffer(
527
+ contentType, data, startTime, endTime, hasClosedCaptions, seeked) {
523
528
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
524
529
 
525
- if (startTime != null && sequenceMode && contentType != ContentType.TEXT) {
526
- // If we just cleared buffer and is on an unbuffered seek, we need to set
527
- // the new timestampOffset of the sourceBuffer.
528
- // Don't do this for text streams, though, since they don't use
529
- // MediaSource anyway.
530
- if (seeked) {
531
- const timestampOffset = /** @type {number} */ (startTime);
532
- this.enqueueOperation_(
533
- contentType,
534
- () => this.setTimestampOffset_(contentType, timestampOffset));
530
+ if (contentType == ContentType.TEXT) {
531
+ if (this.sequenceMode_) {
532
+ // This won't be known until the first video segment is appended.
533
+ const offset = await this.textSequenceModeOffset_;
534
+ this.textEngine_.setTimestampOffset(offset);
535
535
  }
536
+ await this.textEngine_.appendBuffer(data, startTime, endTime);
537
+ return;
536
538
  }
537
539
 
538
- if (contentType == ContentType.TEXT) {
539
- await this.textEngine_.appendBuffer(data, startTime, endTime);
540
- } else if (this.transmuxers_[contentType]) {
540
+ if (this.transmuxers_[contentType]) {
541
541
  const transmuxedData =
542
542
  await this.transmuxers_[contentType].transmux(data);
543
543
  // For HLS CEA-608/708 CLOSED-CAPTIONS, text data is embedded in
544
544
  // the video stream, so textEngine may not have been initialized.
545
545
  if (!this.textEngine_) {
546
- this.reinitText('text/vtt', sequenceMode || false);
546
+ this.reinitText('text/vtt', this.sequenceMode_);
547
547
  }
548
548
 
549
549
  if (transmuxedData.metadata) {
@@ -562,15 +562,10 @@ shaka.media.MediaSourceEngine = class {
562
562
  closedCaptions, startTime, endTime, videoOffset);
563
563
  }
564
564
 
565
- let transmuxedSegment = transmuxedData.data;
566
- transmuxedSegment = this.workAroundBrokenPlatforms_(
567
- transmuxedSegment, startTime, contentType);
568
-
569
- await this.enqueueOperation_(
570
- contentType, () => this.append_(contentType, transmuxedSegment));
565
+ data = transmuxedData.data;
571
566
  } else if (hasClosedCaptions) {
572
567
  if (!this.textEngine_) {
573
- this.reinitText('text/vtt', sequenceMode || false);
568
+ this.reinitText('text/vtt', this.sequenceMode_);
574
569
  }
575
570
  // If it is the init segment for closed captions, initialize the closed
576
571
  // caption parser.
@@ -585,19 +580,78 @@ shaka.media.MediaSourceEngine = class {
585
580
  closedCaptions, startTime, endTime, videoOffset);
586
581
  }
587
582
  }
583
+ }
588
584
 
589
- data = this.workAroundBrokenPlatforms_(data, startTime, contentType);
585
+ data = this.workAroundBrokenPlatforms_(data, startTime, contentType);
586
+
587
+ const sourceBuffer = this.sourceBuffers_[contentType];
588
+ const SEQUENCE = shaka.media.MediaSourceEngine.SourceBufferMode_.SEQUENCE;
589
+
590
+ if (this.sequenceMode_ && sourceBuffer.mode != SEQUENCE &&
591
+ startTime != null) {
592
+ // This is the first media segment to be appended to a SourceBuffer in
593
+ // sequence mode. We set the mode late so that we can trick MediaSource
594
+ // into extracting a timestamp for us to align text segments in sequence
595
+ // mode.
596
+
597
+ // Timestamps can only be reliably extracted from video, not audio.
598
+ // Packed audio formats do not have internal timestamps at all.
599
+ // Prefer video for this when available.
600
+ const isBestSourceBufferForTimestamps =
601
+ contentType == ContentType.VIDEO ||
602
+ !(ContentType.VIDEO in this.sourceBuffers_);
603
+ if (isBestSourceBufferForTimestamps) {
604
+ // Append the segment in segments mode first, with offset of 0 and an
605
+ // open append window.
606
+ const originalRange =
607
+ [sourceBuffer.appendWindowStart, sourceBuffer.appendWindowEnd];
608
+ sourceBuffer.appendWindowStart = 0;
609
+ sourceBuffer.appendWindowEnd = Infinity;
610
+
611
+ const originalOffset = sourceBuffer.timestampOffset;
612
+ sourceBuffer.timestampOffset = 0;
613
+
614
+ await this.enqueueOperation_(
615
+ contentType, () => this.append_(contentType, data));
616
+
617
+ // Reset the offset and append window.
618
+ sourceBuffer.timestampOffset = originalOffset;
619
+ sourceBuffer.appendWindowStart = originalRange[0];
620
+ sourceBuffer.appendWindowEnd = originalRange[1];
621
+
622
+ // Now get the timestamp of the segment and compute the offset for text
623
+ // segments.
624
+ const mediaStartTime = shaka.media.TimeRangesUtils.bufferStart(
625
+ this.getBuffered_(contentType));
626
+ const textOffset = (startTime || 0) - (mediaStartTime || 0);
627
+ this.textSequenceModeOffset_.resolve(textOffset);
628
+
629
+ // Finally, clear the buffer.
630
+ await this.enqueueOperation_(
631
+ contentType,
632
+ () => this.remove_(contentType, 0, this.mediaSource_.duration));
633
+ }
590
634
 
591
- await this.enqueueOperation_(
592
- contentType,
593
- () => this.append_(contentType, data));
594
- } else {
595
- data = this.workAroundBrokenPlatforms_(data, startTime, contentType);
635
+ // Now switch to sequence mode and fall through to our normal operations.
636
+ sourceBuffer.mode = SEQUENCE;
637
+ }
596
638
 
597
- await this.enqueueOperation_(
598
- contentType,
599
- () => this.append_(contentType, data));
639
+ if (startTime != null && this.sequenceMode_ &&
640
+ contentType != ContentType.TEXT) {
641
+ // In sequence mode, for non-text streams, if we just cleared the buffer
642
+ // and are performing an unbuffered seek, we need to set a new
643
+ // timestampOffset on the sourceBuffer.
644
+ if (seeked) {
645
+ const timestampOffset = /** @type {number} */ (startTime);
646
+ this.enqueueOperation_(
647
+ contentType,
648
+ () => this.setTimestampOffset_(contentType, timestampOffset));
649
+ }
600
650
  }
651
+
652
+ await this.enqueueOperation_(
653
+ contentType,
654
+ () => this.append_(contentType, data));
601
655
  }
602
656
 
603
657
  /**
@@ -1605,8 +1605,7 @@ shaka.media.StreamingEngine = class {
1605
1605
  reference.syncTime == null ? reference.startTime : reference.syncTime,
1606
1606
  reference.endTime,
1607
1607
  hasClosedCaptions,
1608
- seeked,
1609
- this.manifest_.sequenceMode);
1608
+ seeked);
1610
1609
  this.destroyer_.ensureNotDestroyed();
1611
1610
  shaka.log.v2(logPrefix, 'appended media segment');
1612
1611
  }
@@ -368,8 +368,10 @@ shaka.offline.indexeddb.StorageMechanism.SESSION_ID_STORE = 'session-ids';
368
368
  shaka.offline.StorageMuxer.register(
369
369
  'idb',
370
370
  () => {
371
- // Offline storage is not supported on the Chromecast platform.
372
- if (shaka.util.Platform.isChromecast()) {
371
+ // Offline storage is not supported on the Chromecast or Xbox One
372
+ // platforms.
373
+ if (shaka.util.Platform.isChromecast() ||
374
+ shaka.util.Platform.isXboxOne()) {
373
375
  return null;
374
376
  }
375
377
  // Offline storage requires the IndexedDB API.