shaka-player 4.2.4 → 4.3.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/.github/workflows/sync-labels.yaml +1 -1
- package/.github/workflows/update-issues.yaml +1 -1
- package/AUTHORS +4 -0
- package/CHANGELOG.md +85 -54
- package/CONTRIBUTORS +4 -0
- package/README.md +14 -0
- package/demo/common/assets.js +58 -0
- package/demo/common/message_ids.js +22 -1
- package/demo/config.js +89 -5
- package/demo/demo.less +7 -1
- package/demo/index.html +8 -1
- package/demo/locales/en.json +21 -1
- package/demo/locales/source.json +82 -2
- package/demo/main.js +57 -1
- package/demo/search.js +2 -0
- package/demo/visualizer.js +337 -0
- package/demo/visualizer_button.js +103 -0
- package/dist/controls.css +1 -1
- package/dist/controls.css.map +1 -1
- package/dist/demo.compiled.debug.js +83 -58
- package/dist/demo.compiled.debug.map +4 -4
- package/dist/demo.compiled.js +83 -58
- package/dist/demo.compiled.map +4 -4
- package/dist/demo.css +1 -1
- package/dist/demo.css.map +1 -1
- package/dist/deps.js +16 -9
- package/dist/locales.js +32 -32
- package/dist/receiver.compiled.debug.js +23 -19
- package/dist/receiver.compiled.debug.map +3 -3
- package/dist/receiver.compiled.js +23 -19
- package/dist/receiver.compiled.map +3 -3
- package/dist/shaka-player.compiled.d.ts +173 -26
- package/dist/shaka-player.compiled.debug.d.ts +173 -26
- package/dist/shaka-player.compiled.debug.externs.js +161 -31
- package/dist/shaka-player.compiled.debug.js +464 -401
- package/dist/shaka-player.compiled.debug.map +4 -4
- package/dist/shaka-player.compiled.externs.js +161 -31
- package/dist/shaka-player.compiled.js +897 -862
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.d.ts +173 -26
- package/dist/shaka-player.ui.debug.d.ts +173 -26
- package/dist/shaka-player.ui.debug.externs.js +161 -31
- package/dist/shaka-player.ui.debug.js +488 -422
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +161 -31
- package/dist/shaka-player.ui.js +1053 -1018
- package/dist/shaka-player.ui.map +4 -4
- package/docs/design/lcevc-architecture.png +0 -0
- package/docs/design/lcevc-demo.png +0 -0
- package/docs/design/lcevc-integration.md +72 -0
- package/docs/tutorials/fairplay.md +7 -0
- package/docs/tutorials/faq.md +1 -1
- package/docs/tutorials/ui.md +7 -7
- package/docs/tutorials/upgrade.md +6 -0
- package/externs/ima.js +13 -1
- package/externs/lcevc.js +81 -0
- package/externs/mux.js +1 -42
- package/externs/shaka/ads.js +11 -1
- package/externs/shaka/manifest_parser.js +5 -1
- package/externs/shaka/player.js +178 -15
- package/lib/abr/simple_abr_manager.js +7 -0
- package/lib/ads/ad_manager.js +0 -8
- package/lib/ads/client_side_ad.js +15 -0
- package/lib/ads/client_side_ad_manager.js +21 -8
- package/lib/ads/server_side_ad.js +15 -0
- package/lib/ads/server_side_ad_manager.js +3 -3
- package/lib/cast/cast_utils.js +2 -0
- package/lib/cea/dummy_cea_parser.js +28 -0
- package/lib/cea/i_cea_parser.js +14 -2
- package/lib/cea/mp4_cea_parser.js +87 -5
- package/lib/config/auto_show_text.js +33 -0
- package/lib/dash/dash_parser.js +1 -1
- package/lib/hls/hls_parser.js +790 -374
- package/lib/lcevc/lcevc_dil.js +152 -0
- package/lib/media/closed_caption_parser.js +9 -6
- package/lib/media/drm_engine.js +14 -6
- package/lib/media/media_source_engine.js +106 -26
- package/lib/media/presentation_timeline.js +26 -11
- package/lib/media/segment_reference.js +49 -1
- package/lib/media/streaming_engine.js +142 -12
- package/lib/media/transmuxer.js +73 -18
- package/lib/offline/storage.js +1 -0
- package/lib/player.js +454 -75
- package/lib/polyfill/patchedmediakeys_apple.js +55 -1
- package/lib/text/ui_text_displayer.js +5 -5
- package/lib/text/vtt_text_parser.js +35 -1
- package/lib/util/dom_utils.js +0 -1
- package/lib/util/error.js +17 -0
- package/lib/util/fake_event.js +2 -0
- package/lib/util/id3_utils.js +300 -0
- package/lib/util/mime_utils.js +3 -6
- package/lib/util/mp4_box_parsers.js +15 -5
- package/lib/util/platform.js +9 -1
- package/lib/util/player_configuration.js +23 -1
- package/lib/util/ts_parser.js +499 -0
- package/maintained-branches.md +2 -2
- package/package.json +4 -1
- package/roadmap.md +5 -3
- package/test/ads/ad_manager_unit.js +180 -29
- package/test/cast/cast_utils_unit.js +3 -1
- package/test/dash/dash_parser_content_protection_unit.js +1 -0
- package/test/dash/dash_parser_live_unit.js +1 -0
- package/test/dash/dash_parser_manifest_unit.js +93 -1
- package/test/dash/dash_parser_segment_base_unit.js +1 -0
- package/test/dash/dash_parser_segment_list_unit.js +1 -0
- package/test/dash/dash_parser_segment_template_unit.js +1 -0
- package/test/demo/demo_unit.js +2 -1
- package/test/hls/hls_live_unit.js +253 -19
- package/test/hls/hls_parser_unit.js +500 -12
- package/test/media/closed_caption_parser_unit.js +2 -1
- package/test/media/drm_engine_integration.js +3 -1
- package/test/media/media_source_engine_integration.js +71 -6
- package/test/media/media_source_engine_unit.js +10 -5
- package/test/media/streaming_engine_integration.js +4 -1
- package/test/media/streaming_engine_unit.js +144 -1
- package/test/media/transmuxer_integration.js +21 -10
- package/test/player_integration.js +224 -82
- package/test/player_unit.js +5 -2
- package/test/polyfill/patchedmediakeys_apple_unit.js +124 -0
- package/test/test/assets/id3-metadata.aac +0 -0
- package/test/test/assets/id3-metadata.ts +0 -0
- package/test/test/assets/thumbnails-sprites.vtt +10 -0
- package/test/test/assets/thumbnails.vtt +10 -0
- package/test/test/util/dash_parser_util.js +2 -0
- package/test/test/util/fake_ad.js +21 -0
- package/test/test/util/fake_media_source_engine.js +7 -0
- package/test/test/util/id3_generator.js +100 -0
- package/test/test/util/manifest_generator.js +3 -6
- package/test/test/util/simple_fakes.js +4 -0
- package/test/test/util/stream_generator.js +38 -0
- package/test/test/util/test_scheme.js +43 -14
- package/test/text/vtt_text_parser_unit.js +11 -0
- package/test/ui/text_displayer_layout_unit.js +14 -0
- package/test/util/id3_utils_unit.js +116 -0
- package/test/util/ts_parser_unit.js +77 -0
- package/ui/less/containers.less +7 -0
- package/ui/ui.js +43 -4
package/lib/hls/hls_parser.js
CHANGED
|
@@ -71,6 +71,24 @@ shaka.hls.HlsParser = class {
|
|
|
71
71
|
*/
|
|
72
72
|
this.groupIdToStreamInfosMap_ = new Map();
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* For media playlist lazy-loading to work in livestreams, we have to assume
|
|
76
|
+
* that each stream of a type (video, audio, etc) has the same mappings of
|
|
77
|
+
* sequence number to start time.
|
|
78
|
+
* This map stores those relationships.
|
|
79
|
+
* Only used during livestreams; we do not assume that VOD content is
|
|
80
|
+
* aligned in that way.
|
|
81
|
+
* @private {!Map.<string, !Map.<number, number>>}
|
|
82
|
+
*/
|
|
83
|
+
this.mediaSequenceToStartTimeByType_ = new Map();
|
|
84
|
+
|
|
85
|
+
// Set initial maps.
|
|
86
|
+
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
87
|
+
this.mediaSequenceToStartTimeByType_.set(ContentType.VIDEO, new Map());
|
|
88
|
+
this.mediaSequenceToStartTimeByType_.set(ContentType.AUDIO, new Map());
|
|
89
|
+
this.mediaSequenceToStartTimeByType_.set(ContentType.TEXT, new Map());
|
|
90
|
+
this.mediaSequenceToStartTimeByType_.set(ContentType.IMAGE, new Map());
|
|
91
|
+
|
|
74
92
|
/**
|
|
75
93
|
* The values are strings of the form "<VIDEO URI> - <AUDIO URI>",
|
|
76
94
|
* where the URIs are the verbatim media playlist URIs as they appeared in
|
|
@@ -122,19 +140,29 @@ shaka.hls.HlsParser = class {
|
|
|
122
140
|
this.updatePlaylistDelay_ = 0;
|
|
123
141
|
|
|
124
142
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
143
|
+
* The minimum sequence number for generated segments, when ignoring
|
|
144
|
+
* EXT-X-PROGRAM-DATE-TIME.
|
|
145
|
+
*
|
|
146
|
+
* @private {number}
|
|
128
147
|
*/
|
|
129
|
-
this.
|
|
148
|
+
this.minSequenceNumber_ = -1;
|
|
130
149
|
|
|
131
150
|
/**
|
|
132
|
-
* The
|
|
133
|
-
* EXT-X-PROGRAM-DATE-TIME.
|
|
151
|
+
* The lowest time value for any of the streams, as defined by the
|
|
152
|
+
* EXT-X-PROGRAM-DATE-TIME value. Measured in seconds since January 1, 1970.
|
|
153
|
+
*
|
|
154
|
+
* @private {number}
|
|
155
|
+
*/
|
|
156
|
+
this.lowestSyncTime_ = Infinity;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Whether the streams have previously been "finalized"; that is to say,
|
|
160
|
+
* whether we have loaded enough streams to know information about the asset
|
|
161
|
+
* such as timing information, live status, etc.
|
|
134
162
|
*
|
|
135
|
-
* @private {
|
|
163
|
+
* @private {boolean}
|
|
136
164
|
*/
|
|
137
|
-
this.
|
|
165
|
+
this.streamsFinalized_ = false;
|
|
138
166
|
|
|
139
167
|
/**
|
|
140
168
|
* This timer is used to trigger the start of a manifest update. A manifest
|
|
@@ -171,9 +199,6 @@ shaka.hls.HlsParser = class {
|
|
|
171
199
|
/** @private {shaka.util.OperationManager} */
|
|
172
200
|
this.operationManager_ = new shaka.util.OperationManager();
|
|
173
201
|
|
|
174
|
-
/** @private {!Array.<!Array.<!shaka.media.SegmentReference>>} */
|
|
175
|
-
this.segmentsToNotifyByStream_ = [];
|
|
176
|
-
|
|
177
202
|
/** A map from closed captions' group id, to a map of closed captions info.
|
|
178
203
|
* {group id -> {closed captions channel id -> language}}
|
|
179
204
|
* @private {Map.<string, Map.<string, string>>}
|
|
@@ -220,12 +245,6 @@ shaka.hls.HlsParser = class {
|
|
|
220
245
|
goog.asserts.assert(response.data, 'Response data should be non-null!');
|
|
221
246
|
await this.parseManifest_(response.data, uri);
|
|
222
247
|
|
|
223
|
-
// Start the update timer if we want updates.
|
|
224
|
-
const delay = this.updatePlaylistDelay_;
|
|
225
|
-
if (delay > 0) {
|
|
226
|
-
this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
248
|
goog.asserts.assert(this.manifest_, 'Manifest should be non-null');
|
|
230
249
|
return this.manifest_;
|
|
231
250
|
}
|
|
@@ -274,22 +293,20 @@ shaka.hls.HlsParser = class {
|
|
|
274
293
|
/** @type {!Array.<!Promise>} */
|
|
275
294
|
const updates = [];
|
|
276
295
|
const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
|
|
277
|
-
// Wait for the first stream info created, so that the start time is fetched
|
|
278
|
-
// and can be reused.
|
|
279
|
-
if (streamInfos.length) {
|
|
280
|
-
await this.updateStream_(streamInfos[0]);
|
|
281
|
-
}
|
|
282
|
-
for (let i = 1; i < streamInfos.length; i++) {
|
|
283
|
-
updates.push(this.updateStream_(streamInfos[i]));
|
|
284
|
-
}
|
|
285
296
|
|
|
297
|
+
// Only update active streams.
|
|
298
|
+
const activeStreamInfos = streamInfos.filter((s) => s.stream.segmentIndex);
|
|
299
|
+
for (const streamInfo of activeStreamInfos) {
|
|
300
|
+
updates.push(this.updateStream_(streamInfo));
|
|
301
|
+
}
|
|
286
302
|
await Promise.all(updates);
|
|
287
303
|
|
|
288
|
-
|
|
304
|
+
// Now that streams have been updated, notify the presentation timeline.
|
|
305
|
+
this.notifySegmentsForStreams_(activeStreamInfos.map((s) => s.stream));
|
|
289
306
|
|
|
290
307
|
// If any hasEndList is false, the stream is still live.
|
|
291
|
-
const stillLive =
|
|
292
|
-
if (!stillLive) {
|
|
308
|
+
const stillLive = activeStreamInfos.some((s) => s.hasEndList == false);
|
|
309
|
+
if (activeStreamInfos.length && !stillLive) {
|
|
293
310
|
// Convert the presentation to VOD and set the duration.
|
|
294
311
|
const PresentationType = shaka.hls.HlsParser.PresentationType_;
|
|
295
312
|
this.setPresentationType_(PresentationType.VOD);
|
|
@@ -301,6 +318,19 @@ shaka.hls.HlsParser = class {
|
|
|
301
318
|
}
|
|
302
319
|
}
|
|
303
320
|
|
|
321
|
+
/**
|
|
322
|
+
* @param {!shaka.hls.HlsParser.StreamInfo} streamInfo
|
|
323
|
+
* @return {!Map.<number, number>}
|
|
324
|
+
* @private
|
|
325
|
+
*/
|
|
326
|
+
getMediaSequenceToStartTimeFor_(streamInfo) {
|
|
327
|
+
if (this.isLive_()) {
|
|
328
|
+
return this.mediaSequenceToStartTimeByType_.get(streamInfo.type);
|
|
329
|
+
} else {
|
|
330
|
+
return streamInfo.mediaSequenceToStartTime;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
304
334
|
/**
|
|
305
335
|
* Updates a stream.
|
|
306
336
|
*
|
|
@@ -317,6 +347,10 @@ shaka.hls.HlsParser = class {
|
|
|
317
347
|
uriObj.setQueryData(new goog.Uri.QueryData('_HLS_skip=YES'));
|
|
318
348
|
}
|
|
319
349
|
const response = await this.requestManifest_(uriObj.toString());
|
|
350
|
+
if (!streamInfo.stream.segmentIndex) {
|
|
351
|
+
// The stream was closed since the update was first requested.
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
320
354
|
|
|
321
355
|
/** @type {shaka.hls.Playlist} */
|
|
322
356
|
const playlist = this.manifestTextParser_.parsePlaylist(
|
|
@@ -337,18 +371,29 @@ shaka.hls.HlsParser = class {
|
|
|
337
371
|
|
|
338
372
|
const stream = streamInfo.stream;
|
|
339
373
|
|
|
374
|
+
const mediaSequenceToStartTime =
|
|
375
|
+
this.getMediaSequenceToStartTimeFor_(streamInfo);
|
|
376
|
+
const {keyIds, drmInfos} = this.parseDrmInfo_(playlist, stream.mimeType);
|
|
377
|
+
|
|
378
|
+
const keysAreEqual =
|
|
379
|
+
(a, b) => a.size === b.size && [...a].every((value) => b.has(value));
|
|
380
|
+
|
|
381
|
+
if (!keysAreEqual(stream.keyIds, keyIds)) {
|
|
382
|
+
stream.keyIds = keyIds;
|
|
383
|
+
stream.drmInfos = drmInfos;
|
|
384
|
+
this.playerInterface_.newDrmInfo(stream);
|
|
385
|
+
}
|
|
386
|
+
|
|
340
387
|
const segments = this.createSegments_(
|
|
341
388
|
streamInfo.verbatimMediaPlaylistUri, playlist, stream.type,
|
|
342
|
-
stream.mimeType,
|
|
343
|
-
stream.codecs);
|
|
344
|
-
this.segmentsToNotifyByStream_.push(segments);
|
|
389
|
+
stream.mimeType, mediaSequenceToStartTime, mediaVariables);
|
|
345
390
|
|
|
346
391
|
stream.segmentIndex.mergeAndEvict(
|
|
347
392
|
segments, this.presentationTimeline_.getSegmentAvailabilityStart());
|
|
348
393
|
if (segments.length) {
|
|
349
394
|
const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
|
|
350
395
|
playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
|
|
351
|
-
const playlistStartTime =
|
|
396
|
+
const playlistStartTime = mediaSequenceToStartTime.get(
|
|
352
397
|
mediaSequenceNumber);
|
|
353
398
|
stream.segmentIndex.evict(playlistStartTime);
|
|
354
399
|
}
|
|
@@ -379,68 +424,70 @@ shaka.hls.HlsParser = class {
|
|
|
379
424
|
}
|
|
380
425
|
|
|
381
426
|
/**
|
|
382
|
-
* Align
|
|
383
|
-
* offset
|
|
427
|
+
* Align the streams by sequence number by dropping early segments. Then
|
|
428
|
+
* offset the streams to begin at presentation time 0.
|
|
429
|
+
* @param {!Array.<!shaka.hls.HlsParser.StreamInfo>} streamInfos
|
|
384
430
|
* @private
|
|
385
431
|
*/
|
|
386
|
-
syncStreamsWithSequenceNumber_() {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
432
|
+
syncStreamsWithSequenceNumber_(streamInfos) {
|
|
433
|
+
// We assume that, when this is first called, we have enough info to
|
|
434
|
+
// determine how to use the program date times (e.g. we have both a video
|
|
435
|
+
// and an audio, and all other videos and audios match those).
|
|
436
|
+
// Thus, we only need to calculate this once.
|
|
437
|
+
const updateMinSequenceNumber = this.minSequenceNumber_ == -1;
|
|
391
438
|
// Sync using media sequence number. Find the highest starting sequence
|
|
392
439
|
// number among all streams. Later, we will drop any references to
|
|
393
440
|
// earlier segments in other streams, then offset everything back to 0.
|
|
394
|
-
|
|
395
|
-
const firstSequenceNumberMap = new Map();
|
|
396
|
-
|
|
397
|
-
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
|
|
441
|
+
for (const streamInfo of streamInfos) {
|
|
398
442
|
const segmentIndex = streamInfo.stream.segmentIndex;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
443
|
+
goog.asserts.assert(segmentIndex,
|
|
444
|
+
'Only loaded streams should be synced');
|
|
445
|
+
const mediaSequenceToStartTime =
|
|
446
|
+
this.getMediaSequenceToStartTimeFor_(streamInfo);
|
|
447
|
+
const segment0 = segmentIndex.earliestReference();
|
|
448
|
+
if (segment0) {
|
|
449
|
+
// This looks inefficient, but iteration order is insertion order.
|
|
450
|
+
// So the very first entry should be the one we want.
|
|
451
|
+
// We assert that this holds true so that we are alerted by debug
|
|
452
|
+
// builds and tests if it changes. We still do a loop, though, so
|
|
453
|
+
// that the code functions correctly in production no matter what.
|
|
454
|
+
if (goog.DEBUG) {
|
|
455
|
+
const firstSequenceStartTime =
|
|
456
|
+
mediaSequenceToStartTime.values().next().value;
|
|
457
|
+
goog.asserts.assert(
|
|
458
|
+
firstSequenceStartTime == segment0.startTime,
|
|
459
|
+
'Sequence number map is not ordered as expected!');
|
|
460
|
+
}
|
|
461
|
+
for (const [sequence, start] of mediaSequenceToStartTime) {
|
|
462
|
+
if (start == segment0.startTime) {
|
|
463
|
+
if (updateMinSequenceNumber) {
|
|
464
|
+
this.minSequenceNumber_ = Math.max(
|
|
465
|
+
this.minSequenceNumber_, sequence);
|
|
421
466
|
}
|
|
467
|
+
// Even if we already have decided on a value for
|
|
468
|
+
// |this.minSequenceNumber_|, we still need to determine the first
|
|
469
|
+
// sequence number for the stream, to offset it in the code below.
|
|
470
|
+
streamInfo.firstSequenceNumber = sequence;
|
|
471
|
+
break;
|
|
422
472
|
}
|
|
423
473
|
}
|
|
424
474
|
}
|
|
425
475
|
}
|
|
426
476
|
|
|
427
|
-
if (
|
|
477
|
+
if (this.minSequenceNumber_ < 0) {
|
|
428
478
|
// Nothing to sync.
|
|
429
479
|
return;
|
|
430
480
|
}
|
|
431
481
|
|
|
432
|
-
// From now on, updates will ignore any references before this number.
|
|
433
|
-
this.minSequenceNumber_ = highestStartingSequenceNumber;
|
|
434
|
-
|
|
435
482
|
shaka.log.debug('Syncing HLS streams against base sequence number:',
|
|
436
483
|
this.minSequenceNumber_);
|
|
437
484
|
|
|
438
|
-
for (const streamInfo of
|
|
485
|
+
for (const streamInfo of streamInfos) {
|
|
439
486
|
const segmentIndex = streamInfo.stream.segmentIndex;
|
|
440
487
|
if (segmentIndex) {
|
|
441
488
|
// Drop any earlier references.
|
|
442
|
-
const numSegmentsToDrop =
|
|
443
|
-
|
|
489
|
+
const numSegmentsToDrop =
|
|
490
|
+
this.minSequenceNumber_ - streamInfo.firstSequenceNumber;
|
|
444
491
|
segmentIndex.dropFirstReferences(numSegmentsToDrop);
|
|
445
492
|
|
|
446
493
|
// Now adjust timestamps back to begin at 0.
|
|
@@ -450,33 +497,34 @@ shaka.hls.HlsParser = class {
|
|
|
450
497
|
}
|
|
451
498
|
}
|
|
452
499
|
}
|
|
453
|
-
|
|
454
|
-
this.streamsSynced_ = true;
|
|
455
500
|
}
|
|
456
501
|
|
|
457
502
|
/**
|
|
458
503
|
* Synchronize streams by the EXT-X-PROGRAM-DATE-TIME tags attached to their
|
|
459
504
|
* segments. Also normalizes segment times so that the earliest segment in
|
|
460
505
|
* any stream is at time 0.
|
|
506
|
+
* @param {!Array.<!shaka.hls.HlsParser.StreamInfo>} streamInfos
|
|
461
507
|
* @private
|
|
462
508
|
*/
|
|
463
|
-
syncStreamsWithProgramDateTime_() {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
509
|
+
syncStreamsWithProgramDateTime_(streamInfos) {
|
|
510
|
+
// We assume that, when this is first called, we have enough info to
|
|
511
|
+
// determine how to use the program date times (e.g. we have both a video
|
|
512
|
+
// and an audio, and all other videos and audios match those).
|
|
513
|
+
// Thus, we only need to calculate this once.
|
|
514
|
+
if (this.lowestSyncTime_ == Infinity) {
|
|
515
|
+
for (const streamInfo of streamInfos) {
|
|
516
|
+
const segmentIndex = streamInfo.stream.segmentIndex;
|
|
517
|
+
goog.asserts.assert(segmentIndex,
|
|
518
|
+
'Only loaded streams should be synced');
|
|
473
519
|
const segment0 = segmentIndex.earliestReference();
|
|
474
520
|
if (segment0 != null && segment0.syncTime != null) {
|
|
475
|
-
|
|
521
|
+
this.lowestSyncTime_ =
|
|
522
|
+
Math.min(this.lowestSyncTime_, segment0.syncTime);
|
|
476
523
|
}
|
|
477
524
|
}
|
|
478
525
|
}
|
|
479
526
|
|
|
527
|
+
const lowestSyncTime = this.lowestSyncTime_;
|
|
480
528
|
if (lowestSyncTime == Infinity) {
|
|
481
529
|
// Nothing to sync.
|
|
482
530
|
return;
|
|
@@ -506,8 +554,6 @@ shaka.hls.HlsParser = class {
|
|
|
506
554
|
}
|
|
507
555
|
}
|
|
508
556
|
}
|
|
509
|
-
|
|
510
|
-
this.streamsSynced_ = true;
|
|
511
557
|
}
|
|
512
558
|
|
|
513
559
|
/**
|
|
@@ -522,8 +568,10 @@ shaka.hls.HlsParser = class {
|
|
|
522
568
|
goog.asserts.assert(streamInfo.maxTimestamp >= 0,
|
|
523
569
|
'Negative maxTimestamp after adjustment!');
|
|
524
570
|
|
|
525
|
-
|
|
526
|
-
|
|
571
|
+
const mediaSequenceToStartTime =
|
|
572
|
+
this.getMediaSequenceToStartTimeFor_(streamInfo);
|
|
573
|
+
for (const [key, value] of mediaSequenceToStartTime) {
|
|
574
|
+
mediaSequenceToStartTime.set(key, value + offset);
|
|
527
575
|
}
|
|
528
576
|
|
|
529
577
|
shaka.log.debug('Offset', offset, 'applied to',
|
|
@@ -539,6 +587,7 @@ shaka.hls.HlsParser = class {
|
|
|
539
587
|
* @private
|
|
540
588
|
*/
|
|
541
589
|
async parseManifest_(data, uri) {
|
|
590
|
+
const HlsParser = shaka.hls.HlsParser;
|
|
542
591
|
const Utils = shaka.hls.Utils;
|
|
543
592
|
|
|
544
593
|
goog.asserts.assert(this.masterPlaylistUri_,
|
|
@@ -564,7 +613,17 @@ shaka.hls.HlsParser = class {
|
|
|
564
613
|
// Get necessary info for this stream, from the config. These are things
|
|
565
614
|
// we would normally find from the master playlist (e.g. from values on
|
|
566
615
|
// EXT-X-MEDIA tags).
|
|
567
|
-
|
|
616
|
+
let fullMimeType = this.config_.hls.mediaPlaylistFullMimeType;
|
|
617
|
+
// Try to infer the full mimetype better.
|
|
618
|
+
if (playlist.segments.length) {
|
|
619
|
+
const parsedUri = new goog.Uri(playlist.segments[0].absoluteUri);
|
|
620
|
+
const extension = parsedUri.getPath().split('.').pop();
|
|
621
|
+
const mimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension];
|
|
622
|
+
if (mimeType) {
|
|
623
|
+
fullMimeType = mimeType;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
568
627
|
const mimeType = shaka.util.MimeUtils.getBasicType(fullMimeType);
|
|
569
628
|
const type = mimeType.split('/')[0];
|
|
570
629
|
const codecs = shaka.util.MimeUtils.getCodecs(fullMimeType);
|
|
@@ -611,6 +670,9 @@ shaka.hls.HlsParser = class {
|
|
|
611
670
|
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
612
671
|
const imageTags = Utils.filterTagsByName(
|
|
613
672
|
playlist.tags, 'EXT-X-IMAGE-STREAM-INF');
|
|
673
|
+
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
674
|
+
const sessionKeyTags = Utils.filterTagsByName(
|
|
675
|
+
playlist.tags, 'EXT-X-SESSION-KEY');
|
|
614
676
|
|
|
615
677
|
this.parseCodecs_(variantTags);
|
|
616
678
|
|
|
@@ -641,10 +703,10 @@ shaka.hls.HlsParser = class {
|
|
|
641
703
|
|
|
642
704
|
// Parse audio and video media tags first, so that we can extract segment
|
|
643
705
|
// start time from audio/video streams and reuse for text streams.
|
|
644
|
-
|
|
706
|
+
this.createStreamInfosFromMediaTags_(mediaTags);
|
|
645
707
|
this.parseClosedCaptions_(mediaTags);
|
|
646
|
-
variants =
|
|
647
|
-
textStreams =
|
|
708
|
+
variants = this.createVariantsForTags_(variantTags, sessionKeyTags);
|
|
709
|
+
textStreams = this.parseTexts_(mediaTags);
|
|
648
710
|
imageStreams = await this.parseImages_(imageTags);
|
|
649
711
|
}
|
|
650
712
|
|
|
@@ -656,32 +718,45 @@ shaka.hls.HlsParser = class {
|
|
|
656
718
|
shaka.util.Error.Code.OPERATION_ABORTED);
|
|
657
719
|
}
|
|
658
720
|
|
|
659
|
-
// Now that we have generated all streams, we can determine the offset to
|
|
660
|
-
// apply to sync times.
|
|
661
|
-
if (this.config_.hls.ignoreManifestProgramDateTime) {
|
|
662
|
-
this.syncStreamsWithSequenceNumber_();
|
|
663
|
-
} else {
|
|
664
|
-
this.syncStreamsWithProgramDateTime_();
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Find the min and max timestamp of the earliest segment in all streams.
|
|
668
|
-
// Find the minimum duration of all streams as well.
|
|
669
|
-
let minDuration = Infinity;
|
|
670
|
-
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
|
|
671
|
-
if (streamInfo.stream.type != 'text') {
|
|
672
|
-
// Since everything is already offset to 0 (either by sync or by being
|
|
673
|
-
// VOD), only maxTimestamp is necessary to compute the duration.
|
|
674
|
-
minDuration = Math.min(minDuration, streamInfo.maxTimestamp);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
721
|
// This assert is our own sanity check.
|
|
679
722
|
goog.asserts.assert(this.presentationTimeline_ == null,
|
|
680
723
|
'Presentation timeline created early!');
|
|
681
|
-
|
|
724
|
+
// We don't know if the presentation is VOD or live until we parse at least
|
|
725
|
+
// one media playlist, so make a VOD-style presentation timeline for now
|
|
726
|
+
// and change the type later if we discover this is live.
|
|
727
|
+
// Since the player will load the first variant chosen early in the process,
|
|
728
|
+
// there isn't a window during playback where the live-ness is unknown.
|
|
729
|
+
this.presentationTimeline_ = new shaka.media.PresentationTimeline(
|
|
730
|
+
/* presentationStartTime= */ null, /* delay= */ 0);
|
|
731
|
+
this.presentationTimeline_.setStatic(true);
|
|
732
|
+
|
|
733
|
+
// Single-variant streams aren't lazy-loaded, so for them we already have
|
|
734
|
+
// enough info here to determine the presentation type and duration.
|
|
735
|
+
if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
|
|
736
|
+
if (this.isLive_()) {
|
|
737
|
+
this.changePresentationTimelineToLive_();
|
|
738
|
+
const delay = this.updatePlaylistDelay_;
|
|
739
|
+
this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
|
|
740
|
+
}
|
|
741
|
+
const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
|
|
742
|
+
this.finalizeStreams_(streamInfos);
|
|
743
|
+
this.determineDuration_();
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
this.manifest_ = {
|
|
747
|
+
presentationTimeline: this.presentationTimeline_,
|
|
748
|
+
variants,
|
|
749
|
+
textStreams,
|
|
750
|
+
imageStreams,
|
|
751
|
+
offlineSessionIds: [],
|
|
752
|
+
minBufferTime: 0,
|
|
753
|
+
sequenceMode: true,
|
|
754
|
+
};
|
|
755
|
+
this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
|
|
756
|
+
}
|
|
682
757
|
|
|
683
|
-
|
|
684
|
-
|
|
758
|
+
/** @private */
|
|
759
|
+
determineDuration_() {
|
|
685
760
|
goog.asserts.assert(this.presentationTimeline_,
|
|
686
761
|
'Presentation timeline not created!');
|
|
687
762
|
|
|
@@ -701,10 +776,14 @@ shaka.hls.HlsParser = class {
|
|
|
701
776
|
const PresentationType = shaka.hls.HlsParser.PresentationType_;
|
|
702
777
|
|
|
703
778
|
if (this.presentationType_ == PresentationType.LIVE) {
|
|
779
|
+
let segmentAvailabilityDuration = this.getMinDuration_();
|
|
780
|
+
|
|
704
781
|
// This defaults to the presentation delay, which has the effect of
|
|
705
782
|
// making the live stream unseekable. This is consistent with Apple's
|
|
706
783
|
// HLS implementation.
|
|
707
|
-
|
|
784
|
+
if (this.config_.hls.useSafariBehaviorForLive) {
|
|
785
|
+
segmentAvailabilityDuration = this.presentationTimeline_.getDelay();
|
|
786
|
+
}
|
|
708
787
|
|
|
709
788
|
// The app can override that with a longer duration, to allow seeking.
|
|
710
789
|
if (!isNaN(this.config_.availabilityWindowOverride)) {
|
|
@@ -716,33 +795,9 @@ shaka.hls.HlsParser = class {
|
|
|
716
795
|
}
|
|
717
796
|
} else {
|
|
718
797
|
// Use the minimum duration as the presentation duration.
|
|
719
|
-
this.presentationTimeline_.setDuration(
|
|
720
|
-
|
|
721
|
-
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
|
|
722
|
-
// Fit the segments to the playlist duration.
|
|
723
|
-
streamInfo.stream.segmentIndex.fit(/* periodStart= */ 0, minDuration);
|
|
724
|
-
}
|
|
798
|
+
this.presentationTimeline_.setDuration(this.getMinDuration_());
|
|
725
799
|
}
|
|
726
800
|
|
|
727
|
-
// Now that the content has been fit, notify segments.
|
|
728
|
-
this.segmentsToNotifyByStream_ = [];
|
|
729
|
-
const streamsToNotify = [];
|
|
730
|
-
for (const variant of variants) {
|
|
731
|
-
for (const stream of [variant.video, variant.audio]) {
|
|
732
|
-
if (stream) {
|
|
733
|
-
streamsToNotify.push(stream);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
await Promise.all(streamsToNotify.map(async (stream) => {
|
|
738
|
-
await stream.createSegmentIndex();
|
|
739
|
-
}));
|
|
740
|
-
for (const stream of streamsToNotify) {
|
|
741
|
-
this.segmentsToNotifyByStream_.push(stream.segmentIndex.references);
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
this.notifySegments_();
|
|
745
|
-
|
|
746
801
|
// This is the first point where we have a meaningful presentation start
|
|
747
802
|
// time, and we need to tell PresentationTimeline that so that it can
|
|
748
803
|
// maintain consistency from here on.
|
|
@@ -753,17 +808,6 @@ shaka.hls.HlsParser = class {
|
|
|
753
808
|
goog.asserts.assert(
|
|
754
809
|
!this.presentationTimeline_.usingPresentationStartTime(),
|
|
755
810
|
'We should not be using the presentation start time in HLS!');
|
|
756
|
-
|
|
757
|
-
this.manifest_ = {
|
|
758
|
-
presentationTimeline: this.presentationTimeline_,
|
|
759
|
-
variants,
|
|
760
|
-
textStreams,
|
|
761
|
-
imageStreams,
|
|
762
|
-
offlineSessionIds: [],
|
|
763
|
-
minBufferTime: 0,
|
|
764
|
-
sequenceMode: true,
|
|
765
|
-
};
|
|
766
|
-
this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
|
|
767
811
|
}
|
|
768
812
|
|
|
769
813
|
/**
|
|
@@ -854,21 +898,20 @@ shaka.hls.HlsParser = class {
|
|
|
854
898
|
* Create text streams for Subtitles, but not Closed Captions.
|
|
855
899
|
*
|
|
856
900
|
* @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
|
|
857
|
-
* @return {!
|
|
901
|
+
* @return {!Array.<!shaka.extern.Stream>}
|
|
858
902
|
* @private
|
|
859
903
|
*/
|
|
860
|
-
|
|
904
|
+
parseTexts_(mediaTags) {
|
|
861
905
|
// Create text stream for each Subtitle media tag.
|
|
862
906
|
const subtitleTags =
|
|
863
907
|
shaka.hls.Utils.filterTagsByType(mediaTags, 'SUBTITLES');
|
|
864
|
-
const
|
|
908
|
+
const textStreams = subtitleTags.map((tag) => {
|
|
865
909
|
const disableText = this.config_.disableText;
|
|
866
910
|
if (disableText) {
|
|
867
911
|
return null;
|
|
868
912
|
}
|
|
869
913
|
try {
|
|
870
|
-
|
|
871
|
-
return streamInfo.stream;
|
|
914
|
+
return this.createStreamInfoFromMediaTag_(tag).stream;
|
|
872
915
|
} catch (e) {
|
|
873
916
|
if (this.config_.hls.ignoreTextStreamFailures) {
|
|
874
917
|
return null;
|
|
@@ -876,7 +919,6 @@ shaka.hls.HlsParser = class {
|
|
|
876
919
|
throw e;
|
|
877
920
|
}
|
|
878
921
|
});
|
|
879
|
-
const textStreams = await Promise.all(textStreamPromises);
|
|
880
922
|
|
|
881
923
|
// Set the codecs for text streams.
|
|
882
924
|
for (const tag of subtitleTags) {
|
|
@@ -926,7 +968,7 @@ shaka.hls.HlsParser = class {
|
|
|
926
968
|
* @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
|
|
927
969
|
* @private
|
|
928
970
|
*/
|
|
929
|
-
|
|
971
|
+
createStreamInfosFromMediaTags_(mediaTags) {
|
|
930
972
|
// Filter out subtitles and media tags without uri.
|
|
931
973
|
mediaTags = mediaTags.filter((tag) => {
|
|
932
974
|
const uri = tag.getAttributeValue('URI') || '';
|
|
@@ -935,20 +977,52 @@ shaka.hls.HlsParser = class {
|
|
|
935
977
|
});
|
|
936
978
|
|
|
937
979
|
// Create stream info for each audio / video media tag.
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
}
|
|
941
|
-
await Promise.all(promises);
|
|
980
|
+
for (const tag of mediaTags) {
|
|
981
|
+
this.createStreamInfoFromMediaTag_(tag);
|
|
982
|
+
}
|
|
942
983
|
}
|
|
943
984
|
|
|
944
985
|
/**
|
|
945
986
|
* @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
|
|
946
|
-
* @
|
|
987
|
+
* @param {!Array.<!shaka.hls.Tag>} sessionKeyTags EXT-X-SESSION-KEY tags
|
|
988
|
+
* from the playlist.
|
|
989
|
+
* @return {!Array.<!shaka.extern.Variant>}
|
|
947
990
|
* @private
|
|
948
991
|
*/
|
|
949
|
-
|
|
992
|
+
createVariantsForTags_(tags, sessionKeyTags) {
|
|
993
|
+
// EXT-X-SESSION-KEY processing
|
|
994
|
+
const drmInfos = [];
|
|
995
|
+
const keyIds = new Set();
|
|
996
|
+
if (sessionKeyTags.length > 0) {
|
|
997
|
+
for (const drmTag of sessionKeyTags) {
|
|
998
|
+
const method = drmTag.getRequiredAttrValue('METHOD');
|
|
999
|
+
if (method != 'NONE' && method != 'AES-128') {
|
|
1000
|
+
// According to the HLS spec, KEYFORMAT is optional and implicitly
|
|
1001
|
+
// defaults to "identity".
|
|
1002
|
+
// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
|
|
1003
|
+
const keyFormat =
|
|
1004
|
+
drmTag.getAttributeValue('KEYFORMAT') || 'identity';
|
|
1005
|
+
const drmParser =
|
|
1006
|
+
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
|
|
1007
|
+
|
|
1008
|
+
const drmInfo = drmParser ?
|
|
1009
|
+
drmParser(drmTag, /* mimeType= */ '') : null;
|
|
1010
|
+
if (drmInfo) {
|
|
1011
|
+
if (drmInfo.keyIds) {
|
|
1012
|
+
for (const keyId of drmInfo.keyIds) {
|
|
1013
|
+
keyIds.add(keyId);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
drmInfos.push(drmInfo);
|
|
1017
|
+
} else {
|
|
1018
|
+
shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
950
1024
|
// Create variants for each variant tag.
|
|
951
|
-
const
|
|
1025
|
+
const allVariants = tags.map((tag) => {
|
|
952
1026
|
const frameRate = tag.getAttributeValue('FRAME-RATE');
|
|
953
1027
|
const bandwidth = Number(tag.getAttributeValue('AVERAGE-BANDWIDTH')) ||
|
|
954
1028
|
Number(tag.getRequiredAttrValue('BANDWIDTH'));
|
|
@@ -958,7 +1032,7 @@ shaka.hls.HlsParser = class {
|
|
|
958
1032
|
|
|
959
1033
|
const videoRange = tag.getAttributeValue('VIDEO-RANGE');
|
|
960
1034
|
|
|
961
|
-
const streamInfos =
|
|
1035
|
+
const streamInfos = this.createStreamInfosForVariantTag_(tag,
|
|
962
1036
|
resolution, frameRate);
|
|
963
1037
|
|
|
964
1038
|
goog.asserts.assert(streamInfos.audio.length ||
|
|
@@ -971,10 +1045,10 @@ shaka.hls.HlsParser = class {
|
|
|
971
1045
|
width,
|
|
972
1046
|
height,
|
|
973
1047
|
frameRate,
|
|
974
|
-
videoRange
|
|
1048
|
+
videoRange,
|
|
1049
|
+
drmInfos,
|
|
1050
|
+
keyIds);
|
|
975
1051
|
});
|
|
976
|
-
|
|
977
|
-
const allVariants = await Promise.all(variantsPromises);
|
|
978
1052
|
let variants = allVariants.reduce(shaka.util.Functional.collapseArrays, []);
|
|
979
1053
|
// Filter out null variants.
|
|
980
1054
|
variants = variants.filter((variant) => variant != null);
|
|
@@ -988,10 +1062,10 @@ shaka.hls.HlsParser = class {
|
|
|
988
1062
|
* @param {!shaka.hls.Tag} tag
|
|
989
1063
|
* @param {?string} resolution
|
|
990
1064
|
* @param {?string} frameRate
|
|
991
|
-
* @return {!
|
|
1065
|
+
* @return {!shaka.hls.HlsParser.StreamInfos}
|
|
992
1066
|
* @private
|
|
993
1067
|
*/
|
|
994
|
-
|
|
1068
|
+
createStreamInfosForVariantTag_(tag, resolution, frameRate) {
|
|
995
1069
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
996
1070
|
/** @type {!Array.<string>} */
|
|
997
1071
|
let allCodecs = this.getCodecsForVariantTag_(tag);
|
|
@@ -1060,10 +1134,9 @@ shaka.hls.HlsParser = class {
|
|
|
1060
1134
|
|
|
1061
1135
|
if (!ignoreStream) {
|
|
1062
1136
|
const streamInfo =
|
|
1063
|
-
|
|
1137
|
+
this.createStreamInfoFromVariantTag_(tag, allCodecs, type);
|
|
1064
1138
|
res[streamInfo.stream.type] = [streamInfo];
|
|
1065
1139
|
}
|
|
1066
|
-
this.filterLegacyCodecs_(res);
|
|
1067
1140
|
return res;
|
|
1068
1141
|
}
|
|
1069
1142
|
|
|
@@ -1194,7 +1267,7 @@ shaka.hls.HlsParser = class {
|
|
|
1194
1267
|
|
|
1195
1268
|
/**
|
|
1196
1269
|
* Get the type value.
|
|
1197
|
-
* Shaka recognizes the content types 'audio', 'video' and '
|
|
1270
|
+
* Shaka recognizes the content types 'audio', 'video', 'text', and 'image'.
|
|
1198
1271
|
* The HLS 'subtitles' type needs to be mapped to 'text'.
|
|
1199
1272
|
* @param {!shaka.hls.Tag} tag
|
|
1200
1273
|
* @return {string}
|
|
@@ -1208,28 +1281,6 @@ shaka.hls.HlsParser = class {
|
|
|
1208
1281
|
return type;
|
|
1209
1282
|
}
|
|
1210
1283
|
|
|
1211
|
-
/**
|
|
1212
|
-
* Filters out unsupported codec strings from an array of stream infos.
|
|
1213
|
-
* @param {shaka.hls.HlsParser.StreamInfos} streamInfos
|
|
1214
|
-
* @private
|
|
1215
|
-
*/
|
|
1216
|
-
filterLegacyCodecs_(streamInfos) {
|
|
1217
|
-
for (const streamInfo of streamInfos.audio.concat(streamInfos.video)) {
|
|
1218
|
-
if (!streamInfo) {
|
|
1219
|
-
continue;
|
|
1220
|
-
}
|
|
1221
|
-
let codecs = streamInfo.stream.codecs.split(',');
|
|
1222
|
-
codecs = codecs.filter((codec) => {
|
|
1223
|
-
// mp4a.40.34 is a nonstandard codec string that is sometimes used in
|
|
1224
|
-
// HLS for legacy reasons. It is not recognized by non-Apple MSE.
|
|
1225
|
-
// See https://bugs.chromium.org/p/chromium/issues/detail?id=489520
|
|
1226
|
-
// Therefore, ignore this codec string.
|
|
1227
|
-
return codec != 'mp4a.40.34';
|
|
1228
|
-
});
|
|
1229
|
-
streamInfo.stream.codecs = codecs.join(',');
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
1284
|
/**
|
|
1234
1285
|
* @param {!Array.<shaka.hls.HlsParser.StreamInfo>} audioInfos
|
|
1235
1286
|
* @param {!Array.<shaka.hls.HlsParser.StreamInfo>} videoInfos
|
|
@@ -1238,11 +1289,14 @@ shaka.hls.HlsParser = class {
|
|
|
1238
1289
|
* @param {?string} height
|
|
1239
1290
|
* @param {?string} frameRate
|
|
1240
1291
|
* @param {?string} videoRange
|
|
1292
|
+
* @param {!Array.<shaka.extern.DrmInfo>} drmInfos
|
|
1293
|
+
* @param {!Set.<string>} keyIds
|
|
1241
1294
|
* @return {!Array.<!shaka.extern.Variant>}
|
|
1242
1295
|
* @private
|
|
1243
1296
|
*/
|
|
1244
1297
|
createVariants_(
|
|
1245
|
-
audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange
|
|
1298
|
+
audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
|
|
1299
|
+
drmInfos, keyIds) {
|
|
1246
1300
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
1247
1301
|
const DrmEngine = shaka.media.DrmEngine;
|
|
1248
1302
|
|
|
@@ -1268,7 +1322,15 @@ shaka.hls.HlsParser = class {
|
|
|
1268
1322
|
for (const audioInfo of audioInfos) {
|
|
1269
1323
|
for (const videoInfo of videoInfos) {
|
|
1270
1324
|
const audioStream = audioInfo ? audioInfo.stream : null;
|
|
1325
|
+
if (audioStream) {
|
|
1326
|
+
audioStream.drmInfos = drmInfos;
|
|
1327
|
+
audioStream.keyIds = keyIds;
|
|
1328
|
+
}
|
|
1271
1329
|
const videoStream = videoInfo ? videoInfo.stream : null;
|
|
1330
|
+
if (videoStream) {
|
|
1331
|
+
videoStream.drmInfos = drmInfos;
|
|
1332
|
+
videoStream.keyIds = keyIds;
|
|
1333
|
+
}
|
|
1272
1334
|
const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
|
|
1273
1335
|
const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
|
|
1274
1336
|
const videoStreamUri =
|
|
@@ -1353,10 +1415,10 @@ shaka.hls.HlsParser = class {
|
|
|
1353
1415
|
* Parse EXT-X-MEDIA media tag into a Stream object.
|
|
1354
1416
|
*
|
|
1355
1417
|
* @param {shaka.hls.Tag} tag
|
|
1356
|
-
* @return {!
|
|
1418
|
+
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1357
1419
|
* @private
|
|
1358
1420
|
*/
|
|
1359
|
-
|
|
1421
|
+
createStreamInfoFromMediaTag_(tag) {
|
|
1360
1422
|
goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
|
|
1361
1423
|
'Should only be called on media tags!');
|
|
1362
1424
|
const groupId = tag.getRequiredAttrValue('GROUP-ID');
|
|
@@ -1398,7 +1460,7 @@ shaka.hls.HlsParser = class {
|
|
|
1398
1460
|
const forced = forcedAttrValue == 'YES';
|
|
1399
1461
|
// TODO: Should we take into account some of the currently ignored
|
|
1400
1462
|
// attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj
|
|
1401
|
-
const streamInfo =
|
|
1463
|
+
const streamInfo = this.createStreamInfo_(
|
|
1402
1464
|
verbatimMediaPlaylistUri, codecs, type, language, primary, name,
|
|
1403
1465
|
channelsCount, /* closedCaptions= */ null, characteristics, forced,
|
|
1404
1466
|
spatialAudio);
|
|
@@ -1445,7 +1507,7 @@ shaka.hls.HlsParser = class {
|
|
|
1445
1507
|
|
|
1446
1508
|
const characteristics = tag.getAttributeValue('CHARACTERISTICS');
|
|
1447
1509
|
|
|
1448
|
-
const streamInfo =
|
|
1510
|
+
const streamInfo = this.createStreamInfo_(
|
|
1449
1511
|
verbatimImagePlaylistUri, codecs, type, language, /* primary= */ false,
|
|
1450
1512
|
name, /* channelsCount= */ null, /* closedCaptions= */ null,
|
|
1451
1513
|
characteristics, /* forced= */ false, /* spatialAudio= */ false);
|
|
@@ -1463,6 +1525,10 @@ shaka.hls.HlsParser = class {
|
|
|
1463
1525
|
// of the entire sheet at once (like we expect in the output).
|
|
1464
1526
|
// So multiply by the layout size.
|
|
1465
1527
|
|
|
1528
|
+
// Since we need to have generated the segment index for this, we can't
|
|
1529
|
+
// lazy-load in this situation.
|
|
1530
|
+
await streamInfo.stream.createSegmentIndex();
|
|
1531
|
+
|
|
1466
1532
|
const reference = streamInfo.stream.segmentIndex.get(0);
|
|
1467
1533
|
const layout = reference.getTilesLayout();
|
|
1468
1534
|
if (layout) {
|
|
@@ -1489,10 +1555,10 @@ shaka.hls.HlsParser = class {
|
|
|
1489
1555
|
* @param {!shaka.hls.Tag} tag
|
|
1490
1556
|
* @param {!Array.<string>} allCodecs
|
|
1491
1557
|
* @param {string} type
|
|
1492
|
-
* @return {!
|
|
1558
|
+
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1493
1559
|
* @private
|
|
1494
1560
|
*/
|
|
1495
|
-
|
|
1561
|
+
createStreamInfoFromVariantTag_(tag, allCodecs, type) {
|
|
1496
1562
|
goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
|
|
1497
1563
|
'Should only be called on variant tags!');
|
|
1498
1564
|
const verbatimMediaPlaylistUri = this.variableSubstitution_(
|
|
@@ -1504,7 +1570,7 @@ shaka.hls.HlsParser = class {
|
|
|
1504
1570
|
|
|
1505
1571
|
const closedCaptions = this.getClosedCaptions_(tag, type);
|
|
1506
1572
|
const codecs = shaka.util.ManifestParserUtils.guessCodecs(type, allCodecs);
|
|
1507
|
-
const streamInfo =
|
|
1573
|
+
const streamInfo = this.createStreamInfo_(verbatimMediaPlaylistUri,
|
|
1508
1574
|
codecs, type, /* language= */ 'und', /* primary= */ false,
|
|
1509
1575
|
/* name= */ null, /* channelcount= */ null, closedCaptions,
|
|
1510
1576
|
/* characteristics= */ null, /* forced= */ false,
|
|
@@ -1532,29 +1598,270 @@ shaka.hls.HlsParser = class {
|
|
|
1532
1598
|
* @param {?string} characteristics
|
|
1533
1599
|
* @param {boolean} forced
|
|
1534
1600
|
* @param {boolean} spatialAudio
|
|
1535
|
-
* @return {!
|
|
1601
|
+
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1536
1602
|
* @private
|
|
1537
1603
|
*/
|
|
1538
|
-
|
|
1604
|
+
createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language,
|
|
1539
1605
|
primary, name, channelsCount, closedCaptions, characteristics, forced,
|
|
1540
1606
|
spatialAudio) {
|
|
1541
1607
|
// TODO: Refactor, too many parameters
|
|
1542
|
-
|
|
1608
|
+
const initialMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
|
|
1543
1609
|
this.masterPlaylistUri_, verbatimMediaPlaylistUri);
|
|
1544
1610
|
|
|
1545
|
-
|
|
1546
|
-
//
|
|
1547
|
-
|
|
1611
|
+
// This stream is lazy-loaded inside the createSegmentIndex function.
|
|
1612
|
+
// So we start out with a stream object that does not contain the actual
|
|
1613
|
+
// segment index, then download when createSegmentIndex is called.
|
|
1614
|
+
const stream = this.makeStreamObject_(codecs, type, language, primary, name,
|
|
1615
|
+
channelsCount, closedCaptions, characteristics, forced, spatialAudio);
|
|
1616
|
+
if (shaka.media.MediaSourceEngine.RAW_FORMATS.includes(stream.mimeType)) {
|
|
1617
|
+
stream.codecs = '';
|
|
1618
|
+
}
|
|
1619
|
+
const streamInfo = {
|
|
1620
|
+
stream,
|
|
1621
|
+
type,
|
|
1622
|
+
verbatimMediaPlaylistUri,
|
|
1623
|
+
// These values are filled out or updated after lazy-loading:
|
|
1624
|
+
absoluteMediaPlaylistUri: initialMediaPlaylistUri,
|
|
1625
|
+
maxTimestamp: 0,
|
|
1626
|
+
mediaSequenceToStartTime: new Map(),
|
|
1627
|
+
canSkipSegments: false,
|
|
1628
|
+
hasEndList: false,
|
|
1629
|
+
firstSequenceNumber: -1,
|
|
1630
|
+
loadedOnce: false,
|
|
1631
|
+
};
|
|
1548
1632
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1633
|
+
/** @param {!AbortSignal} abortSignal */
|
|
1634
|
+
const downloadSegmentIndex = async (abortSignal) => {
|
|
1635
|
+
// Download the actual manifest.
|
|
1636
|
+
const response = await this.requestManifest_(
|
|
1637
|
+
streamInfo.absoluteMediaPlaylistUri);
|
|
1638
|
+
if (abortSignal.aborted) {
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// Record the final URI after redirects.
|
|
1643
|
+
const absoluteMediaPlaylistUri = response.uri;
|
|
1644
|
+
|
|
1645
|
+
// Record the redirected, final URI of this media playlist when we parse
|
|
1646
|
+
// it.
|
|
1647
|
+
/** @type {!shaka.hls.Playlist} */
|
|
1648
|
+
const playlist = this.manifestTextParser_.parsePlaylist(
|
|
1649
|
+
response.data, absoluteMediaPlaylistUri);
|
|
1650
|
+
|
|
1651
|
+
const wasLive = this.isLive_();
|
|
1652
|
+
const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
|
|
1653
|
+
playlist, verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs,
|
|
1654
|
+
type, language, primary, name, channelsCount, closedCaptions,
|
|
1655
|
+
characteristics, forced, spatialAudio);
|
|
1656
|
+
if (abortSignal.aborted) {
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
const realStream = realStreamInfo.stream;
|
|
1661
|
+
|
|
1662
|
+
if (this.isLive_() && !wasLive) {
|
|
1663
|
+
// Now that we know that the presentation is live, convert the timeline
|
|
1664
|
+
// to live.
|
|
1665
|
+
this.changePresentationTimelineToLive_();
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// Copy values from the real stream info to our initial one.
|
|
1669
|
+
streamInfo.absoluteMediaPlaylistUri = absoluteMediaPlaylistUri;
|
|
1670
|
+
streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
|
|
1671
|
+
streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
|
|
1672
|
+
streamInfo.hasEndList = realStreamInfo.hasEndList;
|
|
1673
|
+
streamInfo.mediaSequenceToStartTime =
|
|
1674
|
+
realStreamInfo.mediaSequenceToStartTime;
|
|
1675
|
+
streamInfo.loadedOnce = true;
|
|
1676
|
+
stream.segmentIndex = realStream.segmentIndex;
|
|
1677
|
+
stream.encrypted = realStream.encrypted;
|
|
1678
|
+
stream.drmInfos = realStream.drmInfos;
|
|
1679
|
+
stream.keyIds = realStream.keyIds;
|
|
1680
|
+
stream.kind = realStream.kind;
|
|
1681
|
+
stream.roles = realStream.roles;
|
|
1682
|
+
stream.mimeType = realStream.mimeType;
|
|
1683
|
+
|
|
1684
|
+
// Since we lazy-loaded this content, the player may need to create new
|
|
1685
|
+
// sessions for the DRM info in this stream.
|
|
1686
|
+
if (stream.drmInfos.length) {
|
|
1687
|
+
this.playerInterface_.newDrmInfo(stream);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
1691
|
+
if (type == ContentType.VIDEO || type == ContentType.AUDIO) {
|
|
1692
|
+
for (const otherStreamInfo of this.uriToStreamInfosMap_.values()) {
|
|
1693
|
+
if (!otherStreamInfo.loadedOnce && otherStreamInfo.type == type) {
|
|
1694
|
+
// To aid manifest filtering, assume before loading that all video
|
|
1695
|
+
// renditions have the same MIME type. (And likewise for audio.)
|
|
1696
|
+
otherStreamInfo.stream.mimeType = realStream.mimeType;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// Add finishing touches to the stream that can only be done once we have
|
|
1702
|
+
// more full context on the media as a whole.
|
|
1703
|
+
if (this.hasEnoughInfoToFinalizeStreams_()) {
|
|
1704
|
+
if (!this.streamsFinalized_) {
|
|
1705
|
+
// Mark this manifest as having been finalized, so we don't go through
|
|
1706
|
+
// this whole process of finishing touches a second time.
|
|
1707
|
+
this.streamsFinalized_ = true;
|
|
1708
|
+
// Finalize all of the currently-loaded streams.
|
|
1709
|
+
const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
|
|
1710
|
+
const activeStreamInfos =
|
|
1711
|
+
streamInfos.filter((s) => s.stream.segmentIndex);
|
|
1712
|
+
this.finalizeStreams_(activeStreamInfos);
|
|
1713
|
+
// With the addition of this new stream, we now have enough info to
|
|
1714
|
+
// figure out how long the streams should be. So process all streams
|
|
1715
|
+
// we have downloaded up until this point.
|
|
1716
|
+
this.determineDuration_();
|
|
1717
|
+
// Finally, start the update timer, if this asset has been determined
|
|
1718
|
+
// to be a livestream.
|
|
1719
|
+
const delay = this.updatePlaylistDelay_;
|
|
1720
|
+
if (delay > 0) {
|
|
1721
|
+
this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
|
|
1722
|
+
}
|
|
1723
|
+
} else {
|
|
1724
|
+
// We don't need to go through the full process; just finalize this
|
|
1725
|
+
// single stream.
|
|
1726
|
+
this.finalizeStreams_([streamInfo]);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
|
|
1731
|
+
/** @type {Promise} */
|
|
1732
|
+
let creationPromise = null;
|
|
1733
|
+
/** @type {!AbortController} */
|
|
1734
|
+
let abortController = new AbortController();
|
|
1735
|
+
const safeCreateSegmentIndex = () => {
|
|
1736
|
+
// An operation is already in progress. The second and subsequent
|
|
1737
|
+
// callers receive the same Promise as the first caller, and only one
|
|
1738
|
+
// download operation will occur.
|
|
1739
|
+
if (creationPromise) {
|
|
1740
|
+
return creationPromise;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Create a new AbortController to be able to cancel this specific
|
|
1744
|
+
// download.
|
|
1745
|
+
abortController = new AbortController();
|
|
1746
|
+
|
|
1747
|
+
// Create a Promise tied to the outcome of downloadSegmentIndex(). If
|
|
1748
|
+
// downloadSegmentIndex is rejected, creationPromise will also be
|
|
1749
|
+
// rejected.
|
|
1750
|
+
creationPromise = new Promise((resolve) => {
|
|
1751
|
+
resolve(downloadSegmentIndex(abortController.signal));
|
|
1752
|
+
});
|
|
1753
|
+
return creationPromise;
|
|
1754
|
+
};
|
|
1755
|
+
|
|
1756
|
+
stream.createSegmentIndex = safeCreateSegmentIndex;
|
|
1757
|
+
|
|
1758
|
+
stream.closeSegmentIndex = () => {
|
|
1759
|
+
// If we're mid-creation, cancel it.
|
|
1760
|
+
if (creationPromise && !stream.segmentIndex) {
|
|
1761
|
+
abortController.abort();
|
|
1762
|
+
}
|
|
1763
|
+
// If we have a segment index, release it.
|
|
1764
|
+
if (stream.segmentIndex) {
|
|
1765
|
+
stream.segmentIndex.release();
|
|
1766
|
+
stream.segmentIndex = null;
|
|
1767
|
+
}
|
|
1768
|
+
// Clear the creation Promise so that a new operation can begin.
|
|
1769
|
+
creationPromise = null;
|
|
1770
|
+
};
|
|
1771
|
+
|
|
1772
|
+
return streamInfo;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
/**
|
|
1776
|
+
* @return {number}
|
|
1777
|
+
* @private
|
|
1778
|
+
*/
|
|
1779
|
+
getMinDuration_() {
|
|
1780
|
+
let minDuration = Infinity;
|
|
1781
|
+
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
|
|
1782
|
+
if (streamInfo.stream.segmentIndex && streamInfo.stream.type != 'text') {
|
|
1783
|
+
// Since everything is already offset to 0 (either by sync or by being
|
|
1784
|
+
// VOD), only maxTimestamp is necessary to compute the duration.
|
|
1785
|
+
minDuration = Math.min(minDuration, streamInfo.maxTimestamp);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return minDuration;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* @param {!Array.<!shaka.extern.Stream>} streams
|
|
1793
|
+
* @private
|
|
1794
|
+
*/
|
|
1795
|
+
notifySegmentsForStreams_(streams) {
|
|
1796
|
+
const references = [];
|
|
1797
|
+
for (const stream of streams) {
|
|
1798
|
+
if (!stream.segmentIndex) {
|
|
1799
|
+
// The stream was closed since the list of streams was built.
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
stream.segmentIndex.forEachTopLevelReference((reference) => {
|
|
1803
|
+
references.push(reference);
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
this.presentationTimeline_.notifySegments(references);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* @param {!Array.<!shaka.hls.HlsParser.StreamInfo>} streamInfos
|
|
1811
|
+
* @private
|
|
1812
|
+
*/
|
|
1813
|
+
finalizeStreams_(streamInfos) {
|
|
1814
|
+
if (!this.isLive_()) {
|
|
1815
|
+
const minDuration = this.getMinDuration_();
|
|
1816
|
+
for (const streamInfo of streamInfos) {
|
|
1817
|
+
streamInfo.stream.segmentIndex.fit(/* periodStart= */ 0, minDuration);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
// MediaSource expects no codec strings combined with raw formats.
|
|
1821
|
+
for (const streamInfo of streamInfos) {
|
|
1822
|
+
const stream = streamInfo.stream;
|
|
1823
|
+
if (shaka.media.MediaSourceEngine.RAW_FORMATS.includes(stream.mimeType)) {
|
|
1824
|
+
stream.codecs = '';
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
this.notifySegmentsForStreams_(streamInfos.map((s) => s.stream));
|
|
1828
|
+
if (this.config_.hls.ignoreManifestProgramDateTime) {
|
|
1829
|
+
this.syncStreamsWithSequenceNumber_(streamInfos);
|
|
1830
|
+
} else {
|
|
1831
|
+
this.syncStreamsWithProgramDateTime_(streamInfos);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1553
1834
|
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1835
|
+
/**
|
|
1836
|
+
* There are some values on streams that can only be set once we know about
|
|
1837
|
+
* both the video and audio content, if present.
|
|
1838
|
+
* This checks if there is at least one video downloaded (if the media has
|
|
1839
|
+
* video), and that there is at least one audio downloaded (if the media has
|
|
1840
|
+
* audio).
|
|
1841
|
+
* @return {boolean}
|
|
1842
|
+
* @private
|
|
1843
|
+
*/
|
|
1844
|
+
hasEnoughInfoToFinalizeStreams_() {
|
|
1845
|
+
if (!this.manifest_) {
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
const videos = [];
|
|
1849
|
+
const audios = [];
|
|
1850
|
+
for (const variant of this.manifest_.variants) {
|
|
1851
|
+
if (variant.video) {
|
|
1852
|
+
videos.push(variant.video);
|
|
1853
|
+
}
|
|
1854
|
+
if (variant.audio) {
|
|
1855
|
+
audios.push(variant.audio);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
if (videos.length > 0 && !videos.some((stream) => stream.segmentIndex)) {
|
|
1859
|
+
return false;
|
|
1860
|
+
}
|
|
1861
|
+
if (audios.length > 0 && !audios.some((stream) => stream.segmentIndex)) {
|
|
1862
|
+
return false;
|
|
1863
|
+
}
|
|
1864
|
+
return true;
|
|
1558
1865
|
}
|
|
1559
1866
|
|
|
1560
1867
|
/**
|
|
@@ -1604,51 +1911,8 @@ shaka.hls.HlsParser = class {
|
|
|
1604
1911
|
mediaVariables);
|
|
1605
1912
|
}
|
|
1606
1913
|
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
if (playlist.segments) {
|
|
1610
|
-
for (const segment of playlist.segments) {
|
|
1611
|
-
const segmentKeyTags = shaka.hls.Utils.filterTagsByName(segment.tags,
|
|
1612
|
-
'EXT-X-KEY');
|
|
1613
|
-
drmTags.push(...segmentKeyTags);
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
let encrypted = false;
|
|
1618
|
-
let aesEncrypted = false;
|
|
1619
|
-
|
|
1620
|
-
/** @type {!Array.<shaka.extern.DrmInfo>}*/
|
|
1621
|
-
const drmInfos = [];
|
|
1622
|
-
const keyIds = new Set();
|
|
1623
|
-
|
|
1624
|
-
// TODO: May still need changes to support key rotation.
|
|
1625
|
-
for (const drmTag of drmTags) {
|
|
1626
|
-
const method = drmTag.getRequiredAttrValue('METHOD');
|
|
1627
|
-
if (method != 'NONE') {
|
|
1628
|
-
encrypted = true;
|
|
1629
|
-
|
|
1630
|
-
if (method == 'AES-128') {
|
|
1631
|
-
// These keys are handled separately.
|
|
1632
|
-
aesEncrypted = true;
|
|
1633
|
-
} else {
|
|
1634
|
-
const keyFormat = drmTag.getRequiredAttrValue('KEYFORMAT');
|
|
1635
|
-
const drmParser =
|
|
1636
|
-
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
|
|
1637
|
-
|
|
1638
|
-
const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
|
|
1639
|
-
if (drmInfo) {
|
|
1640
|
-
if (drmInfo.keyIds) {
|
|
1641
|
-
for (const keyId of drmInfo.keyIds) {
|
|
1642
|
-
keyIds.add(keyId);
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
drmInfos.push(drmInfo);
|
|
1646
|
-
} else {
|
|
1647
|
-
shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1914
|
+
const {drmInfos, keyIds, encrypted, aesEncrypted} =
|
|
1915
|
+
this.parseDrmInfo_(playlist, mimeType);
|
|
1652
1916
|
|
|
1653
1917
|
if (encrypted && !drmInfos.length && !aesEncrypted) {
|
|
1654
1918
|
throw new shaka.util.Error(
|
|
@@ -1657,16 +1921,10 @@ shaka.hls.HlsParser = class {
|
|
|
1657
1921
|
shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
|
|
1658
1922
|
}
|
|
1659
1923
|
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
codecs = '';
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
/** @type {!Map.<number, number>} */
|
|
1666
|
-
const mediaSequenceToStartTime = new Map();
|
|
1667
|
-
|
|
1924
|
+
const mediaSequenceToStartTime = this.isLive_() ?
|
|
1925
|
+
this.mediaSequenceToStartTimeByType_.get(type) : new Map();
|
|
1668
1926
|
const segments = this.createSegments_(verbatimMediaPlaylistUri, playlist,
|
|
1669
|
-
type, mimeType, mediaSequenceToStartTime, mediaVariables
|
|
1927
|
+
type, mimeType, mediaSequenceToStartTime, mediaVariables);
|
|
1670
1928
|
|
|
1671
1929
|
const lastEndTime = segments[segments.length - 1].endTime;
|
|
1672
1930
|
/** @type {!shaka.media.SegmentIndex} */
|
|
@@ -1687,18 +1945,67 @@ shaka.hls.HlsParser = class {
|
|
|
1687
1945
|
const canSkipSegments = serverControlTag ?
|
|
1688
1946
|
serverControlTag.getAttribute('CAN-SKIP-UNTIL') != null : false;
|
|
1689
1947
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1948
|
+
const stream = this.makeStreamObject_(codecs, type, language, primary, name,
|
|
1949
|
+
channelsCount, closedCaptions, characteristics, forced, spatialAudio);
|
|
1950
|
+
stream.segmentIndex = segmentIndex;
|
|
1951
|
+
stream.encrypted = encrypted;
|
|
1952
|
+
stream.drmInfos = drmInfos;
|
|
1953
|
+
stream.keyIds = keyIds;
|
|
1954
|
+
stream.kind = kind;
|
|
1955
|
+
stream.roles = roles;
|
|
1956
|
+
stream.mimeType = mimeType;
|
|
1957
|
+
|
|
1958
|
+
return {
|
|
1959
|
+
stream,
|
|
1960
|
+
type,
|
|
1961
|
+
verbatimMediaPlaylistUri,
|
|
1962
|
+
absoluteMediaPlaylistUri,
|
|
1963
|
+
maxTimestamp: lastEndTime,
|
|
1964
|
+
canSkipSegments,
|
|
1965
|
+
hasEndList: false,
|
|
1966
|
+
firstSequenceNumber: -1,
|
|
1967
|
+
mediaSequenceToStartTime,
|
|
1968
|
+
loadedOnce: false,
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
/**
|
|
1974
|
+
* Creates a stream object with the given parameters.
|
|
1975
|
+
* The parameters that are passed into here are only the things that can be
|
|
1976
|
+
* known without downloading the media playlist; other values must be set
|
|
1977
|
+
* manually on the object after creation.
|
|
1978
|
+
* @param {string} codecs
|
|
1979
|
+
* @param {string} type
|
|
1980
|
+
* @param {string} language
|
|
1981
|
+
* @param {boolean} primary
|
|
1982
|
+
* @param {?string} name
|
|
1983
|
+
* @param {?number} channelsCount
|
|
1984
|
+
* @param {Map.<string, string>} closedCaptions
|
|
1985
|
+
* @param {?string} characteristics
|
|
1986
|
+
* @param {boolean} forced
|
|
1987
|
+
* @param {boolean} spatialAudio
|
|
1988
|
+
* @return {!shaka.extern.Stream}
|
|
1989
|
+
* @private
|
|
1990
|
+
*/
|
|
1991
|
+
makeStreamObject_(codecs, type, language, primary, name, channelsCount,
|
|
1992
|
+
closedCaptions, characteristics, forced, spatialAudio) {
|
|
1993
|
+
// Fill out a "best-guess" mimeType, for now. It will be replaced once the
|
|
1994
|
+
// stream is lazy-loaded.
|
|
1995
|
+
const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
|
|
1996
|
+
this.guessMimeTypeFallback_(type);
|
|
1997
|
+
|
|
1998
|
+
return {
|
|
1692
1999
|
id: this.globalId_++,
|
|
1693
2000
|
originalId: name,
|
|
1694
2001
|
createSegmentIndex: () => Promise.resolve(),
|
|
1695
|
-
segmentIndex,
|
|
2002
|
+
segmentIndex: null,
|
|
1696
2003
|
mimeType,
|
|
1697
2004
|
codecs,
|
|
1698
|
-
kind,
|
|
1699
|
-
encrypted,
|
|
1700
|
-
drmInfos,
|
|
1701
|
-
keyIds,
|
|
2005
|
+
kind: undefined,
|
|
2006
|
+
encrypted: false,
|
|
2007
|
+
drmInfos: [],
|
|
2008
|
+
keyIds: new Set(),
|
|
1702
2009
|
language,
|
|
1703
2010
|
label: name, // For historical reasons, since before "originalId".
|
|
1704
2011
|
type,
|
|
@@ -1711,27 +2018,80 @@ shaka.hls.HlsParser = class {
|
|
|
1711
2018
|
width: undefined,
|
|
1712
2019
|
height: undefined,
|
|
1713
2020
|
bandwidth: undefined,
|
|
1714
|
-
roles:
|
|
1715
|
-
forced
|
|
2021
|
+
roles: [],
|
|
2022
|
+
forced,
|
|
1716
2023
|
channelsCount,
|
|
1717
2024
|
audioSamplingRate: null,
|
|
1718
|
-
spatialAudio
|
|
2025
|
+
spatialAudio,
|
|
1719
2026
|
closedCaptions,
|
|
1720
2027
|
hdr: undefined,
|
|
1721
2028
|
tilesLayout: undefined,
|
|
1722
2029
|
};
|
|
1723
|
-
|
|
1724
|
-
return {
|
|
1725
|
-
stream,
|
|
1726
|
-
verbatimMediaPlaylistUri,
|
|
1727
|
-
absoluteMediaPlaylistUri,
|
|
1728
|
-
maxTimestamp: lastEndTime,
|
|
1729
|
-
mediaSequenceToStartTime,
|
|
1730
|
-
canSkipSegments,
|
|
1731
|
-
hasEndList: false,
|
|
1732
|
-
};
|
|
1733
2030
|
}
|
|
1734
2031
|
|
|
2032
|
+
/**
|
|
2033
|
+
* @param {!shaka.hls.Playlist} playlist
|
|
2034
|
+
* @param {string} mimeType
|
|
2035
|
+
* @return {{
|
|
2036
|
+
* drmInfos: !Array.<shaka.extern.DrmInfo>,
|
|
2037
|
+
* keyIds: !Set.<string>,
|
|
2038
|
+
* encrypted: boolean,
|
|
2039
|
+
* aesEncrypted: boolean
|
|
2040
|
+
* }}
|
|
2041
|
+
* @private
|
|
2042
|
+
*/
|
|
2043
|
+
parseDrmInfo_(playlist, mimeType) {
|
|
2044
|
+
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
2045
|
+
const drmTags = [];
|
|
2046
|
+
if (playlist.segments) {
|
|
2047
|
+
for (const segment of playlist.segments) {
|
|
2048
|
+
const segmentKeyTags = shaka.hls.Utils.filterTagsByName(segment.tags,
|
|
2049
|
+
'EXT-X-KEY');
|
|
2050
|
+
drmTags.push(...segmentKeyTags);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
let encrypted = false;
|
|
2055
|
+
let aesEncrypted = false;
|
|
2056
|
+
|
|
2057
|
+
/** @type {!Array.<shaka.extern.DrmInfo>}*/
|
|
2058
|
+
const drmInfos = [];
|
|
2059
|
+
const keyIds = new Set();
|
|
2060
|
+
|
|
2061
|
+
for (const drmTag of drmTags) {
|
|
2062
|
+
const method = drmTag.getRequiredAttrValue('METHOD');
|
|
2063
|
+
if (method != 'NONE') {
|
|
2064
|
+
encrypted = true;
|
|
2065
|
+
|
|
2066
|
+
if (method == 'AES-128') {
|
|
2067
|
+
// These keys are handled separately.
|
|
2068
|
+
aesEncrypted = true;
|
|
2069
|
+
} else {
|
|
2070
|
+
// According to the HLS spec, KEYFORMAT is optional and implicitly
|
|
2071
|
+
// defaults to "identity".
|
|
2072
|
+
// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
|
|
2073
|
+
const keyFormat =
|
|
2074
|
+
drmTag.getAttributeValue('KEYFORMAT') || 'identity';
|
|
2075
|
+
const drmParser =
|
|
2076
|
+
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
|
|
2077
|
+
|
|
2078
|
+
const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
|
|
2079
|
+
if (drmInfo) {
|
|
2080
|
+
if (drmInfo.keyIds) {
|
|
2081
|
+
for (const keyId of drmInfo.keyIds) {
|
|
2082
|
+
keyIds.add(keyId);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
drmInfos.push(drmInfo);
|
|
2086
|
+
} else {
|
|
2087
|
+
shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
return {drmInfos, keyIds, encrypted, aesEncrypted};
|
|
2094
|
+
}
|
|
1735
2095
|
|
|
1736
2096
|
/**
|
|
1737
2097
|
* @param {!shaka.hls.Tag} drmTag
|
|
@@ -1872,39 +2232,34 @@ shaka.hls.HlsParser = class {
|
|
|
1872
2232
|
/**
|
|
1873
2233
|
* @private
|
|
1874
2234
|
*/
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
presentationDelay = this.lowLatencyPresentationDelay_;
|
|
1896
|
-
} else {
|
|
1897
|
-
presentationDelay = this.maxTargetDuration_ * 3;
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
this.presentationTimeline_ = new shaka.media.PresentationTimeline(
|
|
1901
|
-
/* presentationStartTime= */ 0, /* delay= */ presentationDelay);
|
|
1902
|
-
this.presentationTimeline_.setStatic(false);
|
|
2235
|
+
changePresentationTimelineToLive_() {
|
|
2236
|
+
// The live edge will be calculated from segments, so we don't need to
|
|
2237
|
+
// set a presentation start time. We will assert later that this is
|
|
2238
|
+
// working as expected.
|
|
2239
|
+
|
|
2240
|
+
// The HLS spec (RFC 8216) states in 6.3.3:
|
|
2241
|
+
//
|
|
2242
|
+
// "The client SHALL choose which Media Segment to play first ... the
|
|
2243
|
+
// client SHOULD NOT choose a segment that starts less than three target
|
|
2244
|
+
// durations from the end of the Playlist file. Doing so can trigger
|
|
2245
|
+
// playback stalls."
|
|
2246
|
+
//
|
|
2247
|
+
// We accomplish this in our DASH-y model by setting a presentation
|
|
2248
|
+
// delay of configured value, or 3 segments duration if not configured.
|
|
2249
|
+
// This will be the "live edge" of the presentation.
|
|
2250
|
+
let presentationDelay;
|
|
2251
|
+
if (this.config_.defaultPresentationDelay) {
|
|
2252
|
+
presentationDelay = this.config_.defaultPresentationDelay;
|
|
2253
|
+
} else if (this.lowLatencyPresentationDelay_) {
|
|
2254
|
+
presentationDelay = this.lowLatencyPresentationDelay_;
|
|
1903
2255
|
} else {
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
this.presentationTimeline_.setStatic(true);
|
|
2256
|
+
const numberOfSegments = this.config_.hls.liveSegmentsDelay;
|
|
2257
|
+
presentationDelay = this.maxTargetDuration_ * numberOfSegments;
|
|
1907
2258
|
}
|
|
2259
|
+
|
|
2260
|
+
this.presentationTimeline_.setPresentationStartTime(0);
|
|
2261
|
+
this.presentationTimeline_.setDelay(presentationDelay);
|
|
2262
|
+
this.presentationTimeline_.setStatic(false);
|
|
1908
2263
|
}
|
|
1909
2264
|
|
|
1910
2265
|
/**
|
|
@@ -2195,19 +2550,6 @@ shaka.hls.HlsParser = class {
|
|
|
2195
2550
|
return [startByte, endByte];
|
|
2196
2551
|
}
|
|
2197
2552
|
|
|
2198
|
-
/** @private */
|
|
2199
|
-
notifySegments_() {
|
|
2200
|
-
// The presentation timeline may or may not be set yet.
|
|
2201
|
-
// If it does not yet exist, hold onto the segments until it does.
|
|
2202
|
-
if (!this.presentationTimeline_) {
|
|
2203
|
-
return;
|
|
2204
|
-
}
|
|
2205
|
-
for (const segments of this.segmentsToNotifyByStream_) {
|
|
2206
|
-
this.presentationTimeline_.notifySegments(segments);
|
|
2207
|
-
}
|
|
2208
|
-
this.segmentsToNotifyByStream_ = [];
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
2553
|
/**
|
|
2212
2554
|
* Parses shaka.hls.Segment objects into shaka.media.SegmentReferences.
|
|
2213
2555
|
*
|
|
@@ -2217,12 +2559,11 @@ shaka.hls.HlsParser = class {
|
|
|
2217
2559
|
* @param {string} mimeType
|
|
2218
2560
|
* @param {!Map.<number, number>} mediaSequenceToStartTime
|
|
2219
2561
|
* @param {!Map.<string, string>} variables
|
|
2220
|
-
* @param {string} codecs
|
|
2221
2562
|
* @return {!Array.<!shaka.media.SegmentReference>}
|
|
2222
2563
|
* @private
|
|
2223
2564
|
*/
|
|
2224
2565
|
createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType,
|
|
2225
|
-
mediaSequenceToStartTime, variables
|
|
2566
|
+
mediaSequenceToStartTime, variables) {
|
|
2226
2567
|
/** @type {Array.<!shaka.hls.Segment>} */
|
|
2227
2568
|
const hlsSegments = playlist.segments;
|
|
2228
2569
|
goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
|
|
@@ -2425,6 +2766,64 @@ shaka.hls.HlsParser = class {
|
|
|
2425
2766
|
*
|
|
2426
2767
|
* @param {string} contentType
|
|
2427
2768
|
* @param {string} codecs
|
|
2769
|
+
* @return {?string}
|
|
2770
|
+
* @private
|
|
2771
|
+
*/
|
|
2772
|
+
guessMimeTypeBeforeLoading_(contentType, codecs) {
|
|
2773
|
+
if (contentType == shaka.util.ManifestParserUtils.ContentType.TEXT) {
|
|
2774
|
+
if (codecs == 'vtt' || codecs == 'wvtt') {
|
|
2775
|
+
// If codecs is 'vtt', it's WebVTT.
|
|
2776
|
+
return 'text/vtt';
|
|
2777
|
+
} else if (codecs && codecs !== '') {
|
|
2778
|
+
// Otherwise, assume MP4-embedded text, since text-based formats tend
|
|
2779
|
+
// not to have a codecs string at all.
|
|
2780
|
+
return 'application/mp4';
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
if (contentType == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
|
|
2785
|
+
if (!codecs || codecs == 'jpeg') {
|
|
2786
|
+
return 'image/jpeg';
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
if (contentType == shaka.util.ManifestParserUtils.ContentType.AUDIO) {
|
|
2791
|
+
// See: https://bugs.chromium.org/p/chromium/issues/detail?id=489520
|
|
2792
|
+
if (codecs == 'mp4a.40.34') {
|
|
2793
|
+
return 'audio/mpeg';
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
// Not enough information to guess from the content type and codecs.
|
|
2798
|
+
return null;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
/**
|
|
2802
|
+
* Get a fallback mime type for the content. Used if all the better methods
|
|
2803
|
+
* for determining the mime type have failed.
|
|
2804
|
+
*
|
|
2805
|
+
* @param {string} contentType
|
|
2806
|
+
* @return {string}
|
|
2807
|
+
* @private
|
|
2808
|
+
*/
|
|
2809
|
+
guessMimeTypeFallback_(contentType) {
|
|
2810
|
+
if (contentType == shaka.util.ManifestParserUtils.ContentType.TEXT) {
|
|
2811
|
+
// If there was no codecs string and no content-type, assume HLS text
|
|
2812
|
+
// streams are WebVTT.
|
|
2813
|
+
return 'text/vtt';
|
|
2814
|
+
}
|
|
2815
|
+
// If the HLS content is lacking in both MIME type metadata and
|
|
2816
|
+
// segment file extensions, we fall back to assuming it's MP4.
|
|
2817
|
+
const map = shaka.hls.HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
|
|
2818
|
+
return map['mp4'];
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
/**
|
|
2822
|
+
* Attempts to guess stream's mime type based on content type, URI, and
|
|
2823
|
+
* contents of the playlist.
|
|
2824
|
+
*
|
|
2825
|
+
* @param {string} contentType
|
|
2826
|
+
* @param {string} codecs
|
|
2428
2827
|
* @param {!shaka.hls.Playlist} playlist
|
|
2429
2828
|
* @param {!Map.<string, string>} variables
|
|
2430
2829
|
* @return {!Promise.<string>}
|
|
@@ -2432,7 +2831,6 @@ shaka.hls.HlsParser = class {
|
|
|
2432
2831
|
*/
|
|
2433
2832
|
async guessMimeType_(contentType, codecs, playlist, variables) {
|
|
2434
2833
|
const HlsParser = shaka.hls.HlsParser;
|
|
2435
|
-
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
2436
2834
|
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
2437
2835
|
|
|
2438
2836
|
// If you wait long enough, requesting the first segment can fail
|
|
@@ -2458,22 +2856,10 @@ shaka.hls.HlsParser = class {
|
|
|
2458
2856
|
return mimeType;
|
|
2459
2857
|
}
|
|
2460
2858
|
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
return 'text/vtt';
|
|
2466
|
-
} else if (codecs && codecs !== '') {
|
|
2467
|
-
// Otherwise, assume MP4-embedded text, since text-based formats tend
|
|
2468
|
-
// not to have a codecs string at all.
|
|
2469
|
-
return 'application/mp4';
|
|
2470
|
-
}
|
|
2471
|
-
}
|
|
2472
|
-
|
|
2473
|
-
if (contentType == ContentType.IMAGE) {
|
|
2474
|
-
if (!codecs || codecs == 'jpeg') {
|
|
2475
|
-
return 'image/jpeg';
|
|
2476
|
-
}
|
|
2859
|
+
// The extension map didn't work, so guess based on codecs.
|
|
2860
|
+
mimeType = this.guessMimeTypeBeforeLoading_(contentType, codecs);
|
|
2861
|
+
if (mimeType) {
|
|
2862
|
+
return mimeType;
|
|
2477
2863
|
}
|
|
2478
2864
|
|
|
2479
2865
|
// If unable to guess mime type, request a segment and try getting it
|
|
@@ -2486,21 +2872,12 @@ shaka.hls.HlsParser = class {
|
|
|
2486
2872
|
headRequest, requestType);
|
|
2487
2873
|
|
|
2488
2874
|
const contentMimeType = response.headers['content-type'];
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
// If there was no codecs string and no content-type, assume HLS text
|
|
2493
|
-
// streams are WebVTT.
|
|
2494
|
-
return 'text/vtt';
|
|
2495
|
-
}
|
|
2496
|
-
// If the HLS content is lacking in both MIME type metadata and
|
|
2497
|
-
// segment file extensions, we fall back to assuming it's MP4.
|
|
2498
|
-
const fallbackMimeType = map['mp4'];
|
|
2499
|
-
return fallbackMimeType;
|
|
2875
|
+
if (contentMimeType) {
|
|
2876
|
+
// Split the MIME type in case the server sent additional parameters.
|
|
2877
|
+
return contentMimeType.split(';')[0];
|
|
2500
2878
|
}
|
|
2501
2879
|
|
|
2502
|
-
|
|
2503
|
-
return contentMimeType.split(';')[0];
|
|
2880
|
+
return this.guessMimeTypeFallback_(contentType);
|
|
2504
2881
|
}
|
|
2505
2882
|
|
|
2506
2883
|
/**
|
|
@@ -2690,7 +3067,7 @@ shaka.hls.HlsParser = class {
|
|
|
2690
3067
|
*/
|
|
2691
3068
|
const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
|
|
2692
3069
|
'com.apple.fps', [
|
|
2693
|
-
{initDataType: 'sinf', initData: new Uint8Array(0)},
|
|
3070
|
+
{initDataType: 'sinf', initData: new Uint8Array(0), keyId: null},
|
|
2694
3071
|
]);
|
|
2695
3072
|
|
|
2696
3073
|
return drmInfo;
|
|
@@ -2769,18 +3146,48 @@ shaka.hls.HlsParser = class {
|
|
|
2769
3146
|
|
|
2770
3147
|
return drmInfo;
|
|
2771
3148
|
}
|
|
3149
|
+
|
|
3150
|
+
/**
|
|
3151
|
+
* See: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-5.1
|
|
3152
|
+
*
|
|
3153
|
+
* @param {!shaka.hls.Tag} drmTag
|
|
3154
|
+
* @return {?shaka.extern.DrmInfo}
|
|
3155
|
+
* @private
|
|
3156
|
+
*/
|
|
3157
|
+
static identityDrmParser_(drmTag) {
|
|
3158
|
+
const method = drmTag.getRequiredAttrValue('METHOD');
|
|
3159
|
+
const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR'];
|
|
3160
|
+
if (!VALID_METHODS.includes(method)) {
|
|
3161
|
+
shaka.log.error('Identity (ClearKey) in HLS is only supported with [',
|
|
3162
|
+
VALID_METHODS.join(', '), '], not', method);
|
|
3163
|
+
return null;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
// NOTE: The ClearKey CDM requires a key-id to key mapping. HLS doesn't
|
|
3167
|
+
// provide a key ID anywhere. So although we could use the 'URI' attribute
|
|
3168
|
+
// to fetch the actual 16-byte key, without a key ID, we can't provide this
|
|
3169
|
+
// automatically to the ClearKey CDM. Instead, the application will have
|
|
3170
|
+
// to use player.configure('drm.clearKeys', { ... }) to provide the key IDs
|
|
3171
|
+
// and keys or player.configure('drm.servers.org\.w3\.clearkey', ...) to
|
|
3172
|
+
// provide a ClearKey license server URI.
|
|
3173
|
+
return shaka.util.ManifestParserUtils.createDrmInfo(
|
|
3174
|
+
'org.w3.clearkey', /* initDatas= */ null);
|
|
3175
|
+
}
|
|
2772
3176
|
};
|
|
2773
3177
|
|
|
2774
3178
|
|
|
2775
3179
|
/**
|
|
2776
3180
|
* @typedef {{
|
|
2777
3181
|
* stream: !shaka.extern.Stream,
|
|
3182
|
+
* type: string,
|
|
2778
3183
|
* verbatimMediaPlaylistUri: string,
|
|
2779
3184
|
* absoluteMediaPlaylistUri: string,
|
|
2780
3185
|
* maxTimestamp: number,
|
|
2781
3186
|
* mediaSequenceToStartTime: !Map.<number, number>,
|
|
2782
3187
|
* canSkipSegments: boolean,
|
|
2783
|
-
* hasEndList: boolean
|
|
3188
|
+
* hasEndList: boolean,
|
|
3189
|
+
* firstSequenceNumber: number,
|
|
3190
|
+
* loadedOnce: boolean
|
|
2784
3191
|
* }}
|
|
2785
3192
|
*
|
|
2786
3193
|
* @description
|
|
@@ -2788,6 +3195,8 @@ shaka.hls.HlsParser = class {
|
|
|
2788
3195
|
*
|
|
2789
3196
|
* @property {!shaka.extern.Stream} stream
|
|
2790
3197
|
* The Stream itself.
|
|
3198
|
+
* @property {string} type
|
|
3199
|
+
* The type value. Could be 'video', 'audio', 'text', or 'image'.
|
|
2791
3200
|
* @property {string} verbatimMediaPlaylistUri
|
|
2792
3201
|
* The verbatim media playlist URI, as it appeared in the master playlist.
|
|
2793
3202
|
* This has not been canonicalized into an absolute URI. This gives us a
|
|
@@ -2800,11 +3209,16 @@ shaka.hls.HlsParser = class {
|
|
|
2800
3209
|
* The maximum timestamp found in the stream.
|
|
2801
3210
|
* @property {!Map.<number, number>} mediaSequenceToStartTime
|
|
2802
3211
|
* A map of media sequence numbers to media start times.
|
|
3212
|
+
* Only used for VOD content.
|
|
2803
3213
|
* @property {boolean} canSkipSegments
|
|
2804
|
-
*
|
|
2805
|
-
*
|
|
3214
|
+
* True if the server supports delta playlist updates, and we can send a
|
|
3215
|
+
* request for a playlist that can skip older media segments.
|
|
2806
3216
|
* @property {boolean} hasEndList
|
|
2807
|
-
*
|
|
3217
|
+
* True if the stream has an EXT-X-ENDLIST tag.
|
|
3218
|
+
* @property {number} firstSequenceNumber
|
|
3219
|
+
* The sequence number of the first reference. Only calculated if needed.
|
|
3220
|
+
* @property {boolean} loadedOnce
|
|
3221
|
+
* True if the stream has been loaded at least once.
|
|
2808
3222
|
*/
|
|
2809
3223
|
shaka.hls.HlsParser.StreamInfo;
|
|
2810
3224
|
|
|
@@ -2926,6 +3340,8 @@ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = {
|
|
|
2926
3340
|
shaka.hls.HlsParser.widevineDrmParser_,
|
|
2927
3341
|
'com.microsoft.playready':
|
|
2928
3342
|
shaka.hls.HlsParser.playreadyDrmParser_,
|
|
3343
|
+
'identity':
|
|
3344
|
+
shaka.hls.HlsParser.identityDrmParser_,
|
|
2929
3345
|
};
|
|
2930
3346
|
|
|
2931
3347
|
|