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
@@ -295,6 +295,7 @@ shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS = [
295
295
  'EXT-X-SERVER-CONTROL',
296
296
  'EXT-X-SKIP',
297
297
  'EXT-X-PART-INF',
298
+ 'EXT-X-DATERANGE',
298
299
  ];
299
300
 
300
301
 
@@ -115,11 +115,12 @@ shaka.media.PreferenceBasedCriteria = class {
115
115
  * @param {string=} videoLabel
116
116
  * @param {shaka.config.CodecSwitchingStrategy=} codecSwitchingStrategy
117
117
  * @param {boolean=} enableAudioGroups
118
+ * @param {string=} audioCodec
118
119
  */
119
120
  constructor(language, role, channelCount, hdrLevel, spatialAudio,
120
121
  videoLayout, audioLabel = '', videoLabel = '',
121
122
  codecSwitchingStrategy = shaka.config.CodecSwitchingStrategy.RELOAD,
122
- enableAudioGroups = false) {
123
+ enableAudioGroups = false, audioCodec = '') {
123
124
  /** @private {string} */
124
125
  this.language_ = language;
125
126
  /** @private {string} */
@@ -140,6 +141,8 @@ shaka.media.PreferenceBasedCriteria = class {
140
141
  this.codecSwitchingStrategy_ = codecSwitchingStrategy;
141
142
  /** @private {boolean} */
142
143
  this.enableAudioGroups_ = enableAudioGroups;
144
+ /** @private {string} */
145
+ this.audioCodec_ = audioCodec;
143
146
  }
144
147
 
145
148
  /** @override */
@@ -229,6 +232,16 @@ shaka.media.PreferenceBasedCriteria = class {
229
232
  shaka.log.warning('No exact match for spatial audio could be found.');
230
233
  }
231
234
 
235
+ if (this.audioCodec_) {
236
+ const byAudioCodec = Class.filterVariantsByAudioCodec_(
237
+ current, this.audioCodec_);
238
+ if (byAudioCodec.length) {
239
+ current = byAudioCodec;
240
+ } else {
241
+ shaka.log.warning('No exact match for audio codec could be found.');
242
+ }
243
+ }
244
+
232
245
  const supportsSmoothCodecTransitions = this.codecSwitchingStrategy_ ==
233
246
  shaka.config.CodecSwitchingStrategy.SMOOTH &&
234
247
  shaka.media.Capabilities.isChangeTypeSupported();
@@ -403,4 +416,21 @@ shaka.media.PreferenceBasedCriteria = class {
403
416
  return true;
404
417
  });
405
418
  }
419
+
420
+
421
+ /**
422
+ * Filters variants according to the given audio codec.
423
+ *
424
+ * @param {!Array<shaka.extern.Variant>} variants
425
+ * @param {string} audioCodec
426
+ * @private
427
+ */
428
+ static filterVariantsByAudioCodec_(variants, audioCodec) {
429
+ return variants.filter((variant) => {
430
+ if (variant.audio && variant.audio.codecs != audioCodec) {
431
+ return false;
432
+ }
433
+ return true;
434
+ });
435
+ }
406
436
  };
@@ -2225,9 +2225,12 @@ shaka.media.DrmEngine = class {
2225
2225
  /** @type {!Set.<string>} */
2226
2226
  const keyIds = new Set();
2227
2227
 
2228
+ /** @type {!Set.<string>} */
2229
+ const keySystemUris = new Set();
2230
+
2228
2231
  shaka.media.DrmEngine.processDrmInfos_(
2229
2232
  drmInfos, encryptionSchemes, licenseServers, serverCerts,
2230
- serverCertificateUris, initDatas, keyIds);
2233
+ serverCertificateUris, initDatas, keyIds, keySystemUris);
2231
2234
 
2232
2235
  if (encryptionSchemes.length > 1) {
2233
2236
  shaka.log.warning('Multiple unique encryption schemes found! ' +
@@ -2268,6 +2271,10 @@ shaka.media.DrmEngine = class {
2268
2271
  keyIds,
2269
2272
  };
2270
2273
 
2274
+ if (keySystemUris.size > 0) {
2275
+ res.keySystemUris = keySystemUris;
2276
+ }
2277
+
2271
2278
  for (const info of drmInfos) {
2272
2279
  if (info.distinctiveIdentifierRequired) {
2273
2280
  res.distinctiveIdentifierRequired = info.distinctiveIdentifierRequired;
@@ -2369,11 +2376,12 @@ shaka.media.DrmEngine = class {
2369
2376
  * @param {!Array.<string>} serverCertificateUris
2370
2377
  * @param {!Array.<!shaka.extern.InitDataOverride>} initDatas
2371
2378
  * @param {!Set.<string>} keyIds
2379
+ * @param {!Set.<string>} [keySystemUris]
2372
2380
  * @private
2373
2381
  */
2374
2382
  static processDrmInfos_(
2375
2383
  drmInfos, encryptionSchemes, licenseServers, serverCerts,
2376
- serverCertificateUris, initDatas, keyIds) {
2384
+ serverCertificateUris, initDatas, keyIds, keySystemUris) {
2377
2385
  /** @type {function(shaka.extern.InitDataOverride,
2378
2386
  * shaka.extern.InitDataOverride):boolean} */
2379
2387
  const initDataOverrideEqual = (a, b) => {
@@ -2437,6 +2445,12 @@ shaka.media.DrmEngine = class {
2437
2445
  keyIds.add(keyId);
2438
2446
  }
2439
2447
  }
2448
+
2449
+ if (drmInfo.keySystemUris && keySystemUris) {
2450
+ for (const keySystemUri of drmInfo.keySystemUris) {
2451
+ keySystemUris.add(keySystemUri);
2452
+ }
2453
+ }
2440
2454
  }
2441
2455
 
2442
2456
  if (clearKeyLicenseServers.length == 1) {
@@ -51,11 +51,13 @@ shaka.media.ManifestFilterer = class {
51
51
  goog.asserts.assert(manifest, 'Manifest should exist!');
52
52
  await shaka.util.StreamUtils.filterManifest(this.drmEngine_, manifest,
53
53
  this.config_.drm.preferredKeySystems);
54
- shaka.util.StreamUtils.chooseCodecsAndFilterManifest(
55
- manifest,
56
- this.config_.preferredVideoCodecs,
57
- this.config_.preferredAudioCodecs,
58
- this.config_.preferredDecodingAttributes);
54
+ if (!this.config_.streaming.dontChooseCodecs) {
55
+ shaka.util.StreamUtils.chooseCodecsAndFilterManifest(
56
+ manifest,
57
+ this.config_.preferredVideoCodecs,
58
+ this.config_.preferredAudioCodecs,
59
+ this.config_.preferredDecodingAttributes);
60
+ }
59
61
  this.checkPlayableVariants_(manifest);
60
62
  return this.filterManifestWithRestrictions(manifest);
61
63
  }
@@ -38,11 +38,10 @@ shaka.media.PreloadManager = class extends shaka.util.FakeEventTarget {
38
38
  /**
39
39
  * @param {string} assetUri
40
40
  * @param {?string} mimeType
41
- * @param {number} startTimeOfLoad
42
41
  * @param {?number} startTime
43
42
  * @param {*} playerInterface
44
43
  */
45
- constructor(assetUri, mimeType, startTimeOfLoad, startTime, playerInterface) {
44
+ constructor(assetUri, mimeType, startTime, playerInterface) {
46
45
  super();
47
46
 
48
47
  // Making the playerInterface a * and casting it to the right type allows
@@ -67,9 +66,6 @@ shaka.media.PreloadManager = class extends shaka.util.FakeEventTarget {
67
66
  /** @private {?shaka.media.AdaptationSetCriteria} */
68
67
  this.currentAdaptationSetCriteria_ = null;
69
68
 
70
- /** @private {number} */
71
- this.startTimeOfLoad_ = startTimeOfLoad;
72
-
73
69
  /** @private {number} */
74
70
  this.startTimeOfDrm_ = 0;
75
71
 
@@ -196,16 +192,18 @@ shaka.media.PreloadManager = class extends shaka.util.FakeEventTarget {
196
192
  this.queuedOperations_ = null;
197
193
  }
198
194
 
195
+ /** @param {number} offset */
196
+ setOffsetToStartTime(offset) {
197
+ if (this.startTime_ && offset) {
198
+ this.startTime_ += offset;
199
+ }
200
+ }
201
+
199
202
  /** @return {?number} */
200
203
  getStartTime() {
201
204
  return this.startTime_;
202
205
  }
203
206
 
204
- /** @return {number} */
205
- getStartTimeOfLoad() {
206
- return this.startTimeOfLoad_;
207
- }
208
-
209
207
  /** @return {number} */
210
208
  getStartTimeOfDRM() {
211
209
  return this.startTimeOfDrm_;
@@ -247,12 +247,22 @@ shaka.media.PresentationTimeline = class {
247
247
  return;
248
248
  }
249
249
 
250
- const firstStartTime = timeline[0].start + startOffset;
251
- const lastEndTime = timeline[timeline.length - 1].end + startOffset;
250
+ // Date.now() is in milliseconds, from which we compute "now" in seconds.
251
+ const now = (Date.now() + this.clockOffset_) / 1000.0;
252
+ // Exclude time ranges that are in the "future".
253
+ const timelineForStart = timeline.filter((timeRange) =>
254
+ timeRange.start + startOffset < now);
255
+ if (timelineForStart.length == 0) {
256
+ return;
257
+ }
258
+
259
+ const firstStartTime = timelineForStart[0].start + startOffset;
260
+ const lastEndTime = timelineForStart[timelineForStart.length - 1].end +
261
+ startOffset;
252
262
 
253
263
  this.notifyMinSegmentStartTime(firstStartTime);
254
264
 
255
- this.maxSegmentDuration_ = timeline.reduce(
265
+ this.maxSegmentDuration_ = timelineForStart.reduce(
256
266
  (max, r) => { return Math.max(max, r.end - r.start); },
257
267
  this.maxSegmentDuration_);
258
268
 
@@ -263,8 +273,6 @@ shaka.media.PresentationTimeline = class {
263
273
  !this.startTimeLocked_) {
264
274
  // Since we have explicit segment end times, calculate a presentation
265
275
  // start based on them. This start time accounts for drift.
266
- // Date.now() is in milliseconds, from which we compute "now" in seconds.
267
- const now = (Date.now() + this.clockOffset_) / 1000.0;
268
276
  this.presentationStartTime_ =
269
277
  now - this.maxSegmentEndTime_ - this.maxSegmentDuration_;
270
278
  }
@@ -323,6 +331,17 @@ shaka.media.PresentationTimeline = class {
323
331
  }
324
332
 
325
333
 
334
+ /**
335
+ * Gets the end time of the last available segment.
336
+ *
337
+ * @return {?number}
338
+ * @export
339
+ */
340
+ getMaxSegmentEndTime() {
341
+ return this.maxSegmentEndTime_;
342
+ }
343
+
344
+
326
345
  /**
327
346
  * Lock the presentation timeline's start time. After this is called, no
328
347
  * further adjustments to presentationStartTime_ will be permitted.
@@ -630,6 +649,19 @@ shaka.media.PresentationTimeline = class {
630
649
  this.availabilityTimeOffset_ = offset;
631
650
  }
632
651
 
652
+
653
+ /**
654
+ * Gets the presentation's segment availability time offset. This should be
655
+ * only configured for Low Latency Dash.
656
+ *
657
+ * @return {number} availabilityTimeOffset parameter
658
+ * @export
659
+ */
660
+ getAvailabilityTimeOffset() {
661
+ return this.availabilityTimeOffset_;
662
+ }
663
+
664
+
633
665
  /**
634
666
  * Debug only: assert that the timeline parameters make sense for the type
635
667
  * of presentation (VOD, IPR, live).
@@ -8,8 +8,10 @@ goog.provide('shaka.media.QualityObserver');
8
8
 
9
9
  goog.require('shaka.media.IPlayheadObserver');
10
10
  goog.require('shaka.log');
11
+ goog.require('shaka.util.ArrayUtils');
11
12
  goog.require('shaka.util.FakeEvent');
12
13
  goog.require('shaka.util.FakeEventTarget');
14
+ goog.require('shaka.util.ManifestParserUtils');
13
15
 
14
16
  /**
15
17
  * Monitors the quality of content being appended to the source buffers and
@@ -152,12 +154,39 @@ shaka.media.QualityObserver = class extends shaka.util.FakeEventTarget {
152
154
  /** @override */
153
155
  poll(positionInSeconds, wasSeeking) {
154
156
  for (const contentTypeState of this.contentTypeStates_.values()) {
157
+ const currentQuality = contentTypeState.currentQuality;
155
158
  const qualityAtPosition =
156
159
  shaka.media.QualityObserver.getMediaQualityAtPosition_(
157
160
  positionInSeconds, contentTypeState);
158
- if (qualityAtPosition &&
161
+
162
+ const differentQualities = qualityAtPosition &&
159
163
  !shaka.media.QualityObserver.mediaQualitiesAreTheSame_(
160
- contentTypeState.currentQuality, qualityAtPosition)) {
164
+ currentQuality, qualityAtPosition);
165
+ const differentLabel = qualityAtPosition && currentQuality &&
166
+ qualityAtPosition.label && currentQuality.label &&
167
+ currentQuality.label !== qualityAtPosition.label;
168
+ const differentLanguage = qualityAtPosition && currentQuality &&
169
+ qualityAtPosition.language && currentQuality.language &&
170
+ currentQuality.language !== qualityAtPosition.language;
171
+ const differentRoles = qualityAtPosition && currentQuality &&
172
+ qualityAtPosition.roles && currentQuality.roles &&
173
+ !shaka.util.ArrayUtils.equal(currentQuality.roles,
174
+ qualityAtPosition.roles);
175
+
176
+ if (differentLabel || differentLanguage || differentRoles) {
177
+ if (this.positionIsBuffered_(
178
+ positionInSeconds, qualityAtPosition.contentType)) {
179
+ contentTypeState.currentQuality = qualityAtPosition;
180
+
181
+ const event = new shaka.util.FakeEvent('audiotrackchange', new Map([
182
+ ['quality', qualityAtPosition],
183
+ ['position', positionInSeconds],
184
+ ]));
185
+ this.dispatchEvent(event);
186
+ }
187
+ }
188
+
189
+ if (differentQualities) {
161
190
  if (this.positionIsBuffered_(
162
191
  positionInSeconds, qualityAtPosition.contentType)) {
163
192
  contentTypeState.currentQuality = qualityAtPosition;
@@ -232,6 +261,43 @@ shaka.media.QualityObserver = class extends shaka.util.FakeEventTarget {
232
261
  contentTypeState.qualityChangePositions = [];
233
262
  }
234
263
  }
264
+
265
+ /**
266
+ * Create a MediaQualityInfo object from a stream object.
267
+ *
268
+ * @param {!shaka.extern.Stream} stream
269
+ * @return {!shaka.extern.MediaQualityInfo}
270
+ */
271
+ static createQualityInfo(stream) {
272
+ const basicQuality = {
273
+ bandwidth: stream.bandwidth || 0,
274
+ audioSamplingRate: null,
275
+ codecs: stream.codecs,
276
+ contentType: stream.type,
277
+ frameRate: null,
278
+ height: null,
279
+ mimeType: stream.mimeType,
280
+ channelsCount: null,
281
+ pixelAspectRatio: null,
282
+ width: null,
283
+ label: null,
284
+ roles: stream.roles,
285
+ language: null,
286
+ };
287
+ if (stream.type == shaka.util.ManifestParserUtils.ContentType.VIDEO) {
288
+ basicQuality.frameRate = stream.frameRate || null;
289
+ basicQuality.height = stream.height || null;
290
+ basicQuality.pixelAspectRatio = stream.pixelAspectRatio || null;
291
+ basicQuality.width = stream.width || null;
292
+ }
293
+ if (stream.type == shaka.util.ManifestParserUtils.ContentType.AUDIO) {
294
+ basicQuality.audioSamplingRate = stream.audioSamplingRate;
295
+ basicQuality.channelsCount = stream.channelsCount;
296
+ basicQuality.label = stream.label || null;
297
+ basicQuality.language = stream.language;
298
+ }
299
+ return basicQuality;
300
+ }
235
301
  };
236
302
 
237
303
  /**
@@ -46,7 +46,7 @@ shaka.media.InitSegmentReference = class {
46
46
  /** @const {?number} */
47
47
  this.endByte = endByte;
48
48
 
49
- /** @const {shaka.extern.MediaQualityInfo|null} */
49
+ /** @type {shaka.extern.MediaQualityInfo|null} */
50
50
  this.mediaQuality = mediaQuality;
51
51
 
52
52
  /** @type {number|null} */
@@ -327,12 +327,39 @@ shaka.media.SegmentUtils = class {
327
327
  case 'avc3':
328
328
  codecBase = 'dvav';
329
329
  break;
330
+ case 'av01':
331
+ codecBase = 'dav1';
332
+ break;
330
333
  }
331
334
  const parsedDVCCBox = shaka.util.Mp4BoxParsers.parseDVCC(
332
335
  codecBase, box.reader, box.name);
333
336
  videoCodecs.push(parsedDVCCBox.codec);
334
337
  hasVideo = true;
335
338
  })
339
+ .box('dvvC', (box) => {
340
+ let codecBase = baseBox || '';
341
+ switch (baseBox) {
342
+ case 'hvc1':
343
+ codecBase = 'dvh1';
344
+ break;
345
+ case 'hev1':
346
+ codecBase = 'dvhe';
347
+ break;
348
+ case 'avc1':
349
+ codecBase = 'dva1';
350
+ break;
351
+ case 'avc3':
352
+ codecBase = 'dvav';
353
+ break;
354
+ case 'av01':
355
+ codecBase = 'dav1';
356
+ break;
357
+ }
358
+ const parsedDVCCBox = shaka.util.Mp4BoxParsers.parseDVVC(
359
+ codecBase, box.reader, box.name);
360
+ videoCodecs.push(parsedDVCCBox.codec);
361
+ hasVideo = true;
362
+ })
336
363
  .fullBox('vpcC', (box) => {
337
364
  const codecBase = baseBox || '';
338
365
  const parsedVPCCBox = shaka.util.Mp4BoxParsers.parseVPCC(
@@ -341,7 +368,12 @@ shaka.media.SegmentUtils = class {
341
368
  hasVideo = true;
342
369
  })
343
370
  .box('av1C', (box) => {
344
- const codecBase = baseBox || '';
371
+ let codecBase = baseBox || '';
372
+ switch (baseBox) {
373
+ case 'dav1':
374
+ codecBase = 'av01';
375
+ break;
376
+ }
345
377
  const parsedAV1CBox = shaka.util.Mp4BoxParsers.parseAV1C(
346
378
  codecBase, box.reader, box.name);
347
379
  videoCodecs.push(parsedAV1CBox.codec);
@@ -1182,7 +1182,12 @@ shaka.media.StreamingEngine = class {
1182
1182
  if (!mediaState.stream.segmentIndex) {
1183
1183
  const thisStream = mediaState.stream;
1184
1184
 
1185
- await mediaState.stream.createSegmentIndex();
1185
+ try {
1186
+ await mediaState.stream.createSegmentIndex();
1187
+ } catch (error) {
1188
+ await this.handleStreamingError_(mediaState, error);
1189
+ return;
1190
+ }
1186
1191
 
1187
1192
  if (thisStream != mediaState.stream) {
1188
1193
  // We switched streams while in the middle of this async call to
@@ -1562,9 +1567,11 @@ shaka.media.StreamingEngine = class {
1562
1567
  const isMP4 = stream.mimeType == 'video/mp4' ||
1563
1568
  stream.mimeType == 'audio/mp4';
1564
1569
  const isReadableStreamSupported = window.ReadableStream;
1570
+ const lowLatencyMode = this.config_.lowLatencyMode &&
1571
+ this.manifest_.isLowLatency;
1565
1572
  // Enable MP4 low latency streaming with ReadableStream chunked data.
1566
1573
  // And only for DASH and HLS with byterange optimization.
1567
- if (this.config_.lowLatencyMode && isReadableStreamSupported && isMP4 &&
1574
+ if (lowLatencyMode && isReadableStreamSupported && isMP4 &&
1568
1575
  (this.manifest_.type != shaka.media.ManifestParser.HLS ||
1569
1576
  reference.hasByterangeOptimization())) {
1570
1577
  let remaining = new Uint8Array(0);
@@ -1655,7 +1662,7 @@ shaka.media.StreamingEngine = class {
1655
1662
  reference.startTime, /* skipFirst= */ true);
1656
1663
  }
1657
1664
  } else {
1658
- if (this.config_.lowLatencyMode && !isReadableStreamSupported) {
1665
+ if (lowLatencyMode && !isReadableStreamSupported) {
1659
1666
  shaka.log.warning('Low latency streaming mode is enabled, but ' +
1660
1667
  'ReadableStream is not supported by the browser.');
1661
1668
  }
@@ -1986,46 +1993,11 @@ shaka.media.StreamingEngine = class {
1986
1993
  }
1987
1994
  }
1988
1995
 
1989
- const setProperties = async () => {
1990
- /**
1991
- * @type {!Map.<shaka.util.ManifestParserUtils.ContentType,
1992
- * shaka.extern.Stream>}
1993
- */
1994
- const streamsByType = new Map();
1995
- if (this.currentVariant_.audio) {
1996
- streamsByType.set(ContentType.AUDIO, this.currentVariant_.audio);
1997
- }
1998
- if (this.currentVariant_.video) {
1999
- streamsByType.set(ContentType.VIDEO, this.currentVariant_.video);
2000
- }
2001
- try {
2002
- mediaState.lastAppendWindowStart = appendWindowStart;
2003
- mediaState.lastAppendWindowEnd = appendWindowEnd;
2004
- mediaState.lastCodecs = codecs;
2005
- mediaState.lastMimeType = mimeType;
2006
- mediaState.lastTimestampOffset = timestampOffset;
2007
-
2008
- const ignoreTimestampOffset = this.manifest_.sequenceMode ||
2009
- this.manifest_.type == shaka.media.ManifestParser.HLS;
2010
-
2011
- await this.playerInterface_.mediaSourceEngine.setStreamProperties(
2012
- mediaState.type, timestampOffset, appendWindowStart,
2013
- appendWindowEnd, ignoreTimestampOffset,
2014
- reference.mimeType || mediaState.stream.mimeType,
2015
- reference.codecs || mediaState.stream.codecs, streamsByType);
2016
- } catch (error) {
2017
- mediaState.lastAppendWindowStart = null;
2018
- mediaState.lastAppendWindowEnd = null;
2019
- mediaState.lastCodecs = null;
2020
- mediaState.lastTimestampOffset = null;
2021
-
2022
- throw error;
2023
- }
2024
- };
2025
1996
  // Dispatching init asynchronously causes the sourceBuffers in
2026
1997
  // the MediaSourceEngine to become detached do to race conditions
2027
1998
  // with mediaSource and sourceBuffers being created simultaneously.
2028
- await setProperties();
1999
+ await this.setProperties_(mediaState, timestampOffset, appendWindowStart,
2000
+ appendWindowEnd, reference, codecs, mimeType);
2029
2001
  }
2030
2002
 
2031
2003
  if (!shaka.media.InitSegmentReference.equal(
@@ -2146,6 +2118,62 @@ shaka.media.StreamingEngine = class {
2146
2118
  await Promise.all(operations);
2147
2119
  }
2148
2120
 
2121
+ /**
2122
+ *
2123
+ * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
2124
+ * @param {number} timestampOffset
2125
+ * @param {number} appendWindowStart
2126
+ * @param {number} appendWindowEnd
2127
+ * @param {!shaka.media.SegmentReference} reference
2128
+ * @param {?string=} codecs
2129
+ * @param {?string=} mimeType
2130
+ * @private
2131
+ */
2132
+ async setProperties_(mediaState, timestampOffset, appendWindowStart,
2133
+ appendWindowEnd, reference, codecs, mimeType) {
2134
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
2135
+
2136
+ /**
2137
+ * @type {!Map.<shaka.util.ManifestParserUtils.ContentType,
2138
+ * shaka.extern.Stream>}
2139
+ */
2140
+ const streamsByType = new Map();
2141
+ if (this.currentVariant_.audio) {
2142
+ streamsByType.set(ContentType.AUDIO, this.currentVariant_.audio);
2143
+ }
2144
+ if (this.currentVariant_.video) {
2145
+ streamsByType.set(ContentType.VIDEO, this.currentVariant_.video);
2146
+ }
2147
+ try {
2148
+ mediaState.lastAppendWindowStart = appendWindowStart;
2149
+ mediaState.lastAppendWindowEnd = appendWindowEnd;
2150
+ if (codecs) {
2151
+ mediaState.lastCodecs = codecs;
2152
+ }
2153
+ if (mimeType) {
2154
+ mediaState.lastMimeType = mimeType;
2155
+ }
2156
+ mediaState.lastTimestampOffset = timestampOffset;
2157
+
2158
+ const ignoreTimestampOffset = this.manifest_.sequenceMode ||
2159
+ this.manifest_.type == shaka.media.ManifestParser.HLS;
2160
+
2161
+ await this.playerInterface_.mediaSourceEngine.setStreamProperties(
2162
+ mediaState.type, timestampOffset, appendWindowStart,
2163
+ appendWindowEnd, ignoreTimestampOffset,
2164
+ reference.mimeType || mediaState.stream.mimeType,
2165
+ reference.codecs || mediaState.stream.codecs, streamsByType);
2166
+ } catch (error) {
2167
+ mediaState.lastAppendWindowStart = null;
2168
+ mediaState.lastAppendWindowEnd = null;
2169
+ mediaState.lastCodecs = null;
2170
+ mediaState.lastMimeType = null;
2171
+ mediaState.lastTimestampOffset = null;
2172
+
2173
+ throw error;
2174
+ }
2175
+ }
2176
+
2149
2177
 
2150
2178
  /**
2151
2179
  * Appends the given segment and evicts content if required to append.
@@ -2173,7 +2201,18 @@ shaka.media.StreamingEngine = class {
2173
2201
  const shouldParsePrftBox =
2174
2202
  (this.config_.parsePrftBox && !this.parsedPrftEventRaised_);
2175
2203
 
2176
- if (hasEmsg || shouldParsePrftBox) {
2204
+ const isMP4 = stream.mimeType == 'video/mp4' ||
2205
+ stream.mimeType == 'audio/mp4';
2206
+ let timescale = null;
2207
+ if (reference.initSegmentReference) {
2208
+ timescale = reference.initSegmentReference.timescale;
2209
+ }
2210
+ const shouldFixTimestampOffset = isMP4 && timescale &&
2211
+ stream.type === shaka.util.ManifestParserUtils.ContentType.VIDEO &&
2212
+ this.manifest_.type == shaka.media.ManifestParser.DASH &&
2213
+ this.config_.shouldFixTimestampOffset;
2214
+
2215
+ if (hasEmsg || shouldParsePrftBox || shouldFixTimestampOffset) {
2177
2216
  parser = new shaka.util.Mp4Parser();
2178
2217
  }
2179
2218
 
@@ -2193,8 +2232,46 @@ shaka.media.StreamingEngine = class {
2193
2232
  reference, box));
2194
2233
  }
2195
2234
 
2196
- if (hasEmsg || shouldParsePrftBox) {
2197
- parser.parse(segment);
2235
+ if (shouldFixTimestampOffset) {
2236
+ parser
2237
+ .box('moof', shaka.util.Mp4Parser.children)
2238
+ .box('traf', shaka.util.Mp4Parser.children)
2239
+ .fullBox('tfdt', async (box) => {
2240
+ goog.asserts.assert(
2241
+ box.version != null,
2242
+ 'TFDT is a full box and should have a valid version.');
2243
+
2244
+ const parsedTFDT = shaka.util.Mp4BoxParsers.parseTFDTInaccurate(
2245
+ box.reader, box.version);
2246
+
2247
+ const baseMediaDecodeTime = parsedTFDT.baseMediaDecodeTime;
2248
+
2249
+ // In case the time is 0, it is not updated
2250
+ if (!baseMediaDecodeTime) {
2251
+ return;
2252
+ }
2253
+ goog.asserts.assert(typeof(timescale) == 'number',
2254
+ 'Should be an number!');
2255
+
2256
+ const scaledMediaDecodeTime = -baseMediaDecodeTime / timescale;
2257
+
2258
+ const comparison1 = Number(mediaState.lastTimestampOffset) || 0;
2259
+
2260
+ if (comparison1 < scaledMediaDecodeTime) {
2261
+ const lastAppendWindowStart = mediaState.lastAppendWindowStart;
2262
+ const lastAppendWindowEnd = mediaState.lastAppendWindowEnd;
2263
+ goog.asserts.assert(typeof(lastAppendWindowStart) == 'number',
2264
+ 'Should be an number!');
2265
+ goog.asserts.assert(typeof(lastAppendWindowEnd) == 'number',
2266
+ 'Should be an number!');
2267
+ await this.setProperties_(mediaState, scaledMediaDecodeTime,
2268
+ lastAppendWindowStart, lastAppendWindowEnd, reference);
2269
+ }
2270
+ });
2271
+ }
2272
+
2273
+ if (hasEmsg || shouldParsePrftBox || shouldFixTimestampOffset) {
2274
+ parser.parse(segment, /* partialOkay= */ false, isChunkedData);
2198
2275
  }
2199
2276
 
2200
2277
  await this.evict_(mediaState, presentationTime);
@@ -13,6 +13,7 @@ goog.require('shaka.log');
13
13
  goog.require('shaka.media.InitSegmentReference');
14
14
  goog.require('shaka.media.ManifestParser');
15
15
  goog.require('shaka.media.PresentationTimeline');
16
+ goog.require('shaka.media.QualityObserver');
16
17
  goog.require('shaka.media.SegmentIndex');
17
18
  goog.require('shaka.media.SegmentReference');
18
19
  goog.require('shaka.mss.ContentProtection');
@@ -233,6 +234,11 @@ shaka.mss.MssParser = class {
233
234
  // No-op
234
235
  }
235
236
 
237
+ /** @override */
238
+ setMediaElement(mediaElement) {
239
+ // No-op
240
+ }
241
+
236
242
  /**
237
243
  * Makes a network request for the manifest and parses the resulting data.
238
244
  *
@@ -420,6 +426,9 @@ shaka.mss.MssParser = class {
420
426
  type: shaka.media.ManifestParser.MSS,
421
427
  serviceDescription: null,
422
428
  nextUrl: null,
429
+ periodCount: 1,
430
+ gapCount: 0,
431
+ isLowLatency: false,
423
432
  };
424
433
 
425
434
  // This is the first point where we have a meaningful presentation start
@@ -700,12 +709,14 @@ shaka.mss.MssParser = class {
700
709
  initSegmentData = mp4Generator.initSegment();
701
710
  this.initSegmentDataByStreamId_.set(stream.id, initSegmentData);
702
711
  }
712
+ const qualityInfo =
713
+ shaka.media.QualityObserver.createQualityInfo(stream);
703
714
  const initSegmentRef = new shaka.media.InitSegmentReference(
704
715
  () => [],
705
716
  /* startByte= */ 0,
706
717
  /* endByte= */ null,
707
- /* mediaQuality= */ null,
708
- /* timescale= */ undefined,
718
+ qualityInfo,
719
+ stream.mssPrivateData.timescale,
709
720
  initSegmentData);
710
721
 
711
722
  const segments = this.createSegments_(initSegmentRef,