shaka-player 3.1.2 → 3.1.6
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 +73 -0
- package/.github/workflows/release.yaml +53 -0
- package/AUTHORS +3 -0
- package/CHANGELOG.md +170 -1
- package/CONTRIBUTORS +3 -0
- package/build/checkversion.py +1 -1
- package/build/generateExterns.js +1 -1
- package/build/misspellings.txt +1 -0
- package/build/shakaBuildHelpers.py +6 -5
- package/build/subprocessWindowsPatch.py +103 -0
- package/build/test.py +6 -0
- package/demo/common/assets.js +1 -1
- package/dist/controls.css +2 -2
- package/dist/controls.css.map +1 -1
- package/dist/demo.compiled.debug.js +1 -1
- package/dist/demo.compiled.js +1 -1
- package/dist/deps.js +7 -7
- package/dist/locales.js +35 -35
- package/dist/receiver.compiled.debug.js +1 -1
- package/dist/receiver.compiled.js +1 -1
- package/dist/shaka-player.compiled.d.ts +172 -3
- package/dist/shaka-player.compiled.debug.d.ts +172 -3
- package/dist/shaka-player.compiled.debug.externs.js +184 -1
- package/dist/shaka-player.compiled.debug.js +390 -370
- package/dist/shaka-player.compiled.debug.map +3 -3
- package/dist/shaka-player.compiled.externs.js +184 -1
- package/dist/shaka-player.compiled.js +809 -805
- package/dist/shaka-player.compiled.map +3 -3
- package/dist/shaka-player.ui.d.ts +174 -14
- package/dist/shaka-player.ui.debug.d.ts +174 -14
- package/dist/shaka-player.ui.debug.externs.js +200 -26
- package/dist/shaka-player.ui.debug.js +431 -412
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +200 -26
- package/dist/shaka-player.ui.js +934 -929
- package/dist/shaka-player.ui.map +4 -4
- package/docs/tutorials/fairplay.md +1 -1
- package/docs/tutorials/faq.md +14 -0
- package/externs/ima.js +4 -0
- package/externs/shaka/ads.js +2 -0
- package/externs/shaka/text.js +10 -0
- package/karma.conf.js +20 -15
- package/lib/ads/ad_manager.js +30 -11
- package/lib/ads/client_side_ad_manager.js +76 -86
- package/lib/ads/server_side_ad_manager.js +26 -11
- package/lib/cast/cast_proxy.js +6 -2
- package/lib/cast/cast_receiver.js +5 -1
- package/lib/cast/cast_sender.js +2 -2
- package/lib/cea/sei_processor.js +24 -17
- package/lib/dash/dash_parser.js +131 -24
- package/lib/dash/segment_list.js +1 -1
- package/lib/dash/segment_template.js +1 -1
- package/lib/hls/hls_parser.js +44 -17
- package/lib/hls/manifest_text_parser.js +1 -0
- package/lib/media/closed_caption_parser.js +3 -1
- package/lib/media/content_workarounds.js +12 -0
- package/lib/media/drm_engine.js +20 -1
- package/lib/media/gap_jumping_controller.js +8 -4
- package/lib/media/media_source_engine.js +1 -2
- package/lib/media/playhead.js +1 -1
- package/lib/media/region_observer.js +39 -33
- package/lib/media/region_timeline.js +17 -19
- package/lib/media/streaming_engine.js +8 -4
- package/lib/net/networking_engine.js +6 -1
- package/lib/offline/storage.js +10 -11
- package/lib/player.js +66 -30
- package/lib/polyfill/encryption_scheme.js +2 -0
- package/lib/polyfill/fullscreen.js +2 -0
- package/lib/polyfill/mathround.js +2 -0
- package/lib/polyfill/media_capabilities.js +73 -21
- package/lib/polyfill/mediasource.js +8 -0
- package/lib/polyfill/orientation.js +3 -1
- package/lib/polyfill/patchedmediakeys_apple.js +6 -4
- package/lib/polyfill/patchedmediakeys_ms.js +6 -4
- package/lib/polyfill/patchedmediakeys_nop.js +2 -0
- package/lib/polyfill/patchedmediakeys_webkit.js +6 -4
- package/lib/polyfill/pip_webkit.js +2 -0
- package/lib/polyfill/storage_estimate.js +2 -0
- package/lib/polyfill/video_play_promise.js +2 -0
- package/lib/polyfill/videoplaybackquality.js +2 -0
- package/lib/polyfill/vttcue.js +2 -0
- package/lib/text/cue.js +6 -0
- package/lib/text/mp4_ttml_parser.js +3 -0
- package/lib/text/text_engine.js +14 -2
- package/lib/text/ttml_text_parser.js +135 -87
- package/lib/text/ui_text_displayer.js +213 -80
- package/lib/text/vtt_text_parser.js +4 -2
- package/lib/util/fake_event.js +53 -7
- package/lib/util/fake_event_target.js +21 -1
- package/lib/util/mp4_box_parsers.js +1 -1
- package/lib/util/periods.js +4 -1
- package/lib/util/platform.js +13 -1
- package/lib/util/stream_utils.js +77 -10
- package/lib/util/xml_utils.js +19 -0
- package/package.json +3 -6
- package/test/cast/cast_proxy_unit.js +12 -6
- package/test/cast/cast_receiver_integration.js +1 -1
- package/test/cast/cast_utils_unit.js +1 -1
- package/test/cea/mp4_cea_parser_unit.js +26 -0
- package/test/dash/dash_parser_live_unit.js +43 -0
- package/test/dash/dash_parser_manifest_unit.js +222 -3
- package/test/hls/hls_parser_unit.js +79 -1
- package/test/media/closed_caption_parser_unit.js +20 -0
- package/test/media/drm_engine_unit.js +56 -0
- package/test/media/playhead_unit.js +27 -1
- package/test/media/region_observer_unit.js +40 -5
- package/test/media/region_timeline_unit.js +40 -22
- package/test/media/streaming_engine_integration.js +2 -1
- package/test/media/streaming_engine_unit.js +145 -0
- package/test/player_integration.js +38 -0
- package/test/test/assets/3675/chunk-stream0-00001.m4s +0 -0
- package/test/test/assets/3675/chunk-stream0-00002.m4s +0 -0
- package/test/test/assets/3675/chunk-stream0-00003.m4s +0 -0
- package/test/test/assets/3675/chunk-stream0-00004.m4s +0 -0
- package/test/test/assets/3675/chunk-stream0-00005.m4s +0 -0
- package/test/test/assets/3675/chunk-stream0-00006.m4s +0 -0
- package/test/test/assets/3675/chunk-stream0-00007.m4s +0 -0
- package/test/test/assets/3675/dash_0.mpd +30 -0
- package/test/test/assets/3675/dash_1.mpd +30 -0
- package/test/test/assets/3675/dash_2.mpd +30 -0
- package/test/test/assets/3675/dash_3.mpd +30 -0
- package/test/test/assets/3675/dash_4.mpd +30 -0
- package/test/test/assets/3675/dash_5.mpd +26 -0
- package/test/test/assets/3675/init-stream0.m4s +0 -0
- package/test/test/assets/empty_caption_video_init.mp4 +0 -0
- package/test/test/assets/empty_caption_video_segment.mp4 +0 -0
- package/test/test/util/fake_ad_manager.js +4 -1
- package/test/test/util/ttml_utils.js +81 -0
- package/test/test/util/util.js +10 -7
- package/test/text/mp4_ttml_parser_unit.js +11 -18
- package/test/text/ttml_text_parser_unit.js +542 -220
- package/test/text/ui_text_displayer_unit.js +93 -20
- package/test/text/vtt_text_parser_unit.js +17 -1
- package/test/ui/localization_unit.js +15 -14
- package/test/ui/text_displayer_layout_unit.js +21 -8
- package/test/util/mp4_box_parsers_unit.js +28 -0
- package/test/util/stream_utils_unit.js +59 -2
- package/ui/cast_button.js +2 -3
- package/ui/controls.js +11 -6
- package/ui/less/buttons.less +0 -4
- package/ui/less/containers.less +6 -7
- package/ui/locales/ru.json +6 -6
- package/ui/localization.js +40 -87
- package/ui/pip_button.js +2 -3
- package/ui/text_selection.js +1 -0
package/lib/cast/cast_proxy.js
CHANGED
|
@@ -120,6 +120,9 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
|
|
|
120
120
|
this.videoProxy_ = null;
|
|
121
121
|
this.playerProxy_ = null;
|
|
122
122
|
|
|
123
|
+
// FakeEventTarget implements IReleasable
|
|
124
|
+
super.release();
|
|
125
|
+
|
|
123
126
|
return Promise.all(waitFor);
|
|
124
127
|
}
|
|
125
128
|
|
|
@@ -525,7 +528,8 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
|
|
|
525
528
|
goog.asserts.assert(error instanceof shaka.util.Error,
|
|
526
529
|
'Wrong error type!');
|
|
527
530
|
const eventType = shaka.Player.EventName.Error;
|
|
528
|
-
const
|
|
531
|
+
const data = (new Map()).set('detail', error);
|
|
532
|
+
const event = new shaka.util.FakeEvent(eventType, data);
|
|
529
533
|
this.localPlayer_.dispatchEvent(event);
|
|
530
534
|
});
|
|
531
535
|
}
|
|
@@ -598,7 +602,7 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
|
|
|
598
602
|
|
|
599
603
|
// Convert this real Event into a FakeEvent for dispatch from our
|
|
600
604
|
// FakeEventListener.
|
|
601
|
-
const fakeEvent =
|
|
605
|
+
const fakeEvent = shaka.util.FakeEvent.fromRealEvent(event);
|
|
602
606
|
this.videoEventTarget_.dispatchEvent(fakeEvent);
|
|
603
607
|
}
|
|
604
608
|
|
|
@@ -238,6 +238,9 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
|
|
|
238
238
|
this.shakaBus_ = null;
|
|
239
239
|
this.genericBus_ = null;
|
|
240
240
|
|
|
241
|
+
// FakeEventTarget implements IReleasable
|
|
242
|
+
super.release();
|
|
243
|
+
|
|
241
244
|
await Promise.all(waitFor);
|
|
242
245
|
|
|
243
246
|
const manager = cast.receiver.CastReceiverManager.getInstance();
|
|
@@ -407,7 +410,8 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
|
|
|
407
410
|
goog.asserts.assert(error instanceof shaka.util.Error,
|
|
408
411
|
'Wrong error type!');
|
|
409
412
|
const eventType = shaka.Player.EventName.Error;
|
|
410
|
-
const
|
|
413
|
+
const data = (new Map()).set('detail', error);
|
|
414
|
+
const event = new shaka.util.FakeEvent(eventType, data);
|
|
411
415
|
// Only dispatch the event if the player still exists.
|
|
412
416
|
if (this.player_) {
|
|
413
417
|
this.player_.dispatchEvent(event);
|
package/lib/cast/cast_sender.js
CHANGED
|
@@ -647,7 +647,7 @@ shaka.cast.CastSender = class {
|
|
|
647
647
|
case 'event': {
|
|
648
648
|
const targetName = message['targetName'];
|
|
649
649
|
const event = message['event'];
|
|
650
|
-
const fakeEvent =
|
|
650
|
+
const fakeEvent = shaka.util.FakeEvent.fromRealEvent(event);
|
|
651
651
|
this.onRemoteEvent_(targetName, fakeEvent);
|
|
652
652
|
break;
|
|
653
653
|
}
|
|
@@ -725,7 +725,7 @@ shaka.cast.CastSender = class {
|
|
|
725
725
|
|
|
726
726
|
// Dispatch it through the Player proxy
|
|
727
727
|
const fakeEvent = new shaka.util.FakeEvent(
|
|
728
|
-
'error',
|
|
728
|
+
'error', (new Map()).set('detail', shakaError));
|
|
729
729
|
this.onRemoteEvent_('player', fakeEvent);
|
|
730
730
|
|
|
731
731
|
// Force this session to disconnect and transfer playback to the local
|
package/lib/cea/sei_processor.js
CHANGED
|
@@ -17,31 +17,31 @@ shaka.cea.SeiProcessor = class {
|
|
|
17
17
|
* @return {!Iterable.<!Uint8Array>}
|
|
18
18
|
*/
|
|
19
19
|
* process(naluData) {
|
|
20
|
-
const
|
|
20
|
+
const naluClone = this.removeEmu(naluData);
|
|
21
21
|
|
|
22
22
|
// The following is an implementation of section 7.3.2.3.1
|
|
23
23
|
// in Rec. ITU-T H.264 (06/2019), the H.264 spec.
|
|
24
24
|
let offset = 0;
|
|
25
25
|
|
|
26
|
-
while (offset
|
|
26
|
+
while (offset < naluClone.length) {
|
|
27
27
|
let payloadType = 0; // SEI payload type as defined by H.264 spec
|
|
28
|
-
while (
|
|
28
|
+
while (naluClone[offset] == 0xFF) {
|
|
29
29
|
payloadType += 255;
|
|
30
30
|
offset++;
|
|
31
31
|
}
|
|
32
|
-
payloadType +=
|
|
32
|
+
payloadType += naluClone[offset++];
|
|
33
33
|
|
|
34
34
|
let payloadSize = 0; // SEI payload size as defined by H.264 spec
|
|
35
|
-
while (
|
|
35
|
+
while (naluClone[offset] == 0xFF) {
|
|
36
36
|
payloadSize += 255;
|
|
37
37
|
offset++;
|
|
38
38
|
}
|
|
39
|
-
payloadSize +=
|
|
39
|
+
payloadSize += naluClone[offset++];
|
|
40
40
|
|
|
41
41
|
// Payload type 4 is user_data_registered_itu_t_t35, as per the H.264
|
|
42
42
|
// spec. This payload type contains caption data.
|
|
43
43
|
if (payloadType == 0x04) {
|
|
44
|
-
yield
|
|
44
|
+
yield naluClone.subarray(offset, offset + payloadSize);
|
|
45
45
|
}
|
|
46
46
|
offset += payloadSize;
|
|
47
47
|
}
|
|
@@ -50,28 +50,35 @@ shaka.cea.SeiProcessor = class {
|
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Removes H.264 emulation prevention bytes from the byte array.
|
|
53
|
+
*
|
|
54
|
+
* Note: Remove bytes by shifting will cause Chromium (VDA) to complain
|
|
55
|
+
* about conformance. Recreating a new array solves it.
|
|
56
|
+
*
|
|
53
57
|
* @param {!Uint8Array} naluData NALU from which EMUs should be removed.
|
|
54
|
-
* @return {
|
|
55
|
-
* @private
|
|
58
|
+
* @return {!Uint8Array} The NALU with the emulation prevention byte removed.
|
|
56
59
|
*/
|
|
57
|
-
|
|
60
|
+
removeEmu(naluData) {
|
|
61
|
+
let naluClone = naluData;
|
|
58
62
|
let zeroCount = 0;
|
|
59
63
|
let src = 0;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
while (src < naluClone.length) {
|
|
65
|
+
if (zeroCount == 2 && naluClone[src] == 0x03) {
|
|
66
|
+
// 0x00, 0x00, 0x03 pattern detected
|
|
63
67
|
zeroCount = 0;
|
|
68
|
+
|
|
69
|
+
// Splice the array and recreate a new one, instead of shifting bytes
|
|
70
|
+
const newArr = [...naluClone];
|
|
71
|
+
newArr.splice(src, 1);
|
|
72
|
+
naluClone = new Uint8Array(newArr);
|
|
64
73
|
} else {
|
|
65
|
-
if (
|
|
74
|
+
if (naluClone[src] == 0x00) {
|
|
66
75
|
zeroCount++;
|
|
67
76
|
} else {
|
|
68
77
|
zeroCount = 0;
|
|
69
78
|
}
|
|
70
|
-
naluData[dst] = naluData[src];
|
|
71
|
-
dst++;
|
|
72
79
|
}
|
|
73
80
|
src++;
|
|
74
81
|
}
|
|
75
|
-
return
|
|
82
|
+
return naluClone;
|
|
76
83
|
}
|
|
77
84
|
};
|
package/lib/dash/dash_parser.js
CHANGED
|
@@ -95,6 +95,18 @@ shaka.dash.DashParser = class {
|
|
|
95
95
|
/** @private {!shaka.util.OperationManager} */
|
|
96
96
|
this.operationManager_ = new shaka.util.OperationManager();
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Largest period start time seen.
|
|
100
|
+
* @private {?number}
|
|
101
|
+
*/
|
|
102
|
+
this.largestPeriodStartTime_ = null;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Period IDs seen in previous manifest.
|
|
106
|
+
* @private {!Array.<number>}
|
|
107
|
+
*/
|
|
108
|
+
this.lastManifestUpdatePeriodIds_ = [];
|
|
109
|
+
|
|
98
110
|
/**
|
|
99
111
|
* The minimum of the availabilityTimeOffset values among the adaptation
|
|
100
112
|
* sets.
|
|
@@ -384,6 +396,24 @@ shaka.dash.DashParser = class {
|
|
|
384
396
|
this.config_.dash.autoCorrectDrift);
|
|
385
397
|
}
|
|
386
398
|
|
|
399
|
+
presentationTimeline.setStatic(mpdType == 'static');
|
|
400
|
+
|
|
401
|
+
const isLive = presentationTimeline.isLive();
|
|
402
|
+
|
|
403
|
+
// If it's live, we check for an override.
|
|
404
|
+
if (isLive && !isNaN(this.config_.availabilityWindowOverride)) {
|
|
405
|
+
segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// If it's null, that means segments are always available. This is always
|
|
409
|
+
// the case for VOD, and sometimes the case for live.
|
|
410
|
+
if (segmentAvailabilityDuration == null) {
|
|
411
|
+
segmentAvailabilityDuration = Infinity;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
presentationTimeline.setSegmentAvailabilityDuration(
|
|
415
|
+
segmentAvailabilityDuration);
|
|
416
|
+
|
|
387
417
|
const profiles = mpd.getAttribute('profiles') || '';
|
|
388
418
|
|
|
389
419
|
/** @type {shaka.dash.DashParser.Context} */
|
|
@@ -406,7 +436,6 @@ shaka.dash.DashParser = class {
|
|
|
406
436
|
const duration = periodsAndDuration.duration;
|
|
407
437
|
const periods = periodsAndDuration.periods;
|
|
408
438
|
|
|
409
|
-
presentationTimeline.setStatic(mpdType == 'static');
|
|
410
439
|
if (mpdType == 'static' ||
|
|
411
440
|
!periodsAndDuration.durationDerivedFromPeriods) {
|
|
412
441
|
// Ignore duration calculated from Period lengths if this is dynamic.
|
|
@@ -425,6 +454,7 @@ shaka.dash.DashParser = class {
|
|
|
425
454
|
this.lowLatencyMode_ = this.playerInterface_.isLowLatencyMode();
|
|
426
455
|
}
|
|
427
456
|
}
|
|
457
|
+
|
|
428
458
|
if (this.lowLatencyMode_) {
|
|
429
459
|
presentationTimeline.setAvailabilityTimeOffset(
|
|
430
460
|
this.minTotalAvailabilityTimeOffset_);
|
|
@@ -438,22 +468,6 @@ shaka.dash.DashParser = class {
|
|
|
438
468
|
'https://bit.ly/3clctcj for details.');
|
|
439
469
|
}
|
|
440
470
|
|
|
441
|
-
const isLive = presentationTimeline.isLive();
|
|
442
|
-
|
|
443
|
-
// If it's live, we check for an override.
|
|
444
|
-
if (isLive && !isNaN(this.config_.availabilityWindowOverride)) {
|
|
445
|
-
segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// If it's null, that means segments are always available. This is always
|
|
449
|
-
// the case for VOD, and sometimes the case for live.
|
|
450
|
-
if (segmentAvailabilityDuration == null) {
|
|
451
|
-
segmentAvailabilityDuration = Infinity;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
presentationTimeline.setSegmentAvailabilityDuration(
|
|
455
|
-
segmentAvailabilityDuration);
|
|
456
|
-
|
|
457
471
|
// Use @maxSegmentDuration to override smaller, derived values.
|
|
458
472
|
presentationTimeline.notifyMaxSegmentDuration(maxSegmentDuration || 1);
|
|
459
473
|
if (goog.DEBUG) {
|
|
@@ -533,6 +547,7 @@ shaka.dash.DashParser = class {
|
|
|
533
547
|
for (const {i, item: elem, next} of enumerate(periodNodes)) {
|
|
534
548
|
const start = /** @type {number} */ (
|
|
535
549
|
XmlUtils.parseAttr(elem, 'start', XmlUtils.parseDuration, prevEnd));
|
|
550
|
+
const periodId = elem.id;
|
|
536
551
|
const givenDuration =
|
|
537
552
|
XmlUtils.parseAttr(elem, 'duration', XmlUtils.parseDuration);
|
|
538
553
|
|
|
@@ -566,6 +581,42 @@ shaka.dash.DashParser = class {
|
|
|
566
581
|
periodDuration = givenDuration;
|
|
567
582
|
}
|
|
568
583
|
|
|
584
|
+
/**
|
|
585
|
+
* This is to improve robustness when the player observes manifest with
|
|
586
|
+
* past periods that are inconsistent to previous ones.
|
|
587
|
+
*
|
|
588
|
+
* This may happen when a CDN or proxy server switches its upstream from
|
|
589
|
+
* one encoder to another redundant encoder.
|
|
590
|
+
*
|
|
591
|
+
* Skip periods that match all of the following criteria:
|
|
592
|
+
* - Start time is earlier than latest period start time ever seen
|
|
593
|
+
* - Period ID is never seen in the previous manifest
|
|
594
|
+
* - Not the last period in the manifest
|
|
595
|
+
*
|
|
596
|
+
* Periods that meet the aforementioned criteria are considered invalid
|
|
597
|
+
* and should be safe to discard.
|
|
598
|
+
*/
|
|
599
|
+
|
|
600
|
+
if (this.largestPeriodStartTime_ !== null &&
|
|
601
|
+
periodId !== null && start !== null &&
|
|
602
|
+
start < this.largestPeriodStartTime_ &&
|
|
603
|
+
!this.lastManifestUpdatePeriodIds_.includes(periodId) &&
|
|
604
|
+
i + 1 != periodNodes.length) {
|
|
605
|
+
shaka.log.debug(
|
|
606
|
+
`Skipping Period with ID ${periodId} as its start time is smaller` +
|
|
607
|
+
' than the largest period start time that has been seen, and ID ' +
|
|
608
|
+
'is unseen before');
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
// Save maximum period start time if it is the last period
|
|
614
|
+
if (start !== null &&
|
|
615
|
+
(this.largestPeriodStartTime_ === null ||
|
|
616
|
+
start > this.largestPeriodStartTime_)) {
|
|
617
|
+
this.largestPeriodStartTime_ = start;
|
|
618
|
+
}
|
|
619
|
+
|
|
569
620
|
// Parse child nodes.
|
|
570
621
|
const info = {
|
|
571
622
|
start: start,
|
|
@@ -597,6 +648,9 @@ shaka.dash.DashParser = class {
|
|
|
597
648
|
prevEnd = start + periodDuration;
|
|
598
649
|
} // end of period parsing loop
|
|
599
650
|
|
|
651
|
+
// Replace previous seen periods with the current one.
|
|
652
|
+
this.lastManifestUpdatePeriodIds_ = periods.map((el) => el.id);
|
|
653
|
+
|
|
600
654
|
if (presentationDuration != null) {
|
|
601
655
|
if (prevEnd != presentationDuration) {
|
|
602
656
|
shaka.log.warning(
|
|
@@ -648,8 +702,12 @@ shaka.dash.DashParser = class {
|
|
|
648
702
|
|
|
649
703
|
const eventStreamNodes =
|
|
650
704
|
XmlUtils.findChildren(periodInfo.node, 'EventStream');
|
|
705
|
+
const availabilityStart =
|
|
706
|
+
context.presentationTimeline.getSegmentAvailabilityStart();
|
|
707
|
+
|
|
651
708
|
for (const node of eventStreamNodes) {
|
|
652
|
-
this.parseEventStream_(
|
|
709
|
+
this.parseEventStream_(
|
|
710
|
+
periodInfo.start, periodInfo.duration, node, availabilityStart);
|
|
653
711
|
}
|
|
654
712
|
|
|
655
713
|
const adaptationSetNodes =
|
|
@@ -808,6 +866,25 @@ shaka.dash.DashParser = class {
|
|
|
808
866
|
}
|
|
809
867
|
}
|
|
810
868
|
|
|
869
|
+
// Parallel for HLS VIDEO-RANGE as defined in DASH-IF IOP v4.3 6.2.5.1.
|
|
870
|
+
let videoRange;
|
|
871
|
+
const videoRangeScheme = 'urn:mpeg:mpegB:cicp:TransferCharacteristics';
|
|
872
|
+
const getVideoRangeFromTransferCharacteristicCICP = (cicp) => {
|
|
873
|
+
switch (cicp) {
|
|
874
|
+
case 1:
|
|
875
|
+
case 6:
|
|
876
|
+
case 13:
|
|
877
|
+
case 14:
|
|
878
|
+
case 15:
|
|
879
|
+
return 'SDR';
|
|
880
|
+
case 16:
|
|
881
|
+
return 'PQ';
|
|
882
|
+
case 18:
|
|
883
|
+
return 'HLG';
|
|
884
|
+
}
|
|
885
|
+
return undefined;
|
|
886
|
+
};
|
|
887
|
+
|
|
811
888
|
const essentialProperties =
|
|
812
889
|
XmlUtils.findChildren(elem, 'EssentialProperty');
|
|
813
890
|
// ID of real AdaptationSet if this is a trick mode set:
|
|
@@ -817,11 +894,26 @@ shaka.dash.DashParser = class {
|
|
|
817
894
|
const schemeId = prop.getAttribute('schemeIdUri');
|
|
818
895
|
if (schemeId == 'http://dashif.org/guidelines/trickmode') {
|
|
819
896
|
trickModeFor = prop.getAttribute('value');
|
|
897
|
+
} else if (schemeId == videoRangeScheme) {
|
|
898
|
+
videoRange = getVideoRangeFromTransferCharacteristicCICP(
|
|
899
|
+
parseInt(prop.getAttribute('value'), 10),
|
|
900
|
+
);
|
|
820
901
|
} else {
|
|
821
902
|
unrecognizedEssentialProperty = true;
|
|
822
903
|
}
|
|
823
904
|
}
|
|
824
905
|
|
|
906
|
+
const supplementalProperties =
|
|
907
|
+
XmlUtils.findChildren(elem, 'SupplementalProperty');
|
|
908
|
+
for (const prop of supplementalProperties) {
|
|
909
|
+
const schemeId = prop.getAttribute('schemeIdUri');
|
|
910
|
+
if (schemeId == videoRangeScheme) {
|
|
911
|
+
videoRange = getVideoRangeFromTransferCharacteristicCICP(
|
|
912
|
+
parseInt(prop.getAttribute('value'), 10),
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
825
917
|
const accessibilities = XmlUtils.findChildren(elem, 'Accessibility');
|
|
826
918
|
const LanguageUtils = shaka.util.LanguageUtils;
|
|
827
919
|
const closedCaptions = new Map();
|
|
@@ -858,7 +950,9 @@ shaka.dash.DashParser = class {
|
|
|
858
950
|
channel = channelAndLanguage[0].startsWith('CC') ?
|
|
859
951
|
channelAndLanguage[0] : `CC${channelAndLanguage[0]}`;
|
|
860
952
|
|
|
861
|
-
|
|
953
|
+
// 3 letters (ISO 639-2). In b/187442669, we saw a blank string
|
|
954
|
+
// (CC2=;CC3=), so default to "und" (the code for "undetermined").
|
|
955
|
+
language = channelAndLanguage[1] || 'und';
|
|
862
956
|
}
|
|
863
957
|
closedCaptions.set(channel, LanguageUtils.normalize(language));
|
|
864
958
|
}
|
|
@@ -945,8 +1039,13 @@ shaka.dash.DashParser = class {
|
|
|
945
1039
|
// Parse Representations into Streams.
|
|
946
1040
|
const representations = XmlUtils.findChildren(elem, 'Representation');
|
|
947
1041
|
const streams = representations.map((representation) => {
|
|
948
|
-
|
|
949
|
-
language, label, main, roleValues,
|
|
1042
|
+
const parsedRepresentation = this.parseRepresentation_(context,
|
|
1043
|
+
contentProtection, kind, language, label, main, roleValues,
|
|
1044
|
+
closedCaptions, representation);
|
|
1045
|
+
if (parsedRepresentation) {
|
|
1046
|
+
parsedRepresentation.hdr = parsedRepresentation.hdr || videoRange;
|
|
1047
|
+
}
|
|
1048
|
+
return parsedRepresentation;
|
|
950
1049
|
}).filter((s) => !!s);
|
|
951
1050
|
|
|
952
1051
|
if (streams.length == 0) {
|
|
@@ -1122,8 +1221,10 @@ shaka.dash.DashParser = class {
|
|
|
1122
1221
|
|
|
1123
1222
|
let forced = false;
|
|
1124
1223
|
if (isText) {
|
|
1125
|
-
// See: https://github.com/google/shaka-player/issues/2122
|
|
1126
|
-
|
|
1224
|
+
// See: https://github.com/google/shaka-player/issues/2122 and
|
|
1225
|
+
// https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/165
|
|
1226
|
+
forced = roles.includes('forced_subtitle') ||
|
|
1227
|
+
roles.includes('forced-subtitle');
|
|
1127
1228
|
}
|
|
1128
1229
|
|
|
1129
1230
|
let tilesLayout;
|
|
@@ -1637,9 +1738,10 @@ shaka.dash.DashParser = class {
|
|
|
1637
1738
|
* @param {number} periodStart
|
|
1638
1739
|
* @param {?number} periodDuration
|
|
1639
1740
|
* @param {!Element} elem
|
|
1741
|
+
* @param {number} availabilityStart
|
|
1640
1742
|
* @private
|
|
1641
1743
|
*/
|
|
1642
|
-
parseEventStream_(periodStart, periodDuration, elem) {
|
|
1744
|
+
parseEventStream_(periodStart, periodDuration, elem, availabilityStart) {
|
|
1643
1745
|
const XmlUtils = shaka.util.XmlUtils;
|
|
1644
1746
|
const parseNumber = XmlUtils.parseNonNegativeInt;
|
|
1645
1747
|
|
|
@@ -1662,6 +1764,11 @@ shaka.dash.DashParser = class {
|
|
|
1662
1764
|
endTime = Math.min(endTime, periodStart + periodDuration);
|
|
1663
1765
|
}
|
|
1664
1766
|
|
|
1767
|
+
// Don't add unavailable regions to the timeline.
|
|
1768
|
+
if (endTime < availabilityStart) {
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1665
1772
|
/** @type {shaka.extern.TimelineRegionInfo} */
|
|
1666
1773
|
const region = {
|
|
1667
1774
|
schemeIdUri: schemeIdUri,
|
package/lib/dash/segment_list.js
CHANGED
|
@@ -63,12 +63,12 @@ shaka.dash.SegmentList = class {
|
|
|
63
63
|
const start = context.presentationTimeline.getSegmentAvailabilityStart();
|
|
64
64
|
segmentIndex.mergeAndEvict(references, start);
|
|
65
65
|
} else {
|
|
66
|
-
context.presentationTimeline.notifySegments(references);
|
|
67
66
|
segmentIndex = new shaka.media.SegmentIndex(references);
|
|
68
67
|
if (id && context.dynamic) {
|
|
69
68
|
segmentIndexMap[id] = segmentIndex;
|
|
70
69
|
}
|
|
71
70
|
}
|
|
71
|
+
context.presentationTimeline.notifySegments(references);
|
|
72
72
|
|
|
73
73
|
if (!context.dynamic || !context.periodInfo.isLastPeriod) {
|
|
74
74
|
const periodStart = context.periodInfo.start;
|
|
@@ -116,12 +116,12 @@ shaka.dash.SegmentTemplate = class {
|
|
|
116
116
|
segmentIndex.mergeAndEvict(references,
|
|
117
117
|
context.presentationTimeline.getSegmentAvailabilityStart());
|
|
118
118
|
} else {
|
|
119
|
-
context.presentationTimeline.notifySegments(references);
|
|
120
119
|
segmentIndex = new shaka.media.SegmentIndex(references);
|
|
121
120
|
if (id && context.dynamic) {
|
|
122
121
|
segmentIndexMap[id] = segmentIndex;
|
|
123
122
|
}
|
|
124
123
|
}
|
|
124
|
+
context.presentationTimeline.notifySegments(references);
|
|
125
125
|
|
|
126
126
|
if (shouldFit) {
|
|
127
127
|
segmentIndex.fit(periodStart, periodEnd);
|
package/lib/hls/hls_parser.js
CHANGED
|
@@ -414,18 +414,16 @@ shaka.hls.HlsParser = class {
|
|
|
414
414
|
const uri = tag.getAttributeValue('URI');
|
|
415
415
|
const language = tag.getAttributeValue('LANGUAGE');
|
|
416
416
|
const value = tag.getAttributeValue('VALUE');
|
|
417
|
-
const data =
|
|
418
|
-
'id': id,
|
|
419
|
-
};
|
|
417
|
+
const data = (new Map()).set('id', id);
|
|
420
418
|
if (uri) {
|
|
421
|
-
data.uri
|
|
422
|
-
shaka.hls.Utils.constructAbsoluteUri(this.masterPlaylistUri_, uri);
|
|
419
|
+
data.set('uri',
|
|
420
|
+
shaka.hls.Utils.constructAbsoluteUri(this.masterPlaylistUri_, uri));
|
|
423
421
|
}
|
|
424
422
|
if (language) {
|
|
425
|
-
data.language
|
|
423
|
+
data.set('language', language);
|
|
426
424
|
}
|
|
427
425
|
if (value) {
|
|
428
|
-
data.value
|
|
426
|
+
data.set('value', value);
|
|
429
427
|
}
|
|
430
428
|
const event = new shaka.util.FakeEvent('sessiondata', data);
|
|
431
429
|
if (this.playerInterface_) {
|
|
@@ -529,6 +527,30 @@ shaka.hls.HlsParser = class {
|
|
|
529
527
|
}
|
|
530
528
|
}
|
|
531
529
|
|
|
530
|
+
// Now that the content has been fit, notify segments.
|
|
531
|
+
this.segmentsToNotifyByStream_ = [];
|
|
532
|
+
const streamsToNotify = [];
|
|
533
|
+
for (const variant of variants) {
|
|
534
|
+
for (const stream of [variant.video, variant.audio]) {
|
|
535
|
+
if (stream) {
|
|
536
|
+
streamsToNotify.push(stream);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
await Promise.all(streamsToNotify.map(async (stream) => {
|
|
541
|
+
await stream.createSegmentIndex();
|
|
542
|
+
}));
|
|
543
|
+
for (const stream of streamsToNotify) {
|
|
544
|
+
this.segmentsToNotifyByStream_.push(stream.segmentIndex.references);
|
|
545
|
+
}
|
|
546
|
+
this.notifySegments_();
|
|
547
|
+
|
|
548
|
+
// This asserts that the live edge is being calculated from segment times.
|
|
549
|
+
// For VOD and event streams, this check should still pass.
|
|
550
|
+
goog.asserts.assert(
|
|
551
|
+
!this.presentationTimeline_.usingPresentationStartTime(),
|
|
552
|
+
'We should not be using the presentation start time in HLS!');
|
|
553
|
+
|
|
532
554
|
this.manifest_ = {
|
|
533
555
|
presentationTimeline: this.presentationTimeline_,
|
|
534
556
|
variants,
|
|
@@ -1151,7 +1173,7 @@ shaka.hls.HlsParser = class {
|
|
|
1151
1173
|
// "AUTOSELECT=YES". A value of "AUTOSELECT=NO" would imply that it may
|
|
1152
1174
|
// only be selected explicitly by the user, and we don't have a way to
|
|
1153
1175
|
// represent that in our model.
|
|
1154
|
-
const defaultAttrValue = tag.
|
|
1176
|
+
const defaultAttrValue = tag.getAttributeValue('DEFAULT');
|
|
1155
1177
|
const primary = defaultAttrValue == 'YES';
|
|
1156
1178
|
|
|
1157
1179
|
const channelsCount = type == 'audio' ? this.getChannelsCount_(tag) : null;
|
|
@@ -1538,12 +1560,6 @@ shaka.hls.HlsParser = class {
|
|
|
1538
1560
|
}
|
|
1539
1561
|
|
|
1540
1562
|
this.notifySegments_();
|
|
1541
|
-
|
|
1542
|
-
// This asserts that the live edge is being calculated from segment times.
|
|
1543
|
-
// For VOD and event streams, this check should still pass.
|
|
1544
|
-
goog.asserts.assert(
|
|
1545
|
-
!this.presentationTimeline_.usingPresentationStartTime(),
|
|
1546
|
-
'We should not be using the presentation start time in HLS!');
|
|
1547
1563
|
}
|
|
1548
1564
|
|
|
1549
1565
|
/**
|
|
@@ -1915,9 +1931,6 @@ shaka.hls.HlsParser = class {
|
|
|
1915
1931
|
}
|
|
1916
1932
|
}
|
|
1917
1933
|
|
|
1918
|
-
this.segmentsToNotifyByStream_.push(references);
|
|
1919
|
-
this.notifySegments_();
|
|
1920
|
-
|
|
1921
1934
|
return references;
|
|
1922
1935
|
}
|
|
1923
1936
|
|
|
@@ -2253,6 +2266,12 @@ shaka.hls.HlsParser = class {
|
|
|
2253
2266
|
reader.rewind(1);
|
|
2254
2267
|
};
|
|
2255
2268
|
|
|
2269
|
+
// We will look a few packet-lengths forward to find the first sync byte.
|
|
2270
|
+
// Note that we are using this method on what is already a subset of the
|
|
2271
|
+
// file (the first |shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_| bytes), so
|
|
2272
|
+
// we can't look too far ahead to begin with.
|
|
2273
|
+
let syncByteScanLength = Math.min(reader.getLength() - 188, 5 * 188);
|
|
2274
|
+
|
|
2256
2275
|
// TODO: refactor this while loop for better readability.
|
|
2257
2276
|
// eslint-disable-next-line no-constant-condition
|
|
2258
2277
|
while (true) {
|
|
@@ -2261,8 +2280,16 @@ shaka.hls.HlsParser = class {
|
|
|
2261
2280
|
|
|
2262
2281
|
syncByte = reader.readUint8();
|
|
2263
2282
|
if (syncByte != 0x47) {
|
|
2283
|
+
if (syncByteScanLength > 0) {
|
|
2284
|
+
// This file could have started with a cut-off TS packet. Scan forward
|
|
2285
|
+
// until we find a sync byte.
|
|
2286
|
+
syncByteScanLength -= 1;
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2264
2289
|
fail();
|
|
2265
2290
|
}
|
|
2291
|
+
// If we've found a sync byte, stop scanning forward for future packets.
|
|
2292
|
+
syncByteScanLength = 0;
|
|
2266
2293
|
|
|
2267
2294
|
const flagsAndPacketId = reader.readUint16();
|
|
2268
2295
|
const packetId = flagsAndPacketId & 0x1fff;
|
|
@@ -87,7 +87,9 @@ shaka.media.ClosedCaptionParser = class {
|
|
|
87
87
|
for (const captionPacket of captionPackets) {
|
|
88
88
|
const uint8ArrayData =
|
|
89
89
|
shaka.util.BufferUtils.toUint8(captionPacket.packet);
|
|
90
|
-
|
|
90
|
+
if (uint8ArrayData.length > 0) {
|
|
91
|
+
this.ceaDecoder_.extract(uint8ArrayData, captionPacket.pts);
|
|
92
|
+
}
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
// Decode and return the parsed captions.
|
|
@@ -75,6 +75,18 @@ shaka.media.ContentWorkarounds = class {
|
|
|
75
75
|
newType: ContentWorkarounds.BOX_TYPE_ENCV_,
|
|
76
76
|
});
|
|
77
77
|
})
|
|
78
|
+
.fullBox('avc3', (box) => {
|
|
79
|
+
boxesToModify.push({
|
|
80
|
+
box,
|
|
81
|
+
newType: ContentWorkarounds.BOX_TYPE_ENCV_,
|
|
82
|
+
});
|
|
83
|
+
})
|
|
84
|
+
.fullBox('ac-3', (box) => {
|
|
85
|
+
boxesToModify.push({
|
|
86
|
+
box,
|
|
87
|
+
newType: ContentWorkarounds.BOX_TYPE_ENCA_,
|
|
88
|
+
});
|
|
89
|
+
})
|
|
78
90
|
.fullBox('ec-3', (box) => {
|
|
79
91
|
boxesToModify.push({
|
|
80
92
|
box,
|
package/lib/media/drm_engine.js
CHANGED
|
@@ -126,6 +126,9 @@ shaka.media.DrmEngine = class {
|
|
|
126
126
|
|
|
127
127
|
/** @const {!shaka.util.Destroyer} */
|
|
128
128
|
this.destroyer_ = new shaka.util.Destroyer(() => this.destroyNow_());
|
|
129
|
+
|
|
130
|
+
/** @private {boolean} */
|
|
131
|
+
this.srcEquals_ = false;
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
/** @override */
|
|
@@ -181,6 +184,7 @@ shaka.media.DrmEngine = class {
|
|
|
181
184
|
this.config_ = null;
|
|
182
185
|
this.onError_ = () => {};
|
|
183
186
|
this.playerInterface_ = null;
|
|
187
|
+
this.srcEquals_ = false;
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
/**
|
|
@@ -194,6 +198,13 @@ shaka.media.DrmEngine = class {
|
|
|
194
198
|
this.config_ = config;
|
|
195
199
|
}
|
|
196
200
|
|
|
201
|
+
/**
|
|
202
|
+
* @param {!boolean} value
|
|
203
|
+
*/
|
|
204
|
+
setSrcEquals(value) {
|
|
205
|
+
this.srcEquals_ = value;
|
|
206
|
+
}
|
|
207
|
+
|
|
197
208
|
/**
|
|
198
209
|
* Initialize the drm engine for storing and deleting stored content.
|
|
199
210
|
*
|
|
@@ -356,7 +367,7 @@ shaka.media.DrmEngine = class {
|
|
|
356
367
|
// in the drm infos, and before queryMediaKeys_().
|
|
357
368
|
if (useMediaCapabilities) {
|
|
358
369
|
await shaka.util.StreamUtils.getDecodingInfosForVariants(variants,
|
|
359
|
-
this.usePersistentLicenses_);
|
|
370
|
+
this.usePersistentLicenses_, this.srcEquals_);
|
|
360
371
|
} else {
|
|
361
372
|
configsByKeySystem =
|
|
362
373
|
this.prepareMediaKeyConfigsForVariants_(variants);
|
|
@@ -512,6 +523,7 @@ shaka.media.DrmEngine = class {
|
|
|
512
523
|
tasks.push(session.remove());
|
|
513
524
|
|
|
514
525
|
await Promise.all(tasks);
|
|
526
|
+
this.activeSessions_.delete(session);
|
|
515
527
|
}
|
|
516
528
|
|
|
517
529
|
/**
|
|
@@ -1877,9 +1889,16 @@ shaka.media.DrmEngine = class {
|
|
|
1877
1889
|
// audio adaptations, so we shouldn't have to worry about checking
|
|
1878
1890
|
// robustness.
|
|
1879
1891
|
if (drm1.keySystem == drm2.keySystem) {
|
|
1892
|
+
/** @type {Array<shaka.extern.InitDataOverride>} */
|
|
1880
1893
|
let initData = [];
|
|
1881
1894
|
initData = initData.concat(drm1.initData || []);
|
|
1882
1895
|
initData = initData.concat(drm2.initData || []);
|
|
1896
|
+
initData = initData.filter((d, i) => {
|
|
1897
|
+
return d.keyId === undefined || i === initData.findIndex((d2) => {
|
|
1898
|
+
return d2.keyId === d.keyId;
|
|
1899
|
+
});
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1883
1902
|
const keyIds = drm1.keyIds && drm2.keyIds ?
|
|
1884
1903
|
new Set([...drm1.keyIds, ...drm2.keyIds]) :
|
|
1885
1904
|
drm1.keyIds || drm2.keyIds;
|