shaka-player 4.3.6 → 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 (66) hide show
  1. package/.github/workflows/appspot.yaml +39 -0
  2. package/.github/workflows/demo-version-index.yaml +3 -3
  3. package/CHANGELOG.md +46 -0
  4. package/CONTRIBUTORS +1 -0
  5. package/README.md +3 -0
  6. package/demo/custom.js +1 -1
  7. package/demo/demo.less +1 -0
  8. package/dist/demo.compiled.debug.js +2 -2
  9. package/dist/demo.compiled.debug.map +2 -2
  10. package/dist/demo.compiled.js +2 -2
  11. package/dist/demo.compiled.map +2 -2
  12. package/dist/demo.css +1 -1
  13. package/dist/demo.css.map +1 -1
  14. package/dist/deps.js +1 -1
  15. package/dist/locales.js +33 -33
  16. package/dist/shaka-player.compiled.d.ts +30 -0
  17. package/dist/shaka-player.compiled.debug.d.ts +30 -0
  18. package/dist/shaka-player.compiled.debug.externs.js +9 -0
  19. package/dist/shaka-player.compiled.debug.js +263 -256
  20. package/dist/shaka-player.compiled.debug.map +3 -3
  21. package/dist/shaka-player.compiled.externs.js +9 -0
  22. package/dist/shaka-player.compiled.js +901 -895
  23. package/dist/shaka-player.compiled.map +4 -4
  24. package/dist/shaka-player.ui.d.ts +30 -0
  25. package/dist/shaka-player.ui.debug.d.ts +30 -0
  26. package/dist/shaka-player.ui.debug.externs.js +9 -0
  27. package/dist/shaka-player.ui.debug.js +268 -262
  28. package/dist/shaka-player.ui.debug.map +3 -3
  29. package/dist/shaka-player.ui.externs.js +9 -0
  30. package/dist/shaka-player.ui.js +926 -920
  31. package/dist/shaka-player.ui.map +4 -4
  32. package/docs/tutorials/drm-config.md +9 -6
  33. package/docs/tutorials/welcome.md +1 -1
  34. package/externs/ima.js +34 -0
  35. package/lib/abr/simple_abr_manager.js +1 -1
  36. package/lib/cea/mp4_cea_parser.js +29 -20
  37. package/lib/dash/dash_parser.js +18 -3
  38. package/lib/hls/hls_parser.js +66 -6
  39. package/lib/hls/manifest_text_parser.js +10 -1
  40. package/lib/media/drm_engine.js +23 -14
  41. package/lib/media/gap_jumping_controller.js +24 -2
  42. package/lib/media/media_source_engine.js +10 -6
  43. package/lib/media/playhead.js +11 -0
  44. package/lib/media/region_observer.js +19 -1
  45. package/lib/media/segment_reference.js +22 -0
  46. package/lib/media/streaming_engine.js +48 -23
  47. package/lib/media/video_wrapper.js +6 -1
  48. package/lib/player.js +16 -5
  49. package/lib/polyfill/abort_controller.js +7 -0
  50. package/lib/text/vtt_text_parser.js +2 -1
  51. package/lib/util/manifest_parser_utils.js +1 -1
  52. package/lib/util/stream_utils.js +1 -0
  53. package/package.json +9 -4
  54. package/test/cea/mp4_cea_parser_unit.js +45 -3
  55. package/test/dash/dash_parser_manifest_unit.js +120 -0
  56. package/test/hls/hls_live_unit.js +3 -1
  57. package/test/hls/hls_parser_unit.js +144 -0
  58. package/test/media/media_source_engine_unit.js +30 -0
  59. package/test/media/region_observer_unit.js +2 -1
  60. package/test/media/streaming_engine_unit.js +26 -2
  61. package/test/test/assets/h265-cea-init.mp4 +0 -0
  62. package/test/test/assets/h265-cea-segment.mp4 +0 -0
  63. package/test/test/assets/multiple-trun-init.mp4 +0 -0
  64. package/test/test/assets/multiple-trun-segment.mp4 +0 -0
  65. package/test/test/util/layout_tests.js +4 -3
  66. 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
  ```
@@ -29,7 +29,7 @@ You can build Shaka on Linux, Windows, or Mac.
29
29
  To get the sources and compile the library, you will need:
30
30
  * {@link https://git-scm.com/downloads Git v1.9+}
31
31
  * {@link https://www.python.org/downloads/ Python v2.7 or v3.5+}
32
- * {@link https://java.com/en/download/ Java Runtime Environment v14+}
32
+ * {@link https://learn.microsoft.com/en-us/java/openjdk/download Java Runtime Environment v14+}
33
33
  * {@link https://nodejs.org/en/download/ NodeJS v14+}
34
34
  * A local web server, such as {@link https://httpd.apache.org/ Apache}
35
35
  * _NOTE: A local web server is required because browsers place restrictions
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,17 +265,23 @@ 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();
275
281
  const naluHeader = reader.readUint8();
276
282
  let naluType = null;
277
283
  let isSeiMessage = false;
284
+ let naluHeaderSize = 1;
278
285
 
279
286
  goog.asserts.assert(this.bitstreamFormat_ != BitstreamFormat.UNKNOWN,
280
287
  'Bitstream format should have been checked before now!');
@@ -285,6 +292,8 @@ shaka.cea.Mp4CeaParser = class {
285
292
  break;
286
293
 
287
294
  case BitstreamFormat.H265:
295
+ naluHeaderSize = 2;
296
+ reader.skip(1);
288
297
  naluType = (naluHeader >> 1) & 0x3f;
289
298
  isSeiMessage =
290
299
  naluType == ICeaParser.H265_PREFIX_NALU_TYPE_SEI ||
@@ -298,13 +307,13 @@ shaka.cea.Mp4CeaParser = class {
298
307
  if (isSeiMessage) {
299
308
  let timeOffset = 0;
300
309
 
301
- if (sampleData.length > sampleIndex) {
310
+ if (sampleIndex < sampleData.length) {
302
311
  timeOffset = sampleData[sampleIndex].sampleCompositionTimeOffset || 0;
303
312
  }
304
313
 
305
314
  const pts = (time + timeOffset)/timescale;
306
315
  for (const packet of this.seiProcessor_
307
- .process(reader.readBytes(naluSize - 1))) {
316
+ .process(reader.readBytes(naluSize - naluHeaderSize))) {
308
317
  captionPackets.push({
309
318
  packet,
310
319
  pts,
@@ -312,7 +321,7 @@ shaka.cea.Mp4CeaParser = class {
312
321
  }
313
322
  } else {
314
323
  try {
315
- reader.skip(naluSize - 1);
324
+ reader.skip(naluSize - naluHeaderSize);
316
325
  } catch (e) {
317
326
  // It is necessary to ignore this error because it can break the start
318
327
  // of playback even if the user does not want to see the subtitles.
@@ -321,7 +330,7 @@ shaka.cea.Mp4CeaParser = class {
321
330
  }
322
331
  sampleSize -= (naluSize + 4);
323
332
  if (sampleSize == 0) {
324
- if (sampleData.length > sampleIndex) {
333
+ if (sampleIndex < sampleData.length) {
325
334
  time += sampleData[sampleIndex].sampleDuration ||
326
335
  defaultSampleDuration;
327
336
  } else {
@@ -330,7 +339,7 @@ shaka.cea.Mp4CeaParser = class {
330
339
 
331
340
  sampleIndex++;
332
341
 
333
- if (sampleData.length > sampleIndex) {
342
+ if (sampleIndex < sampleData.length) {
334
343
  sampleSize = sampleData[sampleIndex].sampleSize || defaultSampleSize;
335
344
  } else {
336
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
  );
@@ -319,6 +319,9 @@ shaka.hls.HlsParser = class {
319
319
  this.presentationTimeline_.setDuration(Math.min(...maxTimestamps));
320
320
  this.playerInterface_.updateDuration();
321
321
  }
322
+ if (stillLive) {
323
+ this.determineDuration_();
324
+ }
322
325
  }
323
326
 
324
327
  /**
@@ -400,9 +403,16 @@ shaka.hls.HlsParser = class {
400
403
  mediaSequenceNumber);
401
404
  stream.segmentIndex.evict(playlistStartTime);
402
405
  }
406
+ const oldSegment = segments[0];
407
+ goog.asserts.assert(oldSegment, 'Should have segments!');
408
+
409
+ streamInfo.minTimestamp = oldSegment.startTime;
410
+
403
411
  const newestSegment = segments[segments.length - 1];
404
412
  goog.asserts.assert(newestSegment, 'Should have segments!');
405
413
 
414
+ streamInfo.maxTimestamp = newestSegment.endTime;
415
+
406
416
  // Once the last segment has been added to the playlist,
407
417
  // #EXT-X-ENDLIST tag will be appended.
408
418
  // If that happened, treat the rest of the EVENT presentation as VOD.
@@ -413,7 +423,6 @@ shaka.hls.HlsParser = class {
413
423
  // Flag this for later. We don't convert the whole presentation into VOD
414
424
  // until we've seen the ENDLIST tag for all active playlists.
415
425
  streamInfo.hasEndList = true;
416
- streamInfo.maxTimestamp = newestSegment.endTime;
417
426
  }
418
427
  }
419
428
 
@@ -576,6 +585,9 @@ shaka.hls.HlsParser = class {
576
585
  * @private
577
586
  */
578
587
  offsetStreamInfo_(streamInfo, offset) {
588
+ // Adjust our accounting of the minimum timestamp.
589
+ streamInfo.minTimestamp += offset;
590
+
579
591
  // Adjust our accounting of the maximum timestamp.
580
592
  streamInfo.maxTimestamp += offset;
581
593
  goog.asserts.assert(streamInfo.maxTimestamp >= 0,
@@ -804,7 +816,7 @@ shaka.hls.HlsParser = class {
804
816
  const PresentationType = shaka.hls.HlsParser.PresentationType_;
805
817
 
806
818
  if (this.presentationType_ == PresentationType.LIVE) {
807
- let segmentAvailabilityDuration = this.getMinDuration_();
819
+ let segmentAvailabilityDuration = this.getLiveDuration_();
808
820
 
809
821
  // This defaults to the presentation delay, which has the effect of
810
822
  // making the live stream unseekable. This is consistent with Apple's
@@ -1655,6 +1667,7 @@ shaka.hls.HlsParser = class {
1655
1667
  verbatimMediaPlaylistUri,
1656
1668
  // These values are filled out or updated after lazy-loading:
1657
1669
  absoluteMediaPlaylistUri: initialMediaPlaylistUri,
1670
+ minTimestamp: 0,
1658
1671
  maxTimestamp: 0,
1659
1672
  mediaSequenceToStartTime: new Map(),
1660
1673
  canSkipSegments: false,
@@ -1700,6 +1713,7 @@ shaka.hls.HlsParser = class {
1700
1713
 
1701
1714
  // Copy values from the real stream info to our initial one.
1702
1715
  streamInfo.absoluteMediaPlaylistUri = absoluteMediaPlaylistUri;
1716
+ streamInfo.minTimestamp = realStreamInfo.minTimestamp;
1703
1717
  streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
1704
1718
  streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
1705
1719
  streamInfo.hasEndList = realStreamInfo.hasEndList;
@@ -1833,6 +1847,22 @@ shaka.hls.HlsParser = class {
1833
1847
  return minDuration;
1834
1848
  }
1835
1849
 
1850
+ /**
1851
+ * @return {number}
1852
+ * @private
1853
+ */
1854
+ getLiveDuration_() {
1855
+ let maxTimestamp = Infinity;
1856
+ let minTimestamp = Infinity;
1857
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
1858
+ if (streamInfo.stream.segmentIndex && streamInfo.stream.type != 'text') {
1859
+ maxTimestamp = Math.min(maxTimestamp, streamInfo.maxTimestamp);
1860
+ minTimestamp = Math.min(minTimestamp, streamInfo.minTimestamp);
1861
+ }
1862
+ }
1863
+ return maxTimestamp - minTimestamp;
1864
+ }
1865
+
1836
1866
  /**
1837
1867
  * @param {!Array.<!shaka.extern.Stream>} streams
1838
1868
  * @private
@@ -1978,6 +2008,7 @@ shaka.hls.HlsParser = class {
1978
2008
  this.determinePresentationType_(playlist);
1979
2009
  }
1980
2010
 
2011
+ const firstStartTime = segments[0].startTime;
1981
2012
  const lastEndTime = segments[segments.length - 1].endTime;
1982
2013
  /** @type {!shaka.media.SegmentIndex} */
1983
2014
  const segmentIndex = new shaka.media.SegmentIndex(segments);
@@ -2000,6 +2031,7 @@ shaka.hls.HlsParser = class {
2000
2031
  type,
2001
2032
  verbatimMediaPlaylistUri,
2002
2033
  absoluteMediaPlaylistUri,
2034
+ minTimestamp: firstStartTime,
2003
2035
  maxTimestamp: lastEndTime,
2004
2036
  canSkipSegments,
2005
2037
  hasEndList: false,
@@ -2034,6 +2066,22 @@ shaka.hls.HlsParser = class {
2034
2066
  // stream is lazy-loaded.
2035
2067
  const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
2036
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
+ }
2037
2085
 
2038
2086
  return {
2039
2087
  id: this.globalId_++,
@@ -2042,8 +2090,7 @@ shaka.hls.HlsParser = class {
2042
2090
  segmentIndex: null,
2043
2091
  mimeType,
2044
2092
  codecs,
2045
- kind: (type == shaka.util.ManifestParserUtils.ContentType.TEXT) ?
2046
- shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined,
2093
+ kind,
2047
2094
  encrypted: false,
2048
2095
  drmInfos: [],
2049
2096
  keyIds: new Set(),
@@ -2059,7 +2106,7 @@ shaka.hls.HlsParser = class {
2059
2106
  width: undefined,
2060
2107
  height: undefined,
2061
2108
  bandwidth: undefined,
2062
- roles: characteristics ? characteristics.split(',') : [],
2109
+ roles,
2063
2110
  forced,
2064
2111
  channelsCount,
2065
2112
  audioSamplingRate: null,
@@ -2498,6 +2545,9 @@ shaka.hls.HlsParser = class {
2498
2545
  /* tileDuration= */ null,
2499
2546
  /* syncTime= */ null,
2500
2547
  partialStatus);
2548
+ if (item.name == 'EXT-X-PRELOAD-HINT') {
2549
+ partial.markAsPreload();
2550
+ }
2501
2551
  partialSegmentRefs.push(partial);
2502
2552
  } // for-loop of hlsSegment.partialSegments
2503
2553
  }
@@ -2514,6 +2564,10 @@ shaka.hls.HlsParser = class {
2514
2564
  // We're interested in the duration part.
2515
2565
  const extinfValues = extinfTag.value.split(',');
2516
2566
  const duration = Number(extinfValues[0]);
2567
+ // Skip segments without duration
2568
+ if (duration == 0) {
2569
+ return null;
2570
+ }
2517
2571
  endTime = startTime + duration;
2518
2572
  } else {
2519
2573
  endTime = partialSegmentRefs[partialSegmentRefs.length - 1].endTime;
@@ -2694,9 +2748,9 @@ shaka.hls.HlsParser = class {
2694
2748
  type,
2695
2749
  lastDiscontinuityStartTime,
2696
2750
  hlsAes128Key);
2697
- previousReference = reference;
2698
2751
 
2699
2752
  if (reference) {
2753
+ previousReference = reference;
2700
2754
  reference.discontinuitySequence = discontinuitySequence;
2701
2755
 
2702
2756
  if (this.config_.hls.ignoreManifestProgramDateTime &&
@@ -2807,6 +2861,9 @@ shaka.hls.HlsParser = class {
2807
2861
  * @private
2808
2862
  */
2809
2863
  variableSubstitution_(uri, variables) {
2864
+ if (!variables.size) {
2865
+ return uri;
2866
+ }
2810
2867
  let newUri = String(uri).replace(/%7B/g, '{').replace(/%7D/g, '}');
2811
2868
 
2812
2869
  const uriVariables = newUri.match(/{\$\w*}/g);
@@ -3252,6 +3309,7 @@ shaka.hls.HlsParser = class {
3252
3309
  * type: string,
3253
3310
  * verbatimMediaPlaylistUri: string,
3254
3311
  * absoluteMediaPlaylistUri: string,
3312
+ * minTimestamp: number,
3255
3313
  * maxTimestamp: number,
3256
3314
  * mediaSequenceToStartTime: !Map.<number, number>,
3257
3315
  * canSkipSegments: boolean,
@@ -3275,6 +3333,8 @@ shaka.hls.HlsParser = class {
3275
3333
  * @property {string} absoluteMediaPlaylistUri
3276
3334
  * The absolute media playlist URI, resolved relative to the master playlist
3277
3335
  * and updated to reflect any redirects.
3336
+ * @property {number} minTimestamp
3337
+ * The minimum timestamp found in the stream.
3278
3338
  * @property {number} maxTimestamp
3279
3339
  * The maximum timestamp found in the stream.
3280
3340
  * @property {!Map.<number, number>} mediaSequenceToStartTime
@@ -158,7 +158,15 @@ shaka.hls.ManifestTextParser = class {
158
158
  partialSegmentTags.push(tag);
159
159
  } else if (tag.name == 'EXT-X-PRELOAD-HINT') {
160
160
  if (tag.getAttributeValue('TYPE') == 'PART') {
161
- partialSegmentTags.push(tag);
161
+ // Note: BYTERANGE-START without BYTERANGE-LENGTH is being
162
+ // ignored.
163
+ if (tag.getAttributeValue('BYTERANGE-START') != null) {
164
+ if (tag.getAttributeValue('BYTERANGE-LENGTH') != null) {
165
+ partialSegmentTags.push(tag);
166
+ }
167
+ } else {
168
+ partialSegmentTags.push(tag);
169
+ }
162
170
  } else if (tag.getAttributeValue('TYPE') == 'MAP') {
163
171
  // Rename the Preload Hint tag to be a Map tag.
164
172
  tag.setName('EXT-X-MAP');
@@ -300,6 +308,7 @@ shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS = [
300
308
  'EXT-X-ENDLIST',
301
309
  'EXT-X-SERVER-CONTROL',
302
310
  'EXT-X-SKIP',
311
+ 'EXT-X-PART-INF',
303
312
  ];
304
313
 
305
314
 
@@ -133,6 +133,9 @@ shaka.media.DrmEngine = class {
133
133
 
134
134
  /** @private {Promise} */
135
135
  this.mediaKeysAttached_ = null;
136
+
137
+ /** @private {?shaka.extern.InitDataOverride} */
138
+ this.manifestInitData_ = null;
136
139
  }
137
140
 
138
141
  /** @override */
@@ -351,18 +354,24 @@ shaka.media.DrmEngine = class {
351
354
  shaka.media.DrmEngine.replaceDrmInfo_(variants, servers);
352
355
  }
353
356
 
354
- // Make sure all the drm infos are valid and filled in correctly.
357
+ /** @type {!Set<shaka.extern.DrmInfo>} */
358
+ const drmInfos = new Set();
355
359
  for (const variant of variants) {
356
- const drmInfos = this.getVariantDrmInfos_(variant);
357
- for (const info of drmInfos) {
358
- shaka.media.DrmEngine.fillInDrmInfoDefaults_(
359
- info,
360
- shaka.util.MapUtils.asMap(this.config_.servers),
361
- shaka.util.MapUtils.asMap(this.config_.advanced || {}),
362
- this.config_.keySystemsMapping);
360
+ const variantDrmInfos = this.getVariantDrmInfos_(variant);
361
+ for (const info of variantDrmInfos) {
362
+ drmInfos.add(info);
363
363
  }
364
364
  }
365
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
+
366
375
  /** @type {!Map.<string, MediaKeySystemConfiguration>} */
367
376
  let configsByKeySystem;
368
377
 
@@ -480,10 +489,10 @@ shaka.media.DrmEngine = class {
480
489
  () => this.closeOpenSessions_());
481
490
  }
482
491
 
483
- const manifestInitData = this.currentDrmInfo_ ?
484
- this.currentDrmInfo_.initData.find(
492
+ this.manifestInitData_ = this.currentDrmInfo_ ?
493
+ (this.currentDrmInfo_.initData.find(
485
494
  (initDataOverride) => initDataOverride.initData.length > 0,
486
- ) : null;
495
+ ) || null) : null;
487
496
 
488
497
  /**
489
498
  * We can attach media keys before the playback actually begins when:
@@ -491,7 +500,7 @@ shaka.media.DrmEngine = class {
491
500
  * - Some initData already has been generated (through the manifest)
492
501
  * - In case of an offline session
493
502
  */
494
- if (manifestInitData ||
503
+ if (this.manifestInitData_ ||
495
504
  this.currentDrmInfo_.keySystem !== 'com.apple.fps' ||
496
505
  this.offlineSessionIds_.length) {
497
506
  await this.attachMediaKeys_();
@@ -504,7 +513,7 @@ shaka.media.DrmEngine = class {
504
513
  // Also suppress 'encrypted' events when parsing in-band ppsh
505
514
  // from media segments because that serves the same purpose as the
506
515
  // 'encrypted' events.
507
- if (!manifestInitData && !this.offlineSessionIds_.length &&
516
+ if (!this.manifestInitData_ && !this.offlineSessionIds_.length &&
508
517
  !this.config_.parseInbandPsshEnabled) {
509
518
  this.eventManager_.listen(
510
519
  this.video_, 'encrypted', (e) => this.onEncryptedEvent_(e));
@@ -2361,7 +2370,7 @@ shaka.media.DrmEngine = class {
2361
2370
  * @return {!Promise<void>}
2362
2371
  */
2363
2372
  parseInbandPssh(contentType, mediaSegment) {
2364
- if (!this.config_.parseInbandPsshEnabled) {
2373
+ if (!this.config_.parseInbandPsshEnabled || this.manifestInitData_) {
2365
2374
  return Promise.resolve();
2366
2375
  }
2367
2376
 
@@ -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));