shaka-player 4.2.3 → 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 +5 -0
- package/CHANGELOG.md +85 -39
- package/CONTRIBUTORS +5 -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 +18 -11
- 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 +178 -27
- package/dist/shaka-player.compiled.debug.d.ts +178 -27
- package/dist/shaka-player.compiled.debug.externs.js +163 -32
- package/dist/shaka-player.compiled.debug.js +488 -422
- package/dist/shaka-player.compiled.debug.map +4 -4
- package/dist/shaka-player.compiled.externs.js +163 -32
- package/dist/shaka-player.compiled.js +895 -857
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.d.ts +178 -27
- package/dist/shaka-player.ui.debug.d.ts +178 -27
- package/dist/shaka-player.ui.debug.externs.js +163 -32
- package/dist/shaka-player.ui.debug.js +519 -450
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +163 -32
- package/dist/shaka-player.ui.js +1053 -1015
- 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 +4 -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_proxy.js +2 -1
- package/lib/cast/cast_receiver.js +19 -5
- package/lib/cast/cast_sender.js +2 -1
- package/lib/cast/cast_utils.js +18 -6
- 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 +799 -376
- 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 +112 -26
- package/lib/media/playhead.js +23 -1
- package/lib/media/presentation_timeline.js +26 -11
- package/lib/media/segment_reference.js +49 -1
- package/lib/media/streaming_engine.js +145 -24
- package/lib/media/transmuxer.js +79 -18
- package/lib/offline/storage.js +1 -0
- package/lib/player.js +477 -85
- 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/manifest_parser_utils.js +4 -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 +27 -7
- package/test/ads/ad_manager_unit.js +180 -29
- package/test/cast/cast_receiver_integration.js +97 -16
- package/test/cast/cast_receiver_unit.js +17 -12
- package/test/cast/cast_utils_unit.js +4 -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 +102 -6
- package/test/media/media_source_engine_unit.js +10 -5
- package/test/media/playhead_unit.js +41 -0
- package/test/media/streaming_engine_integration.js +17 -1
- package/test/media/streaming_engine_unit.js +180 -24
- package/test/media/transmuxer_integration.js +21 -10
- package/test/player_integration.js +224 -82
- package/test/player_unit.js +92 -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 +7 -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/controls.js +4 -1
- package/ui/less/containers.less +7 -0
- package/ui/settings_menu.js +11 -2
- 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
|
/**
|
|
@@ -831,13 +875,19 @@ shaka.hls.HlsParser = class {
|
|
|
831
875
|
shaka.util.ArrayUtils.remove(allCodecs, textCodecs);
|
|
832
876
|
}
|
|
833
877
|
if (audioGroupId) {
|
|
834
|
-
|
|
878
|
+
let codecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
|
|
835
879
|
ContentType.AUDIO, allCodecs);
|
|
880
|
+
if (!codecs) {
|
|
881
|
+
codecs = this.config_.hls.defaultAudioCodec;
|
|
882
|
+
}
|
|
836
883
|
this.groupIdToCodecsMap_.set(audioGroupId, codecs);
|
|
837
884
|
}
|
|
838
885
|
if (videoGroupId) {
|
|
839
|
-
|
|
886
|
+
let codecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
|
|
840
887
|
ContentType.VIDEO, allCodecs);
|
|
888
|
+
if (!codecs) {
|
|
889
|
+
codecs = this.config_.hls.defaultVideoCodec;
|
|
890
|
+
}
|
|
841
891
|
this.groupIdToCodecsMap_.set(videoGroupId, codecs);
|
|
842
892
|
}
|
|
843
893
|
}
|
|
@@ -848,21 +898,20 @@ shaka.hls.HlsParser = class {
|
|
|
848
898
|
* Create text streams for Subtitles, but not Closed Captions.
|
|
849
899
|
*
|
|
850
900
|
* @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
|
|
851
|
-
* @return {!
|
|
901
|
+
* @return {!Array.<!shaka.extern.Stream>}
|
|
852
902
|
* @private
|
|
853
903
|
*/
|
|
854
|
-
|
|
904
|
+
parseTexts_(mediaTags) {
|
|
855
905
|
// Create text stream for each Subtitle media tag.
|
|
856
906
|
const subtitleTags =
|
|
857
907
|
shaka.hls.Utils.filterTagsByType(mediaTags, 'SUBTITLES');
|
|
858
|
-
const
|
|
908
|
+
const textStreams = subtitleTags.map((tag) => {
|
|
859
909
|
const disableText = this.config_.disableText;
|
|
860
910
|
if (disableText) {
|
|
861
911
|
return null;
|
|
862
912
|
}
|
|
863
913
|
try {
|
|
864
|
-
|
|
865
|
-
return streamInfo.stream;
|
|
914
|
+
return this.createStreamInfoFromMediaTag_(tag).stream;
|
|
866
915
|
} catch (e) {
|
|
867
916
|
if (this.config_.hls.ignoreTextStreamFailures) {
|
|
868
917
|
return null;
|
|
@@ -870,7 +919,6 @@ shaka.hls.HlsParser = class {
|
|
|
870
919
|
throw e;
|
|
871
920
|
}
|
|
872
921
|
});
|
|
873
|
-
const textStreams = await Promise.all(textStreamPromises);
|
|
874
922
|
|
|
875
923
|
// Set the codecs for text streams.
|
|
876
924
|
for (const tag of subtitleTags) {
|
|
@@ -920,7 +968,7 @@ shaka.hls.HlsParser = class {
|
|
|
920
968
|
* @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
|
|
921
969
|
* @private
|
|
922
970
|
*/
|
|
923
|
-
|
|
971
|
+
createStreamInfosFromMediaTags_(mediaTags) {
|
|
924
972
|
// Filter out subtitles and media tags without uri.
|
|
925
973
|
mediaTags = mediaTags.filter((tag) => {
|
|
926
974
|
const uri = tag.getAttributeValue('URI') || '';
|
|
@@ -929,20 +977,52 @@ shaka.hls.HlsParser = class {
|
|
|
929
977
|
});
|
|
930
978
|
|
|
931
979
|
// Create stream info for each audio / video media tag.
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
}
|
|
935
|
-
await Promise.all(promises);
|
|
980
|
+
for (const tag of mediaTags) {
|
|
981
|
+
this.createStreamInfoFromMediaTag_(tag);
|
|
982
|
+
}
|
|
936
983
|
}
|
|
937
984
|
|
|
938
985
|
/**
|
|
939
986
|
* @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
|
|
940
|
-
* @
|
|
987
|
+
* @param {!Array.<!shaka.hls.Tag>} sessionKeyTags EXT-X-SESSION-KEY tags
|
|
988
|
+
* from the playlist.
|
|
989
|
+
* @return {!Array.<!shaka.extern.Variant>}
|
|
941
990
|
* @private
|
|
942
991
|
*/
|
|
943
|
-
|
|
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
|
+
|
|
944
1024
|
// Create variants for each variant tag.
|
|
945
|
-
const
|
|
1025
|
+
const allVariants = tags.map((tag) => {
|
|
946
1026
|
const frameRate = tag.getAttributeValue('FRAME-RATE');
|
|
947
1027
|
const bandwidth = Number(tag.getAttributeValue('AVERAGE-BANDWIDTH')) ||
|
|
948
1028
|
Number(tag.getRequiredAttrValue('BANDWIDTH'));
|
|
@@ -952,7 +1032,7 @@ shaka.hls.HlsParser = class {
|
|
|
952
1032
|
|
|
953
1033
|
const videoRange = tag.getAttributeValue('VIDEO-RANGE');
|
|
954
1034
|
|
|
955
|
-
const streamInfos =
|
|
1035
|
+
const streamInfos = this.createStreamInfosForVariantTag_(tag,
|
|
956
1036
|
resolution, frameRate);
|
|
957
1037
|
|
|
958
1038
|
goog.asserts.assert(streamInfos.audio.length ||
|
|
@@ -965,10 +1045,10 @@ shaka.hls.HlsParser = class {
|
|
|
965
1045
|
width,
|
|
966
1046
|
height,
|
|
967
1047
|
frameRate,
|
|
968
|
-
videoRange
|
|
1048
|
+
videoRange,
|
|
1049
|
+
drmInfos,
|
|
1050
|
+
keyIds);
|
|
969
1051
|
});
|
|
970
|
-
|
|
971
|
-
const allVariants = await Promise.all(variantsPromises);
|
|
972
1052
|
let variants = allVariants.reduce(shaka.util.Functional.collapseArrays, []);
|
|
973
1053
|
// Filter out null variants.
|
|
974
1054
|
variants = variants.filter((variant) => variant != null);
|
|
@@ -982,10 +1062,10 @@ shaka.hls.HlsParser = class {
|
|
|
982
1062
|
* @param {!shaka.hls.Tag} tag
|
|
983
1063
|
* @param {?string} resolution
|
|
984
1064
|
* @param {?string} frameRate
|
|
985
|
-
* @return {!
|
|
1065
|
+
* @return {!shaka.hls.HlsParser.StreamInfos}
|
|
986
1066
|
* @private
|
|
987
1067
|
*/
|
|
988
|
-
|
|
1068
|
+
createStreamInfosForVariantTag_(tag, resolution, frameRate) {
|
|
989
1069
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
990
1070
|
/** @type {!Array.<string>} */
|
|
991
1071
|
let allCodecs = this.getCodecsForVariantTag_(tag);
|
|
@@ -1054,10 +1134,9 @@ shaka.hls.HlsParser = class {
|
|
|
1054
1134
|
|
|
1055
1135
|
if (!ignoreStream) {
|
|
1056
1136
|
const streamInfo =
|
|
1057
|
-
|
|
1137
|
+
this.createStreamInfoFromVariantTag_(tag, allCodecs, type);
|
|
1058
1138
|
res[streamInfo.stream.type] = [streamInfo];
|
|
1059
1139
|
}
|
|
1060
|
-
this.filterLegacyCodecs_(res);
|
|
1061
1140
|
return res;
|
|
1062
1141
|
}
|
|
1063
1142
|
|
|
@@ -1188,7 +1267,7 @@ shaka.hls.HlsParser = class {
|
|
|
1188
1267
|
|
|
1189
1268
|
/**
|
|
1190
1269
|
* Get the type value.
|
|
1191
|
-
* Shaka recognizes the content types 'audio', 'video' and '
|
|
1270
|
+
* Shaka recognizes the content types 'audio', 'video', 'text', and 'image'.
|
|
1192
1271
|
* The HLS 'subtitles' type needs to be mapped to 'text'.
|
|
1193
1272
|
* @param {!shaka.hls.Tag} tag
|
|
1194
1273
|
* @return {string}
|
|
@@ -1202,28 +1281,6 @@ shaka.hls.HlsParser = class {
|
|
|
1202
1281
|
return type;
|
|
1203
1282
|
}
|
|
1204
1283
|
|
|
1205
|
-
/**
|
|
1206
|
-
* Filters out unsupported codec strings from an array of stream infos.
|
|
1207
|
-
* @param {shaka.hls.HlsParser.StreamInfos} streamInfos
|
|
1208
|
-
* @private
|
|
1209
|
-
*/
|
|
1210
|
-
filterLegacyCodecs_(streamInfos) {
|
|
1211
|
-
for (const streamInfo of streamInfos.audio.concat(streamInfos.video)) {
|
|
1212
|
-
if (!streamInfo) {
|
|
1213
|
-
continue;
|
|
1214
|
-
}
|
|
1215
|
-
let codecs = streamInfo.stream.codecs.split(',');
|
|
1216
|
-
codecs = codecs.filter((codec) => {
|
|
1217
|
-
// mp4a.40.34 is a nonstandard codec string that is sometimes used in
|
|
1218
|
-
// HLS for legacy reasons. It is not recognized by non-Apple MSE.
|
|
1219
|
-
// See https://bugs.chromium.org/p/chromium/issues/detail?id=489520
|
|
1220
|
-
// Therefore, ignore this codec string.
|
|
1221
|
-
return codec != 'mp4a.40.34';
|
|
1222
|
-
});
|
|
1223
|
-
streamInfo.stream.codecs = codecs.join(',');
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
1284
|
/**
|
|
1228
1285
|
* @param {!Array.<shaka.hls.HlsParser.StreamInfo>} audioInfos
|
|
1229
1286
|
* @param {!Array.<shaka.hls.HlsParser.StreamInfo>} videoInfos
|
|
@@ -1232,11 +1289,14 @@ shaka.hls.HlsParser = class {
|
|
|
1232
1289
|
* @param {?string} height
|
|
1233
1290
|
* @param {?string} frameRate
|
|
1234
1291
|
* @param {?string} videoRange
|
|
1292
|
+
* @param {!Array.<shaka.extern.DrmInfo>} drmInfos
|
|
1293
|
+
* @param {!Set.<string>} keyIds
|
|
1235
1294
|
* @return {!Array.<!shaka.extern.Variant>}
|
|
1236
1295
|
* @private
|
|
1237
1296
|
*/
|
|
1238
1297
|
createVariants_(
|
|
1239
|
-
audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange
|
|
1298
|
+
audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
|
|
1299
|
+
drmInfos, keyIds) {
|
|
1240
1300
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
1241
1301
|
const DrmEngine = shaka.media.DrmEngine;
|
|
1242
1302
|
|
|
@@ -1262,7 +1322,15 @@ shaka.hls.HlsParser = class {
|
|
|
1262
1322
|
for (const audioInfo of audioInfos) {
|
|
1263
1323
|
for (const videoInfo of videoInfos) {
|
|
1264
1324
|
const audioStream = audioInfo ? audioInfo.stream : null;
|
|
1325
|
+
if (audioStream) {
|
|
1326
|
+
audioStream.drmInfos = drmInfos;
|
|
1327
|
+
audioStream.keyIds = keyIds;
|
|
1328
|
+
}
|
|
1265
1329
|
const videoStream = videoInfo ? videoInfo.stream : null;
|
|
1330
|
+
if (videoStream) {
|
|
1331
|
+
videoStream.drmInfos = drmInfos;
|
|
1332
|
+
videoStream.keyIds = keyIds;
|
|
1333
|
+
}
|
|
1266
1334
|
const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
|
|
1267
1335
|
const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
|
|
1268
1336
|
const videoStreamUri =
|
|
@@ -1347,10 +1415,10 @@ shaka.hls.HlsParser = class {
|
|
|
1347
1415
|
* Parse EXT-X-MEDIA media tag into a Stream object.
|
|
1348
1416
|
*
|
|
1349
1417
|
* @param {shaka.hls.Tag} tag
|
|
1350
|
-
* @return {!
|
|
1418
|
+
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1351
1419
|
* @private
|
|
1352
1420
|
*/
|
|
1353
|
-
|
|
1421
|
+
createStreamInfoFromMediaTag_(tag) {
|
|
1354
1422
|
goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
|
|
1355
1423
|
'Should only be called on media tags!');
|
|
1356
1424
|
const groupId = tag.getRequiredAttrValue('GROUP-ID');
|
|
@@ -1392,7 +1460,7 @@ shaka.hls.HlsParser = class {
|
|
|
1392
1460
|
const forced = forcedAttrValue == 'YES';
|
|
1393
1461
|
// TODO: Should we take into account some of the currently ignored
|
|
1394
1462
|
// attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj
|
|
1395
|
-
const streamInfo =
|
|
1463
|
+
const streamInfo = this.createStreamInfo_(
|
|
1396
1464
|
verbatimMediaPlaylistUri, codecs, type, language, primary, name,
|
|
1397
1465
|
channelsCount, /* closedCaptions= */ null, characteristics, forced,
|
|
1398
1466
|
spatialAudio);
|
|
@@ -1439,7 +1507,7 @@ shaka.hls.HlsParser = class {
|
|
|
1439
1507
|
|
|
1440
1508
|
const characteristics = tag.getAttributeValue('CHARACTERISTICS');
|
|
1441
1509
|
|
|
1442
|
-
const streamInfo =
|
|
1510
|
+
const streamInfo = this.createStreamInfo_(
|
|
1443
1511
|
verbatimImagePlaylistUri, codecs, type, language, /* primary= */ false,
|
|
1444
1512
|
name, /* channelsCount= */ null, /* closedCaptions= */ null,
|
|
1445
1513
|
characteristics, /* forced= */ false, /* spatialAudio= */ false);
|
|
@@ -1457,6 +1525,10 @@ shaka.hls.HlsParser = class {
|
|
|
1457
1525
|
// of the entire sheet at once (like we expect in the output).
|
|
1458
1526
|
// So multiply by the layout size.
|
|
1459
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
|
+
|
|
1460
1532
|
const reference = streamInfo.stream.segmentIndex.get(0);
|
|
1461
1533
|
const layout = reference.getTilesLayout();
|
|
1462
1534
|
if (layout) {
|
|
@@ -1483,10 +1555,10 @@ shaka.hls.HlsParser = class {
|
|
|
1483
1555
|
* @param {!shaka.hls.Tag} tag
|
|
1484
1556
|
* @param {!Array.<string>} allCodecs
|
|
1485
1557
|
* @param {string} type
|
|
1486
|
-
* @return {!
|
|
1558
|
+
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1487
1559
|
* @private
|
|
1488
1560
|
*/
|
|
1489
|
-
|
|
1561
|
+
createStreamInfoFromVariantTag_(tag, allCodecs, type) {
|
|
1490
1562
|
goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
|
|
1491
1563
|
'Should only be called on variant tags!');
|
|
1492
1564
|
const verbatimMediaPlaylistUri = this.variableSubstitution_(
|
|
@@ -1498,7 +1570,7 @@ shaka.hls.HlsParser = class {
|
|
|
1498
1570
|
|
|
1499
1571
|
const closedCaptions = this.getClosedCaptions_(tag, type);
|
|
1500
1572
|
const codecs = shaka.util.ManifestParserUtils.guessCodecs(type, allCodecs);
|
|
1501
|
-
const streamInfo =
|
|
1573
|
+
const streamInfo = this.createStreamInfo_(verbatimMediaPlaylistUri,
|
|
1502
1574
|
codecs, type, /* language= */ 'und', /* primary= */ false,
|
|
1503
1575
|
/* name= */ null, /* channelcount= */ null, closedCaptions,
|
|
1504
1576
|
/* characteristics= */ null, /* forced= */ false,
|
|
@@ -1526,29 +1598,270 @@ shaka.hls.HlsParser = class {
|
|
|
1526
1598
|
* @param {?string} characteristics
|
|
1527
1599
|
* @param {boolean} forced
|
|
1528
1600
|
* @param {boolean} spatialAudio
|
|
1529
|
-
* @return {!
|
|
1601
|
+
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1530
1602
|
* @private
|
|
1531
1603
|
*/
|
|
1532
|
-
|
|
1604
|
+
createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language,
|
|
1533
1605
|
primary, name, channelsCount, closedCaptions, characteristics, forced,
|
|
1534
1606
|
spatialAudio) {
|
|
1535
1607
|
// TODO: Refactor, too many parameters
|
|
1536
|
-
|
|
1608
|
+
const initialMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
|
|
1537
1609
|
this.masterPlaylistUri_, verbatimMediaPlaylistUri);
|
|
1538
1610
|
|
|
1539
|
-
|
|
1540
|
-
//
|
|
1541
|
-
|
|
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
|
+
};
|
|
1542
1632
|
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
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
|
+
}
|
|
1547
1834
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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;
|
|
1552
1865
|
}
|
|
1553
1866
|
|
|
1554
1867
|
/**
|
|
@@ -1598,51 +1911,8 @@ shaka.hls.HlsParser = class {
|
|
|
1598
1911
|
mediaVariables);
|
|
1599
1912
|
}
|
|
1600
1913
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
if (playlist.segments) {
|
|
1604
|
-
for (const segment of playlist.segments) {
|
|
1605
|
-
const segmentKeyTags = shaka.hls.Utils.filterTagsByName(segment.tags,
|
|
1606
|
-
'EXT-X-KEY');
|
|
1607
|
-
drmTags.push(...segmentKeyTags);
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
let encrypted = false;
|
|
1612
|
-
let aesEncrypted = false;
|
|
1613
|
-
|
|
1614
|
-
/** @type {!Array.<shaka.extern.DrmInfo>}*/
|
|
1615
|
-
const drmInfos = [];
|
|
1616
|
-
const keyIds = new Set();
|
|
1617
|
-
|
|
1618
|
-
// TODO: May still need changes to support key rotation.
|
|
1619
|
-
for (const drmTag of drmTags) {
|
|
1620
|
-
const method = drmTag.getRequiredAttrValue('METHOD');
|
|
1621
|
-
if (method != 'NONE') {
|
|
1622
|
-
encrypted = true;
|
|
1623
|
-
|
|
1624
|
-
if (method == 'AES-128') {
|
|
1625
|
-
// These keys are handled separately.
|
|
1626
|
-
aesEncrypted = true;
|
|
1627
|
-
} else {
|
|
1628
|
-
const keyFormat = drmTag.getRequiredAttrValue('KEYFORMAT');
|
|
1629
|
-
const drmParser =
|
|
1630
|
-
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
|
|
1631
|
-
|
|
1632
|
-
const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
|
|
1633
|
-
if (drmInfo) {
|
|
1634
|
-
if (drmInfo.keyIds) {
|
|
1635
|
-
for (const keyId of drmInfo.keyIds) {
|
|
1636
|
-
keyIds.add(keyId);
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
drmInfos.push(drmInfo);
|
|
1640
|
-
} else {
|
|
1641
|
-
shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1914
|
+
const {drmInfos, keyIds, encrypted, aesEncrypted} =
|
|
1915
|
+
this.parseDrmInfo_(playlist, mimeType);
|
|
1646
1916
|
|
|
1647
1917
|
if (encrypted && !drmInfos.length && !aesEncrypted) {
|
|
1648
1918
|
throw new shaka.util.Error(
|
|
@@ -1651,16 +1921,10 @@ shaka.hls.HlsParser = class {
|
|
|
1651
1921
|
shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
|
|
1652
1922
|
}
|
|
1653
1923
|
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
codecs = '';
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
/** @type {!Map.<number, number>} */
|
|
1660
|
-
const mediaSequenceToStartTime = new Map();
|
|
1661
|
-
|
|
1924
|
+
const mediaSequenceToStartTime = this.isLive_() ?
|
|
1925
|
+
this.mediaSequenceToStartTimeByType_.get(type) : new Map();
|
|
1662
1926
|
const segments = this.createSegments_(verbatimMediaPlaylistUri, playlist,
|
|
1663
|
-
type, mimeType, mediaSequenceToStartTime, mediaVariables
|
|
1927
|
+
type, mimeType, mediaSequenceToStartTime, mediaVariables);
|
|
1664
1928
|
|
|
1665
1929
|
const lastEndTime = segments[segments.length - 1].endTime;
|
|
1666
1930
|
/** @type {!shaka.media.SegmentIndex} */
|
|
@@ -1681,18 +1945,67 @@ shaka.hls.HlsParser = class {
|
|
|
1681
1945
|
const canSkipSegments = serverControlTag ?
|
|
1682
1946
|
serverControlTag.getAttribute('CAN-SKIP-UNTIL') != null : false;
|
|
1683
1947
|
|
|
1684
|
-
|
|
1685
|
-
|
|
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 {
|
|
1686
1999
|
id: this.globalId_++,
|
|
1687
2000
|
originalId: name,
|
|
1688
2001
|
createSegmentIndex: () => Promise.resolve(),
|
|
1689
|
-
segmentIndex,
|
|
2002
|
+
segmentIndex: null,
|
|
1690
2003
|
mimeType,
|
|
1691
2004
|
codecs,
|
|
1692
|
-
kind,
|
|
1693
|
-
encrypted,
|
|
1694
|
-
drmInfos,
|
|
1695
|
-
keyIds,
|
|
2005
|
+
kind: undefined,
|
|
2006
|
+
encrypted: false,
|
|
2007
|
+
drmInfos: [],
|
|
2008
|
+
keyIds: new Set(),
|
|
1696
2009
|
language,
|
|
1697
2010
|
label: name, // For historical reasons, since before "originalId".
|
|
1698
2011
|
type,
|
|
@@ -1705,27 +2018,80 @@ shaka.hls.HlsParser = class {
|
|
|
1705
2018
|
width: undefined,
|
|
1706
2019
|
height: undefined,
|
|
1707
2020
|
bandwidth: undefined,
|
|
1708
|
-
roles:
|
|
1709
|
-
forced
|
|
2021
|
+
roles: [],
|
|
2022
|
+
forced,
|
|
1710
2023
|
channelsCount,
|
|
1711
2024
|
audioSamplingRate: null,
|
|
1712
|
-
spatialAudio
|
|
2025
|
+
spatialAudio,
|
|
1713
2026
|
closedCaptions,
|
|
1714
2027
|
hdr: undefined,
|
|
1715
2028
|
tilesLayout: undefined,
|
|
1716
2029
|
};
|
|
1717
|
-
|
|
1718
|
-
return {
|
|
1719
|
-
stream,
|
|
1720
|
-
verbatimMediaPlaylistUri,
|
|
1721
|
-
absoluteMediaPlaylistUri,
|
|
1722
|
-
maxTimestamp: lastEndTime,
|
|
1723
|
-
mediaSequenceToStartTime,
|
|
1724
|
-
canSkipSegments,
|
|
1725
|
-
hasEndList: false,
|
|
1726
|
-
};
|
|
1727
2030
|
}
|
|
1728
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
|
+
}
|
|
1729
2095
|
|
|
1730
2096
|
/**
|
|
1731
2097
|
* @param {!shaka.hls.Tag} drmTag
|
|
@@ -1866,39 +2232,34 @@ shaka.hls.HlsParser = class {
|
|
|
1866
2232
|
/**
|
|
1867
2233
|
* @private
|
|
1868
2234
|
*/
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
presentationDelay = this.lowLatencyPresentationDelay_;
|
|
1890
|
-
} else {
|
|
1891
|
-
presentationDelay = this.maxTargetDuration_ * 3;
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
this.presentationTimeline_ = new shaka.media.PresentationTimeline(
|
|
1895
|
-
/* presentationStartTime= */ 0, /* delay= */ presentationDelay);
|
|
1896
|
-
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_;
|
|
1897
2255
|
} else {
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
this.presentationTimeline_.setStatic(true);
|
|
2256
|
+
const numberOfSegments = this.config_.hls.liveSegmentsDelay;
|
|
2257
|
+
presentationDelay = this.maxTargetDuration_ * numberOfSegments;
|
|
1901
2258
|
}
|
|
2259
|
+
|
|
2260
|
+
this.presentationTimeline_.setPresentationStartTime(0);
|
|
2261
|
+
this.presentationTimeline_.setDelay(presentationDelay);
|
|
2262
|
+
this.presentationTimeline_.setStatic(false);
|
|
1902
2263
|
}
|
|
1903
2264
|
|
|
1904
2265
|
/**
|
|
@@ -2189,19 +2550,6 @@ shaka.hls.HlsParser = class {
|
|
|
2189
2550
|
return [startByte, endByte];
|
|
2190
2551
|
}
|
|
2191
2552
|
|
|
2192
|
-
/** @private */
|
|
2193
|
-
notifySegments_() {
|
|
2194
|
-
// The presentation timeline may or may not be set yet.
|
|
2195
|
-
// If it does not yet exist, hold onto the segments until it does.
|
|
2196
|
-
if (!this.presentationTimeline_) {
|
|
2197
|
-
return;
|
|
2198
|
-
}
|
|
2199
|
-
for (const segments of this.segmentsToNotifyByStream_) {
|
|
2200
|
-
this.presentationTimeline_.notifySegments(segments);
|
|
2201
|
-
}
|
|
2202
|
-
this.segmentsToNotifyByStream_ = [];
|
|
2203
|
-
}
|
|
2204
|
-
|
|
2205
2553
|
/**
|
|
2206
2554
|
* Parses shaka.hls.Segment objects into shaka.media.SegmentReferences.
|
|
2207
2555
|
*
|
|
@@ -2211,12 +2559,11 @@ shaka.hls.HlsParser = class {
|
|
|
2211
2559
|
* @param {string} mimeType
|
|
2212
2560
|
* @param {!Map.<number, number>} mediaSequenceToStartTime
|
|
2213
2561
|
* @param {!Map.<string, string>} variables
|
|
2214
|
-
* @param {string} codecs
|
|
2215
2562
|
* @return {!Array.<!shaka.media.SegmentReference>}
|
|
2216
2563
|
* @private
|
|
2217
2564
|
*/
|
|
2218
2565
|
createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType,
|
|
2219
|
-
mediaSequenceToStartTime, variables
|
|
2566
|
+
mediaSequenceToStartTime, variables) {
|
|
2220
2567
|
/** @type {Array.<!shaka.hls.Segment>} */
|
|
2221
2568
|
const hlsSegments = playlist.segments;
|
|
2222
2569
|
goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
|
|
@@ -2419,6 +2766,64 @@ shaka.hls.HlsParser = class {
|
|
|
2419
2766
|
*
|
|
2420
2767
|
* @param {string} contentType
|
|
2421
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
|
|
2422
2827
|
* @param {!shaka.hls.Playlist} playlist
|
|
2423
2828
|
* @param {!Map.<string, string>} variables
|
|
2424
2829
|
* @return {!Promise.<string>}
|
|
@@ -2426,7 +2831,6 @@ shaka.hls.HlsParser = class {
|
|
|
2426
2831
|
*/
|
|
2427
2832
|
async guessMimeType_(contentType, codecs, playlist, variables) {
|
|
2428
2833
|
const HlsParser = shaka.hls.HlsParser;
|
|
2429
|
-
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
2430
2834
|
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
2431
2835
|
|
|
2432
2836
|
// If you wait long enough, requesting the first segment can fail
|
|
@@ -2452,22 +2856,10 @@ shaka.hls.HlsParser = class {
|
|
|
2452
2856
|
return mimeType;
|
|
2453
2857
|
}
|
|
2454
2858
|
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
return 'text/vtt';
|
|
2460
|
-
} else if (codecs && codecs !== '') {
|
|
2461
|
-
// Otherwise, assume MP4-embedded text, since text-based formats tend
|
|
2462
|
-
// not to have a codecs string at all.
|
|
2463
|
-
return 'application/mp4';
|
|
2464
|
-
}
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
if (contentType == ContentType.IMAGE) {
|
|
2468
|
-
if (!codecs || codecs == 'jpeg') {
|
|
2469
|
-
return 'image/jpeg';
|
|
2470
|
-
}
|
|
2859
|
+
// The extension map didn't work, so guess based on codecs.
|
|
2860
|
+
mimeType = this.guessMimeTypeBeforeLoading_(contentType, codecs);
|
|
2861
|
+
if (mimeType) {
|
|
2862
|
+
return mimeType;
|
|
2471
2863
|
}
|
|
2472
2864
|
|
|
2473
2865
|
// If unable to guess mime type, request a segment and try getting it
|
|
@@ -2480,21 +2872,12 @@ shaka.hls.HlsParser = class {
|
|
|
2480
2872
|
headRequest, requestType);
|
|
2481
2873
|
|
|
2482
2874
|
const contentMimeType = response.headers['content-type'];
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
// If there was no codecs string and no content-type, assume HLS text
|
|
2487
|
-
// streams are WebVTT.
|
|
2488
|
-
return 'text/vtt';
|
|
2489
|
-
}
|
|
2490
|
-
// If the HLS content is lacking in both MIME type metadata and
|
|
2491
|
-
// segment file extensions, we fall back to assuming it's MP4.
|
|
2492
|
-
const fallbackMimeType = map['mp4'];
|
|
2493
|
-
return fallbackMimeType;
|
|
2875
|
+
if (contentMimeType) {
|
|
2876
|
+
// Split the MIME type in case the server sent additional parameters.
|
|
2877
|
+
return contentMimeType.split(';')[0];
|
|
2494
2878
|
}
|
|
2495
2879
|
|
|
2496
|
-
|
|
2497
|
-
return contentMimeType.split(';')[0];
|
|
2880
|
+
return this.guessMimeTypeFallback_(contentType);
|
|
2498
2881
|
}
|
|
2499
2882
|
|
|
2500
2883
|
/**
|
|
@@ -2684,7 +3067,7 @@ shaka.hls.HlsParser = class {
|
|
|
2684
3067
|
*/
|
|
2685
3068
|
const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
|
|
2686
3069
|
'com.apple.fps', [
|
|
2687
|
-
{initDataType: 'sinf', initData: new Uint8Array(0)},
|
|
3070
|
+
{initDataType: 'sinf', initData: new Uint8Array(0), keyId: null},
|
|
2688
3071
|
]);
|
|
2689
3072
|
|
|
2690
3073
|
return drmInfo;
|
|
@@ -2763,18 +3146,48 @@ shaka.hls.HlsParser = class {
|
|
|
2763
3146
|
|
|
2764
3147
|
return drmInfo;
|
|
2765
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
|
+
}
|
|
2766
3176
|
};
|
|
2767
3177
|
|
|
2768
3178
|
|
|
2769
3179
|
/**
|
|
2770
3180
|
* @typedef {{
|
|
2771
3181
|
* stream: !shaka.extern.Stream,
|
|
3182
|
+
* type: string,
|
|
2772
3183
|
* verbatimMediaPlaylistUri: string,
|
|
2773
3184
|
* absoluteMediaPlaylistUri: string,
|
|
2774
3185
|
* maxTimestamp: number,
|
|
2775
3186
|
* mediaSequenceToStartTime: !Map.<number, number>,
|
|
2776
3187
|
* canSkipSegments: boolean,
|
|
2777
|
-
* hasEndList: boolean
|
|
3188
|
+
* hasEndList: boolean,
|
|
3189
|
+
* firstSequenceNumber: number,
|
|
3190
|
+
* loadedOnce: boolean
|
|
2778
3191
|
* }}
|
|
2779
3192
|
*
|
|
2780
3193
|
* @description
|
|
@@ -2782,6 +3195,8 @@ shaka.hls.HlsParser = class {
|
|
|
2782
3195
|
*
|
|
2783
3196
|
* @property {!shaka.extern.Stream} stream
|
|
2784
3197
|
* The Stream itself.
|
|
3198
|
+
* @property {string} type
|
|
3199
|
+
* The type value. Could be 'video', 'audio', 'text', or 'image'.
|
|
2785
3200
|
* @property {string} verbatimMediaPlaylistUri
|
|
2786
3201
|
* The verbatim media playlist URI, as it appeared in the master playlist.
|
|
2787
3202
|
* This has not been canonicalized into an absolute URI. This gives us a
|
|
@@ -2794,11 +3209,16 @@ shaka.hls.HlsParser = class {
|
|
|
2794
3209
|
* The maximum timestamp found in the stream.
|
|
2795
3210
|
* @property {!Map.<number, number>} mediaSequenceToStartTime
|
|
2796
3211
|
* A map of media sequence numbers to media start times.
|
|
3212
|
+
* Only used for VOD content.
|
|
2797
3213
|
* @property {boolean} canSkipSegments
|
|
2798
|
-
*
|
|
2799
|
-
*
|
|
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.
|
|
2800
3216
|
* @property {boolean} hasEndList
|
|
2801
|
-
*
|
|
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.
|
|
2802
3222
|
*/
|
|
2803
3223
|
shaka.hls.HlsParser.StreamInfo;
|
|
2804
3224
|
|
|
@@ -2872,6 +3292,7 @@ shaka.hls.HlsParser.TEXT_EXTENSIONS_TO_MIME_TYPES_ = {
|
|
|
2872
3292
|
'm4f': 'application/mp4',
|
|
2873
3293
|
'cmft': 'application/mp4',
|
|
2874
3294
|
'vtt': 'text/vtt',
|
|
3295
|
+
'webvtt': 'txt/vtt',
|
|
2875
3296
|
'ttml': 'application/ttml+xml',
|
|
2876
3297
|
};
|
|
2877
3298
|
|
|
@@ -2919,6 +3340,8 @@ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = {
|
|
|
2919
3340
|
shaka.hls.HlsParser.widevineDrmParser_,
|
|
2920
3341
|
'com.microsoft.playready':
|
|
2921
3342
|
shaka.hls.HlsParser.playreadyDrmParser_,
|
|
3343
|
+
'identity':
|
|
3344
|
+
shaka.hls.HlsParser.identityDrmParser_,
|
|
2922
3345
|
};
|
|
2923
3346
|
|
|
2924
3347
|
|