shaka-player 4.3.1 → 4.3.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.
- package/.github/workflows/build-and-test.yaml +13 -0
- package/AUTHORS +1 -0
- package/CHANGELOG.md +31 -0
- package/CONTRIBUTORS +1 -0
- package/README.md +1 -1
- package/dist/deps.js +1 -1
- package/dist/locales.js +30 -30
- package/dist/shaka-player.compiled.d.ts +10 -0
- package/dist/shaka-player.compiled.debug.d.ts +10 -0
- package/dist/shaka-player.compiled.debug.externs.js +12 -0
- package/dist/shaka-player.compiled.debug.js +248 -244
- package/dist/shaka-player.compiled.debug.map +3 -3
- package/dist/shaka-player.compiled.externs.js +12 -0
- package/dist/shaka-player.compiled.js +730 -727
- package/dist/shaka-player.compiled.map +3 -3
- package/dist/shaka-player.ui.d.ts +10 -0
- package/dist/shaka-player.ui.debug.d.ts +10 -0
- package/dist/shaka-player.ui.debug.externs.js +12 -0
- package/dist/shaka-player.ui.debug.js +252 -248
- package/dist/shaka-player.ui.debug.map +3 -3
- package/dist/shaka-player.ui.externs.js +12 -0
- package/dist/shaka-player.ui.js +862 -859
- package/dist/shaka-player.ui.map +3 -3
- package/lib/ads/ad_manager.js +2 -2
- package/lib/hls/hls_parser.js +50 -18
- package/lib/media/media_source_engine.js +49 -3
- package/lib/media/segment_index.js +5 -10
- package/lib/media/segment_reference.js +42 -0
- package/lib/media/streaming_engine.js +47 -12
- package/lib/player.js +12 -2
- package/lib/text/ui_text_displayer.js +24 -4
- package/lib/text/web_vtt_generator.js +21 -9
- package/lib/util/dom_utils.js +3 -1
- package/lib/util/stream_utils.js +6 -0
- package/lib/util/timer.js +17 -0
- package/package.json +1 -1
- package/test/hls/hls_live_unit.js +8 -30
- package/test/hls/hls_parser_unit.js +30 -0
- package/test/media/segment_index_unit.js +28 -0
- package/test/media/streaming_engine_unit.js +91 -119
- package/test/player_integration.js +37 -3
- package/test/player_unit.js +4 -2
- package/test/test/boot.js +29 -11
- package/test/test/util/manifest_parser_util.js +4 -3
- package/test/test/util/test_scheme.js +2 -2
- package/test/text/ui_text_displayer_unit.js +70 -0
- package/ui/controls.js +5 -1
package/lib/ads/ad_manager.js
CHANGED
|
@@ -19,8 +19,8 @@ goog.require('shaka.util.IReleasable');
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* @event shaka.ads.AdManager.
|
|
23
|
-
* @description Fired when
|
|
22
|
+
* @event shaka.ads.AdManager.AdsLoadedEvent
|
|
23
|
+
* @description Fired when a sequence of ads has been loaded.
|
|
24
24
|
* @property {string} type
|
|
25
25
|
* 'ads-loaded'
|
|
26
26
|
* @property {number} loadTime
|
package/lib/hls/hls_parser.js
CHANGED
|
@@ -493,7 +493,11 @@ shaka.hls.HlsParser = class {
|
|
|
493
493
|
// Now adjust timestamps back to begin at 0.
|
|
494
494
|
const segmentN = segmentIndex.earliestReference();
|
|
495
495
|
if (segmentN) {
|
|
496
|
-
|
|
496
|
+
const streamOffset = -segmentN.startTime;
|
|
497
|
+
// Modify all SegmentReferences equally.
|
|
498
|
+
streamInfo.stream.segmentIndex.offset(streamOffset);
|
|
499
|
+
// Update other parts of streamInfo the same way.
|
|
500
|
+
this.offsetStreamInfo_(streamInfo, streamOffset);
|
|
497
501
|
}
|
|
498
502
|
}
|
|
499
503
|
}
|
|
@@ -535,22 +539,29 @@ shaka.hls.HlsParser = class {
|
|
|
535
539
|
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
|
|
536
540
|
const segmentIndex = streamInfo.stream.segmentIndex;
|
|
537
541
|
if (segmentIndex != null) {
|
|
542
|
+
// A segment's startTime should be based on its syncTime vs the lowest
|
|
543
|
+
// syncTime across all streams. The earliest segment sync time from
|
|
544
|
+
// any stream will become presentation time 0. If two streams start
|
|
545
|
+
// e.g. 6 seconds apart in syncTime, then their first segments will
|
|
546
|
+
// also start 6 seconds apart in presentation time.
|
|
547
|
+
|
|
538
548
|
const segment0 = segmentIndex.earliestReference();
|
|
539
549
|
if (segment0.syncTime == null) {
|
|
540
550
|
shaka.log.alwaysError('Missing EXT-X-PROGRAM-DATE-TIME for stream',
|
|
541
551
|
streamInfo.verbatimMediaPlaylistUri,
|
|
542
552
|
'Expect AV sync issues!');
|
|
543
553
|
} else {
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
// starting point. The earliest segment sync time from any stream
|
|
547
|
-
// will become presentation time 0. If two streams start e.g. 6
|
|
548
|
-
// seconds apart in syncTime, then their first segments will also
|
|
549
|
-
// start 6 seconds apart in presentation time.
|
|
554
|
+
// Stream metadata are offset by a fixed amount based on the
|
|
555
|
+
// first segment.
|
|
550
556
|
const segment0TargetTime = segment0.syncTime - lowestSyncTime;
|
|
551
557
|
const streamOffset = segment0TargetTime - segment0.startTime;
|
|
558
|
+
this.offsetStreamInfo_(streamInfo, streamOffset);
|
|
552
559
|
|
|
553
|
-
|
|
560
|
+
// This is computed across all segments separately to manage
|
|
561
|
+
// accumulated drift in durations.
|
|
562
|
+
for (const segment of segmentIndex) {
|
|
563
|
+
segment.syncAgainst(lowestSyncTime);
|
|
564
|
+
}
|
|
554
565
|
}
|
|
555
566
|
}
|
|
556
567
|
}
|
|
@@ -561,13 +572,13 @@ shaka.hls.HlsParser = class {
|
|
|
561
572
|
* @param {number} offset
|
|
562
573
|
* @private
|
|
563
574
|
*/
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
575
|
+
offsetStreamInfo_(streamInfo, offset) {
|
|
576
|
+
// Adjust our accounting of the maximum timestamp.
|
|
567
577
|
streamInfo.maxTimestamp += offset;
|
|
568
578
|
goog.asserts.assert(streamInfo.maxTimestamp >= 0,
|
|
569
579
|
'Negative maxTimestamp after adjustment!');
|
|
570
580
|
|
|
581
|
+
// Update our map from sequence number to start time.
|
|
571
582
|
const mediaSequenceToStartTime =
|
|
572
583
|
this.getMediaSequenceToStartTimeFor_(streamInfo);
|
|
573
584
|
for (const [key, value] of mediaSequenceToStartTime) {
|
|
@@ -2592,12 +2603,12 @@ shaka.hls.HlsParser = class {
|
|
|
2592
2603
|
/** @type {shaka.extern.HlsAes128Key|undefined} */
|
|
2593
2604
|
let hlsAes128Key = undefined;
|
|
2594
2605
|
|
|
2595
|
-
|
|
2596
|
-
|
|
2606
|
+
let discontinuitySequence = shaka.hls.Utils.getFirstTagWithNameAsNumber(
|
|
2607
|
+
playlist.tags, 'EXT-X-DISCONTINUITY-SEQUENCE', 0);
|
|
2597
2608
|
const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
|
|
2598
2609
|
playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
|
|
2599
|
-
const skipTag = shaka.hls.Utils.getFirstTagWithName(
|
|
2600
|
-
'EXT-X-SKIP');
|
|
2610
|
+
const skipTag = shaka.hls.Utils.getFirstTagWithName(
|
|
2611
|
+
playlist.tags, 'EXT-X-SKIP');
|
|
2601
2612
|
const skippedSegments =
|
|
2602
2613
|
skipTag ? Number(skipTag.getAttributeValue('SKIPPED-SEGMENTS')) : 0;
|
|
2603
2614
|
let position = mediaSequenceNumber + skippedSegments;
|
|
@@ -2620,11 +2631,20 @@ shaka.hls.HlsParser = class {
|
|
|
2620
2631
|
(i == 0) ? firstStartTime : previousReference.endTime;
|
|
2621
2632
|
position = mediaSequenceNumber + skippedSegments + i;
|
|
2622
2633
|
|
|
2634
|
+
const discontinuityTag = shaka.hls.Utils.getFirstTagWithName(
|
|
2635
|
+
item.tags, 'EXT-X-DISCONTINUITY');
|
|
2636
|
+
if (discontinuityTag) {
|
|
2637
|
+
discontinuitySequence++;
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2623
2640
|
// Apply new AES-128 tags as you see them, keeping a running total.
|
|
2624
2641
|
for (const drmTag of item.tags) {
|
|
2625
|
-
if (drmTag.name == 'EXT-X-KEY'
|
|
2626
|
-
|
|
2627
|
-
|
|
2642
|
+
if (drmTag.name == 'EXT-X-KEY') {
|
|
2643
|
+
if (drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
|
|
2644
|
+
hlsAes128Key = this.parseAES128DrmTag_(drmTag, playlist);
|
|
2645
|
+
} else {
|
|
2646
|
+
hlsAes128Key = undefined;
|
|
2647
|
+
}
|
|
2628
2648
|
}
|
|
2629
2649
|
}
|
|
2630
2650
|
|
|
@@ -2657,6 +2677,8 @@ shaka.hls.HlsParser = class {
|
|
|
2657
2677
|
previousReference = reference;
|
|
2658
2678
|
|
|
2659
2679
|
if (reference) {
|
|
2680
|
+
reference.discontinuitySequence = discontinuitySequence;
|
|
2681
|
+
|
|
2660
2682
|
if (this.config_.hls.ignoreManifestProgramDateTime &&
|
|
2661
2683
|
this.minSequenceNumber_ != null &&
|
|
2662
2684
|
position < this.minSequenceNumber_) {
|
|
@@ -2743,6 +2765,16 @@ shaka.hls.HlsParser = class {
|
|
|
2743
2765
|
}
|
|
2744
2766
|
}
|
|
2745
2767
|
|
|
2768
|
+
// lowestSyncTime is a value from a previous playlist update. Use it to
|
|
2769
|
+
// set reference start times. If this is the first playlist parse, we will
|
|
2770
|
+
// skip this step, and wait until we have sync time across stream types.
|
|
2771
|
+
const lowestSyncTime = this.lowestSyncTime_;
|
|
2772
|
+
if (someSyncTime && lowestSyncTime != Infinity) {
|
|
2773
|
+
for (const reference of references) {
|
|
2774
|
+
reference.syncAgainst(lowestSyncTime);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2746
2778
|
return references;
|
|
2747
2779
|
}
|
|
2748
2780
|
|
|
@@ -682,6 +682,8 @@ shaka.media.MediaSourceEngine = class {
|
|
|
682
682
|
// into extracting a timestamp for us to align text segments in sequence
|
|
683
683
|
// mode.
|
|
684
684
|
|
|
685
|
+
const duration = this.mediaSource_.duration;
|
|
686
|
+
|
|
685
687
|
// Timestamps can only be reliably extracted from video, not audio.
|
|
686
688
|
// Packed audio formats do not have internal timestamps at all.
|
|
687
689
|
// Prefer video for this when available.
|
|
@@ -725,7 +727,7 @@ shaka.media.MediaSourceEngine = class {
|
|
|
725
727
|
// Clear the buffer.
|
|
726
728
|
await this.enqueueOperation_(
|
|
727
729
|
contentType,
|
|
728
|
-
() => this.remove_(contentType, 0,
|
|
730
|
+
() => this.remove_(contentType, 0, duration));
|
|
729
731
|
|
|
730
732
|
// Finally, flush the buffer in case of choppy video start on HLS fMP4.
|
|
731
733
|
if (contentType == ContentType.VIDEO) {
|
|
@@ -737,6 +739,10 @@ shaka.media.MediaSourceEngine = class {
|
|
|
737
739
|
|
|
738
740
|
// Now switch to sequence mode and fall through to our normal operations.
|
|
739
741
|
sourceBuffer.mode = SEQUENCE;
|
|
742
|
+
|
|
743
|
+
// When we change the buffer mode the duration is lost, so we need to set
|
|
744
|
+
// it explicitly.
|
|
745
|
+
await this.setDuration(duration);
|
|
740
746
|
}
|
|
741
747
|
|
|
742
748
|
if (reference && this.sequenceMode_ && contentType != ContentType.TEXT) {
|
|
@@ -771,8 +777,13 @@ shaka.media.MediaSourceEngine = class {
|
|
|
771
777
|
bufferedBefore, bufferedAfter);
|
|
772
778
|
if (newBuffered) {
|
|
773
779
|
const segmentDuration = reference.endTime - reference.startTime;
|
|
774
|
-
|
|
775
|
-
|
|
780
|
+
// Check end times instead of start times. We may be overwriting a
|
|
781
|
+
// buffer and only the end changes, and that would be fine.
|
|
782
|
+
// Also, exclude tiny segments. Sometimes alignment segments as small
|
|
783
|
+
// as 33ms are seen in Google DAI content. For such tiny segments,
|
|
784
|
+
// half a segment duration would be no issue.
|
|
785
|
+
const offset = Math.abs(newBuffered.end - reference.endTime);
|
|
786
|
+
if (segmentDuration > 0.100 && offset > segmentDuration / 2) {
|
|
776
787
|
shaka.log.error('Possible encoding problem detected!',
|
|
777
788
|
'Unexpected buffered range for reference', reference,
|
|
778
789
|
'from URIs', reference.getUris(),
|
|
@@ -924,6 +935,41 @@ shaka.media.MediaSourceEngine = class {
|
|
|
924
935
|
]);
|
|
925
936
|
}
|
|
926
937
|
|
|
938
|
+
/**
|
|
939
|
+
* Adjust timestamp offset to maintain AV sync across discontinuities.
|
|
940
|
+
* Only used in sequence mode.
|
|
941
|
+
*
|
|
942
|
+
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
|
|
943
|
+
* @param {number} timestampOffset
|
|
944
|
+
* @return {!Promise}
|
|
945
|
+
*/
|
|
946
|
+
async resync(contentType, timestampOffset) {
|
|
947
|
+
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
948
|
+
|
|
949
|
+
goog.asserts.assert(this.sequenceMode_,
|
|
950
|
+
'resync only used with sequence mode!');
|
|
951
|
+
|
|
952
|
+
if (contentType == ContentType.TEXT) {
|
|
953
|
+
// This operation is for audio and video only.
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Queue an abort() to help MSE splice together overlapping segments.
|
|
958
|
+
// We set appendWindowEnd when we change periods in DASH content, and the
|
|
959
|
+
// period transition may result in overlap.
|
|
960
|
+
//
|
|
961
|
+
// An abort() also helps with MPEG2-TS. When we append a TS segment, we
|
|
962
|
+
// always enter a PARSING_MEDIA_SEGMENT state and we can't change the
|
|
963
|
+
// timestamp offset. By calling abort(), we reset the state so we can
|
|
964
|
+
// set it.
|
|
965
|
+
this.enqueueOperation_(
|
|
966
|
+
contentType,
|
|
967
|
+
() => this.abort_(contentType));
|
|
968
|
+
await this.enqueueOperation_(
|
|
969
|
+
contentType,
|
|
970
|
+
() => this.setTimestampOffset_(contentType, timestampOffset));
|
|
971
|
+
}
|
|
972
|
+
|
|
927
973
|
/**
|
|
928
974
|
* @param {string=} reason Valid reasons are 'network' and 'decode'.
|
|
929
975
|
* @return {!Promise}
|
|
@@ -183,15 +183,7 @@ shaka.media.SegmentIndex = class {
|
|
|
183
183
|
offset(offset) {
|
|
184
184
|
if (!this.immutable_) {
|
|
185
185
|
for (const ref of this.references) {
|
|
186
|
-
ref.
|
|
187
|
-
ref.endTime += 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
|
-
}
|
|
186
|
+
ref.offset(offset);
|
|
195
187
|
}
|
|
196
188
|
}
|
|
197
189
|
}
|
|
@@ -225,8 +217,11 @@ shaka.media.SegmentIndex = class {
|
|
|
225
217
|
// Partial segments are used for live edge, and should be removed when they
|
|
226
218
|
// get older. Remove the old SegmentReferences after the first new
|
|
227
219
|
// reference's start time.
|
|
220
|
+
// Use times rounded to the millisecond for filtering purposes, so that
|
|
221
|
+
// tiny rounding errors will not result in duplicate segments in the index.
|
|
222
|
+
const firstStartTime = Math.round(references[0].startTime * 1000) / 1000;
|
|
228
223
|
this.references = this.references.filter((r) => {
|
|
229
|
-
return r.startTime <
|
|
224
|
+
return (Math.round(r.startTime * 1000) / 1000) < firstStartTime;
|
|
230
225
|
});
|
|
231
226
|
|
|
232
227
|
this.references.push(...references);
|
|
@@ -8,6 +8,7 @@ goog.provide('shaka.media.InitSegmentReference');
|
|
|
8
8
|
goog.provide('shaka.media.SegmentReference');
|
|
9
9
|
|
|
10
10
|
goog.require('goog.asserts');
|
|
11
|
+
goog.require('shaka.log');
|
|
11
12
|
goog.require('shaka.util.ArrayUtils');
|
|
12
13
|
|
|
13
14
|
|
|
@@ -246,6 +247,9 @@ shaka.media.SegmentReference = class {
|
|
|
246
247
|
|
|
247
248
|
/** @type {?shaka.media.SegmentReference.ThumbnailSprite} */
|
|
248
249
|
this.thumbnailSprite = null;
|
|
250
|
+
|
|
251
|
+
/** @type {number} */
|
|
252
|
+
this.discontinuitySequence = 0;
|
|
249
253
|
}
|
|
250
254
|
|
|
251
255
|
/**
|
|
@@ -380,6 +384,44 @@ shaka.media.SegmentReference = class {
|
|
|
380
384
|
getThumbnailSprite() {
|
|
381
385
|
return this.thumbnailSprite;
|
|
382
386
|
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Offset the segment reference by a fixed amount.
|
|
390
|
+
*
|
|
391
|
+
* @param {number} offset The amount to add to the segment's start and end
|
|
392
|
+
* times.
|
|
393
|
+
* @export
|
|
394
|
+
*/
|
|
395
|
+
offset(offset) {
|
|
396
|
+
this.startTime += offset;
|
|
397
|
+
this.endTime += offset;
|
|
398
|
+
this.trueEndTime += offset;
|
|
399
|
+
|
|
400
|
+
for (const partial of this.partialReferences) {
|
|
401
|
+
partial.startTime += offset;
|
|
402
|
+
partial.endTime += offset;
|
|
403
|
+
partial.trueEndTime += offset;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Sync this segment against a particular sync time that will serve as "0" in
|
|
409
|
+
* the presentation timeline.
|
|
410
|
+
*
|
|
411
|
+
* @param {number} lowestSyncTime
|
|
412
|
+
* @export
|
|
413
|
+
*/
|
|
414
|
+
syncAgainst(lowestSyncTime) {
|
|
415
|
+
if (this.syncTime == null) {
|
|
416
|
+
shaka.log.alwaysError('Sync attempted without sync time!');
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const desiredStart = this.syncTime - lowestSyncTime;
|
|
420
|
+
const offset = desiredStart - this.startTime;
|
|
421
|
+
if (Math.abs(offset) >= 0.001) {
|
|
422
|
+
this.offset(offset);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
383
425
|
};
|
|
384
426
|
|
|
385
427
|
|
|
@@ -622,6 +622,11 @@ shaka.media.StreamingEngine = class {
|
|
|
622
622
|
* within the presentation timeline.
|
|
623
623
|
*/
|
|
624
624
|
seeked() {
|
|
625
|
+
if (!this.playerInterface_) {
|
|
626
|
+
// Already destroyed.
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
625
630
|
const presentationTime = this.playerInterface_.getPresentationTime();
|
|
626
631
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
627
632
|
const newTimeIsBuffered = (type) => {
|
|
@@ -679,7 +684,7 @@ shaka.media.StreamingEngine = class {
|
|
|
679
684
|
|
|
680
685
|
/**
|
|
681
686
|
* Clear the buffer for a given stream. Unlike clearBuffer_, this will handle
|
|
682
|
-
* cases where a MediaState is performing an update. After this runs,
|
|
687
|
+
* cases where a MediaState is performing an update. After this runs, the
|
|
683
688
|
* MediaState will have a pending update.
|
|
684
689
|
* @param {!shaka.media.StreamingEngine.MediaState_} mediaState
|
|
685
690
|
* @private
|
|
@@ -795,9 +800,9 @@ shaka.media.StreamingEngine = class {
|
|
|
795
800
|
for (const type of streamsByType.keys()) {
|
|
796
801
|
const stream = streamsByType.get(type);
|
|
797
802
|
if (!this.mediaStates_.has(type)) {
|
|
798
|
-
const
|
|
799
|
-
this.mediaStates_.set(type,
|
|
800
|
-
this.scheduleUpdate_(
|
|
803
|
+
const mediaState = this.createMediaState_(stream);
|
|
804
|
+
this.mediaStates_.set(type, mediaState);
|
|
805
|
+
this.scheduleUpdate_(mediaState, 0);
|
|
801
806
|
}
|
|
802
807
|
}
|
|
803
808
|
}
|
|
@@ -831,6 +836,7 @@ shaka.media.StreamingEngine = class {
|
|
|
831
836
|
// The playhead might be seeking on startup, if a start time is set, so
|
|
832
837
|
// start "seeked" as true.
|
|
833
838
|
seeked: true,
|
|
839
|
+
needsResync: false,
|
|
834
840
|
recovering: false,
|
|
835
841
|
hasError: false,
|
|
836
842
|
operation: null,
|
|
@@ -909,7 +915,7 @@ shaka.media.StreamingEngine = class {
|
|
|
909
915
|
'mediastate.stream should not have segmentIndex yet.');
|
|
910
916
|
thisStream.closeSegmentIndex();
|
|
911
917
|
}
|
|
912
|
-
if (mediaState.
|
|
918
|
+
if (!mediaState.performingUpdate && !mediaState.updateTimer) {
|
|
913
919
|
this.scheduleUpdate_(mediaState, 0);
|
|
914
920
|
}
|
|
915
921
|
return;
|
|
@@ -1154,6 +1160,11 @@ shaka.media.StreamingEngine = class {
|
|
|
1154
1160
|
shaka.log.v1(
|
|
1155
1161
|
logPrefix, 'looking up segment from new stream endTime:', time);
|
|
1156
1162
|
|
|
1163
|
+
// Using a new iterator means we need to resync the stream in sequence
|
|
1164
|
+
// mode. The buffered range might not align perfectly with the last
|
|
1165
|
+
// segment end time, so we may end up repeating a segment. Resyncing
|
|
1166
|
+
// makes this safe to do.
|
|
1167
|
+
mediaState.needsResync = true;
|
|
1157
1168
|
mediaState.segmentIterator =
|
|
1158
1169
|
mediaState.stream.segmentIndex.getIteratorForTime(time);
|
|
1159
1170
|
const ref = mediaState.segmentIterator &&
|
|
@@ -1394,7 +1405,7 @@ shaka.media.StreamingEngine = class {
|
|
|
1394
1405
|
// If the network slows down, abort the current fetch request and start
|
|
1395
1406
|
// a new one, and ignore the error message.
|
|
1396
1407
|
mediaState.performingUpdate = false;
|
|
1397
|
-
mediaState
|
|
1408
|
+
this.cancelUpdate_(mediaState);
|
|
1398
1409
|
this.scheduleUpdate_(mediaState, 0);
|
|
1399
1410
|
} else if (mediaState.type == ContentType.TEXT &&
|
|
1400
1411
|
this.config_.ignoreTextStreamFailures) {
|
|
@@ -1467,7 +1478,10 @@ shaka.media.StreamingEngine = class {
|
|
|
1467
1478
|
|
|
1468
1479
|
for (const mediaState of this.mediaStates_.values()) {
|
|
1469
1480
|
const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
|
|
1470
|
-
if
|
|
1481
|
+
// Only schedule an update if it has an error, but it's not mid-update
|
|
1482
|
+
// and there is not already an update scheduled.
|
|
1483
|
+
if (mediaState.hasError && !mediaState.performingUpdate &&
|
|
1484
|
+
!mediaState.updateTimer) {
|
|
1471
1485
|
shaka.log.info(logPrefix, 'Retrying after failure...');
|
|
1472
1486
|
mediaState.hasError = false;
|
|
1473
1487
|
this.scheduleUpdate_(mediaState, delaySeconds);
|
|
@@ -1661,6 +1675,21 @@ shaka.media.StreamingEngine = class {
|
|
|
1661
1675
|
}
|
|
1662
1676
|
}
|
|
1663
1677
|
|
|
1678
|
+
if (this.manifest_.sequenceMode) {
|
|
1679
|
+
// Across discontinuity bounds, we should resync timestamps for
|
|
1680
|
+
// sequence mode playbacks. The next segment appended should
|
|
1681
|
+
// land at its theoretical timestamp from the segment index.
|
|
1682
|
+
const lastDiscontinuitySequence =
|
|
1683
|
+
mediaState.lastSegmentReference ?
|
|
1684
|
+
mediaState.lastSegmentReference.discontinuitySequence : null;
|
|
1685
|
+
if (reference.discontinuitySequence != lastDiscontinuitySequence ||
|
|
1686
|
+
mediaState.needsResync) {
|
|
1687
|
+
mediaState.needsResync = false;
|
|
1688
|
+
operations.push(this.playerInterface_.mediaSourceEngine.resync(
|
|
1689
|
+
mediaState.type, reference.startTime));
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1664
1693
|
await Promise.all(operations);
|
|
1665
1694
|
}
|
|
1666
1695
|
|
|
@@ -1715,8 +1744,6 @@ shaka.media.StreamingEngine = class {
|
|
|
1715
1744
|
|
|
1716
1745
|
await this.evict_(mediaState, presentationTime);
|
|
1717
1746
|
this.destroyer_.ensureNotDestroyed();
|
|
1718
|
-
shaka.log.v1(logPrefix, 'appending media segment at',
|
|
1719
|
-
(reference.syncTime == null ? 'unknown' : reference.syncTime));
|
|
1720
1747
|
|
|
1721
1748
|
// 'seeked' or 'adaptation' triggered logic applies only to this
|
|
1722
1749
|
// appendBuffer() call.
|
|
@@ -2079,7 +2106,11 @@ shaka.media.StreamingEngine = class {
|
|
|
2079
2106
|
shaka.log.debug(logPrefix, 'cleared buffer');
|
|
2080
2107
|
mediaState.clearingBuffer = false;
|
|
2081
2108
|
mediaState.endOfStream = false;
|
|
2082
|
-
|
|
2109
|
+
// Since the clear operation was async, check to make sure we're not doing
|
|
2110
|
+
// another update and we don't have one scheduled yet.
|
|
2111
|
+
if (!mediaState.performingUpdate && !mediaState.updateTimer) {
|
|
2112
|
+
this.scheduleUpdate_(mediaState, 0);
|
|
2113
|
+
}
|
|
2083
2114
|
}
|
|
2084
2115
|
|
|
2085
2116
|
|
|
@@ -2258,6 +2289,7 @@ shaka.media.StreamingEngine.PlayerInterface;
|
|
|
2258
2289
|
* clearBufferSafeMargin: number,
|
|
2259
2290
|
* clearingBuffer: boolean,
|
|
2260
2291
|
* seeked: boolean,
|
|
2292
|
+
* needsResync: boolean,
|
|
2261
2293
|
* adaptation: boolean,
|
|
2262
2294
|
* recovering: boolean,
|
|
2263
2295
|
* hasError: boolean,
|
|
@@ -2303,10 +2335,13 @@ shaka.media.StreamingEngine.PlayerInterface;
|
|
|
2303
2335
|
* The amount of buffer to retain when clearing the buffer after the update.
|
|
2304
2336
|
* @property {boolean} clearingBuffer
|
|
2305
2337
|
* True indicates that the buffer is being cleared.
|
|
2306
|
-
|
|
2338
|
+
* @property {boolean} seeked
|
|
2307
2339
|
* True indicates that the presentation just seeked.
|
|
2308
|
-
|
|
2340
|
+
* @property {boolean} adaptation
|
|
2309
2341
|
* True indicates that the presentation just automatically switched variants.
|
|
2342
|
+
* @property {boolean} needsResync
|
|
2343
|
+
* True indicates that the stream needs to be resynced in sequence mode,
|
|
2344
|
+
* regardless of discontinuity sequence.
|
|
2310
2345
|
* @property {boolean} recovering
|
|
2311
2346
|
* True indicates that the last segment was not appended because it could not
|
|
2312
2347
|
* fit in the buffer.
|
package/lib/player.js
CHANGED
|
@@ -1537,6 +1537,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
|
|
|
1537
1537
|
has.startTime = null;
|
|
1538
1538
|
has.uri = null;
|
|
1539
1539
|
|
|
1540
|
+
// Release the region timeline, which is created when parsing the manifest.
|
|
1541
|
+
if (this.regionTimeline_) {
|
|
1542
|
+
this.regionTimeline_.release();
|
|
1543
|
+
this.regionTimeline_ = null;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1540
1546
|
// In most cases we should have a media element. The one exception would
|
|
1541
1547
|
// be if there was an error and we, by chance, did not have a media element.
|
|
1542
1548
|
if (has.mediaElement) {
|
|
@@ -4330,6 +4336,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
|
|
|
4330
4336
|
return [];
|
|
4331
4337
|
}
|
|
4332
4338
|
const chapters = [];
|
|
4339
|
+
const uniqueChapters = new Set();
|
|
4333
4340
|
for (const chaptersTrack of chaptersTracksWithLanguage) {
|
|
4334
4341
|
if (chaptersTrack && chaptersTrack.cues) {
|
|
4335
4342
|
for (const cue of chaptersTrack.cues) {
|
|
@@ -4344,7 +4351,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
|
|
|
4344
4351
|
startTime: cue.startTime,
|
|
4345
4352
|
endTime: cue.endTime,
|
|
4346
4353
|
};
|
|
4347
|
-
|
|
4354
|
+
if (!uniqueChapters.has(id)) {
|
|
4355
|
+
chapters.push(chapter);
|
|
4356
|
+
uniqueChapters.add(id);
|
|
4357
|
+
}
|
|
4348
4358
|
}
|
|
4349
4359
|
}
|
|
4350
4360
|
}
|
|
@@ -6974,7 +6984,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
|
|
|
6974
6984
|
* @define {string} A version number taken from git at compile time.
|
|
6975
6985
|
* @export
|
|
6976
6986
|
*/
|
|
6977
|
-
shaka.Player.version = 'v4.3.
|
|
6987
|
+
shaka.Player.version = 'v4.3.3-uncompiled';
|
|
6978
6988
|
|
|
6979
6989
|
// Initialize the deprecation system using the version string we just set
|
|
6980
6990
|
// on the player.
|
|
@@ -371,6 +371,25 @@ shaka.text.UITextDisplayer = class {
|
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Compute a unique internal id:
|
|
376
|
+
* Regions can reuse the id but have different dimensions, we need to
|
|
377
|
+
* consider those differences
|
|
378
|
+
* @param {shaka.extern.CueRegion} region
|
|
379
|
+
* @private
|
|
380
|
+
*/
|
|
381
|
+
generateRegionId_(region) {
|
|
382
|
+
const percentageUnit = shaka.text.CueRegion.units.PERCENTAGE;
|
|
383
|
+
const heightUnit = region.heightUnits == percentageUnit ? '%' : 'px';
|
|
384
|
+
const viewportAnchorUnit =
|
|
385
|
+
region.viewportAnchorUnits == percentageUnit ? '%' : 'px';
|
|
386
|
+
const uniqueRegionId = `${region.id}_${
|
|
387
|
+
region.width}x${region.height}${heightUnit}-${
|
|
388
|
+
region.viewportAnchorX}x${region.viewportAnchorY}${viewportAnchorUnit}`;
|
|
389
|
+
|
|
390
|
+
return uniqueRegionId;
|
|
391
|
+
}
|
|
392
|
+
|
|
374
393
|
/**
|
|
375
394
|
* Get or create a region element corresponding to the cue region. These are
|
|
376
395
|
* cached by ID.
|
|
@@ -382,8 +401,9 @@ shaka.text.UITextDisplayer = class {
|
|
|
382
401
|
getRegionElement_(cue) {
|
|
383
402
|
const region = cue.region;
|
|
384
403
|
|
|
385
|
-
|
|
386
|
-
|
|
404
|
+
const regionId = this.generateRegionId_(region);
|
|
405
|
+
if (this.regionElements_.has(regionId)) {
|
|
406
|
+
return this.regionElements_.get(regionId);
|
|
387
407
|
}
|
|
388
408
|
|
|
389
409
|
const regionElement = shaka.util.Dom.createHTMLElement('span');
|
|
@@ -394,7 +414,7 @@ shaka.text.UITextDisplayer = class {
|
|
|
394
414
|
const viewportAnchorUnit =
|
|
395
415
|
region.viewportAnchorUnits == percentageUnit ? '%' : 'px';
|
|
396
416
|
|
|
397
|
-
regionElement.id = 'shaka-text-region---' +
|
|
417
|
+
regionElement.id = 'shaka-text-region---' + regionId;
|
|
398
418
|
regionElement.classList.add('shaka-text-region');
|
|
399
419
|
|
|
400
420
|
regionElement.style.height = region.height + heightUnit;
|
|
@@ -415,7 +435,7 @@ shaka.text.UITextDisplayer = class {
|
|
|
415
435
|
regionElement.style.justifyContent = 'flex-end';
|
|
416
436
|
}
|
|
417
437
|
|
|
418
|
-
this.regionElements_.set(
|
|
438
|
+
this.regionElements_.set(regionId, regionElement);
|
|
419
439
|
return regionElement;
|
|
420
440
|
}
|
|
421
441
|
|
|
@@ -81,16 +81,28 @@ shaka.text.WebVttGenerator = class {
|
|
|
81
81
|
// We don't want to modify the array or objects passed in, since we don't
|
|
82
82
|
// technically own them. So we build a new array and replace certain items
|
|
83
83
|
// in it if they need to be flattened.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
// We also don't want to flatten the text payloads starting at a container
|
|
85
|
+
// element; otherwise, for containers encapsulating multiple caption lines,
|
|
86
|
+
// the lines would merge into a single cue. This is undesirable when a
|
|
87
|
+
// subset of the captions are outside of the append time window. To fix
|
|
88
|
+
// this, we only call flattenPayload() starting at elements marked as
|
|
89
|
+
// isContainer = false.
|
|
90
|
+
const getCuesToFlatten = (cues, result) => {
|
|
91
|
+
for (const cue of cues) {
|
|
92
|
+
if (cue.isContainer) {
|
|
93
|
+
// Recurse to find the actual text payload cues.
|
|
94
|
+
getCuesToFlatten(cue.nestedCues, result);
|
|
95
|
+
} else {
|
|
96
|
+
// Flatten the payload.
|
|
97
|
+
const flatCue = cue.clone();
|
|
98
|
+
flatCue.nestedCues = [];
|
|
99
|
+
flatCue.payload = flattenPayload(cue);
|
|
100
|
+
result.push(flatCue);
|
|
101
|
+
}
|
|
92
102
|
}
|
|
93
|
-
|
|
103
|
+
return result;
|
|
104
|
+
};
|
|
105
|
+
const flattenedCues = getCuesToFlatten(cues, []);
|
|
94
106
|
|
|
95
107
|
let webvttString = 'WEBVTT\n\n';
|
|
96
108
|
for (const cue of flattenedCues) {
|
package/lib/util/dom_utils.js
CHANGED
|
@@ -36,7 +36,9 @@ shaka.util.Dom = class {
|
|
|
36
36
|
* @return {!HTMLButtonElement}
|
|
37
37
|
*/
|
|
38
38
|
static createButton() {
|
|
39
|
-
|
|
39
|
+
const button = document.createElement('button');
|
|
40
|
+
button.setAttribute('type', 'button');
|
|
41
|
+
return /** @type {!HTMLButtonElement} */ (button);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
|
package/lib/util/stream_utils.js
CHANGED
|
@@ -459,12 +459,16 @@ shaka.util.StreamUtils = class {
|
|
|
459
459
|
if (!Capabilities.isTypeSupported(audioFullType)) {
|
|
460
460
|
return false;
|
|
461
461
|
}
|
|
462
|
+
// Update the codec string with the (possibly) converted codecs.
|
|
463
|
+
videoCodecs = [videoCodecs, audioCodecs].join(',');
|
|
462
464
|
}
|
|
463
465
|
const fullType = shaka.util.MimeUtils.getFullOrConvertedType(
|
|
464
466
|
video.mimeType, videoCodecs, ContentType.VIDEO);
|
|
465
467
|
if (!Capabilities.isTypeSupported(fullType)) {
|
|
466
468
|
return false;
|
|
467
469
|
}
|
|
470
|
+
// Update the codec string with the (possibly) converted codecs.
|
|
471
|
+
video.codecs = videoCodecs;
|
|
468
472
|
}
|
|
469
473
|
const audio = variant.audio;
|
|
470
474
|
if (audio) {
|
|
@@ -475,6 +479,8 @@ shaka.util.StreamUtils = class {
|
|
|
475
479
|
if (!Capabilities.isTypeSupported(fullType)) {
|
|
476
480
|
return false;
|
|
477
481
|
}
|
|
482
|
+
// Update the codec string with the (possibly) converted codecs.
|
|
483
|
+
audio.codecs = codecs;
|
|
478
484
|
}
|
|
479
485
|
|
|
480
486
|
// See: https://github.com/shaka-project/shaka-player/issues/3380
|
package/lib/util/timer.js
CHANGED
|
@@ -78,6 +78,11 @@ shaka.util.Timer = class {
|
|
|
78
78
|
tickEvery(seconds) {
|
|
79
79
|
this.stop();
|
|
80
80
|
|
|
81
|
+
if (goog.DEBUG) {
|
|
82
|
+
// Capture the stack trace by making a fake error.
|
|
83
|
+
const stackTrace = Error('Timer created').stack;
|
|
84
|
+
shaka.util.Timer.activeTimers.set(this, stackTrace);
|
|
85
|
+
}
|
|
81
86
|
this.ticker_ = new shaka.util.DelayedTick(() => {
|
|
82
87
|
// Schedule the timer again first. |onTick_| could cancel the timer and
|
|
83
88
|
// rescheduling first simplifies the implementation.
|
|
@@ -99,5 +104,17 @@ shaka.util.Timer = class {
|
|
|
99
104
|
this.ticker_.stop();
|
|
100
105
|
this.ticker_ = null;
|
|
101
106
|
}
|
|
107
|
+
if (goog.DEBUG) {
|
|
108
|
+
shaka.util.Timer.activeTimers.delete(this);
|
|
109
|
+
}
|
|
102
110
|
}
|
|
103
111
|
};
|
|
112
|
+
|
|
113
|
+
if (goog.DEBUG) {
|
|
114
|
+
/**
|
|
115
|
+
* Tracks all active timer instances, along with the stack trace that created
|
|
116
|
+
* that timer.
|
|
117
|
+
* @type {!Map.<!shaka.util.Timer, string>}
|
|
118
|
+
*/
|
|
119
|
+
shaka.util.Timer.activeTimers = new Map();
|
|
120
|
+
}
|