shaka-player 4.1.3 → 4.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/selenium-lab-tests.yaml +158 -53
- package/CHANGELOG.md +39 -29
- package/demo/common/assets.js +25 -0
- package/demo/common/message_ids.js +2 -0
- package/demo/config.js +5 -1
- package/demo/index.html +1 -1
- package/demo/locales/en.json +2 -0
- package/demo/locales/source.json +8 -0
- package/dist/controls.css +1 -1
- package/dist/controls.css.map +1 -1
- package/dist/demo.compiled.debug.js +25 -23
- package/dist/demo.compiled.debug.map +3 -3
- package/dist/demo.compiled.js +25 -23
- package/dist/demo.compiled.map +3 -3
- package/dist/deps.js +3 -2
- package/dist/locales.js +32 -32
- package/dist/receiver.compiled.debug.js +24 -22
- package/dist/receiver.compiled.debug.map +3 -3
- package/dist/receiver.compiled.js +24 -22
- package/dist/receiver.compiled.map +3 -3
- package/dist/shaka-player.compiled.d.ts +57 -6
- package/dist/shaka-player.compiled.debug.d.ts +57 -6
- package/dist/shaka-player.compiled.debug.externs.js +62 -6
- package/dist/shaka-player.compiled.debug.js +305 -262
- package/dist/shaka-player.compiled.debug.map +4 -4
- package/dist/shaka-player.compiled.externs.js +62 -6
- package/dist/shaka-player.compiled.js +817 -801
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.d.ts +57 -6
- package/dist/shaka-player.ui.debug.d.ts +57 -6
- package/dist/shaka-player.ui.debug.externs.js +62 -6
- package/dist/shaka-player.ui.debug.js +308 -265
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +62 -6
- package/dist/shaka-player.ui.js +942 -925
- package/dist/shaka-player.ui.map +4 -4
- package/docs/tutorials/fairplay.md +149 -0
- package/docs/tutorials/ui.md +6 -6
- package/externs/shaka/abr_manager.js +8 -0
- package/externs/shaka/manifest.js +43 -0
- package/externs/shaka/player.js +13 -2
- package/lib/abr/simple_abr_manager.js +72 -4
- package/lib/hls/hls_parser.js +138 -96
- package/lib/media/drm_engine.js +0 -2
- package/lib/media/segment_index.js +8 -0
- package/lib/media/segment_reference.js +7 -1
- package/lib/media/streaming_engine.js +38 -2
- package/lib/net/networking_engine.js +1 -0
- package/lib/player.js +20 -1
- package/lib/polyfill/patchedmediakeys_apple.js +729 -0
- package/lib/text/ui_text_displayer.js +8 -0
- package/lib/text/vtt_text_parser.js +61 -4
- package/lib/util/error.js +23 -11
- package/lib/util/fairplay_utils.js +100 -2
- package/lib/util/platform.js +12 -1
- package/lib/util/player_configuration.js +4 -10
- package/package.json +1 -1
- package/shaka-player.uncompiled.js +1 -0
- package/test/hls/hls_parser_unit.js +101 -34
- package/test/media/drm_engine_unit.js +29 -0
- package/test/media/streaming_engine_unit.js +79 -5
- package/test/player_src_equals_integration.js +2 -0
- package/test/player_unit.js +39 -1
- package/test/test/assets/small.mp4 +0 -0
- package/test/test/util/simple_fakes.js +4 -0
- package/test/test/util/streaming_engine_util.js +10 -2
- package/test/text/vtt_text_parser_unit.js +26 -0
- package/ui/less/ad_controls.less +4 -0
package/lib/hls/hls_parser.js
CHANGED
|
@@ -37,6 +37,7 @@ goog.require('shaka.util.OperationManager');
|
|
|
37
37
|
goog.require('shaka.util.Pssh');
|
|
38
38
|
goog.require('shaka.util.Timer');
|
|
39
39
|
goog.require('shaka.util.Platform');
|
|
40
|
+
goog.require('shaka.util.Uint8ArrayUtils');
|
|
40
41
|
goog.require('shaka.util.XmlUtils');
|
|
41
42
|
goog.requireType('shaka.hls.Segment');
|
|
42
43
|
|
|
@@ -179,10 +180,6 @@ shaka.hls.HlsParser = class {
|
|
|
179
180
|
*/
|
|
180
181
|
this.groupIdToClosedCaptionsMap_ = new Map();
|
|
181
182
|
|
|
182
|
-
/** True if some of the variants in the playlist is encrypted with AES-128.
|
|
183
|
-
* @private {boolean} */
|
|
184
|
-
this.aesEncrypted_ = false;
|
|
185
|
-
|
|
186
183
|
/** @private {Map.<string, string>} */
|
|
187
184
|
this.groupIdToCodecsMap_ = new Map();
|
|
188
185
|
|
|
@@ -575,7 +572,6 @@ shaka.hls.HlsParser = class {
|
|
|
575
572
|
playlist, uri, uri, codecs, type, language, primary, name,
|
|
576
573
|
channelsCount, closedCaptions, characteristics, forced, spatialAudio,
|
|
577
574
|
mimeType);
|
|
578
|
-
goog.asserts.assert(streamInfo != null, 'StreamInfo is null!');
|
|
579
575
|
this.uriToStreamInfosMap_.set(uri, streamInfo);
|
|
580
576
|
|
|
581
577
|
// Wrap the stream from that stream info with a variant.
|
|
@@ -653,17 +649,6 @@ shaka.hls.HlsParser = class {
|
|
|
653
649
|
this.syncStreamsWithProgramDateTime_();
|
|
654
650
|
}
|
|
655
651
|
|
|
656
|
-
if (this.aesEncrypted_ && variants.length == 0) {
|
|
657
|
-
// We do not support AES-128 encryption with HLS yet. Variants is null
|
|
658
|
-
// when the playlist is encrypted with AES-128.
|
|
659
|
-
shaka.log.info('No stream is created, because we don\'t support AES-128',
|
|
660
|
-
'encryption yet');
|
|
661
|
-
throw new shaka.util.Error(
|
|
662
|
-
shaka.util.Error.Severity.CRITICAL,
|
|
663
|
-
shaka.util.Error.Category.MANIFEST,
|
|
664
|
-
shaka.util.Error.Code.HLS_AES_128_ENCRYPTION_NOT_SUPPORTED);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
652
|
// Find the min and max timestamp of the earliest segment in all streams.
|
|
668
653
|
// Find the minimum duration of all streams as well.
|
|
669
654
|
let minDuration = Infinity;
|
|
@@ -862,8 +847,6 @@ shaka.hls.HlsParser = class {
|
|
|
862
847
|
}
|
|
863
848
|
try {
|
|
864
849
|
const streamInfo = await this.createStreamInfoFromMediaTag_(tag);
|
|
865
|
-
goog.asserts.assert(
|
|
866
|
-
streamInfo, 'Should always have a streamInfo for text');
|
|
867
850
|
return streamInfo.stream;
|
|
868
851
|
} catch (e) {
|
|
869
852
|
if (this.config_.hls.ignoreTextStreamFailures) {
|
|
@@ -906,8 +889,6 @@ shaka.hls.HlsParser = class {
|
|
|
906
889
|
}
|
|
907
890
|
try {
|
|
908
891
|
const streamInfo = await this.createStreamInfoFromImageTag_(tag);
|
|
909
|
-
goog.asserts.assert(
|
|
910
|
-
streamInfo, 'Should always have a streamInfo for image');
|
|
911
892
|
return streamInfo.stream;
|
|
912
893
|
} catch (e) {
|
|
913
894
|
if (this.config_.hls.ignoreImageStreamFailures) {
|
|
@@ -959,22 +940,17 @@ shaka.hls.HlsParser = class {
|
|
|
959
940
|
const streamInfos = await this.createStreamInfosForVariantTag_(tag,
|
|
960
941
|
resolution, frameRate);
|
|
961
942
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
videoRange);
|
|
974
|
-
}
|
|
975
|
-
// We do not support AES-128 encryption with HLS yet. If the streamInfos
|
|
976
|
-
// is null because of AES-128 encryption, do not create variants for that.
|
|
977
|
-
return [];
|
|
943
|
+
goog.asserts.assert(streamInfos.audio.length ||
|
|
944
|
+
streamInfos.video.length, 'We should have created a stream!');
|
|
945
|
+
|
|
946
|
+
return this.createVariants_(
|
|
947
|
+
streamInfos.audio,
|
|
948
|
+
streamInfos.video,
|
|
949
|
+
bandwidth,
|
|
950
|
+
width,
|
|
951
|
+
height,
|
|
952
|
+
frameRate,
|
|
953
|
+
videoRange);
|
|
978
954
|
});
|
|
979
955
|
|
|
980
956
|
const allVariants = await Promise.all(variantsPromises);
|
|
@@ -991,7 +967,7 @@ shaka.hls.HlsParser = class {
|
|
|
991
967
|
* @param {!shaka.hls.Tag} tag
|
|
992
968
|
* @param {?string} resolution
|
|
993
969
|
* @param {?string} frameRate
|
|
994
|
-
* @return {!Promise
|
|
970
|
+
* @return {!Promise.<!shaka.hls.HlsParser.StreamInfos>}
|
|
995
971
|
* @private
|
|
996
972
|
*/
|
|
997
973
|
async createStreamInfosForVariantTag_(tag, resolution, frameRate) {
|
|
@@ -1061,17 +1037,10 @@ shaka.hls.HlsParser = class {
|
|
|
1061
1037
|
type = ContentType.VIDEO;
|
|
1062
1038
|
}
|
|
1063
1039
|
|
|
1064
|
-
let streamInfo;
|
|
1065
1040
|
if (!ignoreStream) {
|
|
1066
|
-
streamInfo =
|
|
1041
|
+
const streamInfo =
|
|
1067
1042
|
await this.createStreamInfoFromVariantTag_(tag, allCodecs, type);
|
|
1068
|
-
}
|
|
1069
|
-
if (streamInfo) {
|
|
1070
1043
|
res[streamInfo.stream.type] = [streamInfo];
|
|
1071
|
-
} else if (streamInfo === null) {
|
|
1072
|
-
// Triple-equals for undefined.
|
|
1073
|
-
shaka.log.debug('streamInfo is null');
|
|
1074
|
-
return null;
|
|
1075
1044
|
}
|
|
1076
1045
|
this.filterLegacyCodecs_(res);
|
|
1077
1046
|
return res;
|
|
@@ -1363,7 +1332,7 @@ shaka.hls.HlsParser = class {
|
|
|
1363
1332
|
* Parse EXT-X-MEDIA media tag into a Stream object.
|
|
1364
1333
|
*
|
|
1365
1334
|
* @param {shaka.hls.Tag} tag
|
|
1366
|
-
* @return {!Promise
|
|
1335
|
+
* @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
|
|
1367
1336
|
* @private
|
|
1368
1337
|
*/
|
|
1369
1338
|
async createStreamInfoFromMediaTag_(tag) {
|
|
@@ -1417,9 +1386,6 @@ shaka.hls.HlsParser = class {
|
|
|
1417
1386
|
} else {
|
|
1418
1387
|
this.groupIdToStreamInfosMap_.set(groupId, [streamInfo]);
|
|
1419
1388
|
}
|
|
1420
|
-
if (streamInfo == null) {
|
|
1421
|
-
return null;
|
|
1422
|
-
}
|
|
1423
1389
|
|
|
1424
1390
|
// TODO: This check is necessary because of the possibility of multiple
|
|
1425
1391
|
// calls to createStreamInfoFromMediaTag_ before either has resolved.
|
|
@@ -1434,7 +1400,7 @@ shaka.hls.HlsParser = class {
|
|
|
1434
1400
|
* Parse EXT-X-MEDIA media tag into a Stream object.
|
|
1435
1401
|
*
|
|
1436
1402
|
* @param {shaka.hls.Tag} tag
|
|
1437
|
-
* @return {!Promise
|
|
1403
|
+
* @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
|
|
1438
1404
|
* @private
|
|
1439
1405
|
*/
|
|
1440
1406
|
async createStreamInfoFromImageTag_(tag) {
|
|
@@ -1462,9 +1428,6 @@ shaka.hls.HlsParser = class {
|
|
|
1462
1428
|
verbatimImagePlaylistUri, codecs, type, language, /* primary= */ false,
|
|
1463
1429
|
name, /* channelsCount= */ null, /* closedCaptions= */ null,
|
|
1464
1430
|
characteristics, /* forced= */ false, /* spatialAudio= */ false);
|
|
1465
|
-
if (streamInfo == null) {
|
|
1466
|
-
return null;
|
|
1467
|
-
}
|
|
1468
1431
|
|
|
1469
1432
|
// TODO: This check is necessary because of the possibility of multiple
|
|
1470
1433
|
// calls to createStreamInfoFromImageTag_ before either has resolved.
|
|
@@ -1505,7 +1468,7 @@ shaka.hls.HlsParser = class {
|
|
|
1505
1468
|
* @param {!shaka.hls.Tag} tag
|
|
1506
1469
|
* @param {!Array.<string>} allCodecs
|
|
1507
1470
|
* @param {string} type
|
|
1508
|
-
* @return {!Promise
|
|
1471
|
+
* @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
|
|
1509
1472
|
* @private
|
|
1510
1473
|
*/
|
|
1511
1474
|
async createStreamInfoFromVariantTag_(tag, allCodecs, type) {
|
|
@@ -1525,9 +1488,6 @@ shaka.hls.HlsParser = class {
|
|
|
1525
1488
|
/* name= */ null, /* channelcount= */ null, closedCaptions,
|
|
1526
1489
|
/* characteristics= */ null, /* forced= */ false,
|
|
1527
1490
|
/* spatialAudio= */ false);
|
|
1528
|
-
if (streamInfo == null) {
|
|
1529
|
-
return null;
|
|
1530
|
-
}
|
|
1531
1491
|
// TODO: This check is necessary because of the possibility of multiple
|
|
1532
1492
|
// calls to createStreamInfoFromVariantTag_ before either has resolved.
|
|
1533
1493
|
if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
|
|
@@ -1551,7 +1511,7 @@ shaka.hls.HlsParser = class {
|
|
|
1551
1511
|
* @param {?string} characteristics
|
|
1552
1512
|
* @param {boolean} forced
|
|
1553
1513
|
* @param {boolean} spatialAudio
|
|
1554
|
-
* @return {!Promise
|
|
1514
|
+
* @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
|
|
1555
1515
|
* @private
|
|
1556
1516
|
*/
|
|
1557
1517
|
async createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language,
|
|
@@ -1591,7 +1551,7 @@ shaka.hls.HlsParser = class {
|
|
|
1591
1551
|
* @param {boolean} forced
|
|
1592
1552
|
* @param {boolean} spatialAudio
|
|
1593
1553
|
* @param {(string|undefined)} mimeType
|
|
1594
|
-
* @return {!Promise
|
|
1554
|
+
* @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
|
|
1595
1555
|
* @private
|
|
1596
1556
|
*/
|
|
1597
1557
|
async convertParsedPlaylistIntoStreamInfo_(playlist, verbatimMediaPlaylistUri,
|
|
@@ -1634,6 +1594,8 @@ shaka.hls.HlsParser = class {
|
|
|
1634
1594
|
}
|
|
1635
1595
|
|
|
1636
1596
|
let encrypted = false;
|
|
1597
|
+
let aesEncrypted = false;
|
|
1598
|
+
|
|
1637
1599
|
/** @type {!Array.<shaka.extern.DrmInfo>}*/
|
|
1638
1600
|
const drmInfos = [];
|
|
1639
1601
|
const keyIds = new Set();
|
|
@@ -1644,34 +1606,30 @@ shaka.hls.HlsParser = class {
|
|
|
1644
1606
|
if (method != 'NONE') {
|
|
1645
1607
|
encrypted = true;
|
|
1646
1608
|
|
|
1647
|
-
// We do not support AES-128 encryption with HLS yet. So, do not create
|
|
1648
|
-
// StreamInfo for the playlist encrypted with AES-128.
|
|
1649
|
-
// TODO: Remove the error message once we add support for AES-128.
|
|
1650
1609
|
if (method == 'AES-128') {
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
keyIds.add(keyId);
|
|
1610
|
+
// These keys are handled separately.
|
|
1611
|
+
aesEncrypted = true;
|
|
1612
|
+
} else {
|
|
1613
|
+
const keyFormat = drmTag.getRequiredAttrValue('KEYFORMAT');
|
|
1614
|
+
const drmParser =
|
|
1615
|
+
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
|
|
1616
|
+
|
|
1617
|
+
const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
|
|
1618
|
+
if (drmInfo) {
|
|
1619
|
+
if (drmInfo.keyIds) {
|
|
1620
|
+
for (const keyId of drmInfo.keyIds) {
|
|
1621
|
+
keyIds.add(keyId);
|
|
1622
|
+
}
|
|
1665
1623
|
}
|
|
1624
|
+
drmInfos.push(drmInfo);
|
|
1625
|
+
} else {
|
|
1626
|
+
shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
|
|
1666
1627
|
}
|
|
1667
|
-
drmInfos.push(drmInfo);
|
|
1668
|
-
} else {
|
|
1669
|
-
shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
|
|
1670
1628
|
}
|
|
1671
1629
|
}
|
|
1672
1630
|
}
|
|
1673
1631
|
|
|
1674
|
-
if (encrypted && !drmInfos.length) {
|
|
1632
|
+
if (encrypted && !drmInfos.length && !aesEncrypted) {
|
|
1675
1633
|
throw new shaka.util.Error(
|
|
1676
1634
|
shaka.util.Error.Severity.CRITICAL,
|
|
1677
1635
|
shaka.util.Error.Category.MANIFEST,
|
|
@@ -1686,20 +1644,8 @@ shaka.hls.HlsParser = class {
|
|
|
1686
1644
|
/** @type {!Map.<number, number>} */
|
|
1687
1645
|
const mediaSequenceToStartTime = new Map();
|
|
1688
1646
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
segments = this.createSegments_(verbatimMediaPlaylistUri,
|
|
1692
|
-
playlist, type, mimeType, mediaSequenceToStartTime, mediaVariables,
|
|
1693
|
-
codecs);
|
|
1694
|
-
} catch (error) {
|
|
1695
|
-
if (error.code == shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM) {
|
|
1696
|
-
shaka.log.alwaysWarn('Skipping unsupported HLS stream',
|
|
1697
|
-
mimeType, verbatimMediaPlaylistUri);
|
|
1698
|
-
return null;
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
throw error;
|
|
1702
|
-
}
|
|
1647
|
+
const segments = this.createSegments_(verbatimMediaPlaylistUri, playlist,
|
|
1648
|
+
type, mimeType, mediaSequenceToStartTime, mediaVariables, codecs);
|
|
1703
1649
|
|
|
1704
1650
|
const lastEndTime = segments[segments.length - 1].endTime;
|
|
1705
1651
|
/** @type {!shaka.media.SegmentIndex} */
|
|
@@ -1765,6 +1711,78 @@ shaka.hls.HlsParser = class {
|
|
|
1765
1711
|
}
|
|
1766
1712
|
|
|
1767
1713
|
|
|
1714
|
+
/**
|
|
1715
|
+
* @param {!shaka.hls.Tag} drmTag
|
|
1716
|
+
* @param {!shaka.hls.Playlist} playlist
|
|
1717
|
+
* @return {!shaka.extern.HlsAes128Key}
|
|
1718
|
+
* @private
|
|
1719
|
+
*/
|
|
1720
|
+
parseAES128DrmTag_(drmTag, playlist) {
|
|
1721
|
+
// Check if the Web Crypto API is available.
|
|
1722
|
+
if (!window.crypto || !window.crypto.subtle) {
|
|
1723
|
+
shaka.log.alwaysWarn('Web Crypto API is not available to decrypt ' +
|
|
1724
|
+
'AES-128. (Web Crypto only exists in secure origins like https)');
|
|
1725
|
+
throw new shaka.util.Error(
|
|
1726
|
+
shaka.util.Error.Severity.CRITICAL,
|
|
1727
|
+
shaka.util.Error.Category.MANIFEST,
|
|
1728
|
+
shaka.util.Error.Code.NO_WEB_CRYPTO_API);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// HLS RFC 8216 Section 5.2:
|
|
1732
|
+
// An EXT-X-KEY tag with a KEYFORMAT of "identity" that does not have an IV
|
|
1733
|
+
// attribute indicates that the Media Sequence Number is to be used as the
|
|
1734
|
+
// IV when decrypting a Media Segment, by putting its big-endian binary
|
|
1735
|
+
// representation into a 16-octet (128-bit) buffer and padding (on the left)
|
|
1736
|
+
// with zeros.
|
|
1737
|
+
let firstMediaSequenceNumber = 0;
|
|
1738
|
+
let iv;
|
|
1739
|
+
const ivHex = drmTag.getAttributeValue('IV', '');
|
|
1740
|
+
if (!ivHex) {
|
|
1741
|
+
// Media Sequence Number will be used as IV.
|
|
1742
|
+
firstMediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
|
|
1743
|
+
playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
|
|
1744
|
+
} else {
|
|
1745
|
+
// Exclude 0x at the start of string.
|
|
1746
|
+
iv = shaka.util.Uint8ArrayUtils.fromHex(ivHex.substr(2));
|
|
1747
|
+
if (iv.byteLength != 16) {
|
|
1748
|
+
throw new shaka.util.Error(
|
|
1749
|
+
shaka.util.Error.Severity.CRITICAL,
|
|
1750
|
+
shaka.util.Error.Category.MANIFEST,
|
|
1751
|
+
shaka.util.Error.Code.HLS_AES_128_INVALID_IV_LENGTH);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
const keyUri = shaka.hls.Utils.constructAbsoluteUri(
|
|
1756
|
+
playlist.absoluteUri, drmTag.getRequiredAttrValue('URI'));
|
|
1757
|
+
|
|
1758
|
+
const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
|
|
1759
|
+
const request = shaka.net.NetworkingEngine.makeRequest(
|
|
1760
|
+
[keyUri], this.config_.retryParameters);
|
|
1761
|
+
|
|
1762
|
+
const keyInfo = {method: 'AES-128', iv, firstMediaSequenceNumber};
|
|
1763
|
+
|
|
1764
|
+
// Don't download the key object until the segment is parsed, to avoid a
|
|
1765
|
+
// startup delay for long manifests with lots of keys.
|
|
1766
|
+
keyInfo.fetchKey = async () => {
|
|
1767
|
+
const keyResponse = await this.makeNetworkRequest_(request, requestType);
|
|
1768
|
+
|
|
1769
|
+
// keyResponse.status is undefined when URI is "data:text/plain;base64,"
|
|
1770
|
+
if (!keyResponse.data || keyResponse.data.byteLength != 16) {
|
|
1771
|
+
throw new shaka.util.Error(
|
|
1772
|
+
shaka.util.Error.Severity.CRITICAL,
|
|
1773
|
+
shaka.util.Error.Category.MANIFEST,
|
|
1774
|
+
shaka.util.Error.Code.HLS_AES_128_INVALID_KEY_LENGTH);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
keyInfo.cryptoKey = await window.crypto.subtle.importKey(
|
|
1778
|
+
'raw', keyResponse.data, 'AES-CBC', true, ['decrypt']);
|
|
1779
|
+
keyInfo.fetchKey = undefined; // No longer needed.
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
return keyInfo;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
|
|
1768
1786
|
/**
|
|
1769
1787
|
* @param {!shaka.hls.Playlist} playlist
|
|
1770
1788
|
* @private
|
|
@@ -1939,12 +1957,13 @@ shaka.hls.HlsParser = class {
|
|
|
1939
1957
|
* @param {!Map.<string, string>} variables
|
|
1940
1958
|
* @param {string} absoluteMediaPlaylistUri
|
|
1941
1959
|
* @param {string} type
|
|
1960
|
+
* @param {shaka.extern.HlsAes128Key=} hlsAes128Key
|
|
1942
1961
|
* @return {shaka.media.SegmentReference}
|
|
1943
1962
|
* @private
|
|
1944
1963
|
*/
|
|
1945
1964
|
createSegmentReference_(
|
|
1946
1965
|
initSegmentReference, previousReference, hlsSegment, startTime,
|
|
1947
|
-
variables, absoluteMediaPlaylistUri, type) {
|
|
1966
|
+
variables, absoluteMediaPlaylistUri, type, hlsAes128Key) {
|
|
1948
1967
|
const tags = hlsSegment.tags;
|
|
1949
1968
|
const absoluteSegmentUri = this.variableSubstitution_(
|
|
1950
1969
|
hlsSegment.absoluteUri, variables);
|
|
@@ -2039,6 +2058,8 @@ shaka.hls.HlsParser = class {
|
|
|
2039
2058
|
partialStatus = shaka.media.SegmentReference.Status.MISSING;
|
|
2040
2059
|
}
|
|
2041
2060
|
|
|
2061
|
+
// We do not set the AES-128 key information for partial segments, as we
|
|
2062
|
+
// do not support AES-128 and low-latency at the same time.
|
|
2042
2063
|
const partial = new shaka.media.SegmentReference(
|
|
2043
2064
|
pStartTime,
|
|
2044
2065
|
pEndTime,
|
|
@@ -2120,6 +2141,7 @@ shaka.hls.HlsParser = class {
|
|
|
2120
2141
|
tileDuration,
|
|
2121
2142
|
syncTime,
|
|
2122
2143
|
status,
|
|
2144
|
+
hlsAes128Key,
|
|
2123
2145
|
);
|
|
2124
2146
|
}
|
|
2125
2147
|
|
|
@@ -2186,6 +2208,9 @@ shaka.hls.HlsParser = class {
|
|
|
2186
2208
|
/** @type {shaka.media.InitSegmentReference} */
|
|
2187
2209
|
let initSegmentRef;
|
|
2188
2210
|
|
|
2211
|
+
/** @type {shaka.extern.HlsAes128Key|undefined} */
|
|
2212
|
+
let hlsAes128Key = undefined;
|
|
2213
|
+
|
|
2189
2214
|
// We may need to look at the media itself to determine a segment start
|
|
2190
2215
|
// time.
|
|
2191
2216
|
const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
|
|
@@ -2214,6 +2239,14 @@ shaka.hls.HlsParser = class {
|
|
|
2214
2239
|
(i == 0) ? firstStartTime : previousReference.endTime;
|
|
2215
2240
|
position = mediaSequenceNumber + skippedSegments + i;
|
|
2216
2241
|
|
|
2242
|
+
// Apply new AES-128 tags as you see them, keeping a running total.
|
|
2243
|
+
for (const drmTag of item.tags) {
|
|
2244
|
+
if (drmTag.name == 'EXT-X-KEY' &&
|
|
2245
|
+
drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
|
|
2246
|
+
hlsAes128Key = this.parseAES128DrmTag_(drmTag, playlist);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2217
2250
|
mediaSequenceToStartTime.set(position, startTime);
|
|
2218
2251
|
|
|
2219
2252
|
initSegmentRef = this.getInitSegmentReference_(playlist.absoluteUri,
|
|
@@ -2238,7 +2271,8 @@ shaka.hls.HlsParser = class {
|
|
|
2238
2271
|
startTime,
|
|
2239
2272
|
variables,
|
|
2240
2273
|
playlist.absoluteUri,
|
|
2241
|
-
type
|
|
2274
|
+
type,
|
|
2275
|
+
hlsAes128Key);
|
|
2242
2276
|
previousReference = reference;
|
|
2243
2277
|
|
|
2244
2278
|
if (reference) {
|
|
@@ -2611,6 +2645,14 @@ shaka.hls.HlsParser = class {
|
|
|
2611
2645
|
shaka.util.Error.Code.HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED);
|
|
2612
2646
|
}
|
|
2613
2647
|
|
|
2648
|
+
if (shaka.util.Platform.isMediaKeysPolyfilled()) {
|
|
2649
|
+
throw new shaka.util.Error(
|
|
2650
|
+
shaka.util.Error.Severity.CRITICAL,
|
|
2651
|
+
shaka.util.Error.Category.MANIFEST,
|
|
2652
|
+
shaka.util.Error.Code
|
|
2653
|
+
.HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED);
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2614
2656
|
/*
|
|
2615
2657
|
* Even if we're not able to construct initData through the HLS tag, adding
|
|
2616
2658
|
* a DRMInfo will allow DRM Engine to request a media key system access
|
package/lib/media/drm_engine.js
CHANGED
|
@@ -1668,8 +1668,6 @@ shaka.media.DrmEngine = class {
|
|
|
1668
1668
|
'com.widevine.alpha',
|
|
1669
1669
|
'com.microsoft.playready',
|
|
1670
1670
|
'com.microsoft.playready.recommendation',
|
|
1671
|
-
'com.apple.fps.3_0',
|
|
1672
|
-
'com.apple.fps.2_0',
|
|
1673
1671
|
'com.apple.fps.1_0',
|
|
1674
1672
|
'com.apple.fps',
|
|
1675
1673
|
'com.adobe.primetime',
|
|
@@ -529,6 +529,14 @@ shaka.media.SegmentIterator = class {
|
|
|
529
529
|
this.currentPartialPosition_ = partialSegmentIndex;
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
+
/**
|
|
533
|
+
* @return {number}
|
|
534
|
+
* @export
|
|
535
|
+
*/
|
|
536
|
+
currentPosition() {
|
|
537
|
+
return this.currentPosition_;
|
|
538
|
+
}
|
|
539
|
+
|
|
532
540
|
/**
|
|
533
541
|
* @return {shaka.media.SegmentReference}
|
|
534
542
|
* @export
|
|
@@ -166,12 +166,15 @@ shaka.media.SegmentReference = class {
|
|
|
166
166
|
* @param {shaka.media.SegmentReference.Status=} status
|
|
167
167
|
* The segment status is used to indicate that a segment does not exist or is
|
|
168
168
|
* not available.
|
|
169
|
+
* @param {?shaka.extern.HlsAes128Key=} hlsAes128Key
|
|
170
|
+
* The segment's AES-128-CBC full segment encryption key and iv.
|
|
169
171
|
*/
|
|
170
172
|
constructor(
|
|
171
173
|
startTime, endTime, uris, startByte, endByte, initSegmentReference,
|
|
172
174
|
timestampOffset, appendWindowStart, appendWindowEnd,
|
|
173
175
|
partialReferences = [], tilesLayout = '', tileDuration = null,
|
|
174
|
-
syncTime = null, status = shaka.media.SegmentReference.Status.AVAILABLE
|
|
176
|
+
syncTime = null, status = shaka.media.SegmentReference.Status.AVAILABLE,
|
|
177
|
+
hlsAes128Key = null) {
|
|
175
178
|
// A preload hinted Partial Segment has the same startTime and endTime.
|
|
176
179
|
goog.asserts.assert(startTime <= endTime,
|
|
177
180
|
'startTime must be less than or equal to endTime');
|
|
@@ -233,6 +236,9 @@ shaka.media.SegmentReference = class {
|
|
|
233
236
|
|
|
234
237
|
/** @type {shaka.media.SegmentReference.Status} */
|
|
235
238
|
this.status = status;
|
|
239
|
+
|
|
240
|
+
/** @type {?shaka.extern.HlsAes128Key} */
|
|
241
|
+
this.hlsAes128Key = hlsAes128Key;
|
|
236
242
|
}
|
|
237
243
|
|
|
238
244
|
/**
|
|
@@ -1233,7 +1233,10 @@ shaka.media.StreamingEngine = class {
|
|
|
1233
1233
|
stream.mimeType == 'audio/mp4';
|
|
1234
1234
|
const isReadableStreamSupported = window.ReadableStream;
|
|
1235
1235
|
// Enable MP4 low latency streaming with ReadableStream chunked data.
|
|
1236
|
-
|
|
1236
|
+
// Disabled when AES-128 is present, as we cannot decrypt part of a
|
|
1237
|
+
// segment.
|
|
1238
|
+
if (this.config_.lowLatencyMode && isReadableStreamSupported && isMP4 &&
|
|
1239
|
+
!reference.hlsAes128Key) {
|
|
1237
1240
|
let remaining = new Uint8Array(0);
|
|
1238
1241
|
const streamDataCallback = async (data) => {
|
|
1239
1242
|
this.destroyer_.ensureNotDestroyed();
|
|
@@ -1273,11 +1276,16 @@ shaka.media.StreamingEngine = class {
|
|
|
1273
1276
|
'ReadableStream is not supported by the browser.');
|
|
1274
1277
|
}
|
|
1275
1278
|
const fetchSegment = this.fetch_(mediaState, reference);
|
|
1276
|
-
|
|
1279
|
+
let result = await fetchSegment;
|
|
1277
1280
|
this.destroyer_.ensureNotDestroyed();
|
|
1278
1281
|
if (this.fatalError_) {
|
|
1279
1282
|
return;
|
|
1280
1283
|
}
|
|
1284
|
+
if (reference.hlsAes128Key) {
|
|
1285
|
+
goog.asserts.assert(iter, 'mediaState.segmentIterator should exist');
|
|
1286
|
+
result = await this.aes128Decrypt_(result, reference, iter);
|
|
1287
|
+
}
|
|
1288
|
+
this.destroyer_.ensureNotDestroyed();
|
|
1281
1289
|
|
|
1282
1290
|
// If the text stream gets switched between fetch_() and append_(), the
|
|
1283
1291
|
// new text parser is initialized, but the new init segment is not
|
|
@@ -1368,6 +1376,34 @@ shaka.media.StreamingEngine = class {
|
|
|
1368
1376
|
}
|
|
1369
1377
|
}
|
|
1370
1378
|
|
|
1379
|
+
/**
|
|
1380
|
+
* @param {!BufferSource} rawResult
|
|
1381
|
+
* @param {!shaka.media.SegmentReference} reference
|
|
1382
|
+
* @param {!shaka.media.SegmentIterator} iter
|
|
1383
|
+
* @return {!Promise.<!BufferSource>} finalResult
|
|
1384
|
+
* @private
|
|
1385
|
+
*/
|
|
1386
|
+
async aes128Decrypt_(rawResult, reference, iter) {
|
|
1387
|
+
const key = reference.hlsAes128Key;
|
|
1388
|
+
if (!key.cryptoKey) {
|
|
1389
|
+
goog.asserts.assert(key.fetchKey, 'If AES-128 cryptoKey was not ' +
|
|
1390
|
+
'preloaded, fetchKey function should be provided');
|
|
1391
|
+
await key.fetchKey();
|
|
1392
|
+
goog.asserts.assert(key.cryptoKey, 'AES-128 cryptoKey should now be set');
|
|
1393
|
+
}
|
|
1394
|
+
let iv = key.iv;
|
|
1395
|
+
if (!iv) {
|
|
1396
|
+
iv = shaka.util.BufferUtils.toUint8(new ArrayBuffer(16));
|
|
1397
|
+
let sequence = key.firstMediaSequenceNumber + iter.currentPosition();
|
|
1398
|
+
for (let i = iv.byteLength - 1; i >= 0; i--) {
|
|
1399
|
+
iv[i] = sequence & 0xff;
|
|
1400
|
+
sequence >>= 8;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return window.crypto.subtle.decrypt(
|
|
1404
|
+
{name: 'AES-CBC', iv}, key.cryptoKey, rawResult);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1371
1407
|
|
|
1372
1408
|
/**
|
|
1373
1409
|
* Clear per-stream error states and retry any failed streams.
|
package/lib/player.js
CHANGED
|
@@ -1948,6 +1948,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
|
|
|
1948
1948
|
if (!this.abrManager_ || this.abrManagerFactory_ != abrFactory) {
|
|
1949
1949
|
this.abrManagerFactory_ = abrFactory;
|
|
1950
1950
|
this.abrManager_ = abrFactory();
|
|
1951
|
+
if (typeof this.abrManager_.setMediaElement != 'function') {
|
|
1952
|
+
shaka.Deprecate.deprecateFeature(5,
|
|
1953
|
+
'AbrManager',
|
|
1954
|
+
'Please use an AbrManager with setMediaElement function.');
|
|
1955
|
+
this.abrManager_.setMediaElement = () => {};
|
|
1956
|
+
}
|
|
1951
1957
|
this.abrManager_.configure(this.config_.abr);
|
|
1952
1958
|
}
|
|
1953
1959
|
|
|
@@ -1970,6 +1976,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
|
|
|
1970
1976
|
this.abrManager_.init((variant, clearBuffer, safeMargin) => {
|
|
1971
1977
|
return this.switch_(variant, clearBuffer, safeMargin);
|
|
1972
1978
|
});
|
|
1979
|
+
this.abrManager_.setMediaElement(mediaElement);
|
|
1973
1980
|
|
|
1974
1981
|
this.playhead_ = this.createPlayhead(has.startTime);
|
|
1975
1982
|
this.playheadObservers_ = this.createPlayheadObserversForMSE_();
|
|
@@ -4452,6 +4459,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
|
|
|
4452
4459
|
this.stats_.setBandwidthEstimate(estimate);
|
|
4453
4460
|
}
|
|
4454
4461
|
|
|
4462
|
+
if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
|
|
4463
|
+
this.stats_.setResolution(
|
|
4464
|
+
/* width= */ element.videoWidth || NaN,
|
|
4465
|
+
/* height= */ element.videoHeight || NaN);
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4455
4468
|
return this.stats_.getBlob();
|
|
4456
4469
|
}
|
|
4457
4470
|
|
|
@@ -5618,6 +5631,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
|
|
|
5618
5631
|
return false;
|
|
5619
5632
|
}
|
|
5620
5633
|
|
|
5634
|
+
if (!navigator.onLine) {
|
|
5635
|
+
// Don't restrict variants if we're completely offline, or else we end up
|
|
5636
|
+
// rapidly restricting all of them.
|
|
5637
|
+
return false;
|
|
5638
|
+
}
|
|
5639
|
+
|
|
5621
5640
|
let maxDisabledTime = this.config_.streaming.maxDisabledTime;
|
|
5622
5641
|
if (maxDisabledTime == 0) {
|
|
5623
5642
|
if (error.code == shaka.util.Error.Code.SEGMENT_MISSING) {
|
|
@@ -6551,7 +6570,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
|
|
|
6551
6570
|
* @define {string} A version number taken from git at compile time.
|
|
6552
6571
|
* @export
|
|
6553
6572
|
*/
|
|
6554
|
-
shaka.Player.version = 'v4.1
|
|
6573
|
+
shaka.Player.version = 'v4.2.1-uncompiled';
|
|
6555
6574
|
|
|
6556
6575
|
// Initialize the deprecation system using the version string we just set
|
|
6557
6576
|
// on the player.
|