shaka-player 4.1.0 → 4.1.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.
- package/AUTHORS +1 -0
- package/CHANGELOG.md +13 -0
- package/CONTRIBUTORS +1 -0
- package/build/test.py +1 -1
- package/demo/common/assets.js +1 -1
- package/demo/main.js +3 -3
- package/dist/demo.compiled.debug.js +3 -3
- package/dist/demo.compiled.debug.map +2 -2
- package/dist/demo.compiled.js +3 -3
- package/dist/demo.compiled.map +2 -2
- package/dist/locales.js +31 -31
- package/dist/receiver.compiled.debug.js +1 -1
- package/dist/receiver.compiled.debug.map +1 -1
- package/dist/receiver.compiled.js +1 -1
- package/dist/receiver.compiled.map +1 -1
- package/dist/shaka-player.compiled.debug.externs.js +3 -2
- package/dist/shaka-player.compiled.debug.js +192 -186
- package/dist/shaka-player.compiled.debug.map +4 -4
- package/dist/shaka-player.compiled.externs.js +3 -2
- package/dist/shaka-player.compiled.js +727 -723
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.debug.externs.js +3 -2
- package/dist/shaka-player.ui.debug.js +225 -218
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +3 -2
- package/dist/shaka-player.ui.js +890 -887
- package/dist/shaka-player.ui.map +4 -4
- package/externs/shaka/player.js +4 -2
- package/lib/hls/hls_parser.js +177 -68
- package/lib/media/segment_index.js +26 -1
- package/lib/media/segment_reference.js +10 -3
- package/lib/media/streaming_engine.js +1 -1
- package/lib/player.js +1 -1
- package/lib/util/stream_utils.js +1 -0
- package/package.json +2 -2
- package/test/hls/hls_parser_unit.js +102 -8
package/externs/shaka/player.js
CHANGED
|
@@ -777,8 +777,10 @@ shaka.extern.DashManifestConfiguration;
|
|
|
777
777
|
* <i>Defaults to <code>'avc1.42E01E'</code>.</i>
|
|
778
778
|
* @property {boolean} ignoreManifestProgramDateTime
|
|
779
779
|
* If <code>true</code>, the HLS parser will ignore the
|
|
780
|
-
* <code>EXT-X-PROGRAM-DATE-TIME</code> tags in the manifest
|
|
781
|
-
*
|
|
780
|
+
* <code>EXT-X-PROGRAM-DATE-TIME</code> tags in the manifest and use media
|
|
781
|
+
* sequence numbers instead.
|
|
782
|
+
* Meant for streams where <code>EXT-X-PROGRAM-DATE-TIME</code> is incorrect
|
|
783
|
+
* or malformed.
|
|
782
784
|
* <i>Defaults to <code>false</code>.</i>
|
|
783
785
|
* @property {string} mediaPlaylistFullMimeType
|
|
784
786
|
* A string containing a full mime type, including both the basic mime type
|
package/lib/hls/hls_parser.js
CHANGED
|
@@ -121,13 +121,19 @@ shaka.hls.HlsParser = class {
|
|
|
121
121
|
this.updatePlaylistDelay_ = 0;
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
|
|
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.
|
|
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
|
-
*
|
|
365
|
-
*
|
|
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
|
-
|
|
370
|
-
if (this.
|
|
371
|
-
// The offset was already calculated.
|
|
374
|
+
syncStreamsWithSequenceNumber_() {
|
|
375
|
+
if (this.streamsSynced_) {
|
|
372
376
|
return;
|
|
373
377
|
}
|
|
374
378
|
|
|
375
|
-
|
|
376
|
-
|
|
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.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
/**
|
|
@@ -525,7 +647,11 @@ shaka.hls.HlsParser = class {
|
|
|
525
647
|
|
|
526
648
|
// Now that we have generated all streams, we can determine the offset to
|
|
527
649
|
// apply to sync times.
|
|
528
|
-
this.
|
|
650
|
+
if (this.config_.hls.ignoreManifestProgramDateTime) {
|
|
651
|
+
this.syncStreamsWithSequenceNumber_();
|
|
652
|
+
} else {
|
|
653
|
+
this.syncStreamsWithProgramDateTime_();
|
|
654
|
+
}
|
|
529
655
|
|
|
530
656
|
if (this.aesEncrypted_ && variants.length == 0) {
|
|
531
657
|
// We do not support AES-128 encryption with HLS yet. Variants is null
|
|
@@ -540,15 +666,12 @@ shaka.hls.HlsParser = class {
|
|
|
540
666
|
|
|
541
667
|
// Find the min and max timestamp of the earliest segment in all streams.
|
|
542
668
|
// Find the minimum duration of all streams as well.
|
|
543
|
-
let minFirstTimestamp = Infinity;
|
|
544
669
|
let minDuration = Infinity;
|
|
545
|
-
|
|
546
670
|
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
|
|
547
|
-
minFirstTimestamp =
|
|
548
|
-
Math.min(minFirstTimestamp, streamInfo.minTimestamp);
|
|
549
671
|
if (streamInfo.stream.type != 'text') {
|
|
550
|
-
|
|
551
|
-
|
|
672
|
+
// Since everything is already offset to 0 (either by sync or by being
|
|
673
|
+
// VOD), only maxTimestamp is necessary to compute the duration.
|
|
674
|
+
minDuration = Math.min(minDuration, streamInfo.maxTimestamp);
|
|
552
675
|
}
|
|
553
676
|
}
|
|
554
677
|
|
|
@@ -592,18 +715,11 @@ shaka.hls.HlsParser = class {
|
|
|
592
715
|
segmentAvailabilityDuration);
|
|
593
716
|
}
|
|
594
717
|
} else {
|
|
595
|
-
// For VOD/EVENT content, offset everything back to 0.
|
|
596
|
-
// Use the minimum timestamp as the offset for all streams.
|
|
597
718
|
// Use the minimum duration as the presentation duration.
|
|
598
719
|
this.presentationTimeline_.setDuration(minDuration);
|
|
599
|
-
// Use a negative offset to adjust towards 0.
|
|
600
|
-
this.presentationTimeline_.offset(-minFirstTimestamp);
|
|
601
720
|
|
|
602
721
|
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
|
|
603
|
-
//
|
|
604
|
-
// presentation-aligned times, so offset them all now.
|
|
605
|
-
streamInfo.stream.segmentIndex.offset(-minFirstTimestamp);
|
|
606
|
-
// Finally, fit the segments to the playlist duration.
|
|
722
|
+
// Fit the segments to the playlist duration.
|
|
607
723
|
streamInfo.stream.segmentIndex.fit(/* periodStart= */ 0, minDuration);
|
|
608
724
|
}
|
|
609
725
|
}
|
|
@@ -811,12 +927,7 @@ shaka.hls.HlsParser = class {
|
|
|
811
927
|
});
|
|
812
928
|
|
|
813
929
|
// Create stream info for each audio / video media tag.
|
|
814
|
-
|
|
815
|
-
// and can be reused.
|
|
816
|
-
if (mediaTags.length) {
|
|
817
|
-
await this.createStreamInfoFromMediaTag_(mediaTags[0]);
|
|
818
|
-
}
|
|
819
|
-
const promises = mediaTags.slice(1).map((tag) => {
|
|
930
|
+
const promises = mediaTags.map((tag) => {
|
|
820
931
|
return this.createStreamInfoFromMediaTag_(tag);
|
|
821
932
|
});
|
|
822
933
|
await Promise.all(promises);
|
|
@@ -1584,7 +1695,6 @@ shaka.hls.HlsParser = class {
|
|
|
1584
1695
|
throw error;
|
|
1585
1696
|
}
|
|
1586
1697
|
|
|
1587
|
-
const minTimestamp = segments[0].startTime;
|
|
1588
1698
|
const lastEndTime = segments[segments.length - 1].endTime;
|
|
1589
1699
|
/** @type {!shaka.media.SegmentIndex} */
|
|
1590
1700
|
const segmentIndex = new shaka.media.SegmentIndex(segments);
|
|
@@ -1642,7 +1752,6 @@ shaka.hls.HlsParser = class {
|
|
|
1642
1752
|
stream,
|
|
1643
1753
|
verbatimMediaPlaylistUri,
|
|
1644
1754
|
absoluteMediaPlaylistUri,
|
|
1645
|
-
minTimestamp,
|
|
1646
1755
|
maxTimestamp: lastEndTime,
|
|
1647
1756
|
mediaSequenceToStartTime,
|
|
1648
1757
|
canSkipSegments,
|
|
@@ -1854,15 +1963,9 @@ shaka.hls.HlsParser = class {
|
|
|
1854
1963
|
const dateTimeTag =
|
|
1855
1964
|
shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-PROGRAM-DATE-TIME');
|
|
1856
1965
|
if (dateTimeTag && dateTimeTag.value) {
|
|
1857
|
-
|
|
1858
|
-
goog.asserts.assert(
|
|
1966
|
+
syncTime = shaka.util.XmlUtils.parseDate(dateTimeTag.value);
|
|
1967
|
+
goog.asserts.assert(syncTime != null,
|
|
1859
1968
|
'EXT-X-PROGRAM-DATE-TIME format not valid');
|
|
1860
|
-
// Sync time offset is null on the first go-through. This indicates that
|
|
1861
|
-
// we have not yet seen every stream, and thus do not yet have enough
|
|
1862
|
-
// information to determine how to normalize the sync times.
|
|
1863
|
-
// For that first go-through, the sync time will be applied after the
|
|
1864
|
-
// references are all created. Until then, just offset by 0.
|
|
1865
|
-
syncTime = time + (this.syncTimeOffset_ || 0);
|
|
1866
1969
|
}
|
|
1867
1970
|
}
|
|
1868
1971
|
|
|
@@ -2096,16 +2199,13 @@ shaka.hls.HlsParser = class {
|
|
|
2096
2199
|
firstStartTime = mediaSequenceToStartTime.get(position);
|
|
2097
2200
|
}
|
|
2098
2201
|
|
|
2099
|
-
const firstSegmentUri = hlsSegments[0].absoluteUri;
|
|
2100
|
-
shaka.log.debug('First segment', firstSegmentUri.split('/').pop(),
|
|
2101
|
-
'starts at', firstStartTime);
|
|
2102
|
-
|
|
2103
2202
|
/** @type {!Array.<!shaka.media.SegmentReference>} */
|
|
2104
2203
|
const references = [];
|
|
2105
2204
|
|
|
2205
|
+
let previousReference = null;
|
|
2206
|
+
|
|
2106
2207
|
for (let i = 0; i < hlsSegments.length; i++) {
|
|
2107
2208
|
const item = hlsSegments[i];
|
|
2108
|
-
const previousReference = references[references.length - 1];
|
|
2109
2209
|
const startTime =
|
|
2110
2210
|
(i == 0) ? firstStartTime : previousReference.endTime;
|
|
2111
2211
|
position = mediaSequenceNumber + skippedSegments + i;
|
|
@@ -2135,9 +2235,17 @@ shaka.hls.HlsParser = class {
|
|
|
2135
2235
|
variables,
|
|
2136
2236
|
playlist.absoluteUri,
|
|
2137
2237
|
type);
|
|
2238
|
+
previousReference = reference;
|
|
2138
2239
|
|
|
2139
2240
|
if (reference) {
|
|
2140
|
-
|
|
2241
|
+
if (this.config_.hls.ignoreManifestProgramDateTime &&
|
|
2242
|
+
this.minSequenceNumber_ != null &&
|
|
2243
|
+
position < this.minSequenceNumber_) {
|
|
2244
|
+
// This segment is ignored as part of our fallback synchronization
|
|
2245
|
+
// method.
|
|
2246
|
+
} else {
|
|
2247
|
+
references.push(reference);
|
|
2248
|
+
}
|
|
2141
2249
|
}
|
|
2142
2250
|
}
|
|
2143
2251
|
|
|
@@ -2267,12 +2375,16 @@ shaka.hls.HlsParser = class {
|
|
|
2267
2375
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
2268
2376
|
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
2269
2377
|
|
|
2378
|
+
// If you wait long enough, requesting the first segment can fail
|
|
2379
|
+
// because it has fallen off the left edge of DVR, so to be safer,
|
|
2380
|
+
// let's request the middle segment.
|
|
2270
2381
|
goog.asserts.assert(playlist.segments.length,
|
|
2271
2382
|
'Playlist should have segments!');
|
|
2272
|
-
const
|
|
2273
|
-
|
|
2383
|
+
const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2);
|
|
2384
|
+
const middleSegmentUri = this.variableSubstitution_(
|
|
2385
|
+
playlist.segments[middleSegmentIdx].absoluteUri, variables);
|
|
2274
2386
|
|
|
2275
|
-
const parsedUri = new goog.Uri(
|
|
2387
|
+
const parsedUri = new goog.Uri(middleSegmentUri);
|
|
2276
2388
|
const extension = parsedUri.getPath().split('.').pop();
|
|
2277
2389
|
const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
|
|
2278
2390
|
|
|
@@ -2302,7 +2414,7 @@ shaka.hls.HlsParser = class {
|
|
|
2302
2414
|
// If unable to guess mime type, request a segment and try getting it
|
|
2303
2415
|
// from the response.
|
|
2304
2416
|
const headRequest = shaka.net.NetworkingEngine.makeRequest(
|
|
2305
|
-
[
|
|
2417
|
+
[middleSegmentUri], this.config_.retryParameters);
|
|
2306
2418
|
headRequest.method = 'HEAD';
|
|
2307
2419
|
|
|
2308
2420
|
const response = await this.makeNetworkRequest_(
|
|
@@ -2589,7 +2701,6 @@ shaka.hls.HlsParser = class {
|
|
|
2589
2701
|
* stream: !shaka.extern.Stream,
|
|
2590
2702
|
* verbatimMediaPlaylistUri: string,
|
|
2591
2703
|
* absoluteMediaPlaylistUri: string,
|
|
2592
|
-
* minTimestamp: number,
|
|
2593
2704
|
* maxTimestamp: number,
|
|
2594
2705
|
* mediaSequenceToStartTime: !Map.<number, number>,
|
|
2595
2706
|
* canSkipSegments: boolean
|
|
@@ -2608,8 +2719,6 @@ shaka.hls.HlsParser = class {
|
|
|
2608
2719
|
* @property {string} absoluteMediaPlaylistUri
|
|
2609
2720
|
* The absolute media playlist URI, resolved relative to the master playlist
|
|
2610
2721
|
* and updated to reflect any redirects.
|
|
2611
|
-
* @property {number} minTimestamp
|
|
2612
|
-
* The minimum timestamp found in the stream.
|
|
2613
2722
|
* @property {number} maxTimestamp
|
|
2614
2723
|
* The maximum timestamp found in the stream.
|
|
2615
2724
|
* @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.
|
|
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
|
|
164
|
-
*
|
|
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
|
-
/**
|
|
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} */
|
|
@@ -1609,7 +1609,7 @@ shaka.media.StreamingEngine = class {
|
|
|
1609
1609
|
await this.playerInterface_.mediaSourceEngine.appendBuffer(
|
|
1610
1610
|
mediaState.type,
|
|
1611
1611
|
segment,
|
|
1612
|
-
reference.
|
|
1612
|
+
reference.startTime,
|
|
1613
1613
|
reference.endTime,
|
|
1614
1614
|
hasClosedCaptions,
|
|
1615
1615
|
seeked);
|
package/lib/player.js
CHANGED
|
@@ -6556,7 +6556,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
|
|
|
6556
6556
|
* @define {string} A version number taken from git at compile time.
|
|
6557
6557
|
* @export
|
|
6558
6558
|
*/
|
|
6559
|
-
shaka.Player.version = 'v4.1.
|
|
6559
|
+
shaka.Player.version = 'v4.1.1-uncompiled';
|
|
6560
6560
|
|
|
6561
6561
|
// Initialize the deprecation system using the version string we just set
|
|
6562
6562
|
// on the player.
|
package/lib/util/stream_utils.js
CHANGED
|
@@ -1015,6 +1015,7 @@ shaka.util.StreamUtils = class {
|
|
|
1015
1015
|
track.channelsCount = audio.channelsCount;
|
|
1016
1016
|
track.audioSamplingRate = audio.audioSamplingRate;
|
|
1017
1017
|
track.audioBandwidth = audio.bandwidth || null;
|
|
1018
|
+
track.spatialAudio = audio.spatialAudio;
|
|
1018
1019
|
track.label = audio.label;
|
|
1019
1020
|
track.audioRoles = audio.roles;
|
|
1020
1021
|
}
|
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.1.
|
|
4
|
+
"version": "4.1.1",
|
|
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.
|
|
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(
|
|
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,
|
|
2183
|
-
makeReference(12.5, 15,
|
|
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
|
});
|