shaka-player 4.2.5 → 4.2.7
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/dist/deps.js +1 -1
- package/dist/locales.js +31 -31
- 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 +252 -246
- package/dist/shaka-player.compiled.debug.map +3 -3
- package/dist/shaka-player.compiled.externs.js +12 -0
- package/dist/shaka-player.compiled.js +713 -708
- 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 +266 -260
- package/dist/shaka-player.ui.debug.map +3 -3
- package/dist/shaka-player.ui.externs.js +12 -0
- package/dist/shaka-player.ui.js +844 -840
- package/dist/shaka-player.ui.map +3 -3
- package/lib/ads/ad_manager.js +2 -2
- package/lib/hls/hls_parser.js +63 -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/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
|
@@ -136,6 +136,14 @@ shaka.hls.HlsParser = class {
|
|
|
136
136
|
*/
|
|
137
137
|
this.minSequenceNumber_ = null;
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* The lowest time value for any of the streams, as defined by the
|
|
141
|
+
* EXT-X-PROGRAM-DATE-TIME value. Measured in seconds since January 1, 1970.
|
|
142
|
+
*
|
|
143
|
+
* @private {number}
|
|
144
|
+
*/
|
|
145
|
+
this.lowestSyncTime_ = Infinity;
|
|
146
|
+
|
|
139
147
|
/**
|
|
140
148
|
* This timer is used to trigger the start of a manifest update. A manifest
|
|
141
149
|
* update is async. Once the update is finished, the timer will be restarted
|
|
@@ -446,7 +454,11 @@ shaka.hls.HlsParser = class {
|
|
|
446
454
|
// Now adjust timestamps back to begin at 0.
|
|
447
455
|
const segmentN = segmentIndex.earliestReference();
|
|
448
456
|
if (segmentN) {
|
|
449
|
-
|
|
457
|
+
const streamOffset = -segmentN.startTime;
|
|
458
|
+
// Modify all SegmentReferences equally.
|
|
459
|
+
streamInfo.stream.segmentIndex.offset(streamOffset);
|
|
460
|
+
// Update other parts of streamInfo the same way.
|
|
461
|
+
this.offsetStreamInfo_(streamInfo, streamOffset);
|
|
450
462
|
}
|
|
451
463
|
}
|
|
452
464
|
}
|
|
@@ -465,6 +477,10 @@ shaka.hls.HlsParser = class {
|
|
|
465
477
|
return;
|
|
466
478
|
}
|
|
467
479
|
|
|
480
|
+
if (this.lowestSyncTime_ != Infinity) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
468
484
|
let lowestSyncTime = Infinity;
|
|
469
485
|
|
|
470
486
|
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
|
|
@@ -481,28 +497,36 @@ shaka.hls.HlsParser = class {
|
|
|
481
497
|
// Nothing to sync.
|
|
482
498
|
return;
|
|
483
499
|
}
|
|
500
|
+
this.lowestSyncTime_ = lowestSyncTime;
|
|
484
501
|
|
|
485
502
|
shaka.log.debug('Syncing HLS streams against base time:', lowestSyncTime);
|
|
486
503
|
|
|
487
504
|
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
|
|
488
505
|
const segmentIndex = streamInfo.stream.segmentIndex;
|
|
489
506
|
if (segmentIndex != null) {
|
|
507
|
+
// A segment's startTime should be based on its syncTime vs the lowest
|
|
508
|
+
// syncTime across all streams. The earliest segment sync time from
|
|
509
|
+
// any stream will become presentation time 0. If two streams start
|
|
510
|
+
// e.g. 6 seconds apart in syncTime, then their first segments will
|
|
511
|
+
// also start 6 seconds apart in presentation time.
|
|
512
|
+
|
|
490
513
|
const segment0 = segmentIndex.earliestReference();
|
|
491
514
|
if (segment0.syncTime == null) {
|
|
492
515
|
shaka.log.alwaysError('Missing EXT-X-PROGRAM-DATE-TIME for stream',
|
|
493
516
|
streamInfo.verbatimMediaPlaylistUri,
|
|
494
517
|
'Expect AV sync issues!');
|
|
495
518
|
} else {
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
// starting point. The earliest segment sync time from any stream
|
|
499
|
-
// will become presentation time 0. If two streams start e.g. 6
|
|
500
|
-
// seconds apart in syncTime, then their first segments will also
|
|
501
|
-
// start 6 seconds apart in presentation time.
|
|
519
|
+
// Stream metadata are offset by a fixed amount based on the
|
|
520
|
+
// first segment.
|
|
502
521
|
const segment0TargetTime = segment0.syncTime - lowestSyncTime;
|
|
503
522
|
const streamOffset = segment0TargetTime - segment0.startTime;
|
|
523
|
+
this.offsetStreamInfo_(streamInfo, streamOffset);
|
|
504
524
|
|
|
505
|
-
|
|
525
|
+
// This is computed across all segments separately to manage
|
|
526
|
+
// accumulated drift in durations.
|
|
527
|
+
for (const segment of segmentIndex) {
|
|
528
|
+
segment.syncAgainst(lowestSyncTime);
|
|
529
|
+
}
|
|
506
530
|
}
|
|
507
531
|
}
|
|
508
532
|
}
|
|
@@ -515,13 +539,13 @@ shaka.hls.HlsParser = class {
|
|
|
515
539
|
* @param {number} offset
|
|
516
540
|
* @private
|
|
517
541
|
*/
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
542
|
+
offsetStreamInfo_(streamInfo, offset) {
|
|
543
|
+
// Adjust our accounting of the maximum timestamp.
|
|
521
544
|
streamInfo.maxTimestamp += offset;
|
|
522
545
|
goog.asserts.assert(streamInfo.maxTimestamp >= 0,
|
|
523
546
|
'Negative maxTimestamp after adjustment!');
|
|
524
547
|
|
|
548
|
+
// Update our map from sequence number to start time.
|
|
525
549
|
for (const [key, value] of streamInfo.mediaSequenceToStartTime) {
|
|
526
550
|
streamInfo.mediaSequenceToStartTime.set(key, value + offset);
|
|
527
551
|
}
|
|
@@ -2243,12 +2267,12 @@ shaka.hls.HlsParser = class {
|
|
|
2243
2267
|
/** @type {shaka.extern.HlsAes128Key|undefined} */
|
|
2244
2268
|
let hlsAes128Key = undefined;
|
|
2245
2269
|
|
|
2246
|
-
|
|
2247
|
-
|
|
2270
|
+
let discontinuitySequence = shaka.hls.Utils.getFirstTagWithNameAsNumber(
|
|
2271
|
+
playlist.tags, 'EXT-X-DISCONTINUITY-SEQUENCE', 0);
|
|
2248
2272
|
const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
|
|
2249
2273
|
playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
|
|
2250
|
-
const skipTag = shaka.hls.Utils.getFirstTagWithName(
|
|
2251
|
-
'EXT-X-SKIP');
|
|
2274
|
+
const skipTag = shaka.hls.Utils.getFirstTagWithName(
|
|
2275
|
+
playlist.tags, 'EXT-X-SKIP');
|
|
2252
2276
|
const skippedSegments =
|
|
2253
2277
|
skipTag ? Number(skipTag.getAttributeValue('SKIPPED-SEGMENTS')) : 0;
|
|
2254
2278
|
let position = mediaSequenceNumber + skippedSegments;
|
|
@@ -2271,11 +2295,20 @@ shaka.hls.HlsParser = class {
|
|
|
2271
2295
|
(i == 0) ? firstStartTime : previousReference.endTime;
|
|
2272
2296
|
position = mediaSequenceNumber + skippedSegments + i;
|
|
2273
2297
|
|
|
2298
|
+
const discontinuityTag = shaka.hls.Utils.getFirstTagWithName(
|
|
2299
|
+
item.tags, 'EXT-X-DISCONTINUITY');
|
|
2300
|
+
if (discontinuityTag) {
|
|
2301
|
+
discontinuitySequence++;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2274
2304
|
// Apply new AES-128 tags as you see them, keeping a running total.
|
|
2275
2305
|
for (const drmTag of item.tags) {
|
|
2276
|
-
if (drmTag.name == 'EXT-X-KEY'
|
|
2277
|
-
|
|
2278
|
-
|
|
2306
|
+
if (drmTag.name == 'EXT-X-KEY') {
|
|
2307
|
+
if (drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
|
|
2308
|
+
hlsAes128Key = this.parseAES128DrmTag_(drmTag, playlist);
|
|
2309
|
+
} else {
|
|
2310
|
+
hlsAes128Key = undefined;
|
|
2311
|
+
}
|
|
2279
2312
|
}
|
|
2280
2313
|
}
|
|
2281
2314
|
|
|
@@ -2308,6 +2341,8 @@ shaka.hls.HlsParser = class {
|
|
|
2308
2341
|
previousReference = reference;
|
|
2309
2342
|
|
|
2310
2343
|
if (reference) {
|
|
2344
|
+
reference.discontinuitySequence = discontinuitySequence;
|
|
2345
|
+
|
|
2311
2346
|
if (this.config_.hls.ignoreManifestProgramDateTime &&
|
|
2312
2347
|
this.minSequenceNumber_ != null &&
|
|
2313
2348
|
position < this.minSequenceNumber_) {
|
|
@@ -2394,6 +2429,16 @@ shaka.hls.HlsParser = class {
|
|
|
2394
2429
|
}
|
|
2395
2430
|
}
|
|
2396
2431
|
|
|
2432
|
+
// lowestSyncTime is a value from a previous playlist update. Use it to
|
|
2433
|
+
// set reference start times. If this is the first playlist parse, we will
|
|
2434
|
+
// skip this step, and wait until we have sync time across stream types.
|
|
2435
|
+
const lowestSyncTime = this.lowestSyncTime_;
|
|
2436
|
+
if (someSyncTime && lowestSyncTime != Infinity) {
|
|
2437
|
+
for (const reference of references) {
|
|
2438
|
+
reference.syncAgainst(lowestSyncTime);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2397
2442
|
return references;
|
|
2398
2443
|
}
|
|
2399
2444
|
|
|
@@ -616,6 +616,8 @@ shaka.media.MediaSourceEngine = class {
|
|
|
616
616
|
// into extracting a timestamp for us to align text segments in sequence
|
|
617
617
|
// mode.
|
|
618
618
|
|
|
619
|
+
const duration = this.mediaSource_.duration;
|
|
620
|
+
|
|
619
621
|
// Timestamps can only be reliably extracted from video, not audio.
|
|
620
622
|
// Packed audio formats do not have internal timestamps at all.
|
|
621
623
|
// Prefer video for this when available.
|
|
@@ -659,7 +661,7 @@ shaka.media.MediaSourceEngine = class {
|
|
|
659
661
|
// Clear the buffer.
|
|
660
662
|
await this.enqueueOperation_(
|
|
661
663
|
contentType,
|
|
662
|
-
() => this.remove_(contentType, 0,
|
|
664
|
+
() => this.remove_(contentType, 0, duration));
|
|
663
665
|
|
|
664
666
|
// Finally, flush the buffer in case of choppy video start on HLS fMP4.
|
|
665
667
|
if (contentType == ContentType.VIDEO) {
|
|
@@ -671,6 +673,10 @@ shaka.media.MediaSourceEngine = class {
|
|
|
671
673
|
|
|
672
674
|
// Now switch to sequence mode and fall through to our normal operations.
|
|
673
675
|
sourceBuffer.mode = SEQUENCE;
|
|
676
|
+
|
|
677
|
+
// When we change the buffer mode the duration is lost, so we need to set
|
|
678
|
+
// it explicitly.
|
|
679
|
+
await this.setDuration(duration);
|
|
674
680
|
}
|
|
675
681
|
|
|
676
682
|
if (reference && this.sequenceMode_ && contentType != ContentType.TEXT) {
|
|
@@ -705,8 +711,13 @@ shaka.media.MediaSourceEngine = class {
|
|
|
705
711
|
bufferedBefore, bufferedAfter);
|
|
706
712
|
if (newBuffered) {
|
|
707
713
|
const segmentDuration = reference.endTime - reference.startTime;
|
|
708
|
-
|
|
709
|
-
|
|
714
|
+
// Check end times instead of start times. We may be overwriting a
|
|
715
|
+
// buffer and only the end changes, and that would be fine.
|
|
716
|
+
// Also, exclude tiny segments. Sometimes alignment segments as small
|
|
717
|
+
// as 33ms are seen in Google DAI content. For such tiny segments,
|
|
718
|
+
// half a segment duration would be no issue.
|
|
719
|
+
const offset = Math.abs(newBuffered.end - reference.endTime);
|
|
720
|
+
if (segmentDuration > 0.100 && offset > segmentDuration / 2) {
|
|
710
721
|
shaka.log.error('Possible encoding problem detected!',
|
|
711
722
|
'Unexpected buffered range for reference', reference,
|
|
712
723
|
'from URIs', reference.getUris(),
|
|
@@ -856,6 +867,41 @@ shaka.media.MediaSourceEngine = class {
|
|
|
856
867
|
]);
|
|
857
868
|
}
|
|
858
869
|
|
|
870
|
+
/**
|
|
871
|
+
* Adjust timestamp offset to maintain AV sync across discontinuities.
|
|
872
|
+
* Only used in sequence mode.
|
|
873
|
+
*
|
|
874
|
+
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
|
|
875
|
+
* @param {number} timestampOffset
|
|
876
|
+
* @return {!Promise}
|
|
877
|
+
*/
|
|
878
|
+
async resync(contentType, timestampOffset) {
|
|
879
|
+
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
880
|
+
|
|
881
|
+
goog.asserts.assert(this.sequenceMode_,
|
|
882
|
+
'resync only used with sequence mode!');
|
|
883
|
+
|
|
884
|
+
if (contentType == ContentType.TEXT) {
|
|
885
|
+
// This operation is for audio and video only.
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Queue an abort() to help MSE splice together overlapping segments.
|
|
890
|
+
// We set appendWindowEnd when we change periods in DASH content, and the
|
|
891
|
+
// period transition may result in overlap.
|
|
892
|
+
//
|
|
893
|
+
// An abort() also helps with MPEG2-TS. When we append a TS segment, we
|
|
894
|
+
// always enter a PARSING_MEDIA_SEGMENT state and we can't change the
|
|
895
|
+
// timestamp offset. By calling abort(), we reset the state so we can
|
|
896
|
+
// set it.
|
|
897
|
+
this.enqueueOperation_(
|
|
898
|
+
contentType,
|
|
899
|
+
() => this.abort_(contentType));
|
|
900
|
+
await this.enqueueOperation_(
|
|
901
|
+
contentType,
|
|
902
|
+
() => this.setTimestampOffset_(contentType, timestampOffset));
|
|
903
|
+
}
|
|
904
|
+
|
|
859
905
|
/**
|
|
860
906
|
* @param {string=} reason Valid reasons are 'network' and 'decode'.
|
|
861
907
|
* @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
|
|
|
@@ -239,6 +240,9 @@ shaka.media.SegmentReference = class {
|
|
|
239
240
|
|
|
240
241
|
/** @type {?shaka.extern.HlsAes128Key} */
|
|
241
242
|
this.hlsAes128Key = hlsAes128Key;
|
|
243
|
+
|
|
244
|
+
/** @type {number} */
|
|
245
|
+
this.discontinuitySequence = 0;
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
/**
|
|
@@ -353,6 +357,44 @@ shaka.media.SegmentReference = class {
|
|
|
353
357
|
markAsUnavailable() {
|
|
354
358
|
this.status = shaka.media.SegmentReference.Status.UNAVAILABLE;
|
|
355
359
|
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Offset the segment reference by a fixed amount.
|
|
363
|
+
*
|
|
364
|
+
* @param {number} offset The amount to add to the segment's start and end
|
|
365
|
+
* times.
|
|
366
|
+
* @export
|
|
367
|
+
*/
|
|
368
|
+
offset(offset) {
|
|
369
|
+
this.startTime += offset;
|
|
370
|
+
this.endTime += offset;
|
|
371
|
+
this.trueEndTime += offset;
|
|
372
|
+
|
|
373
|
+
for (const partial of this.partialReferences) {
|
|
374
|
+
partial.startTime += offset;
|
|
375
|
+
partial.endTime += offset;
|
|
376
|
+
partial.trueEndTime += offset;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Sync this segment against a particular sync time that will serve as "0" in
|
|
382
|
+
* the presentation timeline.
|
|
383
|
+
*
|
|
384
|
+
* @param {number} lowestSyncTime
|
|
385
|
+
* @export
|
|
386
|
+
*/
|
|
387
|
+
syncAgainst(lowestSyncTime) {
|
|
388
|
+
if (this.syncTime == null) {
|
|
389
|
+
shaka.log.alwaysError('Sync attempted without sync time!');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const desiredStart = this.syncTime - lowestSyncTime;
|
|
393
|
+
const offset = desiredStart - this.startTime;
|
|
394
|
+
if (Math.abs(offset) >= 0.001) {
|
|
395
|
+
this.offset(offset);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
356
398
|
};
|
|
357
399
|
|
|
358
400
|
|
|
@@ -618,6 +618,11 @@ shaka.media.StreamingEngine = class {
|
|
|
618
618
|
* within the presentation timeline.
|
|
619
619
|
*/
|
|
620
620
|
seeked() {
|
|
621
|
+
if (!this.playerInterface_) {
|
|
622
|
+
// Already destroyed.
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
621
626
|
const presentationTime = this.playerInterface_.getPresentationTime();
|
|
622
627
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
623
628
|
const newTimeIsBuffered = (type) => {
|
|
@@ -675,7 +680,7 @@ shaka.media.StreamingEngine = class {
|
|
|
675
680
|
|
|
676
681
|
/**
|
|
677
682
|
* Clear the buffer for a given stream. Unlike clearBuffer_, this will handle
|
|
678
|
-
* cases where a MediaState is performing an update. After this runs,
|
|
683
|
+
* cases where a MediaState is performing an update. After this runs, the
|
|
679
684
|
* MediaState will have a pending update.
|
|
680
685
|
* @param {!shaka.media.StreamingEngine.MediaState_} mediaState
|
|
681
686
|
* @private
|
|
@@ -791,9 +796,9 @@ shaka.media.StreamingEngine = class {
|
|
|
791
796
|
for (const type of streamsByType.keys()) {
|
|
792
797
|
const stream = streamsByType.get(type);
|
|
793
798
|
if (!this.mediaStates_.has(type)) {
|
|
794
|
-
const
|
|
795
|
-
this.mediaStates_.set(type,
|
|
796
|
-
this.scheduleUpdate_(
|
|
799
|
+
const mediaState = this.createMediaState_(stream);
|
|
800
|
+
this.mediaStates_.set(type, mediaState);
|
|
801
|
+
this.scheduleUpdate_(mediaState, 0);
|
|
797
802
|
}
|
|
798
803
|
}
|
|
799
804
|
}
|
|
@@ -827,6 +832,7 @@ shaka.media.StreamingEngine = class {
|
|
|
827
832
|
// The playhead might be seeking on startup, if a start time is set, so
|
|
828
833
|
// start "seeked" as true.
|
|
829
834
|
seeked: true,
|
|
835
|
+
needsResync: false,
|
|
830
836
|
recovering: false,
|
|
831
837
|
hasError: false,
|
|
832
838
|
operation: null,
|
|
@@ -905,7 +911,7 @@ shaka.media.StreamingEngine = class {
|
|
|
905
911
|
'mediastate.stream should not have segmentIndex yet.');
|
|
906
912
|
thisStream.closeSegmentIndex();
|
|
907
913
|
}
|
|
908
|
-
if (mediaState.
|
|
914
|
+
if (!mediaState.performingUpdate && !mediaState.updateTimer) {
|
|
909
915
|
this.scheduleUpdate_(mediaState, 0);
|
|
910
916
|
}
|
|
911
917
|
return;
|
|
@@ -1150,6 +1156,11 @@ shaka.media.StreamingEngine = class {
|
|
|
1150
1156
|
shaka.log.v1(
|
|
1151
1157
|
logPrefix, 'looking up segment from new stream endTime:', time);
|
|
1152
1158
|
|
|
1159
|
+
// Using a new iterator means we need to resync the stream in sequence
|
|
1160
|
+
// mode. The buffered range might not align perfectly with the last
|
|
1161
|
+
// segment end time, so we may end up repeating a segment. Resyncing
|
|
1162
|
+
// makes this safe to do.
|
|
1163
|
+
mediaState.needsResync = true;
|
|
1153
1164
|
mediaState.segmentIterator =
|
|
1154
1165
|
mediaState.stream.segmentIndex.getIteratorForTime(time);
|
|
1155
1166
|
const ref = mediaState.segmentIterator &&
|
|
@@ -1389,7 +1400,7 @@ shaka.media.StreamingEngine = class {
|
|
|
1389
1400
|
// If the network slows down, abort the current fetch request and start
|
|
1390
1401
|
// a new one, and ignore the error message.
|
|
1391
1402
|
mediaState.performingUpdate = false;
|
|
1392
|
-
mediaState
|
|
1403
|
+
this.cancelUpdate_(mediaState);
|
|
1393
1404
|
this.scheduleUpdate_(mediaState, 0);
|
|
1394
1405
|
} else if (mediaState.type == ContentType.TEXT &&
|
|
1395
1406
|
this.config_.ignoreTextStreamFailures) {
|
|
@@ -1462,7 +1473,10 @@ shaka.media.StreamingEngine = class {
|
|
|
1462
1473
|
|
|
1463
1474
|
for (const mediaState of this.mediaStates_.values()) {
|
|
1464
1475
|
const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
|
|
1465
|
-
if
|
|
1476
|
+
// Only schedule an update if it has an error, but it's not mid-update
|
|
1477
|
+
// and there is not already an update scheduled.
|
|
1478
|
+
if (mediaState.hasError && !mediaState.performingUpdate &&
|
|
1479
|
+
!mediaState.updateTimer) {
|
|
1466
1480
|
shaka.log.info(logPrefix, 'Retrying after failure...');
|
|
1467
1481
|
mediaState.hasError = false;
|
|
1468
1482
|
this.scheduleUpdate_(mediaState, delaySeconds);
|
|
@@ -1645,6 +1659,21 @@ shaka.media.StreamingEngine = class {
|
|
|
1645
1659
|
}
|
|
1646
1660
|
}
|
|
1647
1661
|
|
|
1662
|
+
if (this.manifest_.sequenceMode) {
|
|
1663
|
+
// Across discontinuity bounds, we should resync timestamps for
|
|
1664
|
+
// sequence mode playbacks. The next segment appended should
|
|
1665
|
+
// land at its theoretical timestamp from the segment index.
|
|
1666
|
+
const lastDiscontinuitySequence =
|
|
1667
|
+
mediaState.lastSegmentReference ?
|
|
1668
|
+
mediaState.lastSegmentReference.discontinuitySequence : null;
|
|
1669
|
+
if (reference.discontinuitySequence != lastDiscontinuitySequence ||
|
|
1670
|
+
mediaState.needsResync) {
|
|
1671
|
+
mediaState.needsResync = false;
|
|
1672
|
+
operations.push(this.playerInterface_.mediaSourceEngine.resync(
|
|
1673
|
+
mediaState.type, reference.startTime));
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1648
1677
|
await Promise.all(operations);
|
|
1649
1678
|
}
|
|
1650
1679
|
|
|
@@ -1678,8 +1707,6 @@ shaka.media.StreamingEngine = class {
|
|
|
1678
1707
|
|
|
1679
1708
|
await this.evict_(mediaState, presentationTime);
|
|
1680
1709
|
this.destroyer_.ensureNotDestroyed();
|
|
1681
|
-
shaka.log.v1(logPrefix, 'appending media segment at',
|
|
1682
|
-
(reference.syncTime == null ? 'unknown' : reference.syncTime));
|
|
1683
1710
|
|
|
1684
1711
|
// 'seeked' or 'adaptation' triggered logic applies only to this
|
|
1685
1712
|
// appendBuffer() call.
|
|
@@ -1956,7 +1983,11 @@ shaka.media.StreamingEngine = class {
|
|
|
1956
1983
|
shaka.log.debug(logPrefix, 'cleared buffer');
|
|
1957
1984
|
mediaState.clearingBuffer = false;
|
|
1958
1985
|
mediaState.endOfStream = false;
|
|
1959
|
-
|
|
1986
|
+
// Since the clear operation was async, check to make sure we're not doing
|
|
1987
|
+
// another update and we don't have one scheduled yet.
|
|
1988
|
+
if (!mediaState.performingUpdate && !mediaState.updateTimer) {
|
|
1989
|
+
this.scheduleUpdate_(mediaState, 0);
|
|
1990
|
+
}
|
|
1960
1991
|
}
|
|
1961
1992
|
|
|
1962
1993
|
|
|
@@ -2128,6 +2159,7 @@ shaka.media.StreamingEngine.PlayerInterface;
|
|
|
2128
2159
|
* clearBufferSafeMargin: number,
|
|
2129
2160
|
* clearingBuffer: boolean,
|
|
2130
2161
|
* seeked: boolean,
|
|
2162
|
+
* needsResync: boolean,
|
|
2131
2163
|
* adaptation: boolean,
|
|
2132
2164
|
* recovering: boolean,
|
|
2133
2165
|
* hasError: boolean,
|
|
@@ -2173,10 +2205,13 @@ shaka.media.StreamingEngine.PlayerInterface;
|
|
|
2173
2205
|
* The amount of buffer to retain when clearing the buffer after the update.
|
|
2174
2206
|
* @property {boolean} clearingBuffer
|
|
2175
2207
|
* True indicates that the buffer is being cleared.
|
|
2176
|
-
|
|
2208
|
+
* @property {boolean} seeked
|
|
2177
2209
|
* True indicates that the presentation just seeked.
|
|
2178
|
-
|
|
2210
|
+
* @property {boolean} adaptation
|
|
2179
2211
|
* True indicates that the presentation just automatically switched variants.
|
|
2212
|
+
* @property {boolean} needsResync
|
|
2213
|
+
* True indicates that the stream needs to be resynced in sequence mode,
|
|
2214
|
+
* regardless of discontinuity sequence.
|
|
2180
2215
|
* @property {boolean} recovering
|
|
2181
2216
|
* True indicates that the last segment was not appended because it could not
|
|
2182
2217
|
* fit in the buffer.
|
package/lib/player.js
CHANGED
|
@@ -1427,6 +1427,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
|
|
|
1427
1427
|
has.startTime = null;
|
|
1428
1428
|
has.uri = null;
|
|
1429
1429
|
|
|
1430
|
+
// Release the region timeline, which is created when parsing the manifest.
|
|
1431
|
+
if (this.regionTimeline_) {
|
|
1432
|
+
this.regionTimeline_.release();
|
|
1433
|
+
this.regionTimeline_ = null;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1430
1436
|
// In most cases we should have a media element. The one exception would
|
|
1431
1437
|
// be if there was an error and we, by chance, did not have a media element.
|
|
1432
1438
|
if (has.mediaElement) {
|
|
@@ -4140,6 +4146,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
|
|
|
4140
4146
|
return [];
|
|
4141
4147
|
}
|
|
4142
4148
|
const chapters = [];
|
|
4149
|
+
const uniqueChapters = new Set();
|
|
4143
4150
|
for (const chaptersTrack of chaptersTracksWithLanguage) {
|
|
4144
4151
|
if (chaptersTrack && chaptersTrack.cues) {
|
|
4145
4152
|
for (const cue of chaptersTrack.cues) {
|
|
@@ -4154,7 +4161,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
|
|
|
4154
4161
|
startTime: cue.startTime,
|
|
4155
4162
|
endTime: cue.endTime,
|
|
4156
4163
|
};
|
|
4157
|
-
|
|
4164
|
+
if (!uniqueChapters.has(id)) {
|
|
4165
|
+
chapters.push(chapter);
|
|
4166
|
+
uniqueChapters.add(id);
|
|
4167
|
+
}
|
|
4158
4168
|
}
|
|
4159
4169
|
}
|
|
4160
4170
|
}
|
|
@@ -6595,7 +6605,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
|
|
|
6595
6605
|
* @define {string} A version number taken from git at compile time.
|
|
6596
6606
|
* @export
|
|
6597
6607
|
*/
|
|
6598
|
-
shaka.Player.version = 'v4.2.
|
|
6608
|
+
shaka.Player.version = 'v4.2.7-uncompiled';
|
|
6599
6609
|
|
|
6600
6610
|
// Initialize the deprecation system using the version string we just set
|
|
6601
6611
|
// 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
|
@@ -457,12 +457,16 @@ shaka.util.StreamUtils = class {
|
|
|
457
457
|
if (!MediaSource.isTypeSupported(audioFullType)) {
|
|
458
458
|
return false;
|
|
459
459
|
}
|
|
460
|
+
// Update the codec string with the (possibly) converted codecs.
|
|
461
|
+
videoCodecs = [videoCodecs, audioCodecs].join(',');
|
|
460
462
|
}
|
|
461
463
|
const fullType = shaka.util.MimeUtils.getFullOrConvertedType(
|
|
462
464
|
video.mimeType, videoCodecs, ContentType.VIDEO);
|
|
463
465
|
if (!MediaSource.isTypeSupported(fullType)) {
|
|
464
466
|
return false;
|
|
465
467
|
}
|
|
468
|
+
// Update the codec string with the (possibly) converted codecs.
|
|
469
|
+
video.codecs = videoCodecs;
|
|
466
470
|
}
|
|
467
471
|
const audio = variant.audio;
|
|
468
472
|
if (audio) {
|
|
@@ -473,6 +477,8 @@ shaka.util.StreamUtils = class {
|
|
|
473
477
|
if (!MediaSource.isTypeSupported(fullType)) {
|
|
474
478
|
return false;
|
|
475
479
|
}
|
|
480
|
+
// Update the codec string with the (possibly) converted codecs.
|
|
481
|
+
audio.codecs = codecs;
|
|
476
482
|
}
|
|
477
483
|
|
|
478
484
|
// See: https://github.com/shaka-project/shaka-player/issues/3380
|