shaka-player 4.0.2 → 4.0.3

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.
@@ -766,8 +766,10 @@ shaka.extern.DashManifestConfiguration;
766
766
  * <i>Defaults to <code>'avc1.42E01E'</code>.</i>
767
767
  * @property {boolean} ignoreManifestProgramDateTime
768
768
  * If <code>true</code>, the HLS parser will ignore the
769
- * <code>EXT-X-PROGRAM-DATE-TIME</code> tags in the manifest.
770
- * Meant for tags that are incorrect or malformed.
769
+ * <code>EXT-X-PROGRAM-DATE-TIME</code> tags in the manifest and use media
770
+ * sequence numbers instead.
771
+ * Meant for streams where <code>EXT-X-PROGRAM-DATE-TIME</code> is incorrect
772
+ * or malformed.
771
773
  * <i>Defaults to <code>false</code>.</i>
772
774
  * @property {string} mediaPlaylistFullMimeType
773
775
  * A string containing a full mime type, including both the basic mime type
@@ -121,13 +121,19 @@ shaka.hls.HlsParser = class {
121
121
  this.updatePlaylistDelay_ = 0;
122
122
 
123
123
  /**
124
- * A time offset to apply to EXT-X-PROGRAM-DATE-TIME values to normalize
125
- * them so that they start at 0. This is necessary because these times will
126
- * be used to set presentation times for segments.
127
- * null means we don't have enough data yet.
124
+ * If true, we have already calculated offsets to synchronize streams.
125
+ * Offsets are computed in syncStreams*_().
126
+ * @private {boolean}
127
+ */
128
+ this.streamsSynced_ = false;
129
+
130
+ /**
131
+ * The minimum sequence number for generated segments, when ignoring
132
+ * EXT-X-PROGRAM-DATE-TIME.
133
+ *
128
134
  * @private {?number}
129
135
  */
130
- this.syncTimeOffset_ = null;
136
+ this.minSequenceNumber_ = null;
131
137
 
132
138
  /**
133
139
  * This timer is used to trigger the start of a manifest update. A manifest
@@ -361,39 +367,155 @@ shaka.hls.HlsParser = class {
361
367
  }
362
368
 
363
369
  /**
364
- * If necessary, makes sure that sync times will be normalized to 0, so that
365
- * a stream does not start buffering at 50 years in because sync times are
366
- * measured in time since 1970.
370
+ * Align all streams by sequence number by dropping early segments. Then
371
+ * offset all streams to begin at presentation time 0.
367
372
  * @private
368
373
  */
369
- calculateSyncTimeOffset_() {
370
- if (this.syncTimeOffset_ != null) {
371
- // The offset was already calculated.
374
+ syncStreamsWithSequenceNumber_() {
375
+ if (this.streamsSynced_) {
372
376
  return;
373
377
  }
374
378
 
375
- const segments = new Set();
376
- let lowestSyncTime = Infinity;
379
+ // Sync using media sequence number. Find the highest starting sequence
380
+ // number among all streams. Later, we will drop any references to
381
+ // earlier segments in other streams, then offset everything back to 0.
382
+ let highestStartingSequenceNumber = -1;
383
+ const firstSequenceNumberMap = new Map();
384
+
377
385
  for (const streamInfo of this.uriToStreamInfosMap_.values()) {
378
386
  const segmentIndex = streamInfo.stream.segmentIndex;
379
387
  if (segmentIndex) {
380
- segmentIndex.forEachTopLevelReference((segment) => {
381
- if (segment.syncTime != null) {
382
- lowestSyncTime = Math.min(lowestSyncTime, segment.syncTime);
383
- segments.add(segment);
388
+ const segment0 = segmentIndex.earliestReference();
389
+ if (segment0) {
390
+ // This looks inefficient, but iteration order is insertion order.
391
+ // So the very first entry should be the one we want.
392
+ // We assert that this holds true so that we are alerted by debug
393
+ // builds and tests if it changes. We still do a loop, though, so
394
+ // that the code functions correctly in production no matter what.
395
+ if (goog.DEBUG) {
396
+ const firstSequenceStartTime =
397
+ streamInfo.mediaSequenceToStartTime.values().next().value;
398
+ goog.asserts.assert(
399
+ firstSequenceStartTime == segment0.startTime,
400
+ 'Sequence number map is not ordered as expected!');
401
+ }
402
+ for (const [sequence, start] of streamInfo.mediaSequenceToStartTime) {
403
+ if (start == segment0.startTime) {
404
+ firstSequenceNumberMap.set(streamInfo, sequence);
405
+
406
+ highestStartingSequenceNumber = Math.max(
407
+ highestStartingSequenceNumber, sequence);
408
+ break;
409
+ }
384
410
  }
385
- });
411
+ }
412
+ }
413
+ }
414
+
415
+ if (highestStartingSequenceNumber < 0) {
416
+ // Nothing to sync.
417
+ return;
418
+ }
419
+
420
+ // From now on, updates will ignore any references before this number.
421
+ this.minSequenceNumber_ = highestStartingSequenceNumber;
422
+
423
+ shaka.log.debug('Syncing HLS streams against base sequence number:',
424
+ this.minSequenceNumber_);
425
+
426
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
427
+ const segmentIndex = streamInfo.stream.segmentIndex;
428
+ if (segmentIndex) {
429
+ // Drop any earlier references.
430
+ const numSegmentsToDrop = this.minSequenceNumber_ -
431
+ firstSequenceNumberMap.get(streamInfo);
432
+ segmentIndex.dropFirstReferences(numSegmentsToDrop);
433
+
434
+ // Now adjust timestamps back to begin at 0.
435
+ const segmentN = segmentIndex.earliestReference();
436
+ if (segmentN) {
437
+ this.offsetStream_(streamInfo, -segmentN.startTime);
438
+ }
439
+ }
440
+ }
441
+
442
+ this.streamsSynced_ = true;
443
+ }
444
+
445
+ /**
446
+ * Synchronize streams by the EXT-X-PROGRAM-DATE-TIME tags attached to their
447
+ * segments. Also normalizes segment times so that the earliest segment in
448
+ * any stream is at time 0.
449
+ * @private
450
+ */
451
+ syncStreamsWithProgramDateTime_() {
452
+ if (this.streamsSynced_) {
453
+ return;
454
+ }
455
+
456
+ let lowestSyncTime = Infinity;
457
+
458
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
459
+ const segmentIndex = streamInfo.stream.segmentIndex;
460
+ if (segmentIndex) {
461
+ const segment0 = segmentIndex.earliestReference();
462
+ if (segment0 != null && segment0.syncTime != null) {
463
+ lowestSyncTime = Math.min(lowestSyncTime, segment0.syncTime);
464
+ }
386
465
  }
387
466
  }
388
- if (segments.size > 0) {
389
- this.syncTimeOffset_ = -lowestSyncTime;
390
- for (const segment of segments) {
391
- segment.syncTime += this.syncTimeOffset_;
392
- for (const partial of segment.partialReferences) {
393
- partial.syncTime += this.syncTimeOffset_;
467
+
468
+ if (lowestSyncTime == Infinity) {
469
+ // Nothing to sync.
470
+ return;
471
+ }
472
+
473
+ shaka.log.debug('Syncing HLS streams against base time:', lowestSyncTime);
474
+
475
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
476
+ const segmentIndex = streamInfo.stream.segmentIndex;
477
+ if (segmentIndex != null) {
478
+ const segment0 = segmentIndex.earliestReference();
479
+ if (segment0.syncTime == null) {
480
+ shaka.log.alwaysError('Missing EXT-X-PROGRAM-DATE-TIME for stream',
481
+ streamInfo.verbatimMediaPlaylistUri,
482
+ 'Expect AV sync issues!');
483
+ } else {
484
+ // The first segment's target startTime should be based entirely on
485
+ // its syncTime. The rest of the stream will be based on that
486
+ // starting point. The earliest segment sync time from any stream
487
+ // will become presentation time 0. If two streams start e.g. 6
488
+ // seconds apart in syncTime, then their first segments will also
489
+ // start 6 seconds apart in presentation time.
490
+ const segment0TargetTime = segment0.syncTime - lowestSyncTime;
491
+ const streamOffset = segment0TargetTime - segment0.startTime;
492
+
493
+ this.offsetStream_(streamInfo, streamOffset);
394
494
  }
395
495
  }
396
496
  }
497
+
498
+ this.streamsSynced_ = true;
499
+ }
500
+
501
+ /**
502
+ * @param {!shaka.hls.HlsParser.StreamInfo} streamInfo
503
+ * @param {number} offset
504
+ * @private
505
+ */
506
+ offsetStream_(streamInfo, offset) {
507
+ streamInfo.stream.segmentIndex.offset(offset);
508
+
509
+ streamInfo.maxTimestamp += offset;
510
+ goog.asserts.assert(streamInfo.maxTimestamp >= 0,
511
+ 'Negative maxTimestamp after adjustment!');
512
+
513
+ for (const [key, value] of streamInfo.mediaSequenceToStartTime) {
514
+ streamInfo.mediaSequenceToStartTime.set(key, value + offset);
515
+ }
516
+
517
+ shaka.log.debug('Offset', offset, 'applied to',
518
+ streamInfo.verbatimMediaPlaylistUri);
397
519
  }
398
520
 
399
521
  /**
@@ -524,7 +646,11 @@ shaka.hls.HlsParser = class {
524
646
 
525
647
  // Now that we have generated all streams, we can determine the offset to
526
648
  // apply to sync times.
527
- this.calculateSyncTimeOffset_();
649
+ if (this.config_.hls.ignoreManifestProgramDateTime) {
650
+ this.syncStreamsWithSequenceNumber_();
651
+ } else {
652
+ this.syncStreamsWithProgramDateTime_();
653
+ }
528
654
 
529
655
  if (this.aesEncrypted_ && variants.length == 0) {
530
656
  // We do not support AES-128 encryption with HLS yet. Variants is null
@@ -539,15 +665,12 @@ shaka.hls.HlsParser = class {
539
665
 
540
666
  // Find the min and max timestamp of the earliest segment in all streams.
541
667
  // Find the minimum duration of all streams as well.
542
- let minFirstTimestamp = Infinity;
543
668
  let minDuration = Infinity;
544
-
545
669
  for (const streamInfo of this.uriToStreamInfosMap_.values()) {
546
- minFirstTimestamp =
547
- Math.min(minFirstTimestamp, streamInfo.minTimestamp);
548
670
  if (streamInfo.stream.type != 'text') {
549
- minDuration = Math.min(minDuration,
550
- streamInfo.maxTimestamp - streamInfo.minTimestamp);
671
+ // Since everything is already offset to 0 (either by sync or by being
672
+ // VOD), only maxTimestamp is necessary to compute the duration.
673
+ minDuration = Math.min(minDuration, streamInfo.maxTimestamp);
551
674
  }
552
675
  }
553
676
 
@@ -591,18 +714,11 @@ shaka.hls.HlsParser = class {
591
714
  segmentAvailabilityDuration);
592
715
  }
593
716
  } else {
594
- // For VOD/EVENT content, offset everything back to 0.
595
- // Use the minimum timestamp as the offset for all streams.
596
717
  // Use the minimum duration as the presentation duration.
597
718
  this.presentationTimeline_.setDuration(minDuration);
598
- // Use a negative offset to adjust towards 0.
599
- this.presentationTimeline_.offset(-minFirstTimestamp);
600
719
 
601
720
  for (const streamInfo of this.uriToStreamInfosMap_.values()) {
602
- // The segments were created with actual media times, rather than
603
- // presentation-aligned times, so offset them all now.
604
- streamInfo.stream.segmentIndex.offset(-minFirstTimestamp);
605
- // Finally, fit the segments to the playlist duration.
721
+ // Fit the segments to the playlist duration.
606
722
  streamInfo.stream.segmentIndex.fit(/* periodStart= */ 0, minDuration);
607
723
  }
608
724
  }
@@ -810,12 +926,7 @@ shaka.hls.HlsParser = class {
810
926
  });
811
927
 
812
928
  // Create stream info for each audio / video media tag.
813
- // Wait for the first stream info created, so that the start time is fetched
814
- // and can be reused.
815
- if (mediaTags.length) {
816
- await this.createStreamInfoFromMediaTag_(mediaTags[0]);
817
- }
818
- const promises = mediaTags.slice(1).map((tag) => {
929
+ const promises = mediaTags.map((tag) => {
819
930
  return this.createStreamInfoFromMediaTag_(tag);
820
931
  });
821
932
  await Promise.all(promises);
@@ -1583,7 +1694,6 @@ shaka.hls.HlsParser = class {
1583
1694
  throw error;
1584
1695
  }
1585
1696
 
1586
- const minTimestamp = segments[0].startTime;
1587
1697
  const lastEndTime = segments[segments.length - 1].endTime;
1588
1698
  /** @type {!shaka.media.SegmentIndex} */
1589
1699
  const segmentIndex = new shaka.media.SegmentIndex(segments);
@@ -1641,7 +1751,6 @@ shaka.hls.HlsParser = class {
1641
1751
  stream,
1642
1752
  verbatimMediaPlaylistUri,
1643
1753
  absoluteMediaPlaylistUri,
1644
- minTimestamp,
1645
1754
  maxTimestamp: lastEndTime,
1646
1755
  mediaSequenceToStartTime,
1647
1756
  canSkipSegments,
@@ -1853,15 +1962,9 @@ shaka.hls.HlsParser = class {
1853
1962
  const dateTimeTag =
1854
1963
  shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-PROGRAM-DATE-TIME');
1855
1964
  if (dateTimeTag && dateTimeTag.value) {
1856
- const time = shaka.util.XmlUtils.parseDate(dateTimeTag.value);
1857
- goog.asserts.assert(time != null,
1965
+ syncTime = shaka.util.XmlUtils.parseDate(dateTimeTag.value);
1966
+ goog.asserts.assert(syncTime != null,
1858
1967
  'EXT-X-PROGRAM-DATE-TIME format not valid');
1859
- // Sync time offset is null on the first go-through. This indicates that
1860
- // we have not yet seen every stream, and thus do not yet have enough
1861
- // information to determine how to normalize the sync times.
1862
- // For that first go-through, the sync time will be applied after the
1863
- // references are all created. Until then, just offset by 0.
1864
- syncTime = time + (this.syncTimeOffset_ || 0);
1865
1968
  }
1866
1969
  }
1867
1970
 
@@ -2085,16 +2188,13 @@ shaka.hls.HlsParser = class {
2085
2188
  firstStartTime = mediaSequenceToStartTime.get(position);
2086
2189
  }
2087
2190
 
2088
- const firstSegmentUri = hlsSegments[0].absoluteUri;
2089
- shaka.log.debug('First segment', firstSegmentUri.split('/').pop(),
2090
- 'starts at', firstStartTime);
2091
-
2092
2191
  /** @type {!Array.<!shaka.media.SegmentReference>} */
2093
2192
  const references = [];
2094
2193
 
2194
+ let previousReference = null;
2195
+
2095
2196
  for (let i = 0; i < hlsSegments.length; i++) {
2096
2197
  const item = hlsSegments[i];
2097
- const previousReference = references[references.length - 1];
2098
2198
  const startTime =
2099
2199
  (i == 0) ? firstStartTime : previousReference.endTime;
2100
2200
  position = mediaSequenceNumber + skippedSegments + i;
@@ -2124,9 +2224,17 @@ shaka.hls.HlsParser = class {
2124
2224
  variables,
2125
2225
  playlist.absoluteUri,
2126
2226
  type);
2227
+ previousReference = reference;
2127
2228
 
2128
2229
  if (reference) {
2129
- references.push(reference);
2230
+ if (this.config_.hls.ignoreManifestProgramDateTime &&
2231
+ this.minSequenceNumber_ != null &&
2232
+ position < this.minSequenceNumber_) {
2233
+ // This segment is ignored as part of our fallback synchronization
2234
+ // method.
2235
+ } else {
2236
+ references.push(reference);
2237
+ }
2130
2238
  }
2131
2239
  }
2132
2240
 
@@ -2256,12 +2364,16 @@ shaka.hls.HlsParser = class {
2256
2364
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
2257
2365
  const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
2258
2366
 
2367
+ // If you wait long enough, requesting the first segment can fail
2368
+ // because it has fallen off the left edge of DVR, so to be safer,
2369
+ // let's request the middle segment.
2259
2370
  goog.asserts.assert(playlist.segments.length,
2260
2371
  'Playlist should have segments!');
2261
- const firstSegmentUri = this.variableSubstitution_(
2262
- playlist.segments[0].absoluteUri, variables);
2372
+ const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2);
2373
+ const middleSegmentUri = this.variableSubstitution_(
2374
+ playlist.segments[middleSegmentIdx].absoluteUri, variables);
2263
2375
 
2264
- const parsedUri = new goog.Uri(firstSegmentUri);
2376
+ const parsedUri = new goog.Uri(middleSegmentUri);
2265
2377
  const extension = parsedUri.getPath().split('.').pop();
2266
2378
  const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
2267
2379
 
@@ -2291,7 +2403,7 @@ shaka.hls.HlsParser = class {
2291
2403
  // If unable to guess mime type, request a segment and try getting it
2292
2404
  // from the response.
2293
2405
  const headRequest = shaka.net.NetworkingEngine.makeRequest(
2294
- [firstSegmentUri], this.config_.retryParameters);
2406
+ [middleSegmentUri], this.config_.retryParameters);
2295
2407
  headRequest.method = 'HEAD';
2296
2408
 
2297
2409
  const response = await this.makeNetworkRequest_(
@@ -2578,7 +2690,6 @@ shaka.hls.HlsParser = class {
2578
2690
  * stream: !shaka.extern.Stream,
2579
2691
  * verbatimMediaPlaylistUri: string,
2580
2692
  * absoluteMediaPlaylistUri: string,
2581
- * minTimestamp: number,
2582
2693
  * maxTimestamp: number,
2583
2694
  * mediaSequenceToStartTime: !Map.<number, number>,
2584
2695
  * canSkipSegments: boolean
@@ -2597,8 +2708,6 @@ shaka.hls.HlsParser = class {
2597
2708
  * @property {string} absoluteMediaPlaylistUri
2598
2709
  * The absolute media playlist URI, resolved relative to the master playlist
2599
2710
  * and updated to reflect any redirects.
2600
- * @property {number} minTimestamp
2601
- * The minimum timestamp found in the stream.
2602
2711
  * @property {number} maxTimestamp
2603
2712
  * The maximum timestamp found in the stream.
2604
2713
  * @property {!Map.<number, number>} mediaSequenceToStartTime
@@ -94,6 +94,25 @@ shaka.media.SegmentIndex = class {
94
94
  }
95
95
 
96
96
 
97
+ /**
98
+ * Return the earliest reference, or null if empty.
99
+ * @return {shaka.media.SegmentReference}
100
+ */
101
+ earliestReference() {
102
+ return this.references[0] || null;
103
+ }
104
+
105
+
106
+ /**
107
+ * Drop the first N references.
108
+ * Used in early HLS synchronization, and does not count as eviction.
109
+ * @param {number} n
110
+ */
111
+ dropFirstReferences(n) {
112
+ this.references.splice(0, n);
113
+ }
114
+
115
+
97
116
  /**
98
117
  * Finds the position of the segment for the given time, in seconds, relative
99
118
  * to the start of the presentation. Returns the position of the segment
@@ -166,7 +185,13 @@ shaka.media.SegmentIndex = class {
166
185
  for (const ref of this.references) {
167
186
  ref.startTime += offset;
168
187
  ref.endTime += offset;
169
- ref.timestampOffset += offset;
188
+ ref.trueEndTime += offset;
189
+
190
+ for (const partial of ref.partialReferences) {
191
+ partial.startTime += offset;
192
+ partial.endTime += offset;
193
+ partial.trueEndTime += offset;
194
+ }
170
195
  }
171
196
  }
172
197
  }
@@ -160,8 +160,9 @@ shaka.media.SegmentReference = class {
160
160
  * If not provided, the duration should be automatically calculated based on
161
161
  * the duration of the reference.
162
162
  * @param {?number=} syncTime
163
- * A time value, expressed in the same scale as the start and end time, which
164
- * is used to synchronize between streams.
163
+ * A time value, expressed in seconds since 1970, which is used to
164
+ * synchronize between streams. Both produced and consumed by the HLS
165
+ * parser. Other components should not need this value.
165
166
  * @param {shaka.media.SegmentReference.Status=} status
166
167
  * The segment status is used to indicate that a segment does not exist or is
167
168
  * not available.
@@ -221,7 +222,13 @@ shaka.media.SegmentReference = class {
221
222
  /** @type {?number} */
222
223
  this.tileDuration = tileDuration;
223
224
 
224
- /** @type {?number} */
225
+ /**
226
+ * A time value, expressed in seconds since 1970, which is used to
227
+ * synchronize between streams. Both produced and consumed by the HLS
228
+ * parser. Other components should not need this value.
229
+ *
230
+ * @type {?number}
231
+ */
225
232
  this.syncTime = syncTime;
226
233
 
227
234
  /** @type {shaka.media.SegmentReference.Status} */
@@ -1602,7 +1602,7 @@ shaka.media.StreamingEngine = class {
1602
1602
  await this.playerInterface_.mediaSourceEngine.appendBuffer(
1603
1603
  mediaState.type,
1604
1604
  segment,
1605
- reference.syncTime == null ? reference.startTime : reference.syncTime,
1605
+ reference.startTime,
1606
1606
  reference.endTime,
1607
1607
  hasClosedCaptions,
1608
1608
  seeked);
package/lib/player.js CHANGED
@@ -6450,7 +6450,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6450
6450
  * @define {string} A version number taken from git at compile time.
6451
6451
  * @export
6452
6452
  */
6453
- shaka.Player.version = 'v4.0.2-uncompiled';
6453
+ shaka.Player.version = 'v4.0.3-uncompiled';
6454
6454
 
6455
6455
  // Initialize the deprecation system using the version string we just set
6456
6456
  // on the player.
@@ -1008,6 +1008,7 @@ shaka.util.StreamUtils = class {
1008
1008
  track.channelsCount = audio.channelsCount;
1009
1009
  track.audioSamplingRate = audio.audioSamplingRate;
1010
1010
  track.audioBandwidth = audio.bandwidth || null;
1011
+ track.spatialAudio = audio.spatialAudio;
1011
1012
  track.label = audio.label;
1012
1013
  track.audioRoles = audio.roles;
1013
1014
  }
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.0.2",
4
+ "version": "4.0.3",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -96,7 +96,7 @@
96
96
  "prepublishOnly": "python build/checkversion.py && python build/all.py --force"
97
97
  },
98
98
  "dependencies": {
99
- "eme-encryption-scheme-polyfill": "^2.0.3"
99
+ "eme-encryption-scheme-polyfill": "^2.0.5"
100
100
  },
101
101
  "engines": {
102
102
  "node": ">=14"
@@ -2021,6 +2021,10 @@ describe('HlsParser', () => {
2021
2021
  });
2022
2022
 
2023
2023
  describe('produces syncTime', () => {
2024
+ // Corresponds to "2000-01-01T00:00:00.00Z".
2025
+ // All the PROGRAM-DATE-TIME values in the tests below are at or after this.
2026
+ const syncTimeBase = 946684800;
2027
+
2024
2028
  /**
2025
2029
  * @param {number} startTime
2026
2030
  * @param {number} endTime
@@ -2039,9 +2043,10 @@ describe('HlsParser', () => {
2039
2043
  /**
2040
2044
  * @param {string} media
2041
2045
  * @param {!Array.<number>} startTimes
2046
+ * @param {number} syncTimeOffset
2042
2047
  * @param {(function(!shaka.media.SegmentReference))=} modifyFn
2043
2048
  */
2044
- async function test(media, startTimes, modifyFn) {
2049
+ async function test(media, startTimes, syncTimeOffset, modifyFn) {
2045
2050
  const master = [
2046
2051
  '#EXTM3U\n',
2047
2052
  '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,vtt",',
@@ -2053,7 +2058,8 @@ describe('HlsParser', () => {
2053
2058
  for (let i = 0; i < startTimes.length - 1; i++) {
2054
2059
  const startTime = startTimes[i];
2055
2060
  const endTime = startTimes[i + 1];
2056
- const reference = makeReference(startTime, endTime, startTime);
2061
+ const reference = makeReference(
2062
+ startTime, endTime, syncTimeOffset + startTime);
2057
2063
  if (modifyFn) {
2058
2064
  modifyFn(reference);
2059
2065
  }
@@ -2100,7 +2106,7 @@ describe('HlsParser', () => {
2100
2106
  '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:25.00Z\n',
2101
2107
  '#EXTINF:5,\n',
2102
2108
  'main.mp4',
2103
- ].join(''), [0, 5, 10, 15, 20, 25]);
2109
+ ].join(''), [0, 5, 10, 15, 20, 25], syncTimeBase + 5);
2104
2110
  });
2105
2111
 
2106
2112
  it('when some EXT-X-PROGRAM-DATE-TIME values are missing', async () => {
@@ -2122,7 +2128,7 @@ describe('HlsParser', () => {
2122
2128
  'main.mp4\n',
2123
2129
  '#EXTINF:4,\n',
2124
2130
  'main.mp4',
2125
- ].join(''), [0, 2, 7, 12, 17, 19, 23]);
2131
+ ].join(''), [0, 2, 7, 12, 17, 19, 23], syncTimeBase + 8);
2126
2132
  });
2127
2133
 
2128
2134
  it('except when ignoreManifestProgramDateTime is set', async () => {
@@ -2148,7 +2154,7 @@ describe('HlsParser', () => {
2148
2154
  '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:25.00Z\n',
2149
2155
  '#EXTINF:5,\n',
2150
2156
  'main.mp4',
2151
- ].join(''), [0, 5, 10, 15, 20, 25], (reference) => {
2157
+ ].join(''), [0, 5, 10, 15, 20, 25], syncTimeBase + 5, (reference) => {
2152
2158
  reference.syncTime = null;
2153
2159
  });
2154
2160
  });
@@ -2176,11 +2182,11 @@ describe('HlsParser', () => {
2176
2182
  '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:25.00Z\n',
2177
2183
  '#EXTINF:5,\n',
2178
2184
  'main.mp4',
2179
- ].join(''), [0, 5, 10, 15, 20, 25], (reference) => {
2185
+ ].join(''), [0, 5, 10, 15, 20, 25], syncTimeBase + 5, (reference) => {
2180
2186
  if (reference.startTime == 10) {
2181
2187
  reference.partialReferences = [
2182
- makeReference(10, 12.5, 10),
2183
- makeReference(12.5, 15, 12.5),
2188
+ makeReference(10, 12.5, syncTimeBase + 15),
2189
+ makeReference(12.5, 15, syncTimeBase + 17.5),
2184
2190
  ];
2185
2191
  }
2186
2192
  });
@@ -3555,4 +3561,92 @@ describe('HlsParser', () => {
3555
3561
 
3556
3562
  await testHlsParser(media, '', manifest);
3557
3563
  });
3564
+
3565
+ it('syncs on sequence with ignoreManifestProgramDateTime', async () => {
3566
+ const config = shaka.util.PlayerConfiguration.createDefault().manifest;
3567
+ config.hls.ignoreManifestProgramDateTime = true;
3568
+ parser.configure(config);
3569
+
3570
+ const master = [
3571
+ '#EXTM3U\n',
3572
+ '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",URI="audio"\n',
3573
+ '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
3574
+ 'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n',
3575
+ 'video\n',
3576
+ ].join('');
3577
+
3578
+ const video = [
3579
+ '#EXTM3U\n',
3580
+ '#EXT-X-TARGETDURATION:5\n',
3581
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
3582
+ '#EXT-X-MEDIA-SEQUENCE:1\n',
3583
+ '#EXTINF:5,\n',
3584
+ 'video1.mp4\n',
3585
+ '#EXTINF:5,\n',
3586
+ 'video2.mp4\n',
3587
+ '#EXTINF:5,\n',
3588
+ 'video3.mp4\n',
3589
+ ].join('');
3590
+
3591
+ const audio = [
3592
+ '#EXTM3U\n',
3593
+ '#EXT-X-TARGETDURATION:5\n',
3594
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
3595
+ '#EXT-X-MEDIA-SEQUENCE:3\n',
3596
+ '#EXTINF:5,\n',
3597
+ 'audio3.mp4\n',
3598
+ '#EXTINF:5,\n',
3599
+ 'audio4.mp4\n',
3600
+ ].join('');
3601
+
3602
+ const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
3603
+ manifest.sequenceMode = true;
3604
+ manifest.anyTimeline();
3605
+ manifest.addPartialVariant((variant) => {
3606
+ variant.language = 'en';
3607
+ variant.bandwidth = 200;
3608
+ variant.addPartialStream(ContentType.VIDEO, (stream) => {
3609
+ stream.mime('video/mp4', 'avc1');
3610
+ stream.size(960, 540);
3611
+ stream.frameRate = 60;
3612
+ });
3613
+ variant.addPartialStream(ContentType.AUDIO, (stream) => {
3614
+ stream.language = 'en';
3615
+ stream.mime('audio/mp4', 'mp4a');
3616
+ });
3617
+ });
3618
+ });
3619
+
3620
+ fakeNetEngine
3621
+ .setResponseText('test:/master', master)
3622
+ .setResponseText('test:/audio', audio)
3623
+ .setResponseText('test:/video', video)
3624
+ .setResponseValue('test:/init.mp4', initSegmentData);
3625
+
3626
+ const actualManifest = await parser.start('test:/master', playerInterface);
3627
+ expect(actualManifest).toEqual(manifest);
3628
+
3629
+ const actualVideo = actualManifest.variants[0].video;
3630
+ await actualVideo.createSegmentIndex();
3631
+ goog.asserts.assert(actualVideo.segmentIndex != null, 'Null segmentIndex!');
3632
+
3633
+ const actualAudio = actualManifest.variants[0].audio;
3634
+ await actualAudio.createSegmentIndex();
3635
+ goog.asserts.assert(actualAudio.segmentIndex != null, 'Null segmentIndex!');
3636
+
3637
+ // Verify that the references are aligned on sequence number.
3638
+ const videoSegments = Array.from(actualVideo.segmentIndex);
3639
+ const audioSegments = Array.from(actualAudio.segmentIndex);
3640
+
3641
+ // The first two were dropped to align with the audio.
3642
+ expect(videoSegments.map((ref) => ref.getUris()[0])).toEqual([
3643
+ 'test:/video3.mp4',
3644
+ ]);
3645
+ // Audio has a 4th segment that video doesn't, but that doesn't get clipped
3646
+ // or used as the base. Alignment is truly based on media sequence number.
3647
+ expect(audioSegments.map((ref) => ref.getUris()[0])).toEqual([
3648
+ 'test:/audio3.mp4',
3649
+ 'test:/audio4.mp4',
3650
+ ]);
3651
+ });
3558
3652
  });