shaka-player 4.12.1 → 4.12.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.
@@ -18,39 +18,39 @@
18
18
  * @template T
19
19
  * @exportDoc
20
20
  */
21
- shaka.extern.IAbortableOperation = function() {};
22
-
23
-
24
- /**
25
- * A Promise which represents the underlying operation. It is resolved when
26
- * the operation is complete, and rejected if the operation fails or is
27
- * aborted. Aborted operations should be rejected with a shaka.util.Error
28
- * object using the error code OPERATION_ABORTED.
29
- *
30
- * @const {!Promise.<T>}
31
- * @exportDoc
32
- */
33
- shaka.extern.IAbortableOperation.prototype.promise;
34
-
35
-
36
- /**
37
- * Can be called by anyone holding this object to abort the underlying
38
- * operation. This is not cancelation, and will not necessarily result in
39
- * any work being undone. abort() should return a Promise which is resolved
40
- * when the underlying operation has been aborted. The returned Promise
41
- * should never be rejected.
42
- *
43
- * @return {!Promise}
44
- * @exportDoc
45
- */
46
- shaka.extern.IAbortableOperation.prototype.abort = function() {};
47
-
48
-
49
- /**
50
- * @param {function(boolean)} onFinal A callback to be invoked after the
51
- * operation succeeds or fails. The boolean argument is true if the operation
52
- * succeeded and false if it failed.
53
- * @return {!shaka.extern.IAbortableOperation.<T>} Returns this.
54
- * @exportDoc
55
- */
56
- shaka.extern.IAbortableOperation.prototype.finally = function(onFinal) {};
21
+ shaka.extern.IAbortableOperation = class {
22
+ constructor() {
23
+ /**
24
+ * A Promise which represents the underlying operation. It is resolved when
25
+ * the operation is complete, and rejected if the operation fails or is
26
+ * aborted. Aborted operations should be rejected with a shaka.util.Error
27
+ * object using the error code OPERATION_ABORTED.
28
+ *
29
+ * @const {!Promise.<T>}
30
+ * @exportDoc
31
+ */
32
+ this.promise;
33
+ }
34
+
35
+ /**
36
+ * Can be called by anyone holding this object to abort the underlying
37
+ * operation. This is not cancelation, and will not necessarily result in
38
+ * any work being undone. abort() should return a Promise which is resolved
39
+ * when the underlying operation has been aborted. The returned Promise
40
+ * should never be rejected.
41
+ *
42
+ * @return {!Promise}
43
+ * @exportDoc
44
+ */
45
+ abort() {}
46
+
47
+
48
+ /**
49
+ * @param {function(boolean)} onFinal A callback to be invoked after the
50
+ * operation succeeds or fails. The boolean argument is true
51
+ * if the operation succeeded and false if it failed.
52
+ * @return {!shaka.extern.IAbortableOperation.<T>} Returns this.
53
+ * @exportDoc
54
+ */
55
+ finally(onFinal) {}
56
+ };
@@ -1000,6 +1000,8 @@ shaka.ads.InterstitialAdManager = class {
1000
1000
  goog.asserts.assert(this.player_, 'Must have player');
1001
1001
  goog.asserts.assert(this.basePlayer_, 'Must have base player');
1002
1002
  this.player_.configure(this.basePlayer_.getNonDefaultConfiguration());
1003
+ this.player_.configure('ads.disableHLSInterstitial', true);
1004
+ this.player_.configure('ads.disableDASHInterstitial', true);
1003
1005
  const netEngine = this.player_.getNetworkingEngine();
1004
1006
  goog.asserts.assert(netEngine, 'Need networking engine');
1005
1007
  netEngine.clearAllRequestFilters();
@@ -4127,7 +4127,7 @@ shaka.hls.HlsParser = class {
4127
4127
  let aesKey = undefined;
4128
4128
 
4129
4129
  let discontinuitySequence = shaka.hls.Utils.getFirstTagWithNameAsNumber(
4130
- playlist.tags, 'EXT-X-DISCONTINUITY-SEQUENCE', 0);
4130
+ playlist.tags, 'EXT-X-DISCONTINUITY-SEQUENCE', -1);
4131
4131
  const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
4132
4132
  playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
4133
4133
  const skipTag = shaka.hls.Utils.getFirstTagWithName(
@@ -1717,6 +1717,7 @@ shaka.media.DrmEngine = class {
1717
1717
  const testKeySystems = [
1718
1718
  'org.w3.clearkey',
1719
1719
  'com.widevine.alpha',
1720
+ 'com.widevine.alpha.experiment', // Widevine L1 in Windows
1720
1721
  'com.microsoft.playready',
1721
1722
  'com.microsoft.playready.hardware',
1722
1723
  'com.microsoft.playready.recommendation',
@@ -1741,6 +1742,7 @@ shaka.media.DrmEngine = class {
1741
1742
 
1742
1743
  const testRobustness = {
1743
1744
  'com.widevine.alpha': widevineRobustness,
1745
+ 'com.widevine.alpha.experiment': widevineRobustness,
1744
1746
  'com.microsoft.playready.recommendation': playreadyRobustness,
1745
1747
  };
1746
1748
 
@@ -1139,7 +1139,8 @@ shaka.media.MediaSourceEngine = class {
1139
1139
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
1140
1140
 
1141
1141
  if (contentType == ContentType.TEXT) {
1142
- if (this.sequenceMode_) {
1142
+ if (this.manifestType_ == shaka.media.ManifestParser.HLS &&
1143
+ reference.discontinuitySequence >= 0) {
1143
1144
  // This won't be known until the first video segment is appended.
1144
1145
  const offset = await this.textSequenceModeOffset_;
1145
1146
  this.textEngine_.setTimestampOffset(offset);
@@ -1233,7 +1234,7 @@ shaka.media.MediaSourceEngine = class {
1233
1234
  const isBestSourceBufferForTimestamps =
1234
1235
  contentType == ContentType.VIDEO ||
1235
1236
  !(ContentType.VIDEO in this.sourceBuffers_);
1236
- if (this.sequenceMode_ && isBestSourceBufferForTimestamps) {
1237
+ if (isBestSourceBufferForTimestamps) {
1237
1238
  this.textSequenceModeOffset_.resolve(timestampOffset);
1238
1239
  }
1239
1240
  }
@@ -1562,6 +1563,10 @@ shaka.media.MediaSourceEngine = class {
1562
1563
  this.textSequenceModeOffset_ = new shaka.util.PublicPromise();
1563
1564
  }
1564
1565
 
1566
+ if (!this.sequenceMode_) {
1567
+ return;
1568
+ }
1569
+
1565
1570
  // Queue an abort() to help MSE splice together overlapping segments.
1566
1571
  // We set appendWindowEnd when we change periods in DASH content, and the
1567
1572
  // period transition may result in overlap.
@@ -2095,6 +2100,48 @@ shaka.media.MediaSourceEngine = class {
2095
2100
  null);
2096
2101
  }
2097
2102
 
2103
+ /**
2104
+ * Returns the source buffer parameters
2105
+ * @param {shaka.util.ManifestParserUtils.ContentType} contentType
2106
+ * @return {?shaka.media.MediaSourceEngine.SourceBufferParams}
2107
+ * @private
2108
+ */
2109
+ getSourceBufferParams_(contentType) {
2110
+ if (!this.sourceBuffers_[contentType]) {
2111
+ return null;
2112
+ }
2113
+ return {
2114
+ timestampOffset: this.sourceBuffers_[contentType].timestampOffset,
2115
+ appendWindowStart: this.sourceBuffers_[contentType].appendWindowStart,
2116
+ appendWindowEnd: this.sourceBuffers_[contentType].appendWindowEnd,
2117
+ };
2118
+ }
2119
+
2120
+ /**
2121
+ * Restore source buffer parameters
2122
+ * @param {shaka.util.ManifestParserUtils.ContentType} contentType
2123
+ * @param {?shaka.media.MediaSourceEngine.SourceBufferParams} params
2124
+ * @private
2125
+ */
2126
+ restoreSourceBufferParams_(contentType, params) {
2127
+ if (!params) {
2128
+ return;
2129
+ }
2130
+
2131
+ if (!this.sourceBuffers_[contentType]) {
2132
+ shaka.log.warning('Attempted to restore a non-existent source buffer');
2133
+ return;
2134
+ }
2135
+
2136
+ this.sourceBuffers_[contentType].timestampOffset =
2137
+ params.timestampOffset;
2138
+ // `end` needs to be set before `start`
2139
+ this.sourceBuffers_[contentType].appendWindowEnd =
2140
+ params.appendWindowEnd;
2141
+ this.sourceBuffers_[contentType].appendWindowStart =
2142
+ params.appendWindowStart;
2143
+ }
2144
+
2098
2145
  /**
2099
2146
  * Resets the MediaSource and re-adds source buffers due to codec mismatch
2100
2147
  *
@@ -2155,7 +2202,6 @@ shaka.media.MediaSourceEngine = class {
2155
2202
  }
2156
2203
  await Promise.all(cleanup);
2157
2204
  this.transmuxers_ = {};
2158
- this.sourceBuffers_ = {};
2159
2205
 
2160
2206
  const previousDuration = this.mediaSource_.duration;
2161
2207
  this.mediaSourceOpen_ = new shaka.util.PublicPromise();
@@ -2186,6 +2232,7 @@ shaka.media.MediaSourceEngine = class {
2186
2232
  onSourceBufferAdded);
2187
2233
 
2188
2234
  for (const contentType of streamsByType.keys()) {
2235
+ const previousParams = this.getSourceBufferParams_(contentType);
2189
2236
  const stream = streamsByType.get(contentType);
2190
2237
  // eslint-disable-next-line no-await-in-loop
2191
2238
  await this.initSourceBuffer_(contentType, stream, stream.codecs);
@@ -2195,6 +2242,8 @@ shaka.media.MediaSourceEngine = class {
2195
2242
  } else {
2196
2243
  this.queues_[contentType] = [];
2197
2244
  }
2245
+
2246
+ this.restoreSourceBufferParams_(contentType, previousParams);
2198
2247
  }
2199
2248
  const audio = streamsByType.get(ContentType.AUDIO);
2200
2249
  if (audio && audio.isAudioMuxedInVideo) {
@@ -2492,3 +2541,12 @@ shaka.media.MediaSourceEngine.SourceBufferMode_ = {
2492
2541
  * Called when an embedded 'emsg' box should trigger a manifest update.
2493
2542
  */
2494
2543
  shaka.media.MediaSourceEngine.PlayerInterface;
2544
+
2545
+ /**
2546
+ * @typedef {{
2547
+ * timestampOffset: number,
2548
+ * appendWindowStart: number,
2549
+ * appendWindowEnd: number
2550
+ * }}
2551
+ */
2552
+ shaka.media.MediaSourceEngine.SourceBufferParams;
@@ -403,7 +403,12 @@ shaka.media.PreloadManager = class extends shaka.util.FakeEventTarget {
403
403
 
404
404
  this.successPromise_.resolve();
405
405
  } catch (error) {
406
- this.successPromise_.reject(error);
406
+ // Ignore OPERATION_ABORTED and OBJECT_DESTROYED errors.
407
+ if (!(error instanceof shaka.util.Error) ||
408
+ (error.code != shaka.util.Error.Code.OPERATION_ABORTED &&
409
+ error.code != shaka.util.Error.Code.OBJECT_DESTROYED)) {
410
+ this.successPromise_.reject(error);
411
+ }
407
412
  }
408
413
  })();
409
414
  }
@@ -295,7 +295,7 @@ shaka.media.SegmentReference = class {
295
295
  this.thumbnailSprite = null;
296
296
 
297
297
  /** @type {number} */
298
- this.discontinuitySequence = 0;
298
+ this.discontinuitySequence = -1;
299
299
 
300
300
  /** @type {boolean} */
301
301
  this.allPartialSegments = allPartialSegments;
@@ -2156,17 +2156,15 @@ shaka.media.StreamingEngine = class {
2156
2156
  }
2157
2157
  }
2158
2158
 
2159
- if (this.manifest_.sequenceMode) {
2160
- const lastDiscontinuitySequence =
2161
- mediaState.lastSegmentReference ?
2162
- mediaState.lastSegmentReference.discontinuitySequence : null;
2163
- // Across discontinuity bounds, we should resync timestamps for
2164
- // sequence mode playbacks. The next segment appended should
2165
- // land at its theoretical timestamp from the segment index.
2166
- if (reference.discontinuitySequence != lastDiscontinuitySequence) {
2167
- operations.push(this.playerInterface_.mediaSourceEngine.resync(
2168
- mediaState.type, reference.startTime));
2169
- }
2159
+ const lastDiscontinuitySequence =
2160
+ mediaState.lastSegmentReference ?
2161
+ mediaState.lastSegmentReference.discontinuitySequence : null;
2162
+ // Across discontinuity bounds, we should resync timestamps. The next
2163
+ // segment appended should land at its theoretical timestamp from the
2164
+ // segment index.
2165
+ if (reference.discontinuitySequence != lastDiscontinuitySequence) {
2166
+ operations.push(this.playerInterface_.mediaSourceEngine.resync(
2167
+ mediaState.type, reference.startTime));
2170
2168
  }
2171
2169
 
2172
2170
  await Promise.all(operations);
package/lib/player.js CHANGED
@@ -8269,7 +8269,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
8269
8269
  * @export
8270
8270
  */
8271
8271
  // eslint-disable-next-line no-useless-concat, max-len
8272
- shaka.Player.version = 'v4.12.1' + '-uncompiled'; // x-release-please-version
8272
+ shaka.Player.version = 'v4.12.2' + '-uncompiled'; // x-release-please-version
8273
8273
 
8274
8274
  // Initialize the deprecation system using the version string we just set
8275
8275
  // on the player.
@@ -327,18 +327,56 @@ shaka.polyfill.MediaCapabilities = class {
327
327
 
328
328
  let displayType = videoConfig.contentType;
329
329
  if (videoConfig.width && videoConfig.height) {
330
- displayType +=
331
- `; width=${videoConfig.width}; height=${videoConfig.height}`;
330
+ // All Chromecast can support 720p videos
331
+ if (videoConfig.width > 1280 || videoConfig.height > 720) {
332
+ displayType +=
333
+ `; width=${videoConfig.width}; height=${videoConfig.height}`;
334
+ }
332
335
  }
333
336
  if (videoConfig.framerate) {
334
- displayType += `; framerate=${videoConfig.framerate}`;
337
+ // All Chromecast can support a framerate of 24, 25 or 30.
338
+ const framerate = Math.round(videoConfig.framerate);
339
+ if (framerate < 24 || framerate > 30) {
340
+ displayType += `; framerate=${videoConfig.framerate}`;
341
+ }
335
342
  }
336
- if (videoConfig.transferFunction === 'pq') {
337
- // A "PQ" transfer function indicates this is an HDR-capable stream;
338
- // "smpte2084" is the published standard. We need to inform the platform
339
- // this query is specifically for HDR.
340
- displayType += '; eotf=smpte2084';
343
+
344
+ // Don't trust Closure types here. Although transferFunction is string or
345
+ // undefined, we don't want to count on the input type. A switch statement
346
+ // will, however, differentiate between null and undefined. So we default
347
+ // to a blank string.
348
+ const transferFunction = videoConfig.transferFunction || '';
349
+
350
+ // Based on internal sources. Googlers, see go/cast-hdr-queries for source.
351
+ switch (transferFunction) {
352
+ // The empty case falls through to SDR.
353
+ case '':
354
+ // These are the only 3 values defined by MCap as of November 2024.
355
+ case 'srgb':
356
+ // https://en.wikipedia.org/wiki/Standard-dynamic-range_video
357
+ // https://en.wikipedia.org/wiki/SRGB
358
+ // https://en.wikipedia.org/wiki/Rec._709
359
+ // This is SDR, standardized in BT 709.
360
+ // The platform recognizes "eotf=bt709", but we can also omit it.
361
+ break;
362
+
363
+ case 'pq':
364
+ // https://en.wikipedia.org/wiki/Perceptual_quantizer
365
+ // This HDR transfer function is standardized as SMPTE ST 2084.
366
+ displayType += '; eotf=smpte2084';
367
+ break;
368
+
369
+ case 'hlg':
370
+ // https://en.wikipedia.org/wiki/Hybrid_log%E2%80%93gamma
371
+ // This HDR transfer function is standardized as ARIB STD-B67.
372
+ displayType += '; eotf=arib-std-b67';
373
+ break;
374
+
375
+ default:
376
+ // An unrecognized transfer function. Reject this query.
377
+ return false;
341
378
  }
379
+
342
380
  let result = false;
343
381
  if (displayType in shaka.polyfill.MediaCapabilities
344
382
  .memoizedCanDisplayTypeRequests_) {
@@ -86,13 +86,7 @@ shaka.text.VttTextParser = class {
86
86
  // Only use 'X-TIMESTAMP-MAP' with HLS. This overrides offset above.
87
87
  if (blocks[0].includes('X-TIMESTAMP-MAP') &&
88
88
  this.manifestType_ == shaka.media.ManifestParser.HLS) {
89
- if (this.sequenceMode_) {
90
- // Compute a different, rollover-based offset for sequence mode.
91
- offset = this.computeHlsSequenceModeOffset_(blocks[0], time);
92
- } else {
93
- // Calculate the offset from the segment startTime.
94
- offset = time.segmentStart;
95
- }
89
+ offset = this.computeHlsOffset_(blocks[0], time);
96
90
  }
97
91
 
98
92
  // Parse VTT regions.
@@ -129,7 +123,7 @@ shaka.text.VttTextParser = class {
129
123
  * @return {number}
130
124
  * @private
131
125
  */
132
- computeHlsSequenceModeOffset_(headerBlock, time) {
126
+ computeHlsOffset_(headerBlock, time) {
133
127
  // https://bit.ly/2K92l7y
134
128
  // The 'X-TIMESTAMP-MAP' header is used in HLS to align text with
135
129
  // the rest of the media.
@@ -39,8 +39,8 @@ shaka.util.AbortableOperation = class {
39
39
  /** @private {function():!Promise} */
40
40
  this.onAbort_ = onAbort;
41
41
 
42
- /** @private {boolean} */
43
- this.aborted_ = false;
42
+ /** @private {?Promise} */
43
+ this.abortPromise_ = null;
44
44
  }
45
45
 
46
46
  /**
@@ -48,7 +48,7 @@ shaka.util.AbortableOperation = class {
48
48
  * @export
49
49
  */
50
50
  get aborted() {
51
- return this.aborted_;
51
+ return this.abortPromise_ !== null;
52
52
  }
53
53
 
54
54
  /**
@@ -118,8 +118,10 @@ shaka.util.AbortableOperation = class {
118
118
  * @export
119
119
  */
120
120
  abort() {
121
- this.aborted_ = true;
122
- return this.onAbort_();
121
+ if (!this.abortPromise_) {
122
+ this.abortPromise_ = this.onAbort_();
123
+ }
124
+ return this.abortPromise_;
123
125
  }
124
126
 
125
127
  /**
@@ -174,7 +176,7 @@ shaka.util.AbortableOperation = class {
174
176
 
175
177
  const makeCallback = (isSuccess) => {
176
178
  return (value) => {
177
- if (this.aborted_ && isSuccess) {
179
+ if (this.abortPromise_ && isSuccess) {
178
180
  // If "this" is not abortable(), or if abort() is called after "this"
179
181
  // is complete but before the next stage in the chain begins, we
180
182
  // should stop right away.
@@ -205,6 +205,17 @@ shaka.util.Platform = class {
205
205
  shaka.util.Platform.chromeVersion() === 68;
206
206
  }
207
207
 
208
+ /**
209
+ * Check if the current platform is a WebOS 6.
210
+ *
211
+ * @return {boolean}
212
+ */
213
+ static isWebOS6() {
214
+ // See: https://webostv.developer.lge.com/develop/specifications/web-api-and-web-engine#useragent-string
215
+ return shaka.util.Platform.isWebOS() &&
216
+ shaka.util.Platform.chromeVersion() === 79;
217
+ }
218
+
208
219
  /**
209
220
  * Check if the current platform is a Google Chromecast.
210
221
  *
@@ -642,19 +653,19 @@ shaka.util.Platform = class {
642
653
  /**
643
654
  * Returns if codec switching SMOOTH is known reliable device support.
644
655
  *
645
- * Some devices are known not to support `MediaSource.changeType`
656
+ * Some devices are known not to support <code>SourceBuffer.changeType</code>
646
657
  * well. These devices should use the reload strategy. If a device
647
- * reports that it supports `changeType` but support it unreliably
648
- * it should be added to this list.
658
+ * reports that it supports <code<changeType</code> but supports it unreliably
659
+ * it should be disallowed in this method.
649
660
  *
650
661
  * @return {boolean}
651
662
  */
652
663
  static supportsSmoothCodecSwitching() {
653
664
  const Platform = shaka.util.Platform;
654
- if (Platform.isTizen2() || Platform.isTizen3() || Platform.isTizen4() ||
655
- Platform.isTizen5() || Platform.isTizen6() || Platform.isWebOS3() ||
656
- Platform.isWebOS4() || Platform.isWebOS5() || Platform.isPS4() ||
657
- Platform.isPS5()) {
665
+ // All Tizen versions (up to Tizen 8) do not support SMOOTH so far.
666
+ // webOS seems to support SMOOTH from webOS 22.
667
+ if (Platform.isTizen() || Platform.isPS4() || Platform.isPS5() ||
668
+ Platform.isWebOS6()) {
658
669
  return false;
659
670
  }
660
671
  // Older chromecasts without GoogleTV seem to not support SMOOTH properly.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shaka-player",
3
3
  "description": "DASH/EME video player library",
4
- "version": "4.12.1",
4
+ "version": "4.12.2",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -26,7 +26,7 @@
26
26
  "core-js": "^3.21.1",
27
27
  "dialog-polyfill": "^0.5.6",
28
28
  "es6-promise-polyfill": "^1.2.0",
29
- "eslint": "^8.9.0",
29
+ "eslint": "^8.57.1",
30
30
  "eslint-config-google": "^0.14.0",
31
31
  "eslint-plugin-shaka-rules": "file:./build/eslint-plugin-shaka-rules",
32
32
  "esprima": "^4.0.1",
@@ -119,14 +119,18 @@ shaka.ui.ChapterSelection = class extends shaka.ui.SettingsMenu {
119
119
  const currentLocales = this.localization.getCurrentLocales();
120
120
  for (const locale of Array.from(currentLocales)) {
121
121
  nextLanguage = locale;
122
- nextChapters = this.player.getChapters(nextLanguage);
122
+ // If player is a proxy, and the cast receiver doesn't support this
123
+ // method, you get back undefined.
124
+ nextChapters = this.player.getChapters(nextLanguage) || [];
123
125
  if (nextChapters.length) {
124
126
  break;
125
127
  }
126
128
  }
127
129
  if (!nextChapters.length) {
128
130
  nextLanguage = 'und';
129
- nextChapters = this.player.getChapters(nextLanguage);
131
+ // If player is a proxy, and the cast receiver doesn't support this
132
+ // method, you get back undefined.
133
+ nextChapters = this.player.getChapters(nextLanguage) || [];
130
134
  }
131
135
 
132
136
  const languageChanged = nextLanguage !== this.chaptersLanguage_;