shaka-player 4.1.3 → 4.2.0
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/CHANGELOG.md +33 -34
- 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/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 +31 -31
- 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 -4
- package/dist/shaka-player.compiled.debug.d.ts +57 -4
- package/dist/shaka-player.compiled.debug.externs.js +62 -4
- package/dist/shaka-player.compiled.debug.js +285 -241
- package/dist/shaka-player.compiled.debug.map +4 -4
- package/dist/shaka-player.compiled.externs.js +62 -4
- package/dist/shaka-player.compiled.js +815 -798
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.d.ts +57 -4
- package/dist/shaka-player.ui.debug.d.ts +57 -4
- package/dist/shaka-player.ui.debug.externs.js +62 -4
- package/dist/shaka-player.ui.debug.js +283 -239
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +62 -4
- package/dist/shaka-player.ui.js +979 -960
- 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 +117 -25
- 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 +8 -1
- package/lib/polyfill/patchedmediakeys_apple.js +729 -0
- package/lib/text/vtt_text_parser.js +61 -4
- package/lib/util/error.js +21 -1
- package/lib/util/fairplay_utils.js +100 -2
- package/lib/util/platform.js +12 -1
- package/lib/util/player_configuration.js +3 -10
- package/package.json +1 -1
- package/shaka-player.uncompiled.js +1 -0
- package/test/hls/hls_parser_unit.js +49 -33
- package/test/media/streaming_engine_unit.js +79 -5
- 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
|
@@ -412,6 +412,7 @@ shaka.text.VttTextParser = class {
|
|
|
412
412
|
VttTextParser.addDefaultTextColor_(styles);
|
|
413
413
|
}
|
|
414
414
|
payload = VttTextParser.replaceColorPayload_(payload);
|
|
415
|
+
payload = VttTextParser.replaceKaraokeStylePayload_(payload);
|
|
415
416
|
const xmlPayload = '<span>' + payload + '</span>';
|
|
416
417
|
const element = shaka.util.XmlUtils.parseXmlString(xmlPayload, 'span');
|
|
417
418
|
if (element) {
|
|
@@ -437,6 +438,44 @@ shaka.text.VttTextParser = class {
|
|
|
437
438
|
}
|
|
438
439
|
}
|
|
439
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Converts karaoke style tag to be valid for xml parsing
|
|
443
|
+
* For example,
|
|
444
|
+
* input: Text <00:00:00.450> time <00:00:01.450> 1
|
|
445
|
+
* output: Text <div time="00:00:00.450"> time
|
|
446
|
+
* <div time="00:00:01.450"> 1</div></div>
|
|
447
|
+
*
|
|
448
|
+
* @param {string} payload
|
|
449
|
+
* @return {string} processed payload
|
|
450
|
+
* @private
|
|
451
|
+
*/
|
|
452
|
+
static replaceKaraokeStylePayload_(payload) {
|
|
453
|
+
const names = [];
|
|
454
|
+
let nameStart = -1;
|
|
455
|
+
for (let i = 0; i < payload.length; i++) {
|
|
456
|
+
if (payload[i] === '<') {
|
|
457
|
+
nameStart = i + 1;
|
|
458
|
+
} else if (payload[i] === '>') {
|
|
459
|
+
if (nameStart > 0) {
|
|
460
|
+
const name = payload.substr(nameStart, i - nameStart);
|
|
461
|
+
if (name.match(shaka.text.VttTextParser.timeFormat_)) {
|
|
462
|
+
names.push(name);
|
|
463
|
+
}
|
|
464
|
+
nameStart = -1;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
let newPayload = payload;
|
|
469
|
+
for (const name of names) {
|
|
470
|
+
const replaceTag = '<' + name + '>';
|
|
471
|
+
const startTag = '<div time="' + name + '">';
|
|
472
|
+
const endTag = '</div>';
|
|
473
|
+
newPayload = newPayload.replace(replaceTag, startTag);
|
|
474
|
+
newPayload += endTag;
|
|
475
|
+
}
|
|
476
|
+
return newPayload;
|
|
477
|
+
}
|
|
478
|
+
|
|
440
479
|
/**
|
|
441
480
|
* Converts color end tag to be valid for xml parsing
|
|
442
481
|
* For example,
|
|
@@ -560,6 +599,18 @@ shaka.text.VttTextParser = class {
|
|
|
560
599
|
case 'u':
|
|
561
600
|
nestedCue.textDecoration.push(underline);
|
|
562
601
|
break;
|
|
602
|
+
case 'div': {
|
|
603
|
+
const time = /** @type {!Element} */(element).getAttribute('time');
|
|
604
|
+
if (!time) {
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
const parser = new shaka.util.TextParser(time);
|
|
608
|
+
const cueTime = shaka.text.VttTextParser.parseTime_(parser);
|
|
609
|
+
if (cueTime) {
|
|
610
|
+
nestedCue.startTime = cueTime;
|
|
611
|
+
}
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
563
614
|
default:
|
|
564
615
|
break;
|
|
565
616
|
}
|
|
@@ -776,10 +827,7 @@ shaka.text.VttTextParser = class {
|
|
|
776
827
|
* @private
|
|
777
828
|
*/
|
|
778
829
|
static parseTime_(parser) {
|
|
779
|
-
|
|
780
|
-
// 00:00.00 or 00:00:00.00 or 0:00:00.00
|
|
781
|
-
const regexExpresion = /(?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{2,3})/g;
|
|
782
|
-
const results = parser.readRegex(regexExpresion);
|
|
830
|
+
const results = parser.readRegex(shaka.text.VttTextParser.timeFormat_);
|
|
783
831
|
if (results == null) {
|
|
784
832
|
return null;
|
|
785
833
|
}
|
|
@@ -810,6 +858,15 @@ shaka.text.VttTextParser.MPEG_TIMESCALE_ = 90000;
|
|
|
810
858
|
*/
|
|
811
859
|
shaka.text.VttTextParser.TS_ROLLOVER_ = 0x200000000;
|
|
812
860
|
|
|
861
|
+
/**
|
|
862
|
+
* @const
|
|
863
|
+
* @private {!RegExp}
|
|
864
|
+
* @example 00:00.000 or 00:00:00.000 or 0:00:00.000 or
|
|
865
|
+
* 00:00.00 or 00:00:00.00 or 0:00:00.00
|
|
866
|
+
*/
|
|
867
|
+
shaka.text.VttTextParser.timeFormat_ =
|
|
868
|
+
/(?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{2,3})/g;
|
|
869
|
+
|
|
813
870
|
shaka.text.TextEngine.registerParser(
|
|
814
871
|
'text/vtt', () => new shaka.text.VttTextParser());
|
|
815
872
|
|
package/lib/util/error.js
CHANGED
|
@@ -693,7 +693,27 @@ shaka.util.Error.Code = {
|
|
|
693
693
|
*/
|
|
694
694
|
'HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED': 4040,
|
|
695
695
|
|
|
696
|
-
|
|
696
|
+
/**
|
|
697
|
+
* We do not support playing encrypted content (different than mp2t) with MSE
|
|
698
|
+
* and legacy Apple MediaKeys API.
|
|
699
|
+
*/
|
|
700
|
+
'HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED': 4041,
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Web Crypto API is not available (to decrypt AES-128 streams). Web Crypto
|
|
704
|
+
* only exists in secure origins like https.
|
|
705
|
+
*/
|
|
706
|
+
'NO_WEB_CRYPTO_API': 4042,
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* AES-128 iv length should be 16 bytes.
|
|
710
|
+
*/
|
|
711
|
+
'HLS_AES_128_INVALID_IV_LENGTH': 4043,
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* AES-128 encryption key length should be 16 bytes.
|
|
715
|
+
*/
|
|
716
|
+
'HLS_AES_128_INVALID_KEY_LENGTH': 4044,
|
|
697
717
|
|
|
698
718
|
|
|
699
719
|
// RETIRED: 'INCONSISTENT_BUFFER_STATE': 5000,
|
|
@@ -133,13 +133,83 @@ shaka.util.FairPlayUtils = class {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
|
-
*
|
|
136
|
+
* Verimatrix initDataTransform configuration.
|
|
137
|
+
*
|
|
138
|
+
* @param {!Uint8Array} initData
|
|
139
|
+
* @param {string} initDataType
|
|
140
|
+
* @param {?shaka.extern.DrmInfo} drmInfo
|
|
141
|
+
* @export
|
|
142
|
+
*/
|
|
143
|
+
static verimatrixInitDataTransform(initData, initDataType, drmInfo) {
|
|
144
|
+
if (initDataType !== 'skd') {
|
|
145
|
+
return initData;
|
|
146
|
+
}
|
|
147
|
+
const StringUtils = shaka.util.StringUtils;
|
|
148
|
+
const FairPlayUtils = shaka.util.FairPlayUtils;
|
|
149
|
+
const cert = drmInfo.serverCertificate;
|
|
150
|
+
const initDataAsString = StringUtils.fromBytesAutoDetect(initData);
|
|
151
|
+
const contentId = initDataAsString.split('skd://').pop();
|
|
152
|
+
return FairPlayUtils.initDataTransform(initData, contentId, cert);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* EZDRM initDataTransform configuration.
|
|
157
|
+
*
|
|
158
|
+
* @param {!Uint8Array} initData
|
|
159
|
+
* @param {string} initDataType
|
|
160
|
+
* @param {?shaka.extern.DrmInfo} drmInfo
|
|
161
|
+
* @export
|
|
162
|
+
*/
|
|
163
|
+
static ezdrmInitDataTransform(initData, initDataType, drmInfo) {
|
|
164
|
+
if (initDataType !== 'skd') {
|
|
165
|
+
return initData;
|
|
166
|
+
}
|
|
167
|
+
const StringUtils = shaka.util.StringUtils;
|
|
168
|
+
const FairPlayUtils = shaka.util.FairPlayUtils;
|
|
169
|
+
const cert = drmInfo.serverCertificate;
|
|
170
|
+
const initDataAsString = StringUtils.fromBytesAutoDetect(initData);
|
|
171
|
+
const contentId = initDataAsString.split(';').pop();
|
|
172
|
+
return FairPlayUtils.initDataTransform(initData, contentId, cert);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Conax initDataTransform configuration.
|
|
177
|
+
*
|
|
178
|
+
* @param {!Uint8Array} initData
|
|
179
|
+
* @param {string} initDataType
|
|
180
|
+
* @param {?shaka.extern.DrmInfo} drmInfo
|
|
181
|
+
* @export
|
|
182
|
+
*/
|
|
183
|
+
static conaxInitDataTransform(initData, initDataType, drmInfo) {
|
|
184
|
+
if (initDataType !== 'skd') {
|
|
185
|
+
return initData;
|
|
186
|
+
}
|
|
187
|
+
const StringUtils = shaka.util.StringUtils;
|
|
188
|
+
const FairPlayUtils = shaka.util.FairPlayUtils;
|
|
189
|
+
const cert = drmInfo.serverCertificate;
|
|
190
|
+
const initDataAsString = StringUtils.fromBytesAutoDetect(initData);
|
|
191
|
+
const skdValue = initDataAsString.split('skd://').pop().split('?').shift();
|
|
192
|
+
const stringToArray = (string) => {
|
|
193
|
+
// 2 bytes for each char
|
|
194
|
+
const buffer = new ArrayBuffer(string.length * 2);
|
|
195
|
+
const array = new Uint16Array(buffer);
|
|
196
|
+
for (let i = 0, strLen = string.length; i < strLen; i++) {
|
|
197
|
+
array[i] = string.charCodeAt(i);
|
|
198
|
+
}
|
|
199
|
+
return array;
|
|
200
|
+
};
|
|
201
|
+
const contentId = stringToArray(window.atob(skdValue));
|
|
202
|
+
return FairPlayUtils.initDataTransform(initData, contentId, cert);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Verimatrix FairPlay request.
|
|
137
207
|
*
|
|
138
208
|
* @param {shaka.net.NetworkingEngine.RequestType} type
|
|
139
209
|
* @param {shaka.extern.Request} request
|
|
140
210
|
* @export
|
|
141
211
|
*/
|
|
142
|
-
static
|
|
212
|
+
static verimatrixFairPlayRequest(type, request) {
|
|
143
213
|
if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
|
|
144
214
|
return;
|
|
145
215
|
}
|
|
@@ -150,6 +220,34 @@ shaka.util.FairPlayUtils = class {
|
|
|
150
220
|
request.body = shaka.util.StringUtils.toUTF8('spc=' + base64Payload);
|
|
151
221
|
}
|
|
152
222
|
|
|
223
|
+
/**
|
|
224
|
+
* EZDRM FairPlay request.
|
|
225
|
+
*
|
|
226
|
+
* @param {shaka.net.NetworkingEngine.RequestType} type
|
|
227
|
+
* @param {shaka.extern.Request} request
|
|
228
|
+
* @export
|
|
229
|
+
*/
|
|
230
|
+
static ezdrmFairPlayRequest(type, request) {
|
|
231
|
+
if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
request.headers['Content-Type'] = 'application/octet-stream';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Conax FairPlay request.
|
|
239
|
+
*
|
|
240
|
+
* @param {shaka.net.NetworkingEngine.RequestType} type
|
|
241
|
+
* @param {shaka.extern.Request} request
|
|
242
|
+
* @export
|
|
243
|
+
*/
|
|
244
|
+
static conaxFairPlayRequest(type, request) {
|
|
245
|
+
if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
request.headers['Content-Type'] = 'application/octet-stream';
|
|
249
|
+
}
|
|
250
|
+
|
|
153
251
|
/**
|
|
154
252
|
* Common FairPlay response transform for some DRMs providers.
|
|
155
253
|
*
|
package/lib/util/platform.js
CHANGED
|
@@ -199,7 +199,8 @@ shaka.util.Platform = class {
|
|
|
199
199
|
return !!navigator.vendor && navigator.vendor.includes('Apple') &&
|
|
200
200
|
!shaka.util.Platform.isTizen() &&
|
|
201
201
|
!shaka.util.Platform.isEOS() &&
|
|
202
|
-
!shaka.util.Platform.isPS4()
|
|
202
|
+
!shaka.util.Platform.isPS4() &&
|
|
203
|
+
!shaka.util.Platform.isAmazonFireTV();
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
/**
|
|
@@ -222,6 +223,16 @@ shaka.util.Platform = class {
|
|
|
222
223
|
return shaka.util.Platform.userAgentContains_('PlayStation 4');
|
|
223
224
|
}
|
|
224
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Check if the current platform is Amazon Fire TV.
|
|
228
|
+
* https://developer.amazon.com/docs/fire-tv/identify-amazon-fire-tv-devices.html
|
|
229
|
+
*
|
|
230
|
+
* @return {boolean}
|
|
231
|
+
*/
|
|
232
|
+
static isAmazonFireTV() {
|
|
233
|
+
return shaka.util.Platform.userAgentContains_('AFT');
|
|
234
|
+
}
|
|
235
|
+
|
|
225
236
|
/**
|
|
226
237
|
* Returns a major version number for Safari, or Safari-based iOS browsers.
|
|
227
238
|
*
|
|
@@ -147,7 +147,7 @@ shaka.util.PlayerConfiguration = class {
|
|
|
147
147
|
ignoreTextStreamFailures: false,
|
|
148
148
|
alwaysStreamText: false,
|
|
149
149
|
startAtSegmentBoundary: false,
|
|
150
|
-
gapDetectionThreshold: 0.
|
|
150
|
+
gapDetectionThreshold: 0.5,
|
|
151
151
|
durationBackoff: 1,
|
|
152
152
|
forceTransmuxTS: false,
|
|
153
153
|
// Offset by 5 seconds since Chromecast takes a few seconds to start
|
|
@@ -173,15 +173,6 @@ shaka.util.PlayerConfiguration = class {
|
|
|
173
173
|
maxDisabledTime: 30,
|
|
174
174
|
};
|
|
175
175
|
|
|
176
|
-
// Some browsers will stop earlier than others before a gap (e.g., Edge
|
|
177
|
-
// stops 0.5 seconds before a gap). So for some browsers we need to use a
|
|
178
|
-
// larger threshold. See: https://bit.ly/2K5xmJO
|
|
179
|
-
if (shaka.util.Platform.isLegacyEdge() ||
|
|
180
|
-
shaka.util.Platform.isTizen() ||
|
|
181
|
-
shaka.util.Platform.isChromecast()) {
|
|
182
|
-
streaming.gapDetectionThreshold = 0.5;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
176
|
// WebOS, Tizen, and Chromecast have long hardware pipelines that respond
|
|
186
177
|
// slowly to seeking. Therefore we should not seek when we detect a stall
|
|
187
178
|
// on one of these platforms. Instead, default stallSkip to 0 to force the
|
|
@@ -253,6 +244,8 @@ shaka.util.PlayerConfiguration = class {
|
|
|
253
244
|
fastHalfLife: 2,
|
|
254
245
|
slowHalfLife: 5,
|
|
255
246
|
},
|
|
247
|
+
restrictToElementSize: false,
|
|
248
|
+
ignoreDevicePixelRatio: false,
|
|
256
249
|
};
|
|
257
250
|
|
|
258
251
|
const cmcd = {
|
package/package.json
CHANGED
|
@@ -38,6 +38,7 @@ goog.require('shaka.polyfill.Fullscreen');
|
|
|
38
38
|
goog.require('shaka.polyfill.MediaSource');
|
|
39
39
|
goog.require('shaka.polyfill.MediaCapabilities');
|
|
40
40
|
goog.require('shaka.polyfill.Orientation');
|
|
41
|
+
goog.require('shaka.polyfill.PatchedMediaKeysApple');
|
|
41
42
|
goog.require('shaka.polyfill.PatchedMediaKeysNop');
|
|
42
43
|
goog.require('shaka.polyfill.PatchedMediaKeysWebkit');
|
|
43
44
|
goog.require('shaka.polyfill.PiPWebkit');
|
|
@@ -33,6 +33,8 @@ describe('HlsParser', () => {
|
|
|
33
33
|
let segmentData;
|
|
34
34
|
/** @type {!Uint8Array} */
|
|
35
35
|
let selfInitializingSegmentData;
|
|
36
|
+
/** @type {!Uint8Array} */
|
|
37
|
+
let aes128Key;
|
|
36
38
|
|
|
37
39
|
afterEach(() => {
|
|
38
40
|
shaka.log.alwaysWarn = originalAlwaysWarn;
|
|
@@ -75,6 +77,11 @@ describe('HlsParser', () => {
|
|
|
75
77
|
selfInitializingSegmentData =
|
|
76
78
|
shaka.util.Uint8ArrayUtils.concat(initSegmentData, segmentData);
|
|
77
79
|
|
|
80
|
+
aes128Key = new Uint8Array([
|
|
81
|
+
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
|
82
|
+
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
|
|
83
|
+
]);
|
|
84
|
+
|
|
78
85
|
fakeNetEngine = new shaka.test.FakeNetworkingEngine();
|
|
79
86
|
|
|
80
87
|
config = shaka.util.PlayerConfiguration.createDefault().manifest;
|
|
@@ -2432,7 +2439,7 @@ describe('HlsParser', () => {
|
|
|
2432
2439
|
expect(initSegments[1].getUris()[0]).toBe('test:/init2.mp4');
|
|
2433
2440
|
});
|
|
2434
2441
|
|
|
2435
|
-
it('
|
|
2442
|
+
it('parses variants encrypted with AES-128', async () => {
|
|
2436
2443
|
const master = [
|
|
2437
2444
|
'#EXTM3U\n',
|
|
2438
2445
|
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
|
|
@@ -2441,10 +2448,15 @@ describe('HlsParser', () => {
|
|
|
2441
2448
|
'#EXT-X-STREAM-INF:BANDWIDTH=300,CODECS="avc1,mp4a",',
|
|
2442
2449
|
'RESOLUTION=960x540,FRAME-RATE=120,AUDIO="aud2"\n',
|
|
2443
2450
|
'video2\n',
|
|
2451
|
+
'#EXT-X-STREAM-INF:BANDWIDTH=300,CODECS="avc1,mp4a",',
|
|
2452
|
+
'RESOLUTION=960x540,FRAME-RATE=120,AUDIO="aud3"\n',
|
|
2453
|
+
'video3\n',
|
|
2444
2454
|
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
|
|
2445
2455
|
'URI="audio"\n',
|
|
2446
2456
|
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",LANGUAGE="fr",',
|
|
2447
2457
|
'URI="audio2"\n',
|
|
2458
|
+
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud3",LANGUAGE="de",',
|
|
2459
|
+
'URI="audio3"\n',
|
|
2448
2460
|
].join('');
|
|
2449
2461
|
|
|
2450
2462
|
const media = [
|
|
@@ -2456,17 +2468,26 @@ describe('HlsParser', () => {
|
|
|
2456
2468
|
'main.mp4',
|
|
2457
2469
|
].join('');
|
|
2458
2470
|
|
|
2459
|
-
const
|
|
2471
|
+
const mediaWithMp4AesEncryption = [
|
|
2460
2472
|
'#EXTM3U\n',
|
|
2461
2473
|
'#EXT-X-PLAYLIST-TYPE:VOD\n',
|
|
2462
2474
|
'#EXT-X-KEY:METHOD=AES-128,',
|
|
2463
|
-
'URI="800k.key\n',
|
|
2475
|
+
'URI="800k.key"\n',
|
|
2464
2476
|
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
|
|
2465
2477
|
'#EXTINF:5,\n',
|
|
2466
2478
|
'#EXT-X-BYTERANGE:121090@616\n',
|
|
2467
2479
|
'main.mp4',
|
|
2468
2480
|
].join('');
|
|
2469
2481
|
|
|
2482
|
+
const mediaWithTSAesEncryption = [
|
|
2483
|
+
'#EXTM3U\n',
|
|
2484
|
+
'#EXT-X-PLAYLIST-TYPE:VOD\n',
|
|
2485
|
+
'#EXT-X-KEY:METHOD=AES-128,',
|
|
2486
|
+
'URI="800k.key"\n',
|
|
2487
|
+
'#EXTINF:5,\n',
|
|
2488
|
+
'main.ts',
|
|
2489
|
+
].join('');
|
|
2490
|
+
|
|
2470
2491
|
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
|
|
2471
2492
|
manifest.anyTimeline();
|
|
2472
2493
|
manifest.addPartialVariant((variant) => {
|
|
@@ -2478,6 +2499,24 @@ describe('HlsParser', () => {
|
|
|
2478
2499
|
stream.language = 'en';
|
|
2479
2500
|
});
|
|
2480
2501
|
});
|
|
2502
|
+
manifest.addPartialVariant((variant) => {
|
|
2503
|
+
variant.bandwidth = 300;
|
|
2504
|
+
variant.addPartialStream(ContentType.VIDEO, (stream) => {
|
|
2505
|
+
stream.size(960, 540);
|
|
2506
|
+
});
|
|
2507
|
+
variant.addPartialStream(ContentType.AUDIO, (stream) => {
|
|
2508
|
+
stream.language = 'fr';
|
|
2509
|
+
});
|
|
2510
|
+
});
|
|
2511
|
+
manifest.addPartialVariant((variant) => {
|
|
2512
|
+
variant.bandwidth = 300;
|
|
2513
|
+
variant.addPartialStream(ContentType.VIDEO, (stream) => {
|
|
2514
|
+
stream.size(960, 540);
|
|
2515
|
+
});
|
|
2516
|
+
variant.addPartialStream(ContentType.AUDIO, (stream) => {
|
|
2517
|
+
stream.language = 'de';
|
|
2518
|
+
});
|
|
2519
|
+
});
|
|
2481
2520
|
manifest.sequenceMode = true;
|
|
2482
2521
|
});
|
|
2483
2522
|
|
|
@@ -2485,12 +2524,15 @@ describe('HlsParser', () => {
|
|
|
2485
2524
|
.setResponseText('test:/master', master)
|
|
2486
2525
|
.setResponseText('test:/audio', media)
|
|
2487
2526
|
.setResponseText('test:/audio2', media)
|
|
2527
|
+
.setResponseText('test:/audio3', media)
|
|
2488
2528
|
.setResponseText('test:/video', media)
|
|
2489
|
-
.setResponseText('test:/video2',
|
|
2529
|
+
.setResponseText('test:/video2', mediaWithMp4AesEncryption)
|
|
2530
|
+
.setResponseText('test:/video3', mediaWithTSAesEncryption)
|
|
2490
2531
|
.setResponseText('test:/main.vtt', vttText)
|
|
2491
2532
|
.setResponseValue('test:/init.mp4', initSegmentData)
|
|
2492
2533
|
.setResponseValue('test:/main.mp4', segmentData)
|
|
2493
2534
|
.setResponseValue('test:/main.test', segmentData)
|
|
2535
|
+
.setResponseValue('test:/800k.key', aes128Key)
|
|
2494
2536
|
.setResponseValue('test:/selfInit.mp4', selfInitializingSegmentData);
|
|
2495
2537
|
|
|
2496
2538
|
const actual = await parser.start('test:/master', playerInterface);
|
|
@@ -2674,7 +2716,9 @@ describe('HlsParser', () => {
|
|
|
2674
2716
|
.setResponseText('test:/video', media)
|
|
2675
2717
|
.setResponseValue('test:/main.exe', segmentData)
|
|
2676
2718
|
.setResponseValue('test:/init.mp4', initSegmentData)
|
|
2677
|
-
.setResponseValue('test:/main.mp4', segmentData)
|
|
2719
|
+
.setResponseValue('test:/main.mp4', segmentData)
|
|
2720
|
+
.setResponseValue('data:text/plain;base64,AAECAwQFBgcICQoLDA0ODw==',
|
|
2721
|
+
aes128Key);
|
|
2678
2722
|
|
|
2679
2723
|
await expectAsync(parser.start('test:/master', playerInterface))
|
|
2680
2724
|
.toBeRejectedWith(Util.jasmineError(error));
|
|
@@ -2708,34 +2752,6 @@ describe('HlsParser', () => {
|
|
|
2708
2752
|
await verifyError(master, media, error);
|
|
2709
2753
|
});
|
|
2710
2754
|
|
|
2711
|
-
it('if all variants are encrypted with AES-128', async () => {
|
|
2712
|
-
const master = [
|
|
2713
|
-
'#EXTM3U\n',
|
|
2714
|
-
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
|
|
2715
|
-
'RESOLUTION=960x540,FRAME-RATE=60\n',
|
|
2716
|
-
'video\n',
|
|
2717
|
-
].join('');
|
|
2718
|
-
|
|
2719
|
-
const media = [
|
|
2720
|
-
'#EXTM3U\n',
|
|
2721
|
-
'#EXT-X-TARGETDURATION:6\n',
|
|
2722
|
-
'#EXT-X-PLAYLIST-TYPE:VOD\n',
|
|
2723
|
-
'#EXT-X-KEY:METHOD=AES-128,',
|
|
2724
|
-
'URI="data:text/plain;base64\n',
|
|
2725
|
-
'#EXT-X-MAP:URI="init.mp4"\n',
|
|
2726
|
-
'#EXTINF:5,\n',
|
|
2727
|
-
'#EXT-X-BYTERANGE:121090@616\n',
|
|
2728
|
-
'main.mp4',
|
|
2729
|
-
].join('');
|
|
2730
|
-
|
|
2731
|
-
const error = new shaka.util.Error(
|
|
2732
|
-
shaka.util.Error.Severity.CRITICAL,
|
|
2733
|
-
shaka.util.Error.Category.MANIFEST,
|
|
2734
|
-
Code.HLS_AES_128_ENCRYPTION_NOT_SUPPORTED);
|
|
2735
|
-
|
|
2736
|
-
await verifyError(master, media, error);
|
|
2737
|
-
});
|
|
2738
|
-
|
|
2739
2755
|
describe('if required attributes are missing', () => {
|
|
2740
2756
|
/**
|
|
2741
2757
|
* @param {string} master
|
|
@@ -100,10 +100,13 @@ describe('StreamingEngine', () => {
|
|
|
100
100
|
jasmine.clock().mockDate();
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* @param {boolean=} trickMode
|
|
104
105
|
* @param {number=} mediaOffset The offset from 0 for the segment start times
|
|
106
|
+
* @param {shaka.extern.HlsAes128Key=} hlsAes128Key The AES-128 key to put in
|
|
107
|
+
* the manifest, if one should exist
|
|
105
108
|
*/
|
|
106
|
-
function setupVod(trickMode, mediaOffset) {
|
|
109
|
+
function setupVod(trickMode, mediaOffset, hlsAes128Key) {
|
|
107
110
|
// For VOD, we fake a presentation that has 2 Periods of equal duration
|
|
108
111
|
// (20 seconds), where each Period has 1 Variant and 1 text stream.
|
|
109
112
|
//
|
|
@@ -209,7 +212,8 @@ describe('StreamingEngine', () => {
|
|
|
209
212
|
setupManifest(
|
|
210
213
|
/* firstPeriodStartTime= */ 0,
|
|
211
214
|
/* secondPeriodStartTime= */ 20,
|
|
212
|
-
/* presentationDuration= */ 40
|
|
215
|
+
/* presentationDuration= */ 40,
|
|
216
|
+
hlsAes128Key);
|
|
213
217
|
}
|
|
214
218
|
|
|
215
219
|
function setupLive() {
|
|
@@ -356,8 +360,15 @@ describe('StreamingEngine', () => {
|
|
|
356
360
|
/* delays= */ netEngineDelays);
|
|
357
361
|
}
|
|
358
362
|
|
|
363
|
+
/**
|
|
364
|
+
* @param {number} firstPeriodStartTime
|
|
365
|
+
* @param {number} secondPeriodStartTime
|
|
366
|
+
* @param {number} presentationDuration
|
|
367
|
+
* @param {shaka.extern.HlsAes128Key=} hlsAes128Key
|
|
368
|
+
*/
|
|
359
369
|
function setupManifest(
|
|
360
|
-
firstPeriodStartTime, secondPeriodStartTime, presentationDuration
|
|
370
|
+
firstPeriodStartTime, secondPeriodStartTime, presentationDuration,
|
|
371
|
+
hlsAes128Key) {
|
|
361
372
|
const segmentDurations = {
|
|
362
373
|
audio: segmentData[ContentType.AUDIO].segmentDuration,
|
|
363
374
|
video: segmentData[ContentType.VIDEO].segmentDuration,
|
|
@@ -380,7 +391,7 @@ describe('StreamingEngine', () => {
|
|
|
380
391
|
/** @type {!shaka.media.PresentationTimeline} */(timeline),
|
|
381
392
|
[firstPeriodStartTime, secondPeriodStartTime],
|
|
382
393
|
presentationDuration, segmentDurations, initSegmentRanges,
|
|
383
|
-
timestampOffsets);
|
|
394
|
+
timestampOffsets, hlsAes128Key);
|
|
384
395
|
|
|
385
396
|
audioStream = manifest.variants[0].audio;
|
|
386
397
|
videoStream = manifest.variants[0].video;
|
|
@@ -3430,6 +3441,69 @@ describe('StreamingEngine', () => {
|
|
|
3430
3441
|
expect(alternateVideoStream.createSegmentIndex).not.toHaveBeenCalled();
|
|
3431
3442
|
});
|
|
3432
3443
|
|
|
3444
|
+
describe('AES-128', () => {
|
|
3445
|
+
let key;
|
|
3446
|
+
/** @type {!shaka.extern.HlsAes128Key} */
|
|
3447
|
+
let hlsAes128Key;
|
|
3448
|
+
|
|
3449
|
+
beforeEach(async () => {
|
|
3450
|
+
// Get a key.
|
|
3451
|
+
const keyData = new ArrayBuffer(16);
|
|
3452
|
+
const keyDataView = new DataView(keyData);
|
|
3453
|
+
keyDataView.setInt16(0, 31710); // 0111 1011 1101 1110
|
|
3454
|
+
key = await window.crypto.subtle.importKey(
|
|
3455
|
+
'raw', keyData, 'AES-CBC', true, ['decrypt']);
|
|
3456
|
+
|
|
3457
|
+
// Set up a manifest with AES-128 key info.
|
|
3458
|
+
// We don't actually provide the imported key OR the key fetching function
|
|
3459
|
+
// here, though, so that the individual tests can choose what the starting
|
|
3460
|
+
// state of the hlsAes128Key object is.
|
|
3461
|
+
hlsAes128Key = {method: 'AES-128', firstMediaSequenceNumber: 0};
|
|
3462
|
+
|
|
3463
|
+
setupVod(false, 0, hlsAes128Key);
|
|
3464
|
+
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
3465
|
+
presentationTimeInSeconds = 0;
|
|
3466
|
+
createStreamingEngine();
|
|
3467
|
+
streamingEngine.switchVariant(variant);
|
|
3468
|
+
});
|
|
3469
|
+
|
|
3470
|
+
afterEach(async () => {
|
|
3471
|
+
await streamingEngine.destroy();
|
|
3472
|
+
});
|
|
3473
|
+
|
|
3474
|
+
async function runTest() {
|
|
3475
|
+
spyOn(window.crypto.subtle, 'decrypt').and.callThrough();
|
|
3476
|
+
|
|
3477
|
+
await streamingEngine.start();
|
|
3478
|
+
playing = true;
|
|
3479
|
+
await Util.fakeEventLoop(10);
|
|
3480
|
+
|
|
3481
|
+
expect(mediaSourceEngine.appendBuffer).toHaveBeenCalledTimes(2);
|
|
3482
|
+
expect(window.crypto.subtle.decrypt).toHaveBeenCalledTimes(2);
|
|
3483
|
+
expect(window.crypto.subtle.decrypt).toHaveBeenCalledWith(
|
|
3484
|
+
{name: 'AES-CBC', iv: jasmine.any(Object)}, key, jasmine.any(Object));
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
it('decrypts segments', async () => {
|
|
3488
|
+
hlsAes128Key.cryptoKey = key;
|
|
3489
|
+
await runTest();
|
|
3490
|
+
});
|
|
3491
|
+
|
|
3492
|
+
it('downloads key if not pre-filled', async () => {
|
|
3493
|
+
hlsAes128Key.fetchKey = () => {
|
|
3494
|
+
hlsAes128Key.cryptoKey = key;
|
|
3495
|
+
hlsAes128Key.fetchKey = undefined;
|
|
3496
|
+
return Promise.resolve();
|
|
3497
|
+
};
|
|
3498
|
+
|
|
3499
|
+
await runTest();
|
|
3500
|
+
|
|
3501
|
+
// The key should have been fetched.
|
|
3502
|
+
expect(hlsAes128Key.cryptoKey).not.toBeUndefined();
|
|
3503
|
+
expect(hlsAes128Key.fetchKey).toBeUndefined();
|
|
3504
|
+
});
|
|
3505
|
+
});
|
|
3506
|
+
|
|
3433
3507
|
describe('destroy', () => {
|
|
3434
3508
|
it('aborts pending network operations', async () => {
|
|
3435
3509
|
setupVod();
|
|
@@ -481,6 +481,10 @@ shaka.test.FakeSegmentIndex = class {
|
|
|
481
481
|
return this.get(nextPosition - 1);
|
|
482
482
|
},
|
|
483
483
|
|
|
484
|
+
currentPosition: () => {
|
|
485
|
+
return nextPosition;
|
|
486
|
+
},
|
|
487
|
+
|
|
484
488
|
seek: (time) => {
|
|
485
489
|
nextPosition = this.find(time);
|
|
486
490
|
return this.get(nextPosition++);
|
|
@@ -149,11 +149,13 @@ shaka.test.StreamingEngineUtil = class {
|
|
|
149
149
|
* ranges for each type of init segment.
|
|
150
150
|
* @param {!Object.<string,number>=} timestampOffsets The timestamp offset
|
|
151
151
|
* for each type of segment
|
|
152
|
+
* @param {shaka.extern.HlsAes128Key=} hlsAes128Key The AES-128 key to provide
|
|
153
|
+
* to streams, if desired.
|
|
152
154
|
* @return {shaka.extern.Manifest}
|
|
153
155
|
*/
|
|
154
156
|
static createManifest(
|
|
155
157
|
presentationTimeline, periodStartTimes, presentationDuration,
|
|
156
|
-
segmentDurations, initSegmentRanges, timestampOffsets) {
|
|
158
|
+
segmentDurations, initSegmentRanges, timestampOffsets, hlsAes128Key) {
|
|
157
159
|
const Util = shaka.test.Util;
|
|
158
160
|
|
|
159
161
|
/**
|
|
@@ -255,7 +257,7 @@ shaka.test.StreamingEngineUtil = class {
|
|
|
255
257
|
const appendWindowEnd = periodIndex == periodStartTimes.length - 1?
|
|
256
258
|
presentationDuration : periodStartTimes[periodIndex + 1];
|
|
257
259
|
|
|
258
|
-
|
|
260
|
+
const ref = new shaka.media.SegmentReference(
|
|
259
261
|
/* startTime= */ periodStart + positionWithinPeriod * d,
|
|
260
262
|
/* endTime= */ periodStart + (positionWithinPeriod + 1) * d,
|
|
261
263
|
getUris,
|
|
@@ -265,6 +267,12 @@ shaka.test.StreamingEngineUtil = class {
|
|
|
265
267
|
timestampOffset,
|
|
266
268
|
appendWindowStart,
|
|
267
269
|
appendWindowEnd);
|
|
270
|
+
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
271
|
+
if (hlsAes128Key &&
|
|
272
|
+
(type == ContentType.AUDIO || type == ContentType.VIDEO)) {
|
|
273
|
+
ref.hlsAes128Key = hlsAes128Key;
|
|
274
|
+
}
|
|
275
|
+
return ref;
|
|
268
276
|
};
|
|
269
277
|
|
|
270
278
|
/** @type {shaka.extern.Manifest} */
|