shaka-player 4.3.7 → 4.3.8

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 (49) hide show
  1. package/.github/workflows/appspot.yaml +39 -0
  2. package/CHANGELOG.md +25 -0
  3. package/CONTRIBUTORS +1 -0
  4. package/demo/custom.js +1 -1
  5. package/dist/demo.compiled.debug.js +2 -2
  6. package/dist/demo.compiled.debug.map +2 -2
  7. package/dist/demo.compiled.js +2 -2
  8. package/dist/demo.compiled.map +2 -2
  9. package/dist/deps.js +1 -1
  10. package/dist/locales.js +31 -31
  11. package/dist/shaka-player.compiled.d.ts +22 -0
  12. package/dist/shaka-player.compiled.debug.d.ts +22 -0
  13. package/dist/shaka-player.compiled.debug.js +215 -213
  14. package/dist/shaka-player.compiled.debug.map +3 -3
  15. package/dist/shaka-player.compiled.js +789 -786
  16. package/dist/shaka-player.compiled.map +4 -4
  17. package/dist/shaka-player.ui.d.ts +22 -0
  18. package/dist/shaka-player.ui.debug.d.ts +22 -0
  19. package/dist/shaka-player.ui.debug.js +253 -252
  20. package/dist/shaka-player.ui.debug.map +3 -3
  21. package/dist/shaka-player.ui.js +823 -820
  22. package/dist/shaka-player.ui.map +4 -4
  23. package/docs/tutorials/drm-config.md +9 -6
  24. package/externs/ima.js +34 -0
  25. package/lib/abr/simple_abr_manager.js +1 -1
  26. package/lib/cea/mp4_cea_parser.js +24 -18
  27. package/lib/dash/dash_parser.js +18 -3
  28. package/lib/hls/hls_parser.js +23 -4
  29. package/lib/media/drm_engine.js +14 -8
  30. package/lib/media/gap_jumping_controller.js +24 -2
  31. package/lib/media/media_source_engine.js +5 -3
  32. package/lib/media/playhead.js +11 -0
  33. package/lib/media/region_observer.js +19 -1
  34. package/lib/media/streaming_engine.js +39 -19
  35. package/lib/media/video_wrapper.js +6 -1
  36. package/lib/player.js +12 -5
  37. package/lib/polyfill/abort_controller.js +7 -0
  38. package/lib/text/vtt_text_parser.js +2 -1
  39. package/lib/util/manifest_parser_utils.js +1 -1
  40. package/lib/util/stream_utils.js +1 -0
  41. package/package.json +2 -2
  42. package/test/cea/mp4_cea_parser_unit.js +28 -5
  43. package/test/dash/dash_parser_manifest_unit.js +120 -0
  44. package/test/hls/hls_parser_unit.js +144 -0
  45. package/test/media/region_observer_unit.js +2 -1
  46. package/test/media/streaming_engine_unit.js +26 -2
  47. package/test/test/assets/multiple-trun-init.mp4 +0 -0
  48. package/test/test/assets/multiple-trun-segment.mp4 +0 -0
  49. package/ui/resolution_selection.js +7 -1
@@ -59,15 +59,18 @@ If this is the only `<ContentProtection>` element in the manifest, Shaka will
59
59
  try all key systems it knows. (Based on keySystemsByURI in
60
60
  {@linksource shaka.extern.DashManifestConfiguration}.)
61
61
 
62
- Through `player.configure()`, you can change the dash key systems mapping by
62
+ Through `player.configure()`, you can update the dash key systems mapping by
63
63
  scheme URI:
64
+
64
65
  ```js
65
66
  player.configure({
66
- dash: {
67
- keySystemsByURI: {
68
- 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready.recommendation',
69
- 'urn:uuid:79f0049a-4098-8642-ab92-e65be0885f95': 'com.microsoft.playready.recommendation',
70
- }
67
+ manifest: {
68
+ dash: {
69
+ keySystemsByURI: {
70
+ 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready.recommendation',
71
+ 'urn:uuid:79f0049a-4098-8642-ab92-e65be0885f95': 'com.microsoft.playready.recommendation',
72
+ }
73
+ }
71
74
  }
72
75
  });
73
76
  ```
package/externs/ima.js CHANGED
@@ -513,6 +513,14 @@ google.ima.dai.api.StreamRequest.prototype.streamActivityMonitorId;
513
513
  google.ima.dai.api.StreamRequest.prototype.format;
514
514
 
515
515
 
516
+ /**
517
+ * @type {Object.<
518
+ * google.ima.dai.api.OmidVerificationVendor,
519
+ * (google.ima.dai.api.OmidAccessMode | undefined)>}
520
+ */
521
+ google.ima.dai.api.StreamRequest.prototype.omidAccessModeRules;
522
+
523
+
516
524
  /**
517
525
  * @enum {string}
518
526
  */
@@ -599,3 +607,29 @@ google.ima.dai.api.StreamEvent.Type = {
599
607
  SKIPPED: 'skip',
600
608
  VIDEO_CLICKED: 'videoClicked',
601
609
  };
610
+
611
+
612
+ /**
613
+ * @enum {number}
614
+ */
615
+ google.ima.dai.api.OmidVerificationVendor = {
616
+ OTHER: 1,
617
+ MOAT: 2,
618
+ DOUBLEVERIFY: 3,
619
+ INTEGRAL_AD_SCIENCE: 4,
620
+ PIXELATE: 5,
621
+ NIELSEN: 6,
622
+ COMSCORE: 7,
623
+ MEETRICS: 8,
624
+ GOOGLE: 9,
625
+ };
626
+
627
+
628
+ /**
629
+ * @enum {string}
630
+ */
631
+ google.ima.dai.api.OmidAccessMode = {
632
+ FULL: 'full',
633
+ DOMAIN: 'domain',
634
+ LIMITED: 'limited',
635
+ };
@@ -54,7 +54,7 @@ shaka.abr.SimpleAbrManager = class {
54
54
  // of connectivity changes.
55
55
  if (navigator.connection && navigator.connection.addEventListener) {
56
56
  this.onNetworkInformationChange_ = () => {
57
- if (this.config_.useNetworkInformation && this.enabled_) {
57
+ if (this.enabled_ && this.config_.useNetworkInformation) {
58
58
  this.bandwidthEstimator_ = new shaka.abr.EwmaBandwidthEstimator();
59
59
  if (this.config_) {
60
60
  this.bandwidthEstimator_.configure(this.config_.advanced);
@@ -163,15 +163,17 @@ shaka.cea.Mp4CeaParser = class {
163
163
  // Fields that are found in MOOF boxes
164
164
  let defaultSampleDuration = this.defaultSampleDuration_;
165
165
  let defaultSampleSize = this.defaultSampleSize_;
166
- let sampleData = [];
167
- let moofOffset = null;
168
- let trunOffset = null;
166
+ let moofOffset = 0;
167
+ /** @type {!Array<shaka.util.ParsedTRUNBox>} */
168
+ let parsedTRUNs = [];
169
169
  let baseMediaDecodeTime = null;
170
170
  let timescale = shaka.cea.ICeaParser.DEFAULT_TIMESCALE_VALUE;
171
171
 
172
172
  new Mp4Parser()
173
173
  .box('moof', (box) => {
174
174
  moofOffset = box.start;
175
+ // trun box parsing is reset on each moof.
176
+ parsedTRUNs = [];
175
177
  Mp4Parser.children(box);
176
178
  })
177
179
  .box('traf', Mp4Parser.children)
@@ -182,11 +184,8 @@ shaka.cea.Mp4CeaParser = class {
182
184
 
183
185
  const parsedTRUN = shaka.util.Mp4BoxParsers.parseTRUN(
184
186
  box.reader, box.version, box.flags);
185
-
186
- sampleData = parsedTRUN.sampleData;
187
- trunOffset = parsedTRUN.dataOffset;
187
+ parsedTRUNs.push(parsedTRUN);
188
188
  })
189
-
190
189
  .fullBox('tfhd', (box) => {
191
190
  goog.asserts.assert(
192
191
  box.flags != null,
@@ -210,7 +209,6 @@ shaka.cea.Mp4CeaParser = class {
210
209
  timescale = this.trackIdToTimescale_.get(trackId);
211
210
  }
212
211
  })
213
-
214
212
  .fullBox('tfdt', (box) => {
215
213
  goog.asserts.assert(
216
214
  box.version != null,
@@ -223,16 +221,19 @@ shaka.cea.Mp4CeaParser = class {
223
221
  })
224
222
  .box('mdat', (box) => {
225
223
  if (baseMediaDecodeTime === null) {
226
- // This field should have been populated by
227
- // the Base Media Decode time in the TFDT box
224
+ // This field should have been populated by the Base Media Decode
225
+ // Time in the tfdt box.
226
+ shaka.log.alwaysWarn(
227
+ 'Unable to find base media decode time for CEA captions!');
228
228
  throw new shaka.util.Error(
229
229
  shaka.util.Error.Severity.CRITICAL,
230
230
  shaka.util.Error.Category.TEXT,
231
231
  shaka.util.Error.Code.INVALID_MP4_CEA);
232
232
  }
233
- const offset = (moofOffset || 0) + (trunOffset || 0) - box.start - 8;
233
+
234
+ const offset = moofOffset - box.start - 8;
234
235
  this.parseMdat_(box.reader, baseMediaDecodeTime, timescale,
235
- defaultSampleDuration, defaultSampleSize, offset, sampleData,
236
+ defaultSampleDuration, defaultSampleSize, offset, parsedTRUNs,
236
237
  captionPackets);
237
238
  })
238
239
  .parse(mediaSegment, /* partialOkay= */ false);
@@ -248,12 +249,12 @@ shaka.cea.Mp4CeaParser = class {
248
249
  * @param {number} defaultSampleDuration
249
250
  * @param {number} defaultSampleSize
250
251
  * @param {number} offset
251
- * @param {!Array<shaka.util.ParsedTRUNSample>} sampleData
252
+ * @param {!Array<shaka.util.ParsedTRUNBox>} parsedTRUNs
252
253
  * @param {!Array<!shaka.cea.ICeaParser.CaptionPacket>} captionPackets
253
254
  * @private
254
255
  */
255
256
  parseMdat_(reader, time, timescale, defaultSampleDuration,
256
- defaultSampleSize, offset, sampleData, captionPackets) {
257
+ defaultSampleSize, offset, parsedTRUNs, captionPackets) {
257
258
  const BitstreamFormat = shaka.cea.Mp4CeaParser.BitstreamFormat;
258
259
  const ICeaParser = shaka.cea.ICeaParser;
259
260
  let sampleIndex = 0;
@@ -264,11 +265,16 @@ shaka.cea.Mp4CeaParser = class {
264
265
  // composition time offset, we default to 0.
265
266
  let sampleSize = defaultSampleSize;
266
267
 
268
+ // Combine all sample data. This assumes that the samples described across
269
+ // multiple trun boxes are still continuous in the mdat box.
270
+ const sampleDatas = parsedTRUNs.map((t) => t.sampleData);
271
+ const sampleData = [].concat(...sampleDatas);
272
+
267
273
  if (sampleData.length) {
268
274
  sampleSize = sampleData[0].sampleSize || defaultSampleSize;
269
275
  }
270
276
 
271
- reader.skip(offset);
277
+ reader.skip(offset + parsedTRUNs[0].dataOffset);
272
278
 
273
279
  while (reader.hasMoreData()) {
274
280
  const naluSize = reader.readUint32();
@@ -301,7 +307,7 @@ shaka.cea.Mp4CeaParser = class {
301
307
  if (isSeiMessage) {
302
308
  let timeOffset = 0;
303
309
 
304
- if (sampleData.length > sampleIndex) {
310
+ if (sampleIndex < sampleData.length) {
305
311
  timeOffset = sampleData[sampleIndex].sampleCompositionTimeOffset || 0;
306
312
  }
307
313
 
@@ -324,7 +330,7 @@ shaka.cea.Mp4CeaParser = class {
324
330
  }
325
331
  sampleSize -= (naluSize + 4);
326
332
  if (sampleSize == 0) {
327
- if (sampleData.length > sampleIndex) {
333
+ if (sampleIndex < sampleData.length) {
328
334
  time += sampleData[sampleIndex].sampleDuration ||
329
335
  defaultSampleDuration;
330
336
  } else {
@@ -333,7 +339,7 @@ shaka.cea.Mp4CeaParser = class {
333
339
 
334
340
  sampleIndex++;
335
341
 
336
- if (sampleData.length > sampleIndex) {
342
+ if (sampleIndex < sampleData.length) {
337
343
  sampleSize = sampleData[sampleIndex].sampleSize || defaultSampleSize;
338
344
  } else {
339
345
  sampleSize = defaultSampleSize;
@@ -886,7 +886,19 @@ shaka.dash.DashParser = class {
886
886
 
887
887
  // Parallel for HLS VIDEO-RANGE as defined in DASH-IF IOP v4.3 6.2.5.1.
888
888
  let videoRange;
889
- const videoRangeScheme = 'urn:mpeg:mpegB:cicp:TransferCharacteristics';
889
+
890
+ // Ref. https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf
891
+ // If signaled, a Supplemental or Essential Property descriptor
892
+ // shall be used, with the schemeIdUri set to
893
+ // urn:mpeg:mpegB:cicp:<Parameter> as defined in
894
+ // ISO/IEC 23001-8 [49] and <Parameter> one of the
895
+ // following: ColourPrimaries, TransferCharacteristics,
896
+ // or MatrixCoefficients.
897
+ const scheme = 'urn:mpeg:mpegB:cicp';
898
+ const transferCharacteristicsScheme = `${scheme}:TransferCharacteristics`;
899
+ const colourPrimariesScheme = `${scheme}:ColourPrimaries`;
900
+ const matrixCoefficientsScheme = `${scheme}:MatrixCoefficients`;
901
+
890
902
  const getVideoRangeFromTransferCharacteristicCICP = (cicp) => {
891
903
  switch (cicp) {
892
904
  case 1:
@@ -912,10 +924,13 @@ shaka.dash.DashParser = class {
912
924
  const schemeId = prop.getAttribute('schemeIdUri');
913
925
  if (schemeId == 'http://dashif.org/guidelines/trickmode') {
914
926
  trickModeFor = prop.getAttribute('value');
915
- } else if (schemeId == videoRangeScheme) {
927
+ } else if (schemeId == transferCharacteristicsScheme) {
916
928
  videoRange = getVideoRangeFromTransferCharacteristicCICP(
917
929
  parseInt(prop.getAttribute('value'), 10),
918
930
  );
931
+ } else if (schemeId == colourPrimariesScheme ||
932
+ schemeId == matrixCoefficientsScheme) {
933
+ continue;
919
934
  } else {
920
935
  unrecognizedEssentialProperty = true;
921
936
  }
@@ -925,7 +940,7 @@ shaka.dash.DashParser = class {
925
940
  XmlUtils.findChildren(elem, 'SupplementalProperty');
926
941
  for (const prop of supplementalProperties) {
927
942
  const schemeId = prop.getAttribute('schemeIdUri');
928
- if (schemeId == videoRangeScheme) {
943
+ if (schemeId == transferCharacteristicsScheme) {
929
944
  videoRange = getVideoRangeFromTransferCharacteristicCICP(
930
945
  parseInt(prop.getAttribute('value'), 10),
931
946
  );
@@ -2066,6 +2066,22 @@ shaka.hls.HlsParser = class {
2066
2066
  // stream is lazy-loaded.
2067
2067
  const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
2068
2068
  this.guessMimeTypeFallback_(type);
2069
+ const roles = [];
2070
+ if (characteristics) {
2071
+ for (const characteristic of characteristics.split(',')) {
2072
+ roles.push(characteristic);
2073
+ }
2074
+ }
2075
+
2076
+ const kind = (type == shaka.util.ManifestParserUtils.ContentType.TEXT) ?
2077
+ shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined;
2078
+
2079
+ // If there are no roles, and we have defaulted to the subtitle "kind" for
2080
+ // this track, add the implied subtitle role.
2081
+ if (!roles.length &&
2082
+ kind === shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE) {
2083
+ roles.push(shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE);
2084
+ }
2069
2085
 
2070
2086
  return {
2071
2087
  id: this.globalId_++,
@@ -2074,8 +2090,7 @@ shaka.hls.HlsParser = class {
2074
2090
  segmentIndex: null,
2075
2091
  mimeType,
2076
2092
  codecs,
2077
- kind: (type == shaka.util.ManifestParserUtils.ContentType.TEXT) ?
2078
- shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined,
2093
+ kind,
2079
2094
  encrypted: false,
2080
2095
  drmInfos: [],
2081
2096
  keyIds: new Set(),
@@ -2091,7 +2106,7 @@ shaka.hls.HlsParser = class {
2091
2106
  width: undefined,
2092
2107
  height: undefined,
2093
2108
  bandwidth: undefined,
2094
- roles: characteristics ? characteristics.split(',') : [],
2109
+ roles,
2095
2110
  forced,
2096
2111
  channelsCount,
2097
2112
  audioSamplingRate: null,
@@ -2549,6 +2564,10 @@ shaka.hls.HlsParser = class {
2549
2564
  // We're interested in the duration part.
2550
2565
  const extinfValues = extinfTag.value.split(',');
2551
2566
  const duration = Number(extinfValues[0]);
2567
+ // Skip segments without duration
2568
+ if (duration == 0) {
2569
+ return null;
2570
+ }
2552
2571
  endTime = startTime + duration;
2553
2572
  } else {
2554
2573
  endTime = partialSegmentRefs[partialSegmentRefs.length - 1].endTime;
@@ -2729,9 +2748,9 @@ shaka.hls.HlsParser = class {
2729
2748
  type,
2730
2749
  lastDiscontinuityStartTime,
2731
2750
  hlsAes128Key);
2732
- previousReference = reference;
2733
2751
 
2734
2752
  if (reference) {
2753
+ previousReference = reference;
2735
2754
  reference.discontinuitySequence = discontinuitySequence;
2736
2755
 
2737
2756
  if (this.config_.hls.ignoreManifestProgramDateTime &&
@@ -354,18 +354,24 @@ shaka.media.DrmEngine = class {
354
354
  shaka.media.DrmEngine.replaceDrmInfo_(variants, servers);
355
355
  }
356
356
 
357
- // Make sure all the drm infos are valid and filled in correctly.
357
+ /** @type {!Set<shaka.extern.DrmInfo>} */
358
+ const drmInfos = new Set();
358
359
  for (const variant of variants) {
359
- const drmInfos = this.getVariantDrmInfos_(variant);
360
- for (const info of drmInfos) {
361
- shaka.media.DrmEngine.fillInDrmInfoDefaults_(
362
- info,
363
- shaka.util.MapUtils.asMap(this.config_.servers),
364
- shaka.util.MapUtils.asMap(this.config_.advanced || {}),
365
- this.config_.keySystemsMapping);
360
+ const variantDrmInfos = this.getVariantDrmInfos_(variant);
361
+ for (const info of variantDrmInfos) {
362
+ drmInfos.add(info);
366
363
  }
367
364
  }
368
365
 
366
+ for (const info of drmInfos) {
367
+ shaka.media.DrmEngine.fillInDrmInfoDefaults_(
368
+ info,
369
+ shaka.util.MapUtils.asMap(this.config_.servers),
370
+ shaka.util.MapUtils.asMap(this.config_.advanced || {}),
371
+ this.config_.keySystemsMapping);
372
+ }
373
+
374
+
369
375
  /** @type {!Map.<string, MediaKeySystemConfiguration>} */
370
376
  let configsByKeySystem;
371
377
 
@@ -58,6 +58,9 @@ shaka.media.GapJumpingController = class {
58
58
  /** @private {number} */
59
59
  this.prevReadyState_ = video.readyState;
60
60
 
61
+ /** @private {number} */
62
+ this.startTime_ = 0;
63
+
61
64
  /** @private {number} */
62
65
  this.gapsJumped_ = 0;
63
66
 
@@ -123,6 +126,18 @@ shaka.media.GapJumpingController = class {
123
126
  this.onPollGapJump_();
124
127
  }
125
128
 
129
+ /**
130
+ * Called when playback has started and the video element is
131
+ * listening for seeks.
132
+ *
133
+ * @param {number} startTime
134
+ */
135
+ onStarted(startTime) {
136
+ if (this.video_.seeking && !this.seekingEventReceived_) {
137
+ this.seekingEventReceived_ = true;
138
+ this.startTime_ = startTime;
139
+ }
140
+ }
126
141
 
127
142
  /** Called when a seek has started. */
128
143
  onSeeking() {
@@ -164,8 +179,8 @@ shaka.media.GapJumpingController = class {
164
179
  // while paused on a livestream. We make an exception for time 0, since we
165
180
  // may be _required_ to seek on startup before play can begin, but only if
166
181
  // autoplay is enabled.
167
- if (this.video_.paused && (this.video_.currentTime != 0 ||
168
- (!this.video_.autoplay && this.video_.currentTime == 0))) {
182
+ if (this.video_.paused && (this.video_.currentTime != this.startTime_ ||
183
+ (!this.video_.autoplay && this.video_.currentTime == this.startTime_))) {
169
184
  return;
170
185
  }
171
186
 
@@ -228,6 +243,13 @@ shaka.media.GapJumpingController = class {
228
243
  }
229
244
 
230
245
  this.video_.currentTime = jumpTo;
246
+ // This accounts for the possibility that we jump a gap at the start
247
+ // position but we jump _into_ another gap. By setting the start
248
+ // position to the new jumpTo we ensure that the check above will
249
+ // pass even though the video is still paused.
250
+ if (currentTime == this.startTime_) {
251
+ this.startTime_ = jumpTo;
252
+ }
231
253
  this.gapsJumped_++;
232
254
  this.onEvent_(
233
255
  new shaka.util.FakeEvent(shaka.util.FakeEvent.EventName.GapJumped));
@@ -582,11 +582,13 @@ shaka.media.MediaSourceEngine = class {
582
582
  * @param {boolean=} seeked True if we just seeked
583
583
  * @param {boolean=} adaptation True if we just automatically switched active
584
584
  * variant(s).
585
+ * @param {boolean=} isChunkedData True if we add to the buffer from the
586
+ * partial read of the segment.
585
587
  * @return {!Promise}
586
588
  */
587
589
  async appendBuffer(
588
590
  contentType, data, reference, hasClosedCaptions, seeked = false,
589
- adaptation = false) {
591
+ adaptation = false, isChunkedData = false) {
590
592
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
591
593
 
592
594
  if (contentType == ContentType.TEXT) {
@@ -787,13 +789,13 @@ shaka.media.MediaSourceEngine = class {
787
789
  let bufferedBefore = null;
788
790
 
789
791
  await this.enqueueOperation_(contentType, () => {
790
- if (goog.DEBUG && reference) {
792
+ if (goog.DEBUG && reference && !reference.isPreload() && !isChunkedData) {
791
793
  bufferedBefore = this.getBuffered_(contentType);
792
794
  }
793
795
  this.append_(contentType, data);
794
796
  });
795
797
 
796
- if (goog.DEBUG && reference && !reference.isPreload()) {
798
+ if (goog.DEBUG && reference && !reference.isPreload() && !isChunkedData) {
797
799
  const bufferedAfter = this.getBuffered_(contentType);
798
800
  const newBuffered = shaka.media.TimeRangesUtils.computeAddedRange(
799
801
  bufferedBefore, bufferedAfter);
@@ -251,6 +251,7 @@ shaka.media.MediaSourcePlayhead = class {
251
251
  this.videoWrapper_ = new shaka.media.VideoWrapper(
252
252
  mediaElement,
253
253
  () => this.onSeeking_(),
254
+ (realStartTime) => this.onStarted_(realStartTime),
254
255
  this.getStartTime_(startTime));
255
256
 
256
257
  /** @type {shaka.util.Timer} */
@@ -390,6 +391,16 @@ shaka.media.MediaSourcePlayhead = class {
390
391
  }
391
392
  }
392
393
 
394
+ /**
395
+ * Called when the video element has started up and is listening for new seeks
396
+ *
397
+ * @param {number} startTime
398
+ * @private
399
+ */
400
+ onStarted_(startTime) {
401
+ this.gapController_.onStarted(startTime);
402
+ }
403
+
393
404
  /**
394
405
  * Handles when a seek happens on the video.
395
406
  *
@@ -27,13 +27,24 @@ shaka.media.RegionObserver = class extends shaka.util.FakeEventTarget {
27
27
  * destroy the timeline.
28
28
  *
29
29
  * @param {!shaka.media.RegionTimeline} timeline
30
+ * @param {boolean} startsPastZero
30
31
  */
31
- constructor(timeline) {
32
+ constructor(timeline, startsPastZero) {
32
33
  super();
33
34
 
34
35
  /** @private {shaka.media.RegionTimeline} */
35
36
  this.timeline_ = timeline;
36
37
 
38
+ /**
39
+ * Whether the asset is expected to start at a time beyond 0 seconds.
40
+ * For example, if the asset is a live stream.
41
+ * If true, we will not start polling for regions until the playhead has
42
+ * moved past 0 seconds, to avoid bad behaviors where the current time is
43
+ * briefly 0 before we have enough data to play.
44
+ * @private {boolean}
45
+ */
46
+ this.startsPastZero_ = startsPastZero;
47
+
37
48
  /**
38
49
  * A mapping between a region and where we previously were relative to it.
39
50
  * When the value here differs from what we calculate, it means we moved and
@@ -121,6 +132,13 @@ shaka.media.RegionObserver = class extends shaka.util.FakeEventTarget {
121
132
  /** @override */
122
133
  poll(positionInSeconds, wasSeeking) {
123
134
  const RegionObserver = shaka.media.RegionObserver;
135
+ if (this.startsPastZero_ && positionInSeconds == 0) {
136
+ // Don't start checking regions until the timeline has begun moving.
137
+ return;
138
+ }
139
+ // Now that we have seen the playhead go past 0, it's okay if it goes
140
+ // back there (e.g. seeking back to the start).
141
+ this.startsPastZero_ = false;
124
142
 
125
143
  for (const region of this.timeline_.regions()) {
126
144
  const previousPosition = this.oldPosition_.get(region);
@@ -1319,7 +1319,8 @@ shaka.media.StreamingEngine = class {
1319
1319
  const dataToAppend = remaining.subarray(0, offset);
1320
1320
  remaining = remaining.subarray(offset);
1321
1321
  await this.append_(
1322
- mediaState, presentationTime, stream, reference, dataToAppend);
1322
+ mediaState, presentationTime, stream, reference, dataToAppend,
1323
+ /* isChunkedData= */ true);
1323
1324
  }
1324
1325
  };
1325
1326
 
@@ -1731,10 +1732,12 @@ shaka.media.StreamingEngine = class {
1731
1732
  * @param {shaka.extern.Stream} stream
1732
1733
  * @param {!shaka.media.SegmentReference} reference
1733
1734
  * @param {BufferSource} segment
1735
+ * @param {boolean=} isChunkedData
1734
1736
  * @return {!Promise}
1735
1737
  * @private
1736
1738
  */
1737
- async append_(mediaState, presentationTime, stream, reference, segment) {
1739
+ async append_(mediaState, presentationTime, stream, reference, segment,
1740
+ isChunkedData = false) {
1738
1741
  const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
1739
1742
 
1740
1743
  const hasClosedCaptions = stream.closedCaptions &&
@@ -1788,7 +1791,8 @@ shaka.media.StreamingEngine = class {
1788
1791
  reference,
1789
1792
  hasClosedCaptions,
1790
1793
  seeked,
1791
- adaptation);
1794
+ adaptation,
1795
+ isChunkedData);
1792
1796
  this.destroyer_.ensureNotDestroyed();
1793
1797
  shaka.log.v2(logPrefix, 'appended media segment');
1794
1798
  }
@@ -1852,7 +1856,6 @@ shaka.media.StreamingEngine = class {
1852
1856
  const messageData = box.reader.readBytes(
1853
1857
  box.reader.getLength() - box.reader.getPosition());
1854
1858
 
1855
-
1856
1859
  // See DASH sec. 5.10.3.3.1
1857
1860
  // If a DASH client detects an event message box with a scheme that is not
1858
1861
  // defined in MPD, the client is expected to ignore it.
@@ -1862,22 +1865,8 @@ shaka.media.StreamingEngine = class {
1862
1865
  // A special scheme in DASH used to signal manifest updates.
1863
1866
  if (schemeId == 'urn:mpeg:dash:event:2012') {
1864
1867
  this.playerInterface_.onManifestUpdate();
1865
- } else if (schemeId == 'https://aomedia.org/emsg/ID3') {
1866
- // See https://aomediacodec.github.io/id3-emsg/
1867
- const frames = shaka.util.Id3Utils.getID3Frames(messageData);
1868
- if (frames.length && reference) {
1869
- /** @private {shaka.extern.ID3Metadata} */
1870
- const metadata = {
1871
- cueTime: reference.startTime,
1872
- data: messageData,
1873
- frames: frames,
1874
- dts: reference.startTime,
1875
- pts: reference.startTime,
1876
- };
1877
- this.playerInterface_.onMetadata(
1878
- [metadata], /* offset= */ 0, reference.endTime);
1879
- }
1880
1868
  } else {
1869
+ // All other schemes are dispatched as a general 'emsg' event.
1881
1870
  /** @type {shaka.extern.EmsgInfo} */
1882
1871
  const emsg = {
1883
1872
  startTime: startTime,
@@ -1895,7 +1884,38 @@ shaka.media.StreamingEngine = class {
1895
1884
  const eventName = shaka.util.FakeEvent.EventName.Emsg;
1896
1885
  const data = (new Map()).set('detail', emsg);
1897
1886
  const event = new shaka.util.FakeEvent(eventName, data);
1887
+ // A user can call preventDefault() on a cancelable event.
1888
+ event.cancelable = true;
1889
+
1898
1890
  this.playerInterface_.onEvent(event);
1891
+
1892
+ if (event.defaultPrevented) {
1893
+ // If the caller uses preventDefault() on the 'emsg' event, don't
1894
+ // process any further, and don't generate an ID3 'metadata' event
1895
+ // for the same data.
1896
+ return;
1897
+ }
1898
+
1899
+ // Additionally, ID3 events generate a 'metadata' event. This is a
1900
+ // pre-parsed version of the metadata blob already dispatched in the
1901
+ // 'emsg' event.
1902
+ if (schemeId == 'https://aomedia.org/emsg/ID3' ||
1903
+ schemeId == 'https://developer.apple.com/streaming/emsg-id3') {
1904
+ // See https://aomediacodec.github.io/id3-emsg/
1905
+ const frames = shaka.util.Id3Utils.getID3Frames(messageData);
1906
+ if (frames.length && reference) {
1907
+ /** @private {shaka.extern.ID3Metadata} */
1908
+ const metadata = {
1909
+ cueTime: reference.startTime,
1910
+ data: messageData,
1911
+ frames: frames,
1912
+ dts: reference.startTime,
1913
+ pts: reference.startTime,
1914
+ };
1915
+ this.playerInterface_.onMetadata(
1916
+ [metadata], /* offset= */ 0, reference.endTime);
1917
+ }
1918
+ }
1899
1919
  }
1900
1920
  }
1901
1921
  }
@@ -27,15 +27,19 @@ shaka.media.VideoWrapper = class {
27
27
  /**
28
28
  * @param {!HTMLMediaElement} video
29
29
  * @param {function()} onSeek Called when the video seeks.
30
+ * @param {function(number)} onStarted Called when the video has started.
30
31
  * @param {number} startTime The time to start at.
31
32
  */
32
- constructor(video, onSeek, startTime) {
33
+ constructor(video, onSeek, onStarted, startTime) {
33
34
  /** @private {HTMLMediaElement} */
34
35
  this.video_ = video;
35
36
 
36
37
  /** @private {function()} */
37
38
  this.onSeek_ = onSeek;
38
39
 
40
+ /** @private {function(number)} */
41
+ this.onStarted_ = onStarted;
42
+
39
43
  /** @private {number} */
40
44
  this.startTime_ = startTime;
41
45
 
@@ -165,6 +169,7 @@ shaka.media.VideoWrapper = class {
165
169
  this.started_ = true;
166
170
 
167
171
  this.eventManager_.listen(this.video_, 'seeking', () => this.onSeek_());
172
+ this.onStarted_(this.video_.currentTime);
168
173
  }
169
174
  };
170
175