shaka-player 4.2.5 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build-and-test.yaml +7 -0
- package/.github/workflows/sync-labels.yaml +1 -1
- package/.github/workflows/update-issues.yaml +1 -1
- package/AUTHORS +4 -0
- package/CHANGELOG.md +114 -74
- package/CONTRIBUTORS +4 -0
- package/README.md +14 -0
- package/conditional/dummy_cast_proxy.js +120 -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 +107 -88
- package/dist/demo.compiled.debug.map +4 -4
- package/dist/demo.compiled.js +107 -88
- package/dist/demo.compiled.map +4 -4
- package/dist/demo.css +1 -1
- package/dist/demo.css.map +1 -1
- package/dist/deps.js +21 -13
- package/dist/locales.js +31 -31
- package/dist/receiver.compiled.debug.js +75 -77
- package/dist/receiver.compiled.debug.map +3 -3
- package/dist/receiver.compiled.js +75 -77
- package/dist/receiver.compiled.map +3 -3
- package/dist/shaka-player.compiled.d.ts +181 -27
- package/dist/shaka-player.compiled.debug.d.ts +181 -27
- package/dist/shaka-player.compiled.debug.externs.js +169 -31
- package/dist/shaka-player.compiled.debug.js +510 -450
- package/dist/shaka-player.compiled.debug.map +4 -4
- package/dist/shaka-player.compiled.externs.js +169 -31
- package/dist/shaka-player.compiled.js +912 -874
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.d.ts +181 -27
- package/dist/shaka-player.ui.debug.d.ts +181 -27
- package/dist/shaka-player.ui.debug.externs.js +169 -31
- package/dist/shaka-player.ui.debug.js +533 -471
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +169 -31
- package/dist/shaka-player.ui.js +1073 -1036
- 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/mp4_parser.js +3 -0
- 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 +108 -5
- package/lib/config/auto_show_text.js +33 -0
- package/lib/dash/dash_parser.js +1 -1
- package/lib/hls/hls_parser.js +808 -384
- 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_capabilities.js +36 -0
- package/lib/media/media_source_engine.js +113 -28
- 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 +78 -21
- package/lib/offline/storage.js +1 -0
- package/lib/player.js +456 -77
- package/lib/polyfill/media_capabilities.js +9 -5
- package/lib/polyfill/patchedmediakeys_apple.js +63 -4
- 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 +39 -5
- package/lib/util/mp4_parser.js +35 -0
- package/lib/util/player_configuration.js +23 -1
- package/lib/util/stream_utils.js +5 -3
- package/lib/util/ts_parser.js +499 -0
- package/maintained-branches.md +2 -2
- package/package.json +7 -4
- package/roadmap.md +5 -3
- package/test/ads/ad_manager_unit.js +182 -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 +631 -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/media_capabilities_unit.js +21 -23
- package/test/polyfill/patchedmediakeys_apple_unit.js +130 -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/util/id3_utils_unit.js +116 -0
- package/test/util/ts_parser_unit.js +77 -0
- package/third_party/closure-uri/uri.js +1 -1
- 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,31 @@ 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
|
+
let mimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension];
|
|
622
|
+
if (mimeType) {
|
|
623
|
+
fullMimeType = mimeType;
|
|
624
|
+
} else if (extension === 'ts') {
|
|
625
|
+
// TODO: Fetch one segment a use the TsParser to analize if there is
|
|
626
|
+
// video, audio or both.
|
|
627
|
+
} else if (extension === 'mp4') {
|
|
628
|
+
// TODO: Fetch one segment a use the Mp4Parser to analize if there is
|
|
629
|
+
// video, audio or both.
|
|
630
|
+
} else if (HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_[extension]) {
|
|
631
|
+
mimeType = HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_[extension];
|
|
632
|
+
const defaultAudioCodec = this.config_.hls.defaultAudioCodec;
|
|
633
|
+
fullMimeType = `${mimeType}; codecs="${defaultAudioCodec}"`;
|
|
634
|
+
} else if (HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_[extension]) {
|
|
635
|
+
mimeType = HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_[extension];
|
|
636
|
+
const defaultVideoCodec = this.config_.hls.defaultVideoCodec;
|
|
637
|
+
fullMimeType = `${mimeType}; codecs="${defaultVideoCodec}"`;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
568
641
|
const mimeType = shaka.util.MimeUtils.getBasicType(fullMimeType);
|
|
569
642
|
const type = mimeType.split('/')[0];
|
|
570
643
|
const codecs = shaka.util.MimeUtils.getCodecs(fullMimeType);
|
|
@@ -611,6 +684,9 @@ shaka.hls.HlsParser = class {
|
|
|
611
684
|
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
612
685
|
const imageTags = Utils.filterTagsByName(
|
|
613
686
|
playlist.tags, 'EXT-X-IMAGE-STREAM-INF');
|
|
687
|
+
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
688
|
+
const sessionKeyTags = Utils.filterTagsByName(
|
|
689
|
+
playlist.tags, 'EXT-X-SESSION-KEY');
|
|
614
690
|
|
|
615
691
|
this.parseCodecs_(variantTags);
|
|
616
692
|
|
|
@@ -641,10 +717,10 @@ shaka.hls.HlsParser = class {
|
|
|
641
717
|
|
|
642
718
|
// Parse audio and video media tags first, so that we can extract segment
|
|
643
719
|
// start time from audio/video streams and reuse for text streams.
|
|
644
|
-
|
|
720
|
+
this.createStreamInfosFromMediaTags_(mediaTags);
|
|
645
721
|
this.parseClosedCaptions_(mediaTags);
|
|
646
|
-
variants =
|
|
647
|
-
textStreams =
|
|
722
|
+
variants = this.createVariantsForTags_(variantTags, sessionKeyTags);
|
|
723
|
+
textStreams = this.parseTexts_(mediaTags);
|
|
648
724
|
imageStreams = await this.parseImages_(imageTags);
|
|
649
725
|
}
|
|
650
726
|
|
|
@@ -656,32 +732,45 @@ shaka.hls.HlsParser = class {
|
|
|
656
732
|
shaka.util.Error.Code.OPERATION_ABORTED);
|
|
657
733
|
}
|
|
658
734
|
|
|
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
735
|
// This assert is our own sanity check.
|
|
679
736
|
goog.asserts.assert(this.presentationTimeline_ == null,
|
|
680
737
|
'Presentation timeline created early!');
|
|
681
|
-
|
|
738
|
+
// We don't know if the presentation is VOD or live until we parse at least
|
|
739
|
+
// one media playlist, so make a VOD-style presentation timeline for now
|
|
740
|
+
// and change the type later if we discover this is live.
|
|
741
|
+
// Since the player will load the first variant chosen early in the process,
|
|
742
|
+
// there isn't a window during playback where the live-ness is unknown.
|
|
743
|
+
this.presentationTimeline_ = new shaka.media.PresentationTimeline(
|
|
744
|
+
/* presentationStartTime= */ null, /* delay= */ 0);
|
|
745
|
+
this.presentationTimeline_.setStatic(true);
|
|
746
|
+
|
|
747
|
+
// Single-variant streams aren't lazy-loaded, so for them we already have
|
|
748
|
+
// enough info here to determine the presentation type and duration.
|
|
749
|
+
if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
|
|
750
|
+
if (this.isLive_()) {
|
|
751
|
+
this.changePresentationTimelineToLive_();
|
|
752
|
+
const delay = this.updatePlaylistDelay_;
|
|
753
|
+
this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
|
|
754
|
+
}
|
|
755
|
+
const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
|
|
756
|
+
this.finalizeStreams_(streamInfos);
|
|
757
|
+
this.determineDuration_();
|
|
758
|
+
}
|
|
682
759
|
|
|
683
|
-
|
|
684
|
-
|
|
760
|
+
this.manifest_ = {
|
|
761
|
+
presentationTimeline: this.presentationTimeline_,
|
|
762
|
+
variants,
|
|
763
|
+
textStreams,
|
|
764
|
+
imageStreams,
|
|
765
|
+
offlineSessionIds: [],
|
|
766
|
+
minBufferTime: 0,
|
|
767
|
+
sequenceMode: true,
|
|
768
|
+
};
|
|
769
|
+
this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** @private */
|
|
773
|
+
determineDuration_() {
|
|
685
774
|
goog.asserts.assert(this.presentationTimeline_,
|
|
686
775
|
'Presentation timeline not created!');
|
|
687
776
|
|
|
@@ -701,10 +790,14 @@ shaka.hls.HlsParser = class {
|
|
|
701
790
|
const PresentationType = shaka.hls.HlsParser.PresentationType_;
|
|
702
791
|
|
|
703
792
|
if (this.presentationType_ == PresentationType.LIVE) {
|
|
793
|
+
let segmentAvailabilityDuration = this.getMinDuration_();
|
|
794
|
+
|
|
704
795
|
// This defaults to the presentation delay, which has the effect of
|
|
705
796
|
// making the live stream unseekable. This is consistent with Apple's
|
|
706
797
|
// HLS implementation.
|
|
707
|
-
|
|
798
|
+
if (this.config_.hls.useSafariBehaviorForLive) {
|
|
799
|
+
segmentAvailabilityDuration = this.presentationTimeline_.getDelay();
|
|
800
|
+
}
|
|
708
801
|
|
|
709
802
|
// The app can override that with a longer duration, to allow seeking.
|
|
710
803
|
if (!isNaN(this.config_.availabilityWindowOverride)) {
|
|
@@ -716,33 +809,9 @@ shaka.hls.HlsParser = class {
|
|
|
716
809
|
}
|
|
717
810
|
} else {
|
|
718
811
|
// 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
|
-
}
|
|
812
|
+
this.presentationTimeline_.setDuration(this.getMinDuration_());
|
|
725
813
|
}
|
|
726
814
|
|
|
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
815
|
// This is the first point where we have a meaningful presentation start
|
|
747
816
|
// time, and we need to tell PresentationTimeline that so that it can
|
|
748
817
|
// maintain consistency from here on.
|
|
@@ -753,17 +822,6 @@ shaka.hls.HlsParser = class {
|
|
|
753
822
|
goog.asserts.assert(
|
|
754
823
|
!this.presentationTimeline_.usingPresentationStartTime(),
|
|
755
824
|
'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
825
|
}
|
|
768
826
|
|
|
769
827
|
/**
|
|
@@ -854,21 +912,20 @@ shaka.hls.HlsParser = class {
|
|
|
854
912
|
* Create text streams for Subtitles, but not Closed Captions.
|
|
855
913
|
*
|
|
856
914
|
* @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
|
|
857
|
-
* @return {!
|
|
915
|
+
* @return {!Array.<!shaka.extern.Stream>}
|
|
858
916
|
* @private
|
|
859
917
|
*/
|
|
860
|
-
|
|
918
|
+
parseTexts_(mediaTags) {
|
|
861
919
|
// Create text stream for each Subtitle media tag.
|
|
862
920
|
const subtitleTags =
|
|
863
921
|
shaka.hls.Utils.filterTagsByType(mediaTags, 'SUBTITLES');
|
|
864
|
-
const
|
|
922
|
+
const textStreams = subtitleTags.map((tag) => {
|
|
865
923
|
const disableText = this.config_.disableText;
|
|
866
924
|
if (disableText) {
|
|
867
925
|
return null;
|
|
868
926
|
}
|
|
869
927
|
try {
|
|
870
|
-
|
|
871
|
-
return streamInfo.stream;
|
|
928
|
+
return this.createStreamInfoFromMediaTag_(tag).stream;
|
|
872
929
|
} catch (e) {
|
|
873
930
|
if (this.config_.hls.ignoreTextStreamFailures) {
|
|
874
931
|
return null;
|
|
@@ -876,7 +933,6 @@ shaka.hls.HlsParser = class {
|
|
|
876
933
|
throw e;
|
|
877
934
|
}
|
|
878
935
|
});
|
|
879
|
-
const textStreams = await Promise.all(textStreamPromises);
|
|
880
936
|
|
|
881
937
|
// Set the codecs for text streams.
|
|
882
938
|
for (const tag of subtitleTags) {
|
|
@@ -926,7 +982,7 @@ shaka.hls.HlsParser = class {
|
|
|
926
982
|
* @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
|
|
927
983
|
* @private
|
|
928
984
|
*/
|
|
929
|
-
|
|
985
|
+
createStreamInfosFromMediaTags_(mediaTags) {
|
|
930
986
|
// Filter out subtitles and media tags without uri.
|
|
931
987
|
mediaTags = mediaTags.filter((tag) => {
|
|
932
988
|
const uri = tag.getAttributeValue('URI') || '';
|
|
@@ -935,20 +991,52 @@ shaka.hls.HlsParser = class {
|
|
|
935
991
|
});
|
|
936
992
|
|
|
937
993
|
// Create stream info for each audio / video media tag.
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
}
|
|
941
|
-
await Promise.all(promises);
|
|
994
|
+
for (const tag of mediaTags) {
|
|
995
|
+
this.createStreamInfoFromMediaTag_(tag);
|
|
996
|
+
}
|
|
942
997
|
}
|
|
943
998
|
|
|
944
999
|
/**
|
|
945
1000
|
* @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
|
|
946
|
-
* @
|
|
1001
|
+
* @param {!Array.<!shaka.hls.Tag>} sessionKeyTags EXT-X-SESSION-KEY tags
|
|
1002
|
+
* from the playlist.
|
|
1003
|
+
* @return {!Array.<!shaka.extern.Variant>}
|
|
947
1004
|
* @private
|
|
948
1005
|
*/
|
|
949
|
-
|
|
1006
|
+
createVariantsForTags_(tags, sessionKeyTags) {
|
|
1007
|
+
// EXT-X-SESSION-KEY processing
|
|
1008
|
+
const drmInfos = [];
|
|
1009
|
+
const keyIds = new Set();
|
|
1010
|
+
if (sessionKeyTags.length > 0) {
|
|
1011
|
+
for (const drmTag of sessionKeyTags) {
|
|
1012
|
+
const method = drmTag.getRequiredAttrValue('METHOD');
|
|
1013
|
+
if (method != 'NONE' && method != 'AES-128') {
|
|
1014
|
+
// According to the HLS spec, KEYFORMAT is optional and implicitly
|
|
1015
|
+
// defaults to "identity".
|
|
1016
|
+
// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
|
|
1017
|
+
const keyFormat =
|
|
1018
|
+
drmTag.getAttributeValue('KEYFORMAT') || 'identity';
|
|
1019
|
+
const drmParser =
|
|
1020
|
+
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
|
|
1021
|
+
|
|
1022
|
+
const drmInfo = drmParser ?
|
|
1023
|
+
drmParser(drmTag, /* mimeType= */ '') : null;
|
|
1024
|
+
if (drmInfo) {
|
|
1025
|
+
if (drmInfo.keyIds) {
|
|
1026
|
+
for (const keyId of drmInfo.keyIds) {
|
|
1027
|
+
keyIds.add(keyId);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
drmInfos.push(drmInfo);
|
|
1031
|
+
} else {
|
|
1032
|
+
shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
950
1038
|
// Create variants for each variant tag.
|
|
951
|
-
const
|
|
1039
|
+
const allVariants = tags.map((tag) => {
|
|
952
1040
|
const frameRate = tag.getAttributeValue('FRAME-RATE');
|
|
953
1041
|
const bandwidth = Number(tag.getAttributeValue('AVERAGE-BANDWIDTH')) ||
|
|
954
1042
|
Number(tag.getRequiredAttrValue('BANDWIDTH'));
|
|
@@ -958,7 +1046,7 @@ shaka.hls.HlsParser = class {
|
|
|
958
1046
|
|
|
959
1047
|
const videoRange = tag.getAttributeValue('VIDEO-RANGE');
|
|
960
1048
|
|
|
961
|
-
const streamInfos =
|
|
1049
|
+
const streamInfos = this.createStreamInfosForVariantTag_(tag,
|
|
962
1050
|
resolution, frameRate);
|
|
963
1051
|
|
|
964
1052
|
goog.asserts.assert(streamInfos.audio.length ||
|
|
@@ -971,10 +1059,10 @@ shaka.hls.HlsParser = class {
|
|
|
971
1059
|
width,
|
|
972
1060
|
height,
|
|
973
1061
|
frameRate,
|
|
974
|
-
videoRange
|
|
1062
|
+
videoRange,
|
|
1063
|
+
drmInfos,
|
|
1064
|
+
keyIds);
|
|
975
1065
|
});
|
|
976
|
-
|
|
977
|
-
const allVariants = await Promise.all(variantsPromises);
|
|
978
1066
|
let variants = allVariants.reduce(shaka.util.Functional.collapseArrays, []);
|
|
979
1067
|
// Filter out null variants.
|
|
980
1068
|
variants = variants.filter((variant) => variant != null);
|
|
@@ -988,10 +1076,10 @@ shaka.hls.HlsParser = class {
|
|
|
988
1076
|
* @param {!shaka.hls.Tag} tag
|
|
989
1077
|
* @param {?string} resolution
|
|
990
1078
|
* @param {?string} frameRate
|
|
991
|
-
* @return {!
|
|
1079
|
+
* @return {!shaka.hls.HlsParser.StreamInfos}
|
|
992
1080
|
* @private
|
|
993
1081
|
*/
|
|
994
|
-
|
|
1082
|
+
createStreamInfosForVariantTag_(tag, resolution, frameRate) {
|
|
995
1083
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
996
1084
|
/** @type {!Array.<string>} */
|
|
997
1085
|
let allCodecs = this.getCodecsForVariantTag_(tag);
|
|
@@ -1060,10 +1148,9 @@ shaka.hls.HlsParser = class {
|
|
|
1060
1148
|
|
|
1061
1149
|
if (!ignoreStream) {
|
|
1062
1150
|
const streamInfo =
|
|
1063
|
-
|
|
1151
|
+
this.createStreamInfoFromVariantTag_(tag, allCodecs, type);
|
|
1064
1152
|
res[streamInfo.stream.type] = [streamInfo];
|
|
1065
1153
|
}
|
|
1066
|
-
this.filterLegacyCodecs_(res);
|
|
1067
1154
|
return res;
|
|
1068
1155
|
}
|
|
1069
1156
|
|
|
@@ -1194,7 +1281,7 @@ shaka.hls.HlsParser = class {
|
|
|
1194
1281
|
|
|
1195
1282
|
/**
|
|
1196
1283
|
* Get the type value.
|
|
1197
|
-
* Shaka recognizes the content types 'audio', 'video' and '
|
|
1284
|
+
* Shaka recognizes the content types 'audio', 'video', 'text', and 'image'.
|
|
1198
1285
|
* The HLS 'subtitles' type needs to be mapped to 'text'.
|
|
1199
1286
|
* @param {!shaka.hls.Tag} tag
|
|
1200
1287
|
* @return {string}
|
|
@@ -1208,28 +1295,6 @@ shaka.hls.HlsParser = class {
|
|
|
1208
1295
|
return type;
|
|
1209
1296
|
}
|
|
1210
1297
|
|
|
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
1298
|
/**
|
|
1234
1299
|
* @param {!Array.<shaka.hls.HlsParser.StreamInfo>} audioInfos
|
|
1235
1300
|
* @param {!Array.<shaka.hls.HlsParser.StreamInfo>} videoInfos
|
|
@@ -1238,11 +1303,14 @@ shaka.hls.HlsParser = class {
|
|
|
1238
1303
|
* @param {?string} height
|
|
1239
1304
|
* @param {?string} frameRate
|
|
1240
1305
|
* @param {?string} videoRange
|
|
1306
|
+
* @param {!Array.<shaka.extern.DrmInfo>} drmInfos
|
|
1307
|
+
* @param {!Set.<string>} keyIds
|
|
1241
1308
|
* @return {!Array.<!shaka.extern.Variant>}
|
|
1242
1309
|
* @private
|
|
1243
1310
|
*/
|
|
1244
1311
|
createVariants_(
|
|
1245
|
-
audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange
|
|
1312
|
+
audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
|
|
1313
|
+
drmInfos, keyIds) {
|
|
1246
1314
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
1247
1315
|
const DrmEngine = shaka.media.DrmEngine;
|
|
1248
1316
|
|
|
@@ -1268,7 +1336,15 @@ shaka.hls.HlsParser = class {
|
|
|
1268
1336
|
for (const audioInfo of audioInfos) {
|
|
1269
1337
|
for (const videoInfo of videoInfos) {
|
|
1270
1338
|
const audioStream = audioInfo ? audioInfo.stream : null;
|
|
1339
|
+
if (audioStream) {
|
|
1340
|
+
audioStream.drmInfos = drmInfos;
|
|
1341
|
+
audioStream.keyIds = keyIds;
|
|
1342
|
+
}
|
|
1271
1343
|
const videoStream = videoInfo ? videoInfo.stream : null;
|
|
1344
|
+
if (videoStream) {
|
|
1345
|
+
videoStream.drmInfos = drmInfos;
|
|
1346
|
+
videoStream.keyIds = keyIds;
|
|
1347
|
+
}
|
|
1272
1348
|
const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
|
|
1273
1349
|
const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
|
|
1274
1350
|
const videoStreamUri =
|
|
@@ -1353,10 +1429,10 @@ shaka.hls.HlsParser = class {
|
|
|
1353
1429
|
* Parse EXT-X-MEDIA media tag into a Stream object.
|
|
1354
1430
|
*
|
|
1355
1431
|
* @param {shaka.hls.Tag} tag
|
|
1356
|
-
* @return {!
|
|
1432
|
+
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1357
1433
|
* @private
|
|
1358
1434
|
*/
|
|
1359
|
-
|
|
1435
|
+
createStreamInfoFromMediaTag_(tag) {
|
|
1360
1436
|
goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
|
|
1361
1437
|
'Should only be called on media tags!');
|
|
1362
1438
|
const groupId = tag.getRequiredAttrValue('GROUP-ID');
|
|
@@ -1398,7 +1474,7 @@ shaka.hls.HlsParser = class {
|
|
|
1398
1474
|
const forced = forcedAttrValue == 'YES';
|
|
1399
1475
|
// TODO: Should we take into account some of the currently ignored
|
|
1400
1476
|
// attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj
|
|
1401
|
-
const streamInfo =
|
|
1477
|
+
const streamInfo = this.createStreamInfo_(
|
|
1402
1478
|
verbatimMediaPlaylistUri, codecs, type, language, primary, name,
|
|
1403
1479
|
channelsCount, /* closedCaptions= */ null, characteristics, forced,
|
|
1404
1480
|
spatialAudio);
|
|
@@ -1445,7 +1521,7 @@ shaka.hls.HlsParser = class {
|
|
|
1445
1521
|
|
|
1446
1522
|
const characteristics = tag.getAttributeValue('CHARACTERISTICS');
|
|
1447
1523
|
|
|
1448
|
-
const streamInfo =
|
|
1524
|
+
const streamInfo = this.createStreamInfo_(
|
|
1449
1525
|
verbatimImagePlaylistUri, codecs, type, language, /* primary= */ false,
|
|
1450
1526
|
name, /* channelsCount= */ null, /* closedCaptions= */ null,
|
|
1451
1527
|
characteristics, /* forced= */ false, /* spatialAudio= */ false);
|
|
@@ -1463,6 +1539,10 @@ shaka.hls.HlsParser = class {
|
|
|
1463
1539
|
// of the entire sheet at once (like we expect in the output).
|
|
1464
1540
|
// So multiply by the layout size.
|
|
1465
1541
|
|
|
1542
|
+
// Since we need to have generated the segment index for this, we can't
|
|
1543
|
+
// lazy-load in this situation.
|
|
1544
|
+
await streamInfo.stream.createSegmentIndex();
|
|
1545
|
+
|
|
1466
1546
|
const reference = streamInfo.stream.segmentIndex.get(0);
|
|
1467
1547
|
const layout = reference.getTilesLayout();
|
|
1468
1548
|
if (layout) {
|
|
@@ -1489,10 +1569,10 @@ shaka.hls.HlsParser = class {
|
|
|
1489
1569
|
* @param {!shaka.hls.Tag} tag
|
|
1490
1570
|
* @param {!Array.<string>} allCodecs
|
|
1491
1571
|
* @param {string} type
|
|
1492
|
-
* @return {!
|
|
1572
|
+
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1493
1573
|
* @private
|
|
1494
1574
|
*/
|
|
1495
|
-
|
|
1575
|
+
createStreamInfoFromVariantTag_(tag, allCodecs, type) {
|
|
1496
1576
|
goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
|
|
1497
1577
|
'Should only be called on variant tags!');
|
|
1498
1578
|
const verbatimMediaPlaylistUri = this.variableSubstitution_(
|
|
@@ -1504,7 +1584,7 @@ shaka.hls.HlsParser = class {
|
|
|
1504
1584
|
|
|
1505
1585
|
const closedCaptions = this.getClosedCaptions_(tag, type);
|
|
1506
1586
|
const codecs = shaka.util.ManifestParserUtils.guessCodecs(type, allCodecs);
|
|
1507
|
-
const streamInfo =
|
|
1587
|
+
const streamInfo = this.createStreamInfo_(verbatimMediaPlaylistUri,
|
|
1508
1588
|
codecs, type, /* language= */ 'und', /* primary= */ false,
|
|
1509
1589
|
/* name= */ null, /* channelcount= */ null, closedCaptions,
|
|
1510
1590
|
/* characteristics= */ null, /* forced= */ false,
|
|
@@ -1532,29 +1612,275 @@ shaka.hls.HlsParser = class {
|
|
|
1532
1612
|
* @param {?string} characteristics
|
|
1533
1613
|
* @param {boolean} forced
|
|
1534
1614
|
* @param {boolean} spatialAudio
|
|
1535
|
-
* @return {!
|
|
1615
|
+
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1536
1616
|
* @private
|
|
1537
1617
|
*/
|
|
1538
|
-
|
|
1618
|
+
createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language,
|
|
1539
1619
|
primary, name, channelsCount, closedCaptions, characteristics, forced,
|
|
1540
1620
|
spatialAudio) {
|
|
1541
1621
|
// TODO: Refactor, too many parameters
|
|
1542
|
-
|
|
1622
|
+
const initialMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
|
|
1543
1623
|
this.masterPlaylistUri_, verbatimMediaPlaylistUri);
|
|
1544
1624
|
|
|
1545
|
-
|
|
1546
|
-
//
|
|
1547
|
-
|
|
1625
|
+
// This stream is lazy-loaded inside the createSegmentIndex function.
|
|
1626
|
+
// So we start out with a stream object that does not contain the actual
|
|
1627
|
+
// segment index, then download when createSegmentIndex is called.
|
|
1628
|
+
const stream = this.makeStreamObject_(codecs, type, language, primary, name,
|
|
1629
|
+
channelsCount, closedCaptions, characteristics, forced, spatialAudio);
|
|
1630
|
+
if (shaka.media.MediaSourceEngine.RAW_FORMATS.includes(stream.mimeType)) {
|
|
1631
|
+
stream.codecs = '';
|
|
1632
|
+
}
|
|
1633
|
+
const streamInfo = {
|
|
1634
|
+
stream,
|
|
1635
|
+
type,
|
|
1636
|
+
verbatimMediaPlaylistUri,
|
|
1637
|
+
// These values are filled out or updated after lazy-loading:
|
|
1638
|
+
absoluteMediaPlaylistUri: initialMediaPlaylistUri,
|
|
1639
|
+
maxTimestamp: 0,
|
|
1640
|
+
mediaSequenceToStartTime: new Map(),
|
|
1641
|
+
canSkipSegments: false,
|
|
1642
|
+
hasEndList: false,
|
|
1643
|
+
firstSequenceNumber: -1,
|
|
1644
|
+
loadedOnce: false,
|
|
1645
|
+
};
|
|
1548
1646
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1647
|
+
/** @param {!AbortSignal} abortSignal */
|
|
1648
|
+
const downloadSegmentIndex = async (abortSignal) => {
|
|
1649
|
+
// Download the actual manifest.
|
|
1650
|
+
const response = await this.requestManifest_(
|
|
1651
|
+
streamInfo.absoluteMediaPlaylistUri);
|
|
1652
|
+
if (abortSignal.aborted) {
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// Record the final URI after redirects.
|
|
1657
|
+
const absoluteMediaPlaylistUri = response.uri;
|
|
1658
|
+
|
|
1659
|
+
// Record the redirected, final URI of this media playlist when we parse
|
|
1660
|
+
// it.
|
|
1661
|
+
/** @type {!shaka.hls.Playlist} */
|
|
1662
|
+
const playlist = this.manifestTextParser_.parsePlaylist(
|
|
1663
|
+
response.data, absoluteMediaPlaylistUri);
|
|
1664
|
+
|
|
1665
|
+
const wasLive = this.isLive_();
|
|
1666
|
+
const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
|
|
1667
|
+
playlist, verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs,
|
|
1668
|
+
type, language, primary, name, channelsCount, closedCaptions,
|
|
1669
|
+
characteristics, forced, spatialAudio);
|
|
1670
|
+
if (abortSignal.aborted) {
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
const realStream = realStreamInfo.stream;
|
|
1675
|
+
|
|
1676
|
+
if (this.isLive_() && !wasLive) {
|
|
1677
|
+
// Now that we know that the presentation is live, convert the timeline
|
|
1678
|
+
// to live.
|
|
1679
|
+
this.changePresentationTimelineToLive_();
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Copy values from the real stream info to our initial one.
|
|
1683
|
+
streamInfo.absoluteMediaPlaylistUri = absoluteMediaPlaylistUri;
|
|
1684
|
+
streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
|
|
1685
|
+
streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
|
|
1686
|
+
streamInfo.hasEndList = realStreamInfo.hasEndList;
|
|
1687
|
+
streamInfo.mediaSequenceToStartTime =
|
|
1688
|
+
realStreamInfo.mediaSequenceToStartTime;
|
|
1689
|
+
streamInfo.loadedOnce = true;
|
|
1690
|
+
stream.segmentIndex = realStream.segmentIndex;
|
|
1691
|
+
stream.encrypted = realStream.encrypted;
|
|
1692
|
+
stream.drmInfos = realStream.drmInfos;
|
|
1693
|
+
stream.keyIds = realStream.keyIds;
|
|
1694
|
+
stream.mimeType = realStream.mimeType;
|
|
1695
|
+
if (shaka.media.MediaSourceEngine.RAW_FORMATS.includes(stream.mimeType)) {
|
|
1696
|
+
stream.codecs = '';
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// Since we lazy-loaded this content, the player may need to create new
|
|
1700
|
+
// sessions for the DRM info in this stream.
|
|
1701
|
+
if (stream.drmInfos.length) {
|
|
1702
|
+
this.playerInterface_.newDrmInfo(stream);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
1706
|
+
if (type == ContentType.VIDEO || type == ContentType.AUDIO) {
|
|
1707
|
+
for (const otherStreamInfo of this.uriToStreamInfosMap_.values()) {
|
|
1708
|
+
if (!otherStreamInfo.loadedOnce && otherStreamInfo.type == type) {
|
|
1709
|
+
// To aid manifest filtering, assume before loading that all video
|
|
1710
|
+
// renditions have the same MIME type. (And likewise for audio.)
|
|
1711
|
+
otherStreamInfo.stream.mimeType = realStream.mimeType;
|
|
1712
|
+
if (shaka.media.MediaSourceEngine.RAW_FORMATS
|
|
1713
|
+
.includes(otherStreamInfo.stream.mimeType)) {
|
|
1714
|
+
otherStreamInfo.stream.codecs = '';
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// Add finishing touches to the stream that can only be done once we have
|
|
1721
|
+
// more full context on the media as a whole.
|
|
1722
|
+
if (this.hasEnoughInfoToFinalizeStreams_()) {
|
|
1723
|
+
if (!this.streamsFinalized_) {
|
|
1724
|
+
// Mark this manifest as having been finalized, so we don't go through
|
|
1725
|
+
// this whole process of finishing touches a second time.
|
|
1726
|
+
this.streamsFinalized_ = true;
|
|
1727
|
+
// Finalize all of the currently-loaded streams.
|
|
1728
|
+
const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
|
|
1729
|
+
const activeStreamInfos =
|
|
1730
|
+
streamInfos.filter((s) => s.stream.segmentIndex);
|
|
1731
|
+
this.finalizeStreams_(activeStreamInfos);
|
|
1732
|
+
// With the addition of this new stream, we now have enough info to
|
|
1733
|
+
// figure out how long the streams should be. So process all streams
|
|
1734
|
+
// we have downloaded up until this point.
|
|
1735
|
+
this.determineDuration_();
|
|
1736
|
+
// Finally, start the update timer, if this asset has been determined
|
|
1737
|
+
// to be a livestream.
|
|
1738
|
+
const delay = this.updatePlaylistDelay_;
|
|
1739
|
+
if (delay > 0) {
|
|
1740
|
+
this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
|
|
1741
|
+
}
|
|
1742
|
+
} else {
|
|
1743
|
+
// We don't need to go through the full process; just finalize this
|
|
1744
|
+
// single stream.
|
|
1745
|
+
this.finalizeStreams_([streamInfo]);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
/** @type {Promise} */
|
|
1751
|
+
let creationPromise = null;
|
|
1752
|
+
/** @type {!AbortController} */
|
|
1753
|
+
let abortController = new AbortController();
|
|
1754
|
+
const safeCreateSegmentIndex = () => {
|
|
1755
|
+
// An operation is already in progress. The second and subsequent
|
|
1756
|
+
// callers receive the same Promise as the first caller, and only one
|
|
1757
|
+
// download operation will occur.
|
|
1758
|
+
if (creationPromise) {
|
|
1759
|
+
return creationPromise;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// Create a new AbortController to be able to cancel this specific
|
|
1763
|
+
// download.
|
|
1764
|
+
abortController = new AbortController();
|
|
1765
|
+
|
|
1766
|
+
// Create a Promise tied to the outcome of downloadSegmentIndex(). If
|
|
1767
|
+
// downloadSegmentIndex is rejected, creationPromise will also be
|
|
1768
|
+
// rejected.
|
|
1769
|
+
creationPromise = new Promise((resolve) => {
|
|
1770
|
+
resolve(downloadSegmentIndex(abortController.signal));
|
|
1771
|
+
});
|
|
1772
|
+
return creationPromise;
|
|
1773
|
+
};
|
|
1774
|
+
|
|
1775
|
+
stream.createSegmentIndex = safeCreateSegmentIndex;
|
|
1776
|
+
|
|
1777
|
+
stream.closeSegmentIndex = () => {
|
|
1778
|
+
// If we're mid-creation, cancel it.
|
|
1779
|
+
if (creationPromise && !stream.segmentIndex) {
|
|
1780
|
+
abortController.abort();
|
|
1781
|
+
}
|
|
1782
|
+
// If we have a segment index, release it.
|
|
1783
|
+
if (stream.segmentIndex) {
|
|
1784
|
+
stream.segmentIndex.release();
|
|
1785
|
+
stream.segmentIndex = null;
|
|
1786
|
+
}
|
|
1787
|
+
// Clear the creation Promise so that a new operation can begin.
|
|
1788
|
+
creationPromise = null;
|
|
1789
|
+
};
|
|
1553
1790
|
|
|
1554
|
-
return
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1791
|
+
return streamInfo;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
/**
|
|
1795
|
+
* @return {number}
|
|
1796
|
+
* @private
|
|
1797
|
+
*/
|
|
1798
|
+
getMinDuration_() {
|
|
1799
|
+
let minDuration = Infinity;
|
|
1800
|
+
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
|
|
1801
|
+
if (streamInfo.stream.segmentIndex && streamInfo.stream.type != 'text') {
|
|
1802
|
+
// Since everything is already offset to 0 (either by sync or by being
|
|
1803
|
+
// VOD), only maxTimestamp is necessary to compute the duration.
|
|
1804
|
+
minDuration = Math.min(minDuration, streamInfo.maxTimestamp);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
return minDuration;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
/**
|
|
1811
|
+
* @param {!Array.<!shaka.extern.Stream>} streams
|
|
1812
|
+
* @private
|
|
1813
|
+
*/
|
|
1814
|
+
notifySegmentsForStreams_(streams) {
|
|
1815
|
+
const references = [];
|
|
1816
|
+
for (const stream of streams) {
|
|
1817
|
+
if (!stream.segmentIndex) {
|
|
1818
|
+
// The stream was closed since the list of streams was built.
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
stream.segmentIndex.forEachTopLevelReference((reference) => {
|
|
1822
|
+
references.push(reference);
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
this.presentationTimeline_.notifySegments(references);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* @param {!Array.<!shaka.hls.HlsParser.StreamInfo>} streamInfos
|
|
1830
|
+
* @private
|
|
1831
|
+
*/
|
|
1832
|
+
finalizeStreams_(streamInfos) {
|
|
1833
|
+
if (!this.isLive_()) {
|
|
1834
|
+
const minDuration = this.getMinDuration_();
|
|
1835
|
+
for (const streamInfo of streamInfos) {
|
|
1836
|
+
streamInfo.stream.segmentIndex.fit(/* periodStart= */ 0, minDuration);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
// MediaSource expects no codec strings combined with raw formats.
|
|
1840
|
+
for (const streamInfo of streamInfos) {
|
|
1841
|
+
const stream = streamInfo.stream;
|
|
1842
|
+
if (shaka.media.MediaSourceEngine.RAW_FORMATS.includes(stream.mimeType)) {
|
|
1843
|
+
stream.codecs = '';
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
this.notifySegmentsForStreams_(streamInfos.map((s) => s.stream));
|
|
1847
|
+
if (this.config_.hls.ignoreManifestProgramDateTime) {
|
|
1848
|
+
this.syncStreamsWithSequenceNumber_(streamInfos);
|
|
1849
|
+
} else {
|
|
1850
|
+
this.syncStreamsWithProgramDateTime_(streamInfos);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
/**
|
|
1855
|
+
* There are some values on streams that can only be set once we know about
|
|
1856
|
+
* both the video and audio content, if present.
|
|
1857
|
+
* This checks if there is at least one video downloaded (if the media has
|
|
1858
|
+
* video), and that there is at least one audio downloaded (if the media has
|
|
1859
|
+
* audio).
|
|
1860
|
+
* @return {boolean}
|
|
1861
|
+
* @private
|
|
1862
|
+
*/
|
|
1863
|
+
hasEnoughInfoToFinalizeStreams_() {
|
|
1864
|
+
if (!this.manifest_) {
|
|
1865
|
+
return false;
|
|
1866
|
+
}
|
|
1867
|
+
const videos = [];
|
|
1868
|
+
const audios = [];
|
|
1869
|
+
for (const variant of this.manifest_.variants) {
|
|
1870
|
+
if (variant.video) {
|
|
1871
|
+
videos.push(variant.video);
|
|
1872
|
+
}
|
|
1873
|
+
if (variant.audio) {
|
|
1874
|
+
audios.push(variant.audio);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (videos.length > 0 && !videos.some((stream) => stream.segmentIndex)) {
|
|
1878
|
+
return false;
|
|
1879
|
+
}
|
|
1880
|
+
if (audios.length > 0 && !audios.some((stream) => stream.segmentIndex)) {
|
|
1881
|
+
return false;
|
|
1882
|
+
}
|
|
1883
|
+
return true;
|
|
1558
1884
|
}
|
|
1559
1885
|
|
|
1560
1886
|
/**
|
|
@@ -1604,51 +1930,8 @@ shaka.hls.HlsParser = class {
|
|
|
1604
1930
|
mediaVariables);
|
|
1605
1931
|
}
|
|
1606
1932
|
|
|
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
|
-
}
|
|
1933
|
+
const {drmInfos, keyIds, encrypted, aesEncrypted} =
|
|
1934
|
+
this.parseDrmInfo_(playlist, mimeType);
|
|
1652
1935
|
|
|
1653
1936
|
if (encrypted && !drmInfos.length && !aesEncrypted) {
|
|
1654
1937
|
throw new shaka.util.Error(
|
|
@@ -1657,17 +1940,11 @@ shaka.hls.HlsParser = class {
|
|
|
1657
1940
|
shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
|
|
1658
1941
|
}
|
|
1659
1942
|
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
codecs = '';
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
/** @type {!Map.<number, number>} */
|
|
1666
|
-
const mediaSequenceToStartTime = new Map();
|
|
1667
|
-
|
|
1943
|
+
const mediaSequenceToStartTime = this.isLive_() ?
|
|
1944
|
+
this.mediaSequenceToStartTimeByType_.get(type) : new Map();
|
|
1668
1945
|
const prevLowLatencyMode = this.lowLatencyMode_;
|
|
1669
1946
|
const segments = this.createSegments_(verbatimMediaPlaylistUri, playlist,
|
|
1670
|
-
type, mimeType, mediaSequenceToStartTime, mediaVariables
|
|
1947
|
+
type, mimeType, mediaSequenceToStartTime, mediaVariables);
|
|
1671
1948
|
|
|
1672
1949
|
// This happens when autoLowLatencyMode is true, so we need set the
|
|
1673
1950
|
// correct lowLatencyPresentationDelay_
|
|
@@ -1679,33 +1956,71 @@ shaka.hls.HlsParser = class {
|
|
|
1679
1956
|
/** @type {!shaka.media.SegmentIndex} */
|
|
1680
1957
|
const segmentIndex = new shaka.media.SegmentIndex(segments);
|
|
1681
1958
|
|
|
1682
|
-
const kind = (type == shaka.util.ManifestParserUtils.ContentType.TEXT) ?
|
|
1683
|
-
shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined;
|
|
1684
|
-
|
|
1685
|
-
const roles = [];
|
|
1686
|
-
if (characteristics) {
|
|
1687
|
-
for (const characteristic of characteristics.split(',')) {
|
|
1688
|
-
roles.push(characteristic);
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
1959
|
const serverControlTag = shaka.hls.Utils.getFirstTagWithName(
|
|
1693
1960
|
playlist.tags, 'EXT-X-SERVER-CONTROL');
|
|
1694
1961
|
const canSkipSegments = serverControlTag ?
|
|
1695
1962
|
serverControlTag.getAttribute('CAN-SKIP-UNTIL') != null : false;
|
|
1696
1963
|
|
|
1697
|
-
|
|
1698
|
-
|
|
1964
|
+
const stream = this.makeStreamObject_(codecs, type, language, primary, name,
|
|
1965
|
+
channelsCount, closedCaptions, characteristics, forced, spatialAudio);
|
|
1966
|
+
stream.segmentIndex = segmentIndex;
|
|
1967
|
+
stream.encrypted = encrypted;
|
|
1968
|
+
stream.drmInfos = drmInfos;
|
|
1969
|
+
stream.keyIds = keyIds;
|
|
1970
|
+
stream.mimeType = mimeType;
|
|
1971
|
+
|
|
1972
|
+
return {
|
|
1973
|
+
stream,
|
|
1974
|
+
type,
|
|
1975
|
+
verbatimMediaPlaylistUri,
|
|
1976
|
+
absoluteMediaPlaylistUri,
|
|
1977
|
+
maxTimestamp: lastEndTime,
|
|
1978
|
+
canSkipSegments,
|
|
1979
|
+
hasEndList: false,
|
|
1980
|
+
firstSequenceNumber: -1,
|
|
1981
|
+
mediaSequenceToStartTime,
|
|
1982
|
+
loadedOnce: false,
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
|
|
1987
|
+
/**
|
|
1988
|
+
* Creates a stream object with the given parameters.
|
|
1989
|
+
* The parameters that are passed into here are only the things that can be
|
|
1990
|
+
* known without downloading the media playlist; other values must be set
|
|
1991
|
+
* manually on the object after creation.
|
|
1992
|
+
* @param {string} codecs
|
|
1993
|
+
* @param {string} type
|
|
1994
|
+
* @param {string} language
|
|
1995
|
+
* @param {boolean} primary
|
|
1996
|
+
* @param {?string} name
|
|
1997
|
+
* @param {?number} channelsCount
|
|
1998
|
+
* @param {Map.<string, string>} closedCaptions
|
|
1999
|
+
* @param {?string} characteristics
|
|
2000
|
+
* @param {boolean} forced
|
|
2001
|
+
* @param {boolean} spatialAudio
|
|
2002
|
+
* @return {!shaka.extern.Stream}
|
|
2003
|
+
* @private
|
|
2004
|
+
*/
|
|
2005
|
+
makeStreamObject_(codecs, type, language, primary, name, channelsCount,
|
|
2006
|
+
closedCaptions, characteristics, forced, spatialAudio) {
|
|
2007
|
+
// Fill out a "best-guess" mimeType, for now. It will be replaced once the
|
|
2008
|
+
// stream is lazy-loaded.
|
|
2009
|
+
const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
|
|
2010
|
+
this.guessMimeTypeFallback_(type);
|
|
2011
|
+
|
|
2012
|
+
return {
|
|
1699
2013
|
id: this.globalId_++,
|
|
1700
2014
|
originalId: name,
|
|
1701
2015
|
createSegmentIndex: () => Promise.resolve(),
|
|
1702
|
-
segmentIndex,
|
|
2016
|
+
segmentIndex: null,
|
|
1703
2017
|
mimeType,
|
|
1704
2018
|
codecs,
|
|
1705
|
-
kind
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
2019
|
+
kind: (type == shaka.util.ManifestParserUtils.ContentType.TEXT) ?
|
|
2020
|
+
shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined,
|
|
2021
|
+
encrypted: false,
|
|
2022
|
+
drmInfos: [],
|
|
2023
|
+
keyIds: new Set(),
|
|
1709
2024
|
language,
|
|
1710
2025
|
label: name, // For historical reasons, since before "originalId".
|
|
1711
2026
|
type,
|
|
@@ -1718,27 +2033,80 @@ shaka.hls.HlsParser = class {
|
|
|
1718
2033
|
width: undefined,
|
|
1719
2034
|
height: undefined,
|
|
1720
2035
|
bandwidth: undefined,
|
|
1721
|
-
roles:
|
|
1722
|
-
forced
|
|
2036
|
+
roles: characteristics ? characteristics.split(',') : [],
|
|
2037
|
+
forced,
|
|
1723
2038
|
channelsCount,
|
|
1724
2039
|
audioSamplingRate: null,
|
|
1725
|
-
spatialAudio
|
|
2040
|
+
spatialAudio,
|
|
1726
2041
|
closedCaptions,
|
|
1727
2042
|
hdr: undefined,
|
|
1728
2043
|
tilesLayout: undefined,
|
|
1729
2044
|
};
|
|
1730
|
-
|
|
1731
|
-
return {
|
|
1732
|
-
stream,
|
|
1733
|
-
verbatimMediaPlaylistUri,
|
|
1734
|
-
absoluteMediaPlaylistUri,
|
|
1735
|
-
maxTimestamp: lastEndTime,
|
|
1736
|
-
mediaSequenceToStartTime,
|
|
1737
|
-
canSkipSegments,
|
|
1738
|
-
hasEndList: false,
|
|
1739
|
-
};
|
|
1740
2045
|
}
|
|
1741
2046
|
|
|
2047
|
+
/**
|
|
2048
|
+
* @param {!shaka.hls.Playlist} playlist
|
|
2049
|
+
* @param {string} mimeType
|
|
2050
|
+
* @return {{
|
|
2051
|
+
* drmInfos: !Array.<shaka.extern.DrmInfo>,
|
|
2052
|
+
* keyIds: !Set.<string>,
|
|
2053
|
+
* encrypted: boolean,
|
|
2054
|
+
* aesEncrypted: boolean
|
|
2055
|
+
* }}
|
|
2056
|
+
* @private
|
|
2057
|
+
*/
|
|
2058
|
+
parseDrmInfo_(playlist, mimeType) {
|
|
2059
|
+
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
2060
|
+
const drmTags = [];
|
|
2061
|
+
if (playlist.segments) {
|
|
2062
|
+
for (const segment of playlist.segments) {
|
|
2063
|
+
const segmentKeyTags = shaka.hls.Utils.filterTagsByName(segment.tags,
|
|
2064
|
+
'EXT-X-KEY');
|
|
2065
|
+
drmTags.push(...segmentKeyTags);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
let encrypted = false;
|
|
2070
|
+
let aesEncrypted = false;
|
|
2071
|
+
|
|
2072
|
+
/** @type {!Array.<shaka.extern.DrmInfo>}*/
|
|
2073
|
+
const drmInfos = [];
|
|
2074
|
+
const keyIds = new Set();
|
|
2075
|
+
|
|
2076
|
+
for (const drmTag of drmTags) {
|
|
2077
|
+
const method = drmTag.getRequiredAttrValue('METHOD');
|
|
2078
|
+
if (method != 'NONE') {
|
|
2079
|
+
encrypted = true;
|
|
2080
|
+
|
|
2081
|
+
if (method == 'AES-128') {
|
|
2082
|
+
// These keys are handled separately.
|
|
2083
|
+
aesEncrypted = true;
|
|
2084
|
+
} else {
|
|
2085
|
+
// According to the HLS spec, KEYFORMAT is optional and implicitly
|
|
2086
|
+
// defaults to "identity".
|
|
2087
|
+
// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
|
|
2088
|
+
const keyFormat =
|
|
2089
|
+
drmTag.getAttributeValue('KEYFORMAT') || 'identity';
|
|
2090
|
+
const drmParser =
|
|
2091
|
+
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
|
|
2092
|
+
|
|
2093
|
+
const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
|
|
2094
|
+
if (drmInfo) {
|
|
2095
|
+
if (drmInfo.keyIds) {
|
|
2096
|
+
for (const keyId of drmInfo.keyIds) {
|
|
2097
|
+
keyIds.add(keyId);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
drmInfos.push(drmInfo);
|
|
2101
|
+
} else {
|
|
2102
|
+
shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
return {drmInfos, keyIds, encrypted, aesEncrypted};
|
|
2109
|
+
}
|
|
1742
2110
|
|
|
1743
2111
|
/**
|
|
1744
2112
|
* @param {!shaka.hls.Tag} drmTag
|
|
@@ -1882,39 +2250,34 @@ shaka.hls.HlsParser = class {
|
|
|
1882
2250
|
/**
|
|
1883
2251
|
* @private
|
|
1884
2252
|
*/
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
presentationDelay = this.lowLatencyPresentationDelay_;
|
|
1906
|
-
} else {
|
|
1907
|
-
presentationDelay = this.maxTargetDuration_ * 3;
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
this.presentationTimeline_ = new shaka.media.PresentationTimeline(
|
|
1911
|
-
/* presentationStartTime= */ 0, /* delay= */ presentationDelay);
|
|
1912
|
-
this.presentationTimeline_.setStatic(false);
|
|
2253
|
+
changePresentationTimelineToLive_() {
|
|
2254
|
+
// The live edge will be calculated from segments, so we don't need to
|
|
2255
|
+
// set a presentation start time. We will assert later that this is
|
|
2256
|
+
// working as expected.
|
|
2257
|
+
|
|
2258
|
+
// The HLS spec (RFC 8216) states in 6.3.3:
|
|
2259
|
+
//
|
|
2260
|
+
// "The client SHALL choose which Media Segment to play first ... the
|
|
2261
|
+
// client SHOULD NOT choose a segment that starts less than three target
|
|
2262
|
+
// durations from the end of the Playlist file. Doing so can trigger
|
|
2263
|
+
// playback stalls."
|
|
2264
|
+
//
|
|
2265
|
+
// We accomplish this in our DASH-y model by setting a presentation
|
|
2266
|
+
// delay of configured value, or 3 segments duration if not configured.
|
|
2267
|
+
// This will be the "live edge" of the presentation.
|
|
2268
|
+
let presentationDelay;
|
|
2269
|
+
if (this.config_.defaultPresentationDelay) {
|
|
2270
|
+
presentationDelay = this.config_.defaultPresentationDelay;
|
|
2271
|
+
} else if (this.lowLatencyPresentationDelay_) {
|
|
2272
|
+
presentationDelay = this.lowLatencyPresentationDelay_;
|
|
1913
2273
|
} else {
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
this.presentationTimeline_.setStatic(true);
|
|
2274
|
+
const numberOfSegments = this.config_.hls.liveSegmentsDelay;
|
|
2275
|
+
presentationDelay = this.maxTargetDuration_ * numberOfSegments;
|
|
1917
2276
|
}
|
|
2277
|
+
|
|
2278
|
+
this.presentationTimeline_.setPresentationStartTime(0);
|
|
2279
|
+
this.presentationTimeline_.setDelay(presentationDelay);
|
|
2280
|
+
this.presentationTimeline_.setStatic(false);
|
|
1918
2281
|
}
|
|
1919
2282
|
|
|
1920
2283
|
/**
|
|
@@ -2205,19 +2568,6 @@ shaka.hls.HlsParser = class {
|
|
|
2205
2568
|
return [startByte, endByte];
|
|
2206
2569
|
}
|
|
2207
2570
|
|
|
2208
|
-
/** @private */
|
|
2209
|
-
notifySegments_() {
|
|
2210
|
-
// The presentation timeline may or may not be set yet.
|
|
2211
|
-
// If it does not yet exist, hold onto the segments until it does.
|
|
2212
|
-
if (!this.presentationTimeline_) {
|
|
2213
|
-
return;
|
|
2214
|
-
}
|
|
2215
|
-
for (const segments of this.segmentsToNotifyByStream_) {
|
|
2216
|
-
this.presentationTimeline_.notifySegments(segments);
|
|
2217
|
-
}
|
|
2218
|
-
this.segmentsToNotifyByStream_ = [];
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
2571
|
/**
|
|
2222
2572
|
* Parses shaka.hls.Segment objects into shaka.media.SegmentReferences.
|
|
2223
2573
|
*
|
|
@@ -2227,12 +2577,11 @@ shaka.hls.HlsParser = class {
|
|
|
2227
2577
|
* @param {string} mimeType
|
|
2228
2578
|
* @param {!Map.<number, number>} mediaSequenceToStartTime
|
|
2229
2579
|
* @param {!Map.<string, string>} variables
|
|
2230
|
-
* @param {string} codecs
|
|
2231
2580
|
* @return {!Array.<!shaka.media.SegmentReference>}
|
|
2232
2581
|
* @private
|
|
2233
2582
|
*/
|
|
2234
2583
|
createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType,
|
|
2235
|
-
mediaSequenceToStartTime, variables
|
|
2584
|
+
mediaSequenceToStartTime, variables) {
|
|
2236
2585
|
/** @type {Array.<!shaka.hls.Segment>} */
|
|
2237
2586
|
const hlsSegments = playlist.segments;
|
|
2238
2587
|
goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
|
|
@@ -2435,6 +2784,64 @@ shaka.hls.HlsParser = class {
|
|
|
2435
2784
|
*
|
|
2436
2785
|
* @param {string} contentType
|
|
2437
2786
|
* @param {string} codecs
|
|
2787
|
+
* @return {?string}
|
|
2788
|
+
* @private
|
|
2789
|
+
*/
|
|
2790
|
+
guessMimeTypeBeforeLoading_(contentType, codecs) {
|
|
2791
|
+
if (contentType == shaka.util.ManifestParserUtils.ContentType.TEXT) {
|
|
2792
|
+
if (codecs == 'vtt' || codecs == 'wvtt') {
|
|
2793
|
+
// If codecs is 'vtt', it's WebVTT.
|
|
2794
|
+
return 'text/vtt';
|
|
2795
|
+
} else if (codecs && codecs !== '') {
|
|
2796
|
+
// Otherwise, assume MP4-embedded text, since text-based formats tend
|
|
2797
|
+
// not to have a codecs string at all.
|
|
2798
|
+
return 'application/mp4';
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
if (contentType == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
|
|
2803
|
+
if (!codecs || codecs == 'jpeg') {
|
|
2804
|
+
return 'image/jpeg';
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
if (contentType == shaka.util.ManifestParserUtils.ContentType.AUDIO) {
|
|
2809
|
+
// See: https://bugs.chromium.org/p/chromium/issues/detail?id=489520
|
|
2810
|
+
if (codecs == 'mp4a.40.34') {
|
|
2811
|
+
return 'audio/mpeg';
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
// Not enough information to guess from the content type and codecs.
|
|
2816
|
+
return null;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
/**
|
|
2820
|
+
* Get a fallback mime type for the content. Used if all the better methods
|
|
2821
|
+
* for determining the mime type have failed.
|
|
2822
|
+
*
|
|
2823
|
+
* @param {string} contentType
|
|
2824
|
+
* @return {string}
|
|
2825
|
+
* @private
|
|
2826
|
+
*/
|
|
2827
|
+
guessMimeTypeFallback_(contentType) {
|
|
2828
|
+
if (contentType == shaka.util.ManifestParserUtils.ContentType.TEXT) {
|
|
2829
|
+
// If there was no codecs string and no content-type, assume HLS text
|
|
2830
|
+
// streams are WebVTT.
|
|
2831
|
+
return 'text/vtt';
|
|
2832
|
+
}
|
|
2833
|
+
// If the HLS content is lacking in both MIME type metadata and
|
|
2834
|
+
// segment file extensions, we fall back to assuming it's MP4.
|
|
2835
|
+
const map = shaka.hls.HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
|
|
2836
|
+
return map['mp4'];
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
/**
|
|
2840
|
+
* Attempts to guess stream's mime type based on content type, URI, and
|
|
2841
|
+
* contents of the playlist.
|
|
2842
|
+
*
|
|
2843
|
+
* @param {string} contentType
|
|
2844
|
+
* @param {string} codecs
|
|
2438
2845
|
* @param {!shaka.hls.Playlist} playlist
|
|
2439
2846
|
* @param {!Map.<string, string>} variables
|
|
2440
2847
|
* @return {!Promise.<string>}
|
|
@@ -2442,7 +2849,6 @@ shaka.hls.HlsParser = class {
|
|
|
2442
2849
|
*/
|
|
2443
2850
|
async guessMimeType_(contentType, codecs, playlist, variables) {
|
|
2444
2851
|
const HlsParser = shaka.hls.HlsParser;
|
|
2445
|
-
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
2446
2852
|
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
2447
2853
|
|
|
2448
2854
|
// If you wait long enough, requesting the first segment can fail
|
|
@@ -2468,22 +2874,10 @@ shaka.hls.HlsParser = class {
|
|
|
2468
2874
|
return mimeType;
|
|
2469
2875
|
}
|
|
2470
2876
|
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
return 'text/vtt';
|
|
2476
|
-
} else if (codecs && codecs !== '') {
|
|
2477
|
-
// Otherwise, assume MP4-embedded text, since text-based formats tend
|
|
2478
|
-
// not to have a codecs string at all.
|
|
2479
|
-
return 'application/mp4';
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
if (contentType == ContentType.IMAGE) {
|
|
2484
|
-
if (!codecs || codecs == 'jpeg') {
|
|
2485
|
-
return 'image/jpeg';
|
|
2486
|
-
}
|
|
2877
|
+
// The extension map didn't work, so guess based on codecs.
|
|
2878
|
+
mimeType = this.guessMimeTypeBeforeLoading_(contentType, codecs);
|
|
2879
|
+
if (mimeType) {
|
|
2880
|
+
return mimeType;
|
|
2487
2881
|
}
|
|
2488
2882
|
|
|
2489
2883
|
// If unable to guess mime type, request a segment and try getting it
|
|
@@ -2496,21 +2890,12 @@ shaka.hls.HlsParser = class {
|
|
|
2496
2890
|
headRequest, requestType);
|
|
2497
2891
|
|
|
2498
2892
|
const contentMimeType = response.headers['content-type'];
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
// If there was no codecs string and no content-type, assume HLS text
|
|
2503
|
-
// streams are WebVTT.
|
|
2504
|
-
return 'text/vtt';
|
|
2505
|
-
}
|
|
2506
|
-
// If the HLS content is lacking in both MIME type metadata and
|
|
2507
|
-
// segment file extensions, we fall back to assuming it's MP4.
|
|
2508
|
-
const fallbackMimeType = map['mp4'];
|
|
2509
|
-
return fallbackMimeType;
|
|
2893
|
+
if (contentMimeType) {
|
|
2894
|
+
// Split the MIME type in case the server sent additional parameters.
|
|
2895
|
+
return contentMimeType.split(';')[0];
|
|
2510
2896
|
}
|
|
2511
2897
|
|
|
2512
|
-
|
|
2513
|
-
return contentMimeType.split(';')[0];
|
|
2898
|
+
return this.guessMimeTypeFallback_(contentType);
|
|
2514
2899
|
}
|
|
2515
2900
|
|
|
2516
2901
|
/**
|
|
@@ -2700,7 +3085,7 @@ shaka.hls.HlsParser = class {
|
|
|
2700
3085
|
*/
|
|
2701
3086
|
const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
|
|
2702
3087
|
'com.apple.fps', [
|
|
2703
|
-
{initDataType: 'sinf', initData: new Uint8Array(0)},
|
|
3088
|
+
{initDataType: 'sinf', initData: new Uint8Array(0), keyId: null},
|
|
2704
3089
|
]);
|
|
2705
3090
|
|
|
2706
3091
|
return drmInfo;
|
|
@@ -2779,18 +3164,48 @@ shaka.hls.HlsParser = class {
|
|
|
2779
3164
|
|
|
2780
3165
|
return drmInfo;
|
|
2781
3166
|
}
|
|
3167
|
+
|
|
3168
|
+
/**
|
|
3169
|
+
* See: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-5.1
|
|
3170
|
+
*
|
|
3171
|
+
* @param {!shaka.hls.Tag} drmTag
|
|
3172
|
+
* @return {?shaka.extern.DrmInfo}
|
|
3173
|
+
* @private
|
|
3174
|
+
*/
|
|
3175
|
+
static identityDrmParser_(drmTag) {
|
|
3176
|
+
const method = drmTag.getRequiredAttrValue('METHOD');
|
|
3177
|
+
const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR'];
|
|
3178
|
+
if (!VALID_METHODS.includes(method)) {
|
|
3179
|
+
shaka.log.error('Identity (ClearKey) in HLS is only supported with [',
|
|
3180
|
+
VALID_METHODS.join(', '), '], not', method);
|
|
3181
|
+
return null;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
// NOTE: The ClearKey CDM requires a key-id to key mapping. HLS doesn't
|
|
3185
|
+
// provide a key ID anywhere. So although we could use the 'URI' attribute
|
|
3186
|
+
// to fetch the actual 16-byte key, without a key ID, we can't provide this
|
|
3187
|
+
// automatically to the ClearKey CDM. Instead, the application will have
|
|
3188
|
+
// to use player.configure('drm.clearKeys', { ... }) to provide the key IDs
|
|
3189
|
+
// and keys or player.configure('drm.servers.org\.w3\.clearkey', ...) to
|
|
3190
|
+
// provide a ClearKey license server URI.
|
|
3191
|
+
return shaka.util.ManifestParserUtils.createDrmInfo(
|
|
3192
|
+
'org.w3.clearkey', /* initDatas= */ null);
|
|
3193
|
+
}
|
|
2782
3194
|
};
|
|
2783
3195
|
|
|
2784
3196
|
|
|
2785
3197
|
/**
|
|
2786
3198
|
* @typedef {{
|
|
2787
3199
|
* stream: !shaka.extern.Stream,
|
|
3200
|
+
* type: string,
|
|
2788
3201
|
* verbatimMediaPlaylistUri: string,
|
|
2789
3202
|
* absoluteMediaPlaylistUri: string,
|
|
2790
3203
|
* maxTimestamp: number,
|
|
2791
3204
|
* mediaSequenceToStartTime: !Map.<number, number>,
|
|
2792
3205
|
* canSkipSegments: boolean,
|
|
2793
|
-
* hasEndList: boolean
|
|
3206
|
+
* hasEndList: boolean,
|
|
3207
|
+
* firstSequenceNumber: number,
|
|
3208
|
+
* loadedOnce: boolean
|
|
2794
3209
|
* }}
|
|
2795
3210
|
*
|
|
2796
3211
|
* @description
|
|
@@ -2798,6 +3213,8 @@ shaka.hls.HlsParser = class {
|
|
|
2798
3213
|
*
|
|
2799
3214
|
* @property {!shaka.extern.Stream} stream
|
|
2800
3215
|
* The Stream itself.
|
|
3216
|
+
* @property {string} type
|
|
3217
|
+
* The type value. Could be 'video', 'audio', 'text', or 'image'.
|
|
2801
3218
|
* @property {string} verbatimMediaPlaylistUri
|
|
2802
3219
|
* The verbatim media playlist URI, as it appeared in the master playlist.
|
|
2803
3220
|
* This has not been canonicalized into an absolute URI. This gives us a
|
|
@@ -2810,11 +3227,16 @@ shaka.hls.HlsParser = class {
|
|
|
2810
3227
|
* The maximum timestamp found in the stream.
|
|
2811
3228
|
* @property {!Map.<number, number>} mediaSequenceToStartTime
|
|
2812
3229
|
* A map of media sequence numbers to media start times.
|
|
3230
|
+
* Only used for VOD content.
|
|
2813
3231
|
* @property {boolean} canSkipSegments
|
|
2814
|
-
*
|
|
2815
|
-
*
|
|
3232
|
+
* True if the server supports delta playlist updates, and we can send a
|
|
3233
|
+
* request for a playlist that can skip older media segments.
|
|
2816
3234
|
* @property {boolean} hasEndList
|
|
2817
|
-
*
|
|
3235
|
+
* True if the stream has an EXT-X-ENDLIST tag.
|
|
3236
|
+
* @property {number} firstSequenceNumber
|
|
3237
|
+
* The sequence number of the first reference. Only calculated if needed.
|
|
3238
|
+
* @property {boolean} loadedOnce
|
|
3239
|
+
* True if the stream has been loaded at least once.
|
|
2818
3240
|
*/
|
|
2819
3241
|
shaka.hls.HlsParser.StreamInfo;
|
|
2820
3242
|
|
|
@@ -2936,6 +3358,8 @@ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = {
|
|
|
2936
3358
|
shaka.hls.HlsParser.widevineDrmParser_,
|
|
2937
3359
|
'com.microsoft.playready':
|
|
2938
3360
|
shaka.hls.HlsParser.playreadyDrmParser_,
|
|
3361
|
+
'identity':
|
|
3362
|
+
shaka.hls.HlsParser.identityDrmParser_,
|
|
2939
3363
|
};
|
|
2940
3364
|
|
|
2941
3365
|
|