shaka-player 4.5.0 → 4.6.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/ISSUE_TEMPLATE/bug_report.md +1 -1
- package/AUTHORS +2 -0
- package/CHANGELOG.md +98 -0
- package/CODE_OF_CONDUCT.md +1 -1
- package/CONTRIBUTORS +2 -0
- package/README.md +38 -32
- package/demo/common/asset.js +11 -17
- package/demo/common/assets.js +117 -11
- package/demo/config.js +28 -1
- package/demo/custom.js +52 -38
- package/demo/main.js +11 -3
- package/demo/search.js +7 -0
- package/dist/controls.css +1 -1
- package/dist/controls.css.map +1 -1
- package/dist/demo.compiled.debug.js +133 -124
- package/dist/demo.compiled.debug.map +3 -3
- package/dist/demo.compiled.js +133 -124
- package/dist/demo.compiled.map +3 -3
- package/dist/deps.js +20 -18
- package/dist/locales.js +35 -31
- package/dist/receiver.compiled.debug.js +58 -50
- package/dist/receiver.compiled.debug.map +3 -3
- package/dist/receiver.compiled.js +58 -50
- package/dist/receiver.compiled.map +3 -3
- package/dist/shaka-player.compiled.d.ts +440 -390
- package/dist/shaka-player.compiled.debug.d.ts +440 -390
- package/dist/shaka-player.compiled.debug.externs.js +162 -77
- package/dist/shaka-player.compiled.debug.js +786 -736
- package/dist/shaka-player.compiled.debug.map +4 -4
- package/dist/shaka-player.compiled.externs.js +162 -77
- package/dist/shaka-player.compiled.js +1154 -1121
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.d.ts +491 -437
- package/dist/shaka-player.ui.debug.d.ts +491 -437
- package/dist/shaka-player.ui.debug.externs.js +167 -77
- package/dist/shaka-player.ui.debug.js +877 -811
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +167 -77
- package/dist/shaka-player.ui.js +1362 -1318
- package/dist/shaka-player.ui.map +4 -4
- package/docs/tutorials/ad_monetization.md +54 -0
- package/docs/tutorials/basic-usage.md +2 -1
- package/docs/tutorials/errors.md +2 -1
- package/docs/tutorials/offline.md +10 -8
- package/docs/tutorials/plugins.md +5 -0
- package/docs/tutorials/ui-customization.md +13 -0
- package/docs/tutorials/ui.md +2 -1
- package/docs/tutorials/upgrade.md +3 -0
- package/externs/managedmediasource.js +24 -0
- package/externs/shaka/abr_manager.js +6 -2
- package/externs/shaka/cea.js +7 -0
- package/externs/shaka/codecs.js +5 -3
- package/externs/shaka/manifest.js +21 -5
- package/externs/shaka/manifest_parser.js +17 -0
- package/externs/shaka/net.js +2 -2
- package/externs/shaka/offline.js +9 -2
- package/externs/shaka/player.js +53 -7
- package/externs/shaka/text.js +3 -1
- package/externs/shaka/transmuxer.js +1 -1
- package/lib/abr/simple_abr_manager.js +25 -8
- package/lib/ads/media_tailor_ad_manager.js +14 -3
- package/lib/cast/cast_receiver.js +1 -1
- package/lib/cea/cea_decoder.js +28 -0
- package/lib/cea/dummy_caption_decoder.js +5 -0
- package/lib/dash/dash_parser.js +249 -83
- package/lib/dash/mpd_utils.js +52 -31
- package/lib/dash/segment_base.js +4 -5
- package/lib/dash/segment_list.js +5 -4
- package/lib/dash/segment_template.js +83 -26
- package/lib/hls/hls_classes.js +28 -43
- package/lib/hls/hls_parser.js +761 -649
- package/lib/hls/hls_utils.js +64 -5
- package/lib/hls/manifest_text_parser.js +7 -11
- package/lib/media/adaptation_set.js +20 -13
- package/lib/media/adaptation_set_criteria.js +70 -18
- package/lib/media/closed_caption_parser.js +13 -0
- package/lib/media/content_workarounds.js +6 -0
- package/lib/media/drm_engine.js +8 -13
- package/lib/media/manifest_parser.js +8 -113
- package/lib/media/media_source_capabilities.js +10 -3
- package/lib/media/media_source_engine.js +75 -21
- package/lib/media/play_rate_controller.js +8 -38
- package/lib/media/presentation_timeline.js +7 -1
- package/lib/media/quality_observer.js +1 -1
- package/lib/media/segment_index.js +5 -5
- package/lib/media/segment_prefetch.js +92 -24
- package/lib/media/segment_reference.js +67 -1
- package/lib/media/segment_utils.js +343 -0
- package/lib/media/stall_detector.js +0 -3
- package/lib/media/streaming_engine.js +41 -21
- package/lib/mss/mss_parser.js +18 -2
- package/lib/net/networking_engine.js +66 -12
- package/lib/net/networking_utils.js +95 -0
- package/lib/offline/indexeddb/v1_storage_cell.js +2 -0
- package/lib/offline/indexeddb/v2_storage_cell.js +2 -0
- package/lib/offline/manifest_converter.js +2 -0
- package/lib/offline/offline_manifest_parser.js +10 -0
- package/lib/offline/storage.js +11 -7
- package/lib/player.js +993 -1502
- package/lib/polyfill/mediasource.js +22 -1
- package/lib/text/mp4_ttml_parser.js +2 -2
- package/lib/text/srt_text_parser.js +2 -2
- package/lib/text/stub_text_displayer.js +51 -0
- package/lib/text/text_engine.js +3 -2
- package/lib/text/ttml_text_parser.js +28 -5
- package/lib/text/ui_text_displayer.js +4 -0
- package/lib/text/vtt_text_parser.js +7 -2
- package/lib/transmuxer/aac_transmuxer.js +1 -1
- package/lib/transmuxer/ac3_transmuxer.js +1 -1
- package/lib/transmuxer/ec3_transmuxer.js +1 -1
- package/lib/transmuxer/h264.js +5 -4
- package/lib/transmuxer/h265.js +5 -4
- package/lib/transmuxer/mp3_transmuxer.js +1 -1
- package/lib/transmuxer/mpeg_ts_transmuxer.js +1 -1
- package/lib/transmuxer/mss_transmuxer.js +1 -1
- package/lib/transmuxer/ts_transmuxer.js +33 -13
- package/lib/util/buffer_utils.js +26 -4
- package/lib/util/content_steering_manager.js +358 -0
- package/lib/util/error.js +2 -6
- package/lib/util/fairplay_utils.js +1 -1
- package/lib/util/fake_event.js +5 -1
- package/lib/util/id3_utils.js +50 -1
- package/lib/util/manifest_parser_utils.js +12 -8
- package/lib/util/mp4_generator.js +2 -4
- package/lib/util/mutex.js +59 -0
- package/lib/util/object_utils.js +1 -1
- package/lib/util/periods.js +154 -179
- package/lib/util/platform.js +4 -3
- package/lib/util/player_configuration.js +35 -6
- package/lib/util/stream_utils.js +105 -98
- package/lib/util/ts_parser.js +124 -73
- package/lib/util/uint8array_utils.js +5 -1
- package/lib/util/xml_utils.js +5 -0
- package/maintained-branches.md +2 -2
- package/package.json +1 -1
- package/roadmap.md +15 -5
- package/shaka-player.uncompiled.js +0 -1
- package/test/abr/simple_abr_manager_unit.js +33 -33
- package/test/ads/ad_manager_unit.js +3 -2
- package/test/cast/cast_receiver_integration.js +3 -4
- package/test/cast/cast_utils_unit.js +2 -0
- package/test/cea/cea_decoder_unit.js +7 -0
- package/test/codec_switching/codec_switching_integration.js +2 -1
- package/test/dash/dash_parser_integration.js +2 -1
- package/test/dash/dash_parser_live_unit.js +0 -19
- package/test/dash/dash_parser_manifest_unit.js +494 -2
- package/test/dash/mpd_utils_unit.js +61 -30
- package/test/hls/hls_live_unit.js +12 -3
- package/test/hls/hls_parser_unit.js +377 -27
- package/test/hls/manifest_text_parser_unit.js +32 -99
- package/test/media/adaptation_set_criteria_unit.js +172 -21
- package/test/media/adaptation_set_unit.js +1 -0
- package/test/media/content_workarounds_unit.js +1 -1
- package/test/media/drm_engine_unit.js +3 -3
- package/test/media/media_source_engine_unit.js +1 -1
- package/test/media/play_rate_controller_unit.js +0 -48
- package/test/media/segment_index_unit.js +2 -1
- package/test/mss/mss_player_integration.js +3 -2
- package/test/net/networking_engine_unit.js +3 -3
- package/test/{media/manifest_parser_unit.js → net/networking_utils_unit.js} +2 -2
- package/test/offline/manifest_convert_unit.js +9 -0
- package/test/offline/offline_integration.js +2 -1
- package/test/offline/storage_integration.js +21 -9
- package/test/player_external.js +4 -2
- package/test/player_integration.js +15 -4
- package/test/player_load_graph_integration.js +47 -151
- package/test/player_src_equals_external.js +1 -0
- package/test/player_src_equals_integration.js +71 -4
- package/test/player_unit.js +80 -27
- package/test/test/util/fake_media_source_engine.js +5 -0
- package/test/test/util/fake_networking_engine.js +9 -1
- package/test/test/util/fake_segment_prefetch.js +3 -0
- package/test/test/util/manifest_generator.js +4 -0
- package/test/test/util/manifest_parser_util.js +14 -2
- package/test/test/util/offline_utils.js +1 -0
- package/test/test/util/simple_fakes.js +2 -2
- package/test/test/util/streaming_engine_util.js +3 -0
- package/test/test/util/test_scheme.js +6 -0
- package/test/test/util/ui_utils.js +4 -3
- package/test/text/mp4_ttml_parser_unit.js +4 -4
- package/test/text/srt_text_parser_unit.js +1 -1
- package/test/text/text_displayer_layout_unit.js +1 -1
- package/test/text/text_engine_unit.js +6 -0
- package/test/text/ttml_text_parser_unit.js +33 -2
- package/test/transmuxer/transmuxer_engine_integration.js +9 -0
- package/test/transmuxer/transmuxer_integration.js +33 -3
- package/test/ui/ad_ui_unit.js +2 -2
- package/test/ui/ui_customization_unit.js +17 -16
- package/test/ui/ui_integration.js +30 -20
- package/test/ui/ui_unit.js +25 -19
- package/test/util/buffer_utils_unit.js +1 -1
- package/test/util/content_steering_manager_unit.js +427 -0
- package/test/util/id3_utils_unit.js +52 -0
- package/test/util/stream_utils_unit.js +17 -113
- package/test/util/ts_parser_unit.js +3 -3
- package/third_party/closure-uri/uri.js +13 -1
- package/ui/audio_language_selection.js +9 -2
- package/ui/externs/ui.js +17 -2
- package/ui/language_utils.js +43 -8
- package/ui/less/range_elements.less +33 -0
- package/ui/locales/en.json +1 -0
- package/ui/locales/es-419.json +1 -0
- package/ui/locales/es.json +1 -0
- package/ui/locales/source.json +4 -0
- package/ui/localization.js +10 -0
- package/ui/remote_button.js +3 -1
- package/ui/resolution_selection.js +47 -10
- package/ui/seek_bar.js +296 -3
- package/ui/text_selection.js +2 -1
- package/ui/ui.js +6 -2
- package/lib/routing/node.js +0 -23
- package/lib/routing/payload.js +0 -47
- package/lib/routing/walker.js +0 -587
- package/test/routing/walker_unit.js +0 -505
package/lib/hls/hls_parser.js
CHANGED
|
@@ -26,18 +26,16 @@ goog.require('shaka.net.DataUriPlugin');
|
|
|
26
26
|
goog.require('shaka.net.NetworkingEngine');
|
|
27
27
|
goog.require('shaka.util.ArrayUtils');
|
|
28
28
|
goog.require('shaka.util.BufferUtils');
|
|
29
|
+
goog.require('shaka.util.ContentSteeringManager');
|
|
29
30
|
goog.require('shaka.util.Error');
|
|
30
31
|
goog.require('shaka.util.FakeEvent');
|
|
31
|
-
goog.require('shaka.util.Functional');
|
|
32
32
|
goog.require('shaka.util.LanguageUtils');
|
|
33
33
|
goog.require('shaka.util.ManifestParserUtils');
|
|
34
34
|
goog.require('shaka.util.MimeUtils');
|
|
35
|
-
goog.require('shaka.util.Mp4BoxParsers');
|
|
36
|
-
goog.require('shaka.util.Mp4Parser');
|
|
37
35
|
goog.require('shaka.util.OperationManager');
|
|
38
36
|
goog.require('shaka.util.Pssh');
|
|
37
|
+
goog.require('shaka.media.SegmentUtils');
|
|
39
38
|
goog.require('shaka.util.Timer');
|
|
40
|
-
goog.require('shaka.util.TsParser');
|
|
41
39
|
goog.require('shaka.util.Platform');
|
|
42
40
|
goog.require('shaka.util.Uint8ArrayUtils');
|
|
43
41
|
goog.require('shaka.util.XmlUtils');
|
|
@@ -112,8 +110,8 @@ shaka.hls.HlsParser = class {
|
|
|
112
110
|
* timestamps, offsets, and to handle TS rollover.
|
|
113
111
|
*
|
|
114
112
|
* During parsing, used to avoid duplicates in the async methods
|
|
115
|
-
*
|
|
116
|
-
*
|
|
113
|
+
* createStreamInfoFromMediaTags_, createStreamInfoFromImageTag_ and
|
|
114
|
+
* createStreamInfoFromVariantTags_.
|
|
117
115
|
*
|
|
118
116
|
* @private {!Map.<string, shaka.hls.HlsParser.StreamInfo>}
|
|
119
117
|
*/
|
|
@@ -157,6 +155,13 @@ shaka.hls.HlsParser = class {
|
|
|
157
155
|
*/
|
|
158
156
|
this.streamsFinalized_ = false;
|
|
159
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Whether the manifest informs about the codec to use.
|
|
160
|
+
*
|
|
161
|
+
* @private
|
|
162
|
+
*/
|
|
163
|
+
this.codecInfoInManifest_ = false;
|
|
164
|
+
|
|
160
165
|
/**
|
|
161
166
|
* This timer is used to trigger the start of a manifest update. A manifest
|
|
162
167
|
* update is async. Once the update is finished, the timer will be restarted
|
|
@@ -223,6 +228,9 @@ shaka.hls.HlsParser = class {
|
|
|
223
228
|
* @private {!shaka.abr.Ewma}
|
|
224
229
|
*/
|
|
225
230
|
this.averageUpdateDuration_ = new shaka.abr.Ewma(5);
|
|
231
|
+
|
|
232
|
+
/** @private {?shaka.util.ContentSteeringManager} */
|
|
233
|
+
this.contentSteeringManager_ = null;
|
|
226
234
|
}
|
|
227
235
|
|
|
228
236
|
|
|
@@ -232,6 +240,10 @@ shaka.hls.HlsParser = class {
|
|
|
232
240
|
*/
|
|
233
241
|
configure(config) {
|
|
234
242
|
this.config_ = config;
|
|
243
|
+
|
|
244
|
+
if (this.contentSteeringManager_) {
|
|
245
|
+
this.contentSteeringManager_.configure(this.config_);
|
|
246
|
+
}
|
|
235
247
|
}
|
|
236
248
|
|
|
237
249
|
/**
|
|
@@ -243,7 +255,7 @@ shaka.hls.HlsParser = class {
|
|
|
243
255
|
this.playerInterface_ = playerInterface;
|
|
244
256
|
this.lowLatencyMode_ = playerInterface.isLowLatencyMode();
|
|
245
257
|
|
|
246
|
-
const response = await this.requestManifest_(uri);
|
|
258
|
+
const response = await this.requestManifest_([uri]);
|
|
247
259
|
|
|
248
260
|
// Record the master playlist URI after redirects.
|
|
249
261
|
this.masterPlaylistUri_ = response.uri;
|
|
@@ -284,6 +296,10 @@ shaka.hls.HlsParser = class {
|
|
|
284
296
|
this.groupIdToCodecsMap_.clear();
|
|
285
297
|
this.globalVariables_.clear();
|
|
286
298
|
|
|
299
|
+
if (this.contentSteeringManager_) {
|
|
300
|
+
this.contentSteeringManager_.destroy();
|
|
301
|
+
}
|
|
302
|
+
|
|
287
303
|
return Promise.all(pending);
|
|
288
304
|
}
|
|
289
305
|
|
|
@@ -354,40 +370,42 @@ shaka.hls.HlsParser = class {
|
|
|
354
370
|
* @private
|
|
355
371
|
*/
|
|
356
372
|
async updateStream_(streamInfo) {
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (streamInfo.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
373
|
+
const manifestUris = [];
|
|
374
|
+
for (const uri of streamInfo.getUris()) {
|
|
375
|
+
const uriObj = new goog.Uri(uri);
|
|
376
|
+
const queryData = uriObj.getQueryData();
|
|
377
|
+
if (streamInfo.canBlockReload) {
|
|
378
|
+
if (streamInfo.nextMediaSequence >= 0) {
|
|
379
|
+
// Indicates that the server must hold the request until a Playlist
|
|
380
|
+
// contains a Media Segment with Media Sequence
|
|
381
|
+
queryData.add('_HLS_msn', String(streamInfo.nextMediaSequence));
|
|
382
|
+
}
|
|
383
|
+
if (streamInfo.nextPart >= 0) {
|
|
384
|
+
// Indicates, in combination with _HLS_msn, that the server must hold
|
|
385
|
+
// the request until a Playlist contains Partial Segment N of Media
|
|
386
|
+
// Sequence Number M or later.
|
|
387
|
+
queryData.add('_HLS_part', String(streamInfo.nextPart));
|
|
388
|
+
}
|
|
365
389
|
}
|
|
366
|
-
if (streamInfo.
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
queryData.add('_HLS_part', String(streamInfo.nextPart));
|
|
390
|
+
if (streamInfo.canSkipSegments) {
|
|
391
|
+
// Enable delta updates. This will replace older segments with
|
|
392
|
+
// 'EXT-X-SKIP' tag in the media playlist.
|
|
393
|
+
queryData.add('_HLS_skip', 'YES');
|
|
371
394
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
queryData.add('_HLS_skip', 'YES');
|
|
377
|
-
}
|
|
378
|
-
if (queryData.getCount()) {
|
|
379
|
-
uriObj.setQueryData(queryData);
|
|
395
|
+
if (queryData.getCount()) {
|
|
396
|
+
uriObj.setQueryData(queryData);
|
|
397
|
+
}
|
|
398
|
+
manifestUris.push(uriObj.toString());
|
|
380
399
|
}
|
|
381
400
|
const response =
|
|
382
|
-
await this.requestManifest_(
|
|
401
|
+
await this.requestManifest_(manifestUris, /* isPlaylist= */ true);
|
|
383
402
|
if (!streamInfo.stream.segmentIndex) {
|
|
384
403
|
// The stream was closed since the update was first requested.
|
|
385
404
|
return;
|
|
386
405
|
}
|
|
387
406
|
|
|
388
407
|
/** @type {shaka.hls.Playlist} */
|
|
389
|
-
const playlist = this.manifestTextParser_.parsePlaylist(
|
|
390
|
-
response.data, response.uri);
|
|
408
|
+
const playlist = this.manifestTextParser_.parsePlaylist(response.data);
|
|
391
409
|
|
|
392
410
|
if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
|
|
393
411
|
throw new shaka.util.Error(
|
|
@@ -396,11 +414,19 @@ shaka.hls.HlsParser = class {
|
|
|
396
414
|
shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
|
|
397
415
|
}
|
|
398
416
|
|
|
417
|
+
// Record the final URI after redirects.
|
|
418
|
+
const responseUri = response.uri;
|
|
419
|
+
if (responseUri != response.originalUri &&
|
|
420
|
+
!streamInfo.getUris().includes(responseUri)) {
|
|
421
|
+
streamInfo.redirectUris.push(responseUri);
|
|
422
|
+
}
|
|
423
|
+
|
|
399
424
|
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
400
425
|
const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
|
|
401
426
|
'EXT-X-DEFINE');
|
|
402
427
|
|
|
403
|
-
const mediaVariables = this.parseMediaVariables_(
|
|
428
|
+
const mediaVariables = this.parseMediaVariables_(
|
|
429
|
+
variablesTags, responseUri);
|
|
404
430
|
|
|
405
431
|
const stream = streamInfo.stream;
|
|
406
432
|
|
|
@@ -418,8 +444,8 @@ shaka.hls.HlsParser = class {
|
|
|
418
444
|
}
|
|
419
445
|
|
|
420
446
|
const {segments, bandwidth} = this.createSegments_(
|
|
421
|
-
|
|
422
|
-
|
|
447
|
+
playlist, stream, mediaSequenceToStartTime, mediaVariables,
|
|
448
|
+
streamInfo.getUris);
|
|
423
449
|
|
|
424
450
|
stream.bandwidth = bandwidth;
|
|
425
451
|
|
|
@@ -474,6 +500,24 @@ shaka.hls.HlsParser = class {
|
|
|
474
500
|
// No-op
|
|
475
501
|
}
|
|
476
502
|
|
|
503
|
+
/**
|
|
504
|
+
* @override
|
|
505
|
+
* @exportInterface
|
|
506
|
+
*/
|
|
507
|
+
onInitialVariantChosen(variant) {
|
|
508
|
+
// No-op
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* @override
|
|
513
|
+
* @exportInterface
|
|
514
|
+
*/
|
|
515
|
+
banLocation(uri) {
|
|
516
|
+
if (this.contentSteeringManager_) {
|
|
517
|
+
this.contentSteeringManager_.banLocation(uri);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
477
521
|
/**
|
|
478
522
|
* Align the streams by sequence number by dropping early segments. Then
|
|
479
523
|
* offset the streams to begin at presentation time 0.
|
|
@@ -599,7 +643,7 @@ shaka.hls.HlsParser = class {
|
|
|
599
643
|
const segment0 = segmentIndex.earliestReference();
|
|
600
644
|
if (segment0.syncTime == null) {
|
|
601
645
|
shaka.log.alwaysError('Missing EXT-X-PROGRAM-DATE-TIME for stream',
|
|
602
|
-
streamInfo.
|
|
646
|
+
streamInfo.getUris(),
|
|
603
647
|
'Expect AV sync issues!');
|
|
604
648
|
} else {
|
|
605
649
|
// Stream metadata are offset by a fixed amount based on the
|
|
@@ -640,7 +684,7 @@ shaka.hls.HlsParser = class {
|
|
|
640
684
|
}
|
|
641
685
|
|
|
642
686
|
shaka.log.debug('Offset', offset, 'applied to',
|
|
643
|
-
streamInfo.
|
|
687
|
+
streamInfo.getUris());
|
|
644
688
|
}
|
|
645
689
|
|
|
646
690
|
/**
|
|
@@ -657,14 +701,11 @@ shaka.hls.HlsParser = class {
|
|
|
657
701
|
goog.asserts.assert(this.masterPlaylistUri_,
|
|
658
702
|
'Master playlist URI must be set before calling parseManifest_!');
|
|
659
703
|
|
|
660
|
-
const playlist = this.manifestTextParser_.parsePlaylist(
|
|
661
|
-
data, this.masterPlaylistUri_);
|
|
704
|
+
const playlist = this.manifestTextParser_.parsePlaylist(data);
|
|
662
705
|
|
|
663
706
|
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
664
707
|
const variablesTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-DEFINE');
|
|
665
708
|
|
|
666
|
-
this.parseMasterVariables_(variablesTags);
|
|
667
|
-
|
|
668
709
|
/** @type {!Array.<!shaka.extern.Variant>} */
|
|
669
710
|
let variants = [];
|
|
670
711
|
/** @type {!Array.<!shaka.extern.Stream>} */
|
|
@@ -674,9 +715,14 @@ shaka.hls.HlsParser = class {
|
|
|
674
715
|
|
|
675
716
|
// Parsing a media playlist results in a single-variant stream.
|
|
676
717
|
if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
|
|
718
|
+
const getUris = () => {
|
|
719
|
+
return [uri];
|
|
720
|
+
};
|
|
721
|
+
|
|
677
722
|
// Get necessary info for this stream. These are things we would normally
|
|
678
723
|
// find from the master playlist (e.g. from values on EXT-X-MEDIA tags).
|
|
679
|
-
const basicInfo =
|
|
724
|
+
const basicInfo =
|
|
725
|
+
await this.getMediaPlaylistBasicInfo_(playlist, getUris);
|
|
680
726
|
const type = basicInfo.type;
|
|
681
727
|
const mimeType = basicInfo.mimeType;
|
|
682
728
|
const codecs = basicInfo.codecs;
|
|
@@ -685,6 +731,7 @@ shaka.hls.HlsParser = class {
|
|
|
685
731
|
const width = basicInfo.width;
|
|
686
732
|
const channelsCount = basicInfo.channelCount;
|
|
687
733
|
const sampleRate = basicInfo.sampleRate;
|
|
734
|
+
const closedCaptions = basicInfo.closedCaptions;
|
|
688
735
|
|
|
689
736
|
// Some values we cannot figure out, and aren't important enough to ask
|
|
690
737
|
// the user to provide through config values. A lot of these are only
|
|
@@ -692,21 +739,21 @@ shaka.hls.HlsParser = class {
|
|
|
692
739
|
// So these unknowns should be set to false or null, largely.
|
|
693
740
|
const spatialAudio = false;
|
|
694
741
|
const characteristics = null;
|
|
695
|
-
const closedCaptions = new Map();
|
|
696
742
|
const forced = false; // Only relevant for text.
|
|
697
743
|
const primary = true; // This is the only stream!
|
|
698
744
|
const name = 'Media Playlist';
|
|
699
745
|
|
|
700
746
|
// Make the stream info, with those values.
|
|
701
747
|
const streamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
|
|
702
|
-
playlist,
|
|
703
|
-
|
|
704
|
-
spatialAudio, mimeType);
|
|
748
|
+
this.globalId_++, playlist, getUris, uri, codecs, type,
|
|
749
|
+
languageValue, primary, name, channelsCount, closedCaptions,
|
|
750
|
+
characteristics, forced, sampleRate, spatialAudio, mimeType);
|
|
705
751
|
this.uriToStreamInfosMap_.set(uri, streamInfo);
|
|
706
752
|
|
|
707
753
|
if (type == 'video') {
|
|
708
754
|
this.addVideoAttributes_(streamInfo.stream, width, height,
|
|
709
|
-
/* frameRate= */ null, /* videoRange= */ null
|
|
755
|
+
/* frameRate= */ null, /* videoRange= */ null,
|
|
756
|
+
/* videoLayout= */ null);
|
|
710
757
|
}
|
|
711
758
|
|
|
712
759
|
// Wrap the stream from that stream info with a variant.
|
|
@@ -723,8 +770,11 @@ shaka.hls.HlsParser = class {
|
|
|
723
770
|
decodingInfos: [],
|
|
724
771
|
});
|
|
725
772
|
} else {
|
|
773
|
+
this.parseMasterVariables_(variablesTags);
|
|
774
|
+
|
|
726
775
|
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
727
|
-
const mediaTags = Utils.filterTagsByName(
|
|
776
|
+
const mediaTags = Utils.filterTagsByName(
|
|
777
|
+
playlist.tags, 'EXT-X-MEDIA');
|
|
728
778
|
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
729
779
|
const variantTags = Utils.filterTagsByName(
|
|
730
780
|
playlist.tags, 'EXT-X-STREAM-INF');
|
|
@@ -732,43 +782,28 @@ shaka.hls.HlsParser = class {
|
|
|
732
782
|
const imageTags = Utils.filterTagsByName(
|
|
733
783
|
playlist.tags, 'EXT-X-IMAGE-STREAM-INF');
|
|
734
784
|
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
785
|
+
const iFrameTags = Utils.filterTagsByName(
|
|
786
|
+
playlist.tags, 'EXT-X-I-FRAME-STREAM-INF');
|
|
787
|
+
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
735
788
|
const sessionKeyTags = Utils.filterTagsByName(
|
|
736
789
|
playlist.tags, 'EXT-X-SESSION-KEY');
|
|
790
|
+
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
791
|
+
const sessionDataTags = Utils.filterTagsByName(
|
|
792
|
+
playlist.tags, 'EXT-X-SESSION-DATA');
|
|
793
|
+
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
794
|
+
const contentSteeringTags = Utils.filterTagsByName(
|
|
795
|
+
playlist.tags, 'EXT-X-CONTENT-STEERING');
|
|
737
796
|
|
|
738
|
-
this.
|
|
797
|
+
this.processSessionData_(sessionDataTags);
|
|
798
|
+
await this.processContentSteering_(contentSteeringTags);
|
|
739
799
|
|
|
740
|
-
|
|
741
|
-
const sesionDataTags =
|
|
742
|
-
Utils.filterTagsByName(playlist.tags, 'EXT-X-SESSION-DATA');
|
|
743
|
-
for (const tag of sesionDataTags) {
|
|
744
|
-
const id = tag.getAttributeValue('DATA-ID');
|
|
745
|
-
const uri = tag.getAttributeValue('URI');
|
|
746
|
-
const language = tag.getAttributeValue('LANGUAGE');
|
|
747
|
-
const value = tag.getAttributeValue('VALUE');
|
|
748
|
-
const data = (new Map()).set('id', id);
|
|
749
|
-
if (uri) {
|
|
750
|
-
data.set('uri', shaka.hls.Utils.constructAbsoluteUri(
|
|
751
|
-
this.masterPlaylistUri_, uri));
|
|
752
|
-
}
|
|
753
|
-
if (language) {
|
|
754
|
-
data.set('language', language);
|
|
755
|
-
}
|
|
756
|
-
if (value) {
|
|
757
|
-
data.set('value', value);
|
|
758
|
-
}
|
|
759
|
-
const event = new shaka.util.FakeEvent('sessiondata', data);
|
|
760
|
-
if (this.playerInterface_) {
|
|
761
|
-
this.playerInterface_.onEvent(event);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
800
|
+
this.parseCodecs_(variantTags);
|
|
764
801
|
|
|
765
|
-
// Parse audio and video media tags first, so that we can extract segment
|
|
766
|
-
// start time from audio/video streams and reuse for text streams.
|
|
767
|
-
this.createStreamInfosFromMediaTags_(mediaTags);
|
|
768
802
|
this.parseClosedCaptions_(mediaTags);
|
|
769
|
-
variants =
|
|
803
|
+
variants =
|
|
804
|
+
this.createVariantsForTags_(variantTags, sessionKeyTags, mediaTags);
|
|
770
805
|
textStreams = this.parseTexts_(mediaTags);
|
|
771
|
-
imageStreams = await this.parseImages_(imageTags);
|
|
806
|
+
imageStreams = await this.parseImages_(imageTags, iFrameTags);
|
|
772
807
|
}
|
|
773
808
|
|
|
774
809
|
// Make sure that the parser has not been destroyed.
|
|
@@ -817,64 +852,72 @@ shaka.hls.HlsParser = class {
|
|
|
817
852
|
type: shaka.media.ManifestParser.HLS,
|
|
818
853
|
serviceDescription: null,
|
|
819
854
|
};
|
|
855
|
+
|
|
856
|
+
// If there is no 'CODECS' attribute in the manifest and codec guessing is
|
|
857
|
+
// disabled, we need to create the segment indexes now so that missing info
|
|
858
|
+
// can be parsed from the media data and added to the stream objects.
|
|
859
|
+
if (!this.codecInfoInManifest_ && this.config_.hls.disableCodecGuessing) {
|
|
860
|
+
const createIndexes = [];
|
|
861
|
+
for (const variant of this.manifest_.variants) {
|
|
862
|
+
if (variant.audio && variant.audio.codecs === '') {
|
|
863
|
+
createIndexes.push(variant.audio.createSegmentIndex());
|
|
864
|
+
}
|
|
865
|
+
if (variant.video && variant.video.codecs === '') {
|
|
866
|
+
createIndexes.push(variant.video.createSegmentIndex());
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
await Promise.all(createIndexes);
|
|
871
|
+
}
|
|
872
|
+
|
|
820
873
|
this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
|
|
821
874
|
}
|
|
822
875
|
|
|
823
876
|
/**
|
|
824
877
|
* @param {shaka.hls.Playlist} playlist
|
|
825
|
-
* @
|
|
878
|
+
* @param {function():!Array.<string>} getUris
|
|
879
|
+
* @return {!Promise.<shaka.media.SegmentUtils.BasicInfo>}
|
|
826
880
|
* @private
|
|
827
881
|
*/
|
|
828
|
-
async getMediaPlaylistBasicInfo_(playlist) {
|
|
882
|
+
async getMediaPlaylistBasicInfo_(playlist, getUris) {
|
|
829
883
|
const HlsParser = shaka.hls.HlsParser;
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
shaka.util.MimeUtils.getBasicType(defaultFullMimeType);
|
|
833
|
-
const defaultType = defaultMimeType.split('/')[0];
|
|
834
|
-
const defaultCodecs = shaka.util.MimeUtils.getCodecs(defaultFullMimeType);
|
|
835
|
-
const defaultBasicInfo = {
|
|
836
|
-
type: defaultType,
|
|
837
|
-
mimeType: defaultMimeType,
|
|
838
|
-
codecs: defaultCodecs,
|
|
839
|
-
language: null,
|
|
840
|
-
height: null,
|
|
841
|
-
width: null,
|
|
842
|
-
channelCount: null,
|
|
843
|
-
sampleRate: null,
|
|
844
|
-
};
|
|
884
|
+
const defaultBasicInfo = shaka.media.SegmentUtils.getBasicInfoFromMimeType(
|
|
885
|
+
this.config_.hls.mediaPlaylistFullMimeType);
|
|
845
886
|
if (!playlist.segments.length) {
|
|
846
887
|
return defaultBasicInfo;
|
|
847
888
|
}
|
|
848
|
-
const
|
|
849
|
-
const
|
|
889
|
+
const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2);
|
|
890
|
+
const middleSegment = playlist.segments[middleSegmentIdx];
|
|
891
|
+
const middleSegmentUris = shaka.hls.Utils.constructSegmentUris(
|
|
892
|
+
getUris(),
|
|
893
|
+
playlist.segments[0].verbatimSegmentUri);
|
|
894
|
+
const middleSegmentUri = middleSegmentUris[0];
|
|
895
|
+
const parsedUri = new goog.Uri(middleSegmentUri);
|
|
850
896
|
const extension = parsedUri.getPath().split('.').pop();
|
|
851
897
|
const rawMimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension];
|
|
852
898
|
if (rawMimeType) {
|
|
853
|
-
return
|
|
854
|
-
|
|
855
|
-
mimeType: rawMimeType,
|
|
856
|
-
codecs: '',
|
|
857
|
-
language: null,
|
|
858
|
-
height: null,
|
|
859
|
-
width: null,
|
|
860
|
-
channelCount: null,
|
|
861
|
-
sampleRate: null,
|
|
862
|
-
};
|
|
899
|
+
return shaka.media.SegmentUtils.getBasicInfoFromMimeType(
|
|
900
|
+
rawMimeType);
|
|
863
901
|
}
|
|
864
902
|
|
|
865
|
-
|
|
903
|
+
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
904
|
+
|
|
905
|
+
let initData = null;
|
|
866
906
|
const initSegmentRef = this.getInitSegmentReference_(
|
|
867
|
-
playlist,
|
|
907
|
+
playlist, middleSegment.tags, getUris);
|
|
868
908
|
if (initSegmentRef) {
|
|
869
|
-
|
|
909
|
+
const initSegmentRequest = shaka.net.NetworkingEngine.makeRequest(
|
|
910
|
+
initSegmentRef.getUris(), this.config_.retryParameters);
|
|
911
|
+
const initType =
|
|
912
|
+
shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT;
|
|
913
|
+
const initResponse = await this.makeNetworkRequest_(
|
|
914
|
+
initSegmentRequest, requestType, {type: initType});
|
|
915
|
+
initData = initResponse.data;
|
|
870
916
|
}
|
|
871
917
|
|
|
872
|
-
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
873
918
|
const segmentRequest = shaka.net.NetworkingEngine.makeRequest(
|
|
874
|
-
|
|
875
|
-
const type =
|
|
876
|
-
shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT :
|
|
877
|
-
shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
|
|
919
|
+
middleSegmentUris, this.config_.retryParameters);
|
|
920
|
+
const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
|
|
878
921
|
const response = await this.makeNetworkRequest_(
|
|
879
922
|
segmentRequest, requestType, {type});
|
|
880
923
|
|
|
@@ -885,13 +928,16 @@ shaka.hls.HlsParser = class {
|
|
|
885
928
|
}
|
|
886
929
|
|
|
887
930
|
if (extension == 'ts' || contentMimeType == 'video/mp2t') {
|
|
888
|
-
const basicInfo =
|
|
931
|
+
const basicInfo =
|
|
932
|
+
shaka.media.SegmentUtils.getBasicInfoFromTs(response.data);
|
|
889
933
|
if (basicInfo) {
|
|
890
934
|
return basicInfo;
|
|
891
935
|
}
|
|
892
|
-
} else if (extension == 'mp4' ||
|
|
893
|
-
contentMimeType == 'video/mp4' || contentMimeType == 'audio/mp4'
|
|
894
|
-
|
|
936
|
+
} else if (extension == 'mp4' || extension == 'cmfv' ||
|
|
937
|
+
contentMimeType == 'video/mp4' || contentMimeType == 'audio/mp4' ||
|
|
938
|
+
contentMimeType == 'video/iso.segment') {
|
|
939
|
+
const basicInfo = shaka.media.SegmentUtils.getBasicInfoFromMp4(
|
|
940
|
+
initData, response.data);
|
|
895
941
|
if (basicInfo) {
|
|
896
942
|
return basicInfo;
|
|
897
943
|
}
|
|
@@ -899,236 +945,6 @@ shaka.hls.HlsParser = class {
|
|
|
899
945
|
return defaultBasicInfo;
|
|
900
946
|
}
|
|
901
947
|
|
|
902
|
-
/**
|
|
903
|
-
* @param {shaka.extern.Response} response
|
|
904
|
-
* @return {?shaka.hls.HlsParser.BasicInfo}
|
|
905
|
-
* @private
|
|
906
|
-
*/
|
|
907
|
-
getBasicInfoFromTs_(response) {
|
|
908
|
-
const uint8ArrayData = shaka.util.BufferUtils.toUint8(response.data);
|
|
909
|
-
const tsParser = new shaka.util.TsParser().parse(uint8ArrayData);
|
|
910
|
-
const tsCodecs = tsParser.getCodecs();
|
|
911
|
-
const videoInfo = tsParser.getVideoInfo();
|
|
912
|
-
const codecs = [];
|
|
913
|
-
let hasAudio = false;
|
|
914
|
-
let hasVideo = false;
|
|
915
|
-
switch (tsCodecs.audio) {
|
|
916
|
-
case 'aac':
|
|
917
|
-
codecs.push('mp4a.40.2');
|
|
918
|
-
hasAudio = true;
|
|
919
|
-
break;
|
|
920
|
-
case 'mp3':
|
|
921
|
-
codecs.push('mp4a.40.34');
|
|
922
|
-
hasAudio = true;
|
|
923
|
-
break;
|
|
924
|
-
case 'ac3':
|
|
925
|
-
codecs.push('ac-3');
|
|
926
|
-
hasAudio = true;
|
|
927
|
-
break;
|
|
928
|
-
case 'ec3':
|
|
929
|
-
codecs.push('ec-3');
|
|
930
|
-
hasAudio = true;
|
|
931
|
-
break;
|
|
932
|
-
}
|
|
933
|
-
switch (tsCodecs.video) {
|
|
934
|
-
case 'avc':
|
|
935
|
-
if (videoInfo.codec) {
|
|
936
|
-
codecs.push(videoInfo.codec);
|
|
937
|
-
} else {
|
|
938
|
-
codecs.push('avc1.42E01E');
|
|
939
|
-
}
|
|
940
|
-
hasVideo = true;
|
|
941
|
-
break;
|
|
942
|
-
case 'hvc':
|
|
943
|
-
if (videoInfo.codec) {
|
|
944
|
-
codecs.push(videoInfo.codec);
|
|
945
|
-
} else {
|
|
946
|
-
codecs.push('hvc1.1.6.L93.90');
|
|
947
|
-
}
|
|
948
|
-
hasVideo = true;
|
|
949
|
-
break;
|
|
950
|
-
}
|
|
951
|
-
if (!codecs.length) {
|
|
952
|
-
return null;
|
|
953
|
-
}
|
|
954
|
-
const onlyAudio = hasAudio && !hasVideo;
|
|
955
|
-
return {
|
|
956
|
-
type: onlyAudio ? 'audio' : 'video',
|
|
957
|
-
mimeType: 'video/mp2t',
|
|
958
|
-
codecs: codecs.join(', '),
|
|
959
|
-
language: null,
|
|
960
|
-
height: videoInfo.height,
|
|
961
|
-
width: videoInfo.width,
|
|
962
|
-
channelCount: null,
|
|
963
|
-
sampleRate: null,
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
/**
|
|
968
|
-
* @param {shaka.extern.Response} response
|
|
969
|
-
* @return {?shaka.hls.HlsParser.BasicInfo}
|
|
970
|
-
* @private
|
|
971
|
-
*/
|
|
972
|
-
getBasicInfoFromMp4_(response) {
|
|
973
|
-
const Mp4Parser = shaka.util.Mp4Parser;
|
|
974
|
-
|
|
975
|
-
const codecs = [];
|
|
976
|
-
|
|
977
|
-
let hasAudio = false;
|
|
978
|
-
let hasVideo = false;
|
|
979
|
-
|
|
980
|
-
const addCodec = (codec) => {
|
|
981
|
-
const codecLC = codec.toLowerCase();
|
|
982
|
-
switch (codecLC) {
|
|
983
|
-
case 'avc1':
|
|
984
|
-
case 'avc3':
|
|
985
|
-
codecs.push(codecLC + '.42E01E');
|
|
986
|
-
hasVideo = true;
|
|
987
|
-
break;
|
|
988
|
-
case 'hev1':
|
|
989
|
-
case 'hvc1':
|
|
990
|
-
codecs.push(codecLC + '.1.6.L93.90');
|
|
991
|
-
hasVideo = true;
|
|
992
|
-
break;
|
|
993
|
-
case 'dvh1':
|
|
994
|
-
case 'dvhe':
|
|
995
|
-
codecs.push(codecLC + '.05.04');
|
|
996
|
-
hasVideo = true;
|
|
997
|
-
break;
|
|
998
|
-
case 'vp09':
|
|
999
|
-
codecs.push(codecLC + '.00.10.08');
|
|
1000
|
-
hasVideo = true;
|
|
1001
|
-
break;
|
|
1002
|
-
case 'av01':
|
|
1003
|
-
codecs.push(codecLC + '.0.01M.08');
|
|
1004
|
-
hasVideo = true;
|
|
1005
|
-
break;
|
|
1006
|
-
case 'mp4a':
|
|
1007
|
-
// We assume AAC, but this can be wrong since mp4a supports
|
|
1008
|
-
// others codecs
|
|
1009
|
-
codecs.push('mp4a.40.2');
|
|
1010
|
-
hasAudio = true;
|
|
1011
|
-
break;
|
|
1012
|
-
case 'ac-3':
|
|
1013
|
-
case 'ec-3':
|
|
1014
|
-
case 'opus':
|
|
1015
|
-
case 'flac':
|
|
1016
|
-
codecs.push(codecLC);
|
|
1017
|
-
hasAudio = true;
|
|
1018
|
-
break;
|
|
1019
|
-
}
|
|
1020
|
-
};
|
|
1021
|
-
|
|
1022
|
-
const codecBoxParser = (box) => addCodec(box.name);
|
|
1023
|
-
|
|
1024
|
-
/** @type {?string} */
|
|
1025
|
-
let language = null;
|
|
1026
|
-
/** @type {?string} */
|
|
1027
|
-
let height = null;
|
|
1028
|
-
/** @type {?string} */
|
|
1029
|
-
let width = null;
|
|
1030
|
-
/** @type {?number} */
|
|
1031
|
-
let channelCount = null;
|
|
1032
|
-
/** @type {?number} */
|
|
1033
|
-
let sampleRate = null;
|
|
1034
|
-
|
|
1035
|
-
new Mp4Parser()
|
|
1036
|
-
.box('moov', Mp4Parser.children)
|
|
1037
|
-
.box('trak', Mp4Parser.children)
|
|
1038
|
-
.fullBox('tkhd', (box) => {
|
|
1039
|
-
goog.asserts.assert(
|
|
1040
|
-
box.version != null,
|
|
1041
|
-
'TKHD is a full box and should have a valid version.');
|
|
1042
|
-
const parsedTKHDBox = shaka.util.Mp4BoxParsers.parseTKHD(
|
|
1043
|
-
box.reader, box.version);
|
|
1044
|
-
height = String(parsedTKHDBox.height);
|
|
1045
|
-
width = String(parsedTKHDBox.width);
|
|
1046
|
-
})
|
|
1047
|
-
.box('mdia', Mp4Parser.children)
|
|
1048
|
-
.fullBox('mdhd', (box) => {
|
|
1049
|
-
goog.asserts.assert(
|
|
1050
|
-
box.version != null,
|
|
1051
|
-
'MDHD is a full box and should have a valid version.');
|
|
1052
|
-
const parsedMDHDBox = shaka.util.Mp4BoxParsers.parseMDHD(
|
|
1053
|
-
box.reader, box.version);
|
|
1054
|
-
language = parsedMDHDBox.language;
|
|
1055
|
-
})
|
|
1056
|
-
.box('minf', Mp4Parser.children)
|
|
1057
|
-
.box('stbl', Mp4Parser.children)
|
|
1058
|
-
.fullBox('stsd', Mp4Parser.sampleDescription)
|
|
1059
|
-
|
|
1060
|
-
// AUDIO
|
|
1061
|
-
// These are the various boxes that signal a codec.
|
|
1062
|
-
.box('mp4a', (box) => {
|
|
1063
|
-
const parsedMP4ABox = shaka.util.Mp4BoxParsers.parseMP4A(box.reader);
|
|
1064
|
-
channelCount = parsedMP4ABox.channelCount;
|
|
1065
|
-
sampleRate = parsedMP4ABox.sampleRate;
|
|
1066
|
-
if (box.reader.hasMoreData()) {
|
|
1067
|
-
Mp4Parser.children(box);
|
|
1068
|
-
} else {
|
|
1069
|
-
codecBoxParser(box);
|
|
1070
|
-
}
|
|
1071
|
-
})
|
|
1072
|
-
.box('esds', (box) => {
|
|
1073
|
-
const parsedESDSBox = shaka.util.Mp4BoxParsers.parseESDS(box.reader);
|
|
1074
|
-
codecs.push(parsedESDSBox.codec);
|
|
1075
|
-
hasAudio = true;
|
|
1076
|
-
})
|
|
1077
|
-
.box('ac-3', codecBoxParser)
|
|
1078
|
-
.box('ec-3', codecBoxParser)
|
|
1079
|
-
.box('opus', codecBoxParser)
|
|
1080
|
-
.box('Opus', codecBoxParser)
|
|
1081
|
-
.box('fLaC', codecBoxParser)
|
|
1082
|
-
|
|
1083
|
-
// VIDEO
|
|
1084
|
-
// These are the various boxes that signal a codec.
|
|
1085
|
-
.box('avc1', (box) => {
|
|
1086
|
-
const parsedAVCBox =
|
|
1087
|
-
shaka.util.Mp4BoxParsers.parseAVC(box.reader, box.name);
|
|
1088
|
-
codecs.push(parsedAVCBox.codec);
|
|
1089
|
-
hasVideo = true;
|
|
1090
|
-
})
|
|
1091
|
-
.box('avc3', (box) => {
|
|
1092
|
-
const parsedAVCBox =
|
|
1093
|
-
shaka.util.Mp4BoxParsers.parseAVC(box.reader, box.name);
|
|
1094
|
-
codecs.push(parsedAVCBox.codec);
|
|
1095
|
-
hasVideo = true;
|
|
1096
|
-
})
|
|
1097
|
-
.box('hev1', codecBoxParser)
|
|
1098
|
-
.box('hvc1', codecBoxParser)
|
|
1099
|
-
.box('dvh1', codecBoxParser)
|
|
1100
|
-
.box('dvhe', codecBoxParser)
|
|
1101
|
-
.box('vp09', codecBoxParser)
|
|
1102
|
-
.box('av01', codecBoxParser)
|
|
1103
|
-
|
|
1104
|
-
// This signals an encrypted sample, which we can go inside of to
|
|
1105
|
-
// find the codec used.
|
|
1106
|
-
// Note: If encrypted, you can only have audio or video, not both.
|
|
1107
|
-
.box('enca', Mp4Parser.visualSampleEntry)
|
|
1108
|
-
.box('encv', Mp4Parser.visualSampleEntry)
|
|
1109
|
-
.box('sinf', Mp4Parser.children)
|
|
1110
|
-
.box('frma', (box) => {
|
|
1111
|
-
const {codec} = shaka.util.Mp4BoxParsers.parseFRMA(box.reader);
|
|
1112
|
-
addCodec(codec);
|
|
1113
|
-
})
|
|
1114
|
-
|
|
1115
|
-
.parse(response.data, /* partialOkay= */ true);
|
|
1116
|
-
if (!codecs.length) {
|
|
1117
|
-
return null;
|
|
1118
|
-
}
|
|
1119
|
-
const onlyAudio = hasAudio && !hasVideo;
|
|
1120
|
-
return {
|
|
1121
|
-
type: onlyAudio ? 'audio' : 'video',
|
|
1122
|
-
mimeType: onlyAudio ? 'audio/mp4' : 'video/mp4',
|
|
1123
|
-
codecs: this.filterDuplicateCodecs_(codecs).join(', '),
|
|
1124
|
-
language: language,
|
|
1125
|
-
height: height,
|
|
1126
|
-
width: width,
|
|
1127
|
-
channelCount: channelCount,
|
|
1128
|
-
sampleRate: sampleRate,
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
948
|
/** @private */
|
|
1133
949
|
determineDuration_() {
|
|
1134
950
|
goog.asserts.assert(this.presentationTimeline_,
|
|
@@ -1183,31 +999,50 @@ shaka.hls.HlsParser = class {
|
|
|
1183
999
|
* @private
|
|
1184
1000
|
*/
|
|
1185
1001
|
parseMasterVariables_(tags) {
|
|
1002
|
+
const queryParams = new goog.Uri(this.masterPlaylistUri_).getQueryData();
|
|
1186
1003
|
for (const variableTag of tags) {
|
|
1187
1004
|
const name = variableTag.getAttributeValue('NAME');
|
|
1188
1005
|
const value = variableTag.getAttributeValue('VALUE');
|
|
1006
|
+
const queryParam = variableTag.getAttributeValue('QUERYPARAM');
|
|
1189
1007
|
if (name && value) {
|
|
1190
1008
|
if (!this.globalVariables_.has(name)) {
|
|
1191
1009
|
this.globalVariables_.set(name, value);
|
|
1192
1010
|
}
|
|
1193
1011
|
}
|
|
1012
|
+
if (queryParam) {
|
|
1013
|
+
const queryParamValue = queryParams.get(queryParam)[0];
|
|
1014
|
+
if (queryParamValue && !this.globalVariables_.has(queryParamValue)) {
|
|
1015
|
+
this.globalVariables_.set(queryParam, queryParamValue);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1194
1018
|
}
|
|
1195
1019
|
}
|
|
1196
1020
|
|
|
1197
1021
|
/**
|
|
1198
1022
|
* Get the variables of each variant tag, and store in a map.
|
|
1199
1023
|
* @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
|
|
1024
|
+
* @param {string} uri Media playlist URI.
|
|
1200
1025
|
* @return {!Map.<string, string>}
|
|
1201
1026
|
* @private
|
|
1202
1027
|
*/
|
|
1203
|
-
parseMediaVariables_(tags) {
|
|
1028
|
+
parseMediaVariables_(tags, uri) {
|
|
1029
|
+
const queryParams = new goog.Uri(uri).getQueryData();
|
|
1204
1030
|
const mediaVariables = new Map();
|
|
1205
1031
|
for (const variableTag of tags) {
|
|
1206
1032
|
const name = variableTag.getAttributeValue('NAME');
|
|
1207
1033
|
const value = variableTag.getAttributeValue('VALUE');
|
|
1034
|
+
const queryParam = variableTag.getAttributeValue('QUERYPARAM');
|
|
1208
1035
|
const mediaImport = variableTag.getAttributeValue('IMPORT');
|
|
1209
1036
|
if (name && value) {
|
|
1210
|
-
mediaVariables.
|
|
1037
|
+
if (!mediaVariables.has(name)) {
|
|
1038
|
+
mediaVariables.set(name, value);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (queryParam) {
|
|
1042
|
+
const queryParamValue = queryParams.get(queryParam)[0];
|
|
1043
|
+
if (queryParamValue && !mediaVariables.has(queryParamValue)) {
|
|
1044
|
+
mediaVariables.set(queryParam, queryParamValue);
|
|
1045
|
+
}
|
|
1211
1046
|
}
|
|
1212
1047
|
if (mediaImport) {
|
|
1213
1048
|
const globalValue = this.globalVariables_.get(mediaImport);
|
|
@@ -1260,6 +1095,68 @@ shaka.hls.HlsParser = class {
|
|
|
1260
1095
|
}
|
|
1261
1096
|
}
|
|
1262
1097
|
|
|
1098
|
+
/**
|
|
1099
|
+
* Process EXT-X-SESSION-DATA tags.
|
|
1100
|
+
*
|
|
1101
|
+
* @param {!Array.<!shaka.hls.Tag>} tags
|
|
1102
|
+
* @private
|
|
1103
|
+
*/
|
|
1104
|
+
processSessionData_(tags) {
|
|
1105
|
+
for (const tag of tags) {
|
|
1106
|
+
const id = tag.getAttributeValue('DATA-ID');
|
|
1107
|
+
const uri = tag.getAttributeValue('URI');
|
|
1108
|
+
const language = tag.getAttributeValue('LANGUAGE');
|
|
1109
|
+
const value = tag.getAttributeValue('VALUE');
|
|
1110
|
+
const data = (new Map()).set('id', id);
|
|
1111
|
+
if (uri) {
|
|
1112
|
+
data.set('uri', shaka.hls.Utils.constructSegmentUris(
|
|
1113
|
+
[this.masterPlaylistUri_], uri, this.globalVariables_)[0]);
|
|
1114
|
+
}
|
|
1115
|
+
if (language) {
|
|
1116
|
+
data.set('language', language);
|
|
1117
|
+
}
|
|
1118
|
+
if (value) {
|
|
1119
|
+
data.set('value', value);
|
|
1120
|
+
}
|
|
1121
|
+
const event = new shaka.util.FakeEvent('sessiondata', data);
|
|
1122
|
+
if (this.playerInterface_) {
|
|
1123
|
+
this.playerInterface_.onEvent(event);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Process EXT-X-CONTENT-STEERING tags.
|
|
1130
|
+
*
|
|
1131
|
+
* @param {!Array.<!shaka.hls.Tag>} tags
|
|
1132
|
+
* @return {!Promise}
|
|
1133
|
+
* @private
|
|
1134
|
+
*/
|
|
1135
|
+
async processContentSteering_(tags) {
|
|
1136
|
+
if (!this.playerInterface_ || !this.config_) {
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
let contentSteeringPromise;
|
|
1140
|
+
for (const tag of tags) {
|
|
1141
|
+
const defaultPathwayId = tag.getAttributeValue('PATHWAY-ID');
|
|
1142
|
+
const uri = tag.getAttributeValue('SERVER-URI');
|
|
1143
|
+
if (!defaultPathwayId || !uri) {
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
this.contentSteeringManager_ =
|
|
1147
|
+
new shaka.util.ContentSteeringManager(this.playerInterface_);
|
|
1148
|
+
this.contentSteeringManager_.configure(this.config_);
|
|
1149
|
+
this.contentSteeringManager_.setBaseUris([this.masterPlaylistUri_]);
|
|
1150
|
+
this.contentSteeringManager_.setManifestType(
|
|
1151
|
+
shaka.media.ManifestParser.HLS);
|
|
1152
|
+
this.contentSteeringManager_.setDefaultPathwayId(defaultPathwayId);
|
|
1153
|
+
contentSteeringPromise =
|
|
1154
|
+
this.contentSteeringManager_.requestInfo(uri);
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
await contentSteeringPromise;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1263
1160
|
/**
|
|
1264
1161
|
* Parse Subtitles and Closed Captions from 'EXT-X-MEDIA' tags.
|
|
1265
1162
|
* Create text streams for Subtitles, but not Closed Captions.
|
|
@@ -1278,7 +1175,7 @@ shaka.hls.HlsParser = class {
|
|
|
1278
1175
|
return null;
|
|
1279
1176
|
}
|
|
1280
1177
|
try {
|
|
1281
|
-
return this.
|
|
1178
|
+
return this.createStreamInfoFromMediaTags_([tag], new Map()).stream;
|
|
1282
1179
|
} catch (e) {
|
|
1283
1180
|
if (this.config_.hls.ignoreTextStreamFailures) {
|
|
1284
1181
|
return null;
|
|
@@ -1312,10 +1209,11 @@ shaka.hls.HlsParser = class {
|
|
|
1312
1209
|
|
|
1313
1210
|
/**
|
|
1314
1211
|
* @param {!Array.<!shaka.hls.Tag>} imageTags from the playlist.
|
|
1212
|
+
* @param {!Array.<!shaka.hls.Tag>} iFrameTags from the playlist.
|
|
1315
1213
|
* @return {!Promise.<!Array.<!shaka.extern.Stream>>}
|
|
1316
1214
|
* @private
|
|
1317
1215
|
*/
|
|
1318
|
-
async parseImages_(imageTags) {
|
|
1216
|
+
async parseImages_(imageTags, iFrameTags) {
|
|
1319
1217
|
// Create image stream for each image tag.
|
|
1320
1218
|
const imageStreamPromises = imageTags.map(async (tag) => {
|
|
1321
1219
|
const disableThumbnails = this.config_.disableThumbnails;
|
|
@@ -1331,16 +1229,34 @@ shaka.hls.HlsParser = class {
|
|
|
1331
1229
|
}
|
|
1332
1230
|
throw e;
|
|
1333
1231
|
}
|
|
1334
|
-
})
|
|
1232
|
+
}).concat(iFrameTags.map((tag) => {
|
|
1233
|
+
const disableThumbnails = this.config_.disableThumbnails;
|
|
1234
|
+
if (disableThumbnails) {
|
|
1235
|
+
return null;
|
|
1236
|
+
}
|
|
1237
|
+
try {
|
|
1238
|
+
const streamInfo = this.createStreamInfoFromIframeTag_(tag);
|
|
1239
|
+
if (streamInfo.stream.codecs !== 'mjpg') {
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
return streamInfo.stream;
|
|
1243
|
+
} catch (e) {
|
|
1244
|
+
if (this.config_.hls.ignoreImageStreamFailures) {
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1247
|
+
throw e;
|
|
1248
|
+
}
|
|
1249
|
+
}));
|
|
1335
1250
|
const imageStreams = await Promise.all(imageStreamPromises);
|
|
1336
1251
|
return imageStreams.filter((s) => s);
|
|
1337
1252
|
}
|
|
1338
1253
|
|
|
1339
1254
|
/**
|
|
1340
1255
|
* @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
|
|
1256
|
+
* @param {!Map.<string, string>} groupIdPathwayIdMapping
|
|
1341
1257
|
* @private
|
|
1342
1258
|
*/
|
|
1343
|
-
createStreamInfosFromMediaTags_(mediaTags) {
|
|
1259
|
+
createStreamInfosFromMediaTags_(mediaTags, groupIdPathwayIdMapping) {
|
|
1344
1260
|
// Filter out subtitles and media tags without uri.
|
|
1345
1261
|
mediaTags = mediaTags.filter((tag) => {
|
|
1346
1262
|
const uri = tag.getAttributeValue('URI') || '';
|
|
@@ -1348,9 +1264,20 @@ shaka.hls.HlsParser = class {
|
|
|
1348
1264
|
return type != 'SUBTITLES' && uri != '';
|
|
1349
1265
|
});
|
|
1350
1266
|
|
|
1351
|
-
|
|
1267
|
+
const groupedTags = {};
|
|
1352
1268
|
for (const tag of mediaTags) {
|
|
1353
|
-
|
|
1269
|
+
const key = tag.getTagKey();
|
|
1270
|
+
if (!groupedTags[key]) {
|
|
1271
|
+
groupedTags[key] = [tag];
|
|
1272
|
+
} else {
|
|
1273
|
+
groupedTags[key].push(tag);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
for (const key in groupedTags) {
|
|
1278
|
+
// Create stream info for each audio / video media grouped tag.
|
|
1279
|
+
this.createStreamInfoFromMediaTags_(
|
|
1280
|
+
groupedTags[key], groupIdPathwayIdMapping);
|
|
1354
1281
|
}
|
|
1355
1282
|
}
|
|
1356
1283
|
|
|
@@ -1358,10 +1285,12 @@ shaka.hls.HlsParser = class {
|
|
|
1358
1285
|
* @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
|
|
1359
1286
|
* @param {!Array.<!shaka.hls.Tag>} sessionKeyTags EXT-X-SESSION-KEY tags
|
|
1360
1287
|
* from the playlist.
|
|
1288
|
+
* @param {!Array.<!shaka.hls.Tag>} mediaTags EXT-X-MEDIA tags from the
|
|
1289
|
+
* playlist.
|
|
1361
1290
|
* @return {!Array.<!shaka.extern.Variant>}
|
|
1362
1291
|
* @private
|
|
1363
1292
|
*/
|
|
1364
|
-
createVariantsForTags_(tags, sessionKeyTags) {
|
|
1293
|
+
createVariantsForTags_(tags, sessionKeyTags, mediaTags) {
|
|
1365
1294
|
// EXT-X-SESSION-KEY processing
|
|
1366
1295
|
const drmInfos = [];
|
|
1367
1296
|
const keyIds = new Set();
|
|
@@ -1393,24 +1322,54 @@ shaka.hls.HlsParser = class {
|
|
|
1393
1322
|
}
|
|
1394
1323
|
}
|
|
1395
1324
|
|
|
1396
|
-
|
|
1397
|
-
const
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1325
|
+
const groupedTags = {};
|
|
1326
|
+
for (const tag of tags) {
|
|
1327
|
+
const key = tag.getTagKey();
|
|
1328
|
+
if (!groupedTags[key]) {
|
|
1329
|
+
groupedTags[key] = [tag];
|
|
1330
|
+
} else {
|
|
1331
|
+
groupedTags[key].push(tag);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const allVariants = [];
|
|
1336
|
+
// Create variants for each group of variant tag.
|
|
1337
|
+
for (const key in groupedTags) {
|
|
1338
|
+
const tags = groupedTags[key];
|
|
1339
|
+
const firstTag = tags[0];
|
|
1340
|
+
|
|
1341
|
+
const frameRate = firstTag.getAttributeValue('FRAME-RATE');
|
|
1342
|
+
const bandwidth =
|
|
1343
|
+
Number(firstTag.getAttributeValue('AVERAGE-BANDWIDTH')) ||
|
|
1344
|
+
Number(firstTag.getRequiredAttrValue('BANDWIDTH'));
|
|
1401
1345
|
|
|
1402
|
-
const resolution =
|
|
1346
|
+
const resolution = firstTag.getAttributeValue('RESOLUTION');
|
|
1403
1347
|
const [width, height] = resolution ? resolution.split('x') : [null, null];
|
|
1404
1348
|
|
|
1405
|
-
const videoRange =
|
|
1349
|
+
const videoRange = firstTag.getAttributeValue('VIDEO-RANGE');
|
|
1350
|
+
|
|
1351
|
+
let videoLayout = firstTag.getAttributeValue('REQ-VIDEO-LAYOUT');
|
|
1352
|
+
if (videoLayout && videoLayout.includes(',')) {
|
|
1353
|
+
// If multiple video layout strings are present, pick the first valid
|
|
1354
|
+
// one.
|
|
1355
|
+
const layoutStrings = videoLayout.split(',').filter((layoutString) => {
|
|
1356
|
+
return layoutString == 'CH-STEREO' || layoutString == 'CH-MONO';
|
|
1357
|
+
});
|
|
1358
|
+
videoLayout = layoutStrings[0];
|
|
1359
|
+
}
|
|
1360
|
+
// According to the HLS spec:
|
|
1361
|
+
// By default a video variant is monoscopic, so an attribute
|
|
1362
|
+
// consisting entirely of REQ-VIDEO-LAYOUT="CH-MONO" is unnecessary
|
|
1363
|
+
// and SHOULD NOT be present.
|
|
1364
|
+
videoLayout = videoLayout || 'CH-MONO';
|
|
1406
1365
|
|
|
1407
|
-
const streamInfos = this.
|
|
1408
|
-
resolution, frameRate);
|
|
1366
|
+
const streamInfos = this.createStreamInfosForVariantTags_(tags,
|
|
1367
|
+
mediaTags, resolution, frameRate);
|
|
1409
1368
|
|
|
1410
1369
|
goog.asserts.assert(streamInfos.audio.length ||
|
|
1411
1370
|
streamInfos.video.length, 'We should have created a stream!');
|
|
1412
1371
|
|
|
1413
|
-
|
|
1372
|
+
allVariants.push(...this.createVariants_(
|
|
1414
1373
|
streamInfos.audio,
|
|
1415
1374
|
streamInfos.video,
|
|
1416
1375
|
bandwidth,
|
|
@@ -1418,47 +1377,78 @@ shaka.hls.HlsParser = class {
|
|
|
1418
1377
|
height,
|
|
1419
1378
|
frameRate,
|
|
1420
1379
|
videoRange,
|
|
1380
|
+
videoLayout,
|
|
1421
1381
|
drmInfos,
|
|
1422
|
-
keyIds);
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
// Filter out null variants.
|
|
1426
|
-
variants = variants.filter((variant) => variant != null);
|
|
1427
|
-
return variants;
|
|
1382
|
+
keyIds));
|
|
1383
|
+
}
|
|
1384
|
+
return allVariants.filter((variant) => variant != null);
|
|
1428
1385
|
}
|
|
1429
1386
|
|
|
1430
1387
|
/**
|
|
1431
1388
|
* Create audio and video streamInfos from an 'EXT-X-STREAM-INF' tag and its
|
|
1432
1389
|
* related media tags.
|
|
1433
1390
|
*
|
|
1434
|
-
* @param {!shaka.hls.Tag}
|
|
1391
|
+
* @param {!Array.<!shaka.hls.Tag>} tags
|
|
1392
|
+
* @param {!Array.<!shaka.hls.Tag>} mediaTags
|
|
1435
1393
|
* @param {?string} resolution
|
|
1436
1394
|
* @param {?string} frameRate
|
|
1437
1395
|
* @return {!shaka.hls.HlsParser.StreamInfos}
|
|
1438
1396
|
* @private
|
|
1439
1397
|
*/
|
|
1440
|
-
|
|
1398
|
+
createStreamInfosForVariantTags_(tags, mediaTags, resolution, frameRate) {
|
|
1441
1399
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
1442
|
-
/** @type {!Array.<string>} */
|
|
1443
|
-
let allCodecs = this.getCodecsForVariantTag_(tag);
|
|
1444
|
-
const audioGroupId = tag.getAttributeValue('AUDIO');
|
|
1445
|
-
const videoGroupId = tag.getAttributeValue('VIDEO');
|
|
1446
|
-
goog.asserts.assert(audioGroupId == null || videoGroupId == null,
|
|
1447
|
-
'Unexpected: both video and audio described by media tags!');
|
|
1448
|
-
|
|
1449
|
-
const groupId = audioGroupId || videoGroupId;
|
|
1450
|
-
const streamInfos =
|
|
1451
|
-
(groupId && this.groupIdToStreamInfosMap_.has(groupId)) ?
|
|
1452
|
-
this.groupIdToStreamInfosMap_.get(groupId) : [];
|
|
1453
1400
|
|
|
1454
1401
|
/** @type {shaka.hls.HlsParser.StreamInfos} */
|
|
1455
1402
|
const res = {
|
|
1456
|
-
audio:
|
|
1457
|
-
video:
|
|
1403
|
+
audio: [],
|
|
1404
|
+
video: [],
|
|
1458
1405
|
};
|
|
1406
|
+
const groupIdPathwayIdMapping = new Map();
|
|
1407
|
+
const globalGroupIds = [];
|
|
1408
|
+
let isAudioGroup = false;
|
|
1409
|
+
let isVideoGroup = false;
|
|
1410
|
+
for (const tag of tags) {
|
|
1411
|
+
const audioGroupId = tag.getAttributeValue('AUDIO');
|
|
1412
|
+
const videoGroupId = tag.getAttributeValue('VIDEO');
|
|
1413
|
+
goog.asserts.assert(audioGroupId == null || videoGroupId == null,
|
|
1414
|
+
'Unexpected: both video and audio described by media tags!');
|
|
1415
|
+
|
|
1416
|
+
const groupId = audioGroupId || videoGroupId;
|
|
1417
|
+
if (!groupId) {
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
if (!globalGroupIds.includes(groupId)) {
|
|
1421
|
+
globalGroupIds.push(groupId);
|
|
1422
|
+
}
|
|
1423
|
+
const pathwayId = tag.getAttributeValue('PATHWAY-ID');
|
|
1424
|
+
if (pathwayId) {
|
|
1425
|
+
groupIdPathwayIdMapping.set(groupId, pathwayId);
|
|
1426
|
+
}
|
|
1427
|
+
if (audioGroupId) {
|
|
1428
|
+
isAudioGroup = true;
|
|
1429
|
+
} else if (videoGroupId) {
|
|
1430
|
+
isVideoGroup = true;
|
|
1431
|
+
}
|
|
1432
|
+
// Make an educated guess about the stream type.
|
|
1433
|
+
shaka.log.debug('Guessing stream type for', tag.toString());
|
|
1434
|
+
}
|
|
1435
|
+
if (globalGroupIds.length && mediaTags.length) {
|
|
1436
|
+
const mediaTagsForVariant = mediaTags.filter((tag) => {
|
|
1437
|
+
return globalGroupIds.includes(tag.getRequiredAttrValue('GROUP-ID'));
|
|
1438
|
+
});
|
|
1439
|
+
this.createStreamInfosFromMediaTags_(
|
|
1440
|
+
mediaTagsForVariant, groupIdPathwayIdMapping);
|
|
1441
|
+
}
|
|
1442
|
+
const globalGroupId = globalGroupIds.sort().join(',');
|
|
1443
|
+
const streamInfos =
|
|
1444
|
+
(globalGroupId && this.groupIdToStreamInfosMap_.has(globalGroupId)) ?
|
|
1445
|
+
this.groupIdToStreamInfosMap_.get(globalGroupId) : [];
|
|
1446
|
+
if (isAudioGroup) {
|
|
1447
|
+
res.audio.push(...streamInfos);
|
|
1448
|
+
} else if (isVideoGroup) {
|
|
1449
|
+
res.video.push(...streamInfos);
|
|
1450
|
+
}
|
|
1459
1451
|
|
|
1460
|
-
// Make an educated guess about the stream type.
|
|
1461
|
-
shaka.log.debug('Guessing stream type for', tag.toString());
|
|
1462
1452
|
let type;
|
|
1463
1453
|
let ignoreStream = false;
|
|
1464
1454
|
|
|
@@ -1467,11 +1457,15 @@ shaka.hls.HlsParser = class {
|
|
|
1467
1457
|
// In that case, ignore the local URI and use the version in the
|
|
1468
1458
|
// AUDIO tag, so you inherit its language.
|
|
1469
1459
|
// As an example, see the manifest linked in issue #860.
|
|
1470
|
-
const
|
|
1460
|
+
const allStreamUris = tags.map((tag) => tag.getRequiredAttrValue('URI'));
|
|
1471
1461
|
const hasSameUri = res.audio.find((audio) => {
|
|
1472
|
-
return audio && audio.
|
|
1462
|
+
return audio && audio.getUris().find((uri) => {
|
|
1463
|
+
return allStreamUris.includes(uri);
|
|
1464
|
+
});
|
|
1473
1465
|
});
|
|
1474
1466
|
|
|
1467
|
+
/** @type {!Array.<string>} */
|
|
1468
|
+
let allCodecs = this.getCodecsForVariantTag_(tags[0]);
|
|
1475
1469
|
const videoCodecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
|
|
1476
1470
|
ContentType.VIDEO, allCodecs);
|
|
1477
1471
|
const audioCodecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
|
|
@@ -1483,7 +1477,8 @@ shaka.hls.HlsParser = class {
|
|
|
1483
1477
|
type = ContentType.AUDIO;
|
|
1484
1478
|
shaka.log.debug('Guessing audio-only.');
|
|
1485
1479
|
ignoreStream = res.audio.length > 0;
|
|
1486
|
-
} else if (!
|
|
1480
|
+
} else if (!res.audio.length && !res.video.length &&
|
|
1481
|
+
audioCodecs && videoCodecs) {
|
|
1487
1482
|
// There are both audio and video codecs, so assume multiplexed content.
|
|
1488
1483
|
// Note that the default used when CODECS is missing assumes multiple
|
|
1489
1484
|
// (and therefore multiplexed).
|
|
@@ -1506,8 +1501,35 @@ shaka.hls.HlsParser = class {
|
|
|
1506
1501
|
}
|
|
1507
1502
|
|
|
1508
1503
|
if (!ignoreStream) {
|
|
1509
|
-
|
|
1510
|
-
|
|
1504
|
+
let language = null;
|
|
1505
|
+
let name = null;
|
|
1506
|
+
let channelsCount = null;
|
|
1507
|
+
let spatialAudio = false;
|
|
1508
|
+
let characteristics = null;
|
|
1509
|
+
let sampleRate = null;
|
|
1510
|
+
if (!streamInfos.length) {
|
|
1511
|
+
const mediaTag = mediaTags.find((tag) => {
|
|
1512
|
+
const uri = tag.getAttributeValue('URI') || '';
|
|
1513
|
+
const type = tag.getAttributeValue('TYPE');
|
|
1514
|
+
const groupId = tag.getRequiredAttrValue('GROUP-ID');
|
|
1515
|
+
return type != 'SUBTITLES' && uri == '' &&
|
|
1516
|
+
globalGroupIds.includes(groupId);
|
|
1517
|
+
});
|
|
1518
|
+
if (mediaTag) {
|
|
1519
|
+
language = mediaTag.getAttributeValue('LANGUAGE');
|
|
1520
|
+
name = mediaTag.getAttributeValue('NAME');
|
|
1521
|
+
channelsCount = this.getChannelsCount_(mediaTag);
|
|
1522
|
+
spatialAudio = this.isSpatialAudio_(mediaTag);
|
|
1523
|
+
characteristics = mediaTag.getAttributeValue('CHARACTERISTICS');
|
|
1524
|
+
sampleRate = this.getSampleRate_(mediaTag);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
const streamInfo = this.createStreamInfoFromVariantTags_(
|
|
1528
|
+
tags, allCodecs, type, language, name, channelsCount,
|
|
1529
|
+
characteristics, sampleRate, spatialAudio);
|
|
1530
|
+
if (globalGroupId) {
|
|
1531
|
+
streamInfo.stream.groupId = globalGroupId;
|
|
1532
|
+
}
|
|
1511
1533
|
res[streamInfo.stream.type] = [streamInfo];
|
|
1512
1534
|
}
|
|
1513
1535
|
return res;
|
|
@@ -1522,17 +1544,24 @@ shaka.hls.HlsParser = class {
|
|
|
1522
1544
|
* @private
|
|
1523
1545
|
*/
|
|
1524
1546
|
getCodecsForVariantTag_(tag) {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
defaultCodecsArray
|
|
1547
|
+
let codecsString = tag.getAttributeValue('CODECS') || '';
|
|
1548
|
+
|
|
1549
|
+
this.codecInfoInManifest_ = codecsString.length > 0;
|
|
1550
|
+
|
|
1551
|
+
if (!this.codecInfoInManifest_ && !this.config_.hls.disableCodecGuessing) {
|
|
1552
|
+
// These are the default codecs to assume if none are specified.
|
|
1553
|
+
const defaultCodecsArray = [];
|
|
1554
|
+
|
|
1555
|
+
if (!this.config_.disableVideo) {
|
|
1556
|
+
defaultCodecsArray.push(this.config_.hls.defaultVideoCodec);
|
|
1557
|
+
}
|
|
1558
|
+
if (!this.config_.disableAudio) {
|
|
1559
|
+
defaultCodecsArray.push(this.config_.hls.defaultAudioCodec);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
codecsString = defaultCodecsArray.join(',');
|
|
1532
1563
|
}
|
|
1533
|
-
const defaultCodecs = defaultCodecsArray.join(',');
|
|
1534
1564
|
|
|
1535
|
-
const codecsString = tag.getAttributeValue('CODECS', defaultCodecs);
|
|
1536
1565
|
// Strip out internal whitespace while splitting on commas:
|
|
1537
1566
|
/** @type {!Array.<string>} */
|
|
1538
1567
|
const codecs = codecsString.split(/\s*,\s*/);
|
|
@@ -1686,6 +1715,7 @@ shaka.hls.HlsParser = class {
|
|
|
1686
1715
|
* @param {?string} height
|
|
1687
1716
|
* @param {?string} frameRate
|
|
1688
1717
|
* @param {?string} videoRange
|
|
1718
|
+
* @param {?string} videoLayout
|
|
1689
1719
|
* @param {!Array.<shaka.extern.DrmInfo>} drmInfos
|
|
1690
1720
|
* @param {!Set.<string>} keyIds
|
|
1691
1721
|
* @return {!Array.<!shaka.extern.Variant>}
|
|
@@ -1693,13 +1723,13 @@ shaka.hls.HlsParser = class {
|
|
|
1693
1723
|
*/
|
|
1694
1724
|
createVariants_(
|
|
1695
1725
|
audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
|
|
1696
|
-
drmInfos, keyIds) {
|
|
1726
|
+
videoLayout, drmInfos, keyIds) {
|
|
1697
1727
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
1698
1728
|
const DrmEngine = shaka.media.DrmEngine;
|
|
1699
1729
|
|
|
1700
1730
|
for (const info of videoInfos) {
|
|
1701
1731
|
this.addVideoAttributes_(
|
|
1702
|
-
info.stream, width, height, frameRate, videoRange);
|
|
1732
|
+
info.stream, width, height, frameRate, videoRange, videoLayout);
|
|
1703
1733
|
}
|
|
1704
1734
|
|
|
1705
1735
|
// In case of audio-only or video-only content or the audio/video is
|
|
@@ -1731,9 +1761,9 @@ shaka.hls.HlsParser = class {
|
|
|
1731
1761
|
const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
|
|
1732
1762
|
const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
|
|
1733
1763
|
const videoStreamUri =
|
|
1734
|
-
videoInfo ? videoInfo.
|
|
1764
|
+
videoInfo ? videoInfo.getUris().sort().join(',') : '';
|
|
1735
1765
|
const audioStreamUri =
|
|
1736
|
-
audioInfo ? audioInfo.
|
|
1766
|
+
audioInfo ? audioInfo.getUris().sort().join(',') : '';
|
|
1737
1767
|
const variantUriKey = videoStreamUri + ' - ' + audioStreamUri;
|
|
1738
1768
|
|
|
1739
1769
|
if (audioStream && videoStream) {
|
|
@@ -1821,34 +1851,59 @@ shaka.hls.HlsParser = class {
|
|
|
1821
1851
|
/**
|
|
1822
1852
|
* Parse EXT-X-MEDIA media tag into a Stream object.
|
|
1823
1853
|
*
|
|
1824
|
-
* @param {shaka.hls.Tag}
|
|
1854
|
+
* @param {!Array.<!shaka.hls.Tag>} tags
|
|
1855
|
+
* @param {!Map.<string, string>} groupIdPathwayIdMapping
|
|
1825
1856
|
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1826
1857
|
* @private
|
|
1827
1858
|
*/
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
const
|
|
1859
|
+
createStreamInfoFromMediaTags_(tags, groupIdPathwayIdMapping) {
|
|
1860
|
+
const verbatimMediaPlaylistUris = [];
|
|
1861
|
+
const globalGroupIds = [];
|
|
1862
|
+
const groupIdUriMappping = new Map();
|
|
1863
|
+
for (const tag of tags) {
|
|
1864
|
+
goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
|
|
1865
|
+
'Should only be called on media tags!');
|
|
1866
|
+
const uri = tag.getRequiredAttrValue('URI');
|
|
1867
|
+
const groupId = tag.getRequiredAttrValue('GROUP-ID');
|
|
1868
|
+
verbatimMediaPlaylistUris.push(uri);
|
|
1869
|
+
globalGroupIds.push(groupId);
|
|
1870
|
+
groupIdUriMappping.set(groupId, uri);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
const globalGroupId = globalGroupIds.sort().join(',');
|
|
1874
|
+
const firstTag = tags[0];
|
|
1832
1875
|
let codecs = '';
|
|
1833
1876
|
/** @type {string} */
|
|
1834
|
-
const type = this.getType_(
|
|
1877
|
+
const type = this.getType_(firstTag);
|
|
1835
1878
|
if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
|
|
1836
|
-
codecs =
|
|
1837
|
-
} else
|
|
1838
|
-
|
|
1879
|
+
codecs = firstTag.getAttributeValue('CODECS') || '';
|
|
1880
|
+
} else {
|
|
1881
|
+
for (const groupId of globalGroupIds) {
|
|
1882
|
+
if (this.groupIdToCodecsMap_.has(groupId)) {
|
|
1883
|
+
codecs = this.groupIdToCodecsMap_.get(groupId);
|
|
1884
|
+
break;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1839
1887
|
}
|
|
1840
1888
|
|
|
1841
|
-
const verbatimMediaPlaylistUri = this.variableSubstitution_(
|
|
1842
|
-
tag.getRequiredAttrValue('URI'), this.globalVariables_);
|
|
1843
|
-
|
|
1844
1889
|
// Check if the stream has already been created as part of another Variant
|
|
1845
1890
|
// and return it if it has.
|
|
1846
|
-
|
|
1847
|
-
|
|
1891
|
+
const key = verbatimMediaPlaylistUris.sort().join(',');
|
|
1892
|
+
if (this.uriToStreamInfosMap_.has(key)) {
|
|
1893
|
+
return this.uriToStreamInfosMap_.get(key);
|
|
1894
|
+
}
|
|
1895
|
+
const streamId = this.globalId_++;
|
|
1896
|
+
if (this.contentSteeringManager_) {
|
|
1897
|
+
for (const [groupId, uri] of groupIdUriMappping) {
|
|
1898
|
+
const pathwayId = groupIdPathwayIdMapping.get(groupId);
|
|
1899
|
+
if (pathwayId) {
|
|
1900
|
+
this.contentSteeringManager_.addLocation(streamId, pathwayId, uri);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1848
1903
|
}
|
|
1849
1904
|
|
|
1850
|
-
const language =
|
|
1851
|
-
const name =
|
|
1905
|
+
const language = firstTag.getAttributeValue('LANGUAGE');
|
|
1906
|
+
const name = firstTag.getAttributeValue('NAME');
|
|
1852
1907
|
|
|
1853
1908
|
// NOTE: According to the HLS spec, "DEFAULT=YES" requires "AUTOSELECT=YES".
|
|
1854
1909
|
// However, we don't bother to validate "AUTOSELECT", since we don't
|
|
@@ -1856,39 +1911,39 @@ shaka.hls.HlsParser = class {
|
|
|
1856
1911
|
// "AUTOSELECT=YES". A value of "AUTOSELECT=NO" would imply that it may
|
|
1857
1912
|
// only be selected explicitly by the user, and we don't have a way to
|
|
1858
1913
|
// represent that in our model.
|
|
1859
|
-
const defaultAttrValue =
|
|
1914
|
+
const defaultAttrValue = firstTag.getAttributeValue('DEFAULT');
|
|
1860
1915
|
const primary = defaultAttrValue == 'YES';
|
|
1861
1916
|
|
|
1862
|
-
const channelsCount =
|
|
1863
|
-
|
|
1864
|
-
const
|
|
1917
|
+
const channelsCount =
|
|
1918
|
+
type == 'audio' ? this.getChannelsCount_(firstTag) : null;
|
|
1919
|
+
const spatialAudio =
|
|
1920
|
+
type == 'audio' ? this.isSpatialAudio_(firstTag) : false;
|
|
1921
|
+
const characteristics = firstTag.getAttributeValue('CHARACTERISTICS');
|
|
1865
1922
|
|
|
1866
|
-
const forcedAttrValue =
|
|
1923
|
+
const forcedAttrValue = firstTag.getAttributeValue('FORCED');
|
|
1867
1924
|
const forced = forcedAttrValue == 'YES';
|
|
1868
|
-
const sampleRate = type == 'audio' ? this.getSampleRate_(
|
|
1925
|
+
const sampleRate = type == 'audio' ? this.getSampleRate_(firstTag) : null;
|
|
1869
1926
|
// TODO: Should we take into account some of the currently ignored
|
|
1870
1927
|
// attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj
|
|
1871
1928
|
const streamInfo = this.createStreamInfo_(
|
|
1872
|
-
|
|
1873
|
-
channelsCount, /* closedCaptions= */ null,
|
|
1874
|
-
sampleRate, spatialAudio);
|
|
1875
|
-
if (
|
|
1876
|
-
|
|
1929
|
+
streamId, verbatimMediaPlaylistUris, codecs, type, language,
|
|
1930
|
+
primary, name, channelsCount, /* closedCaptions= */ null,
|
|
1931
|
+
characteristics, forced, sampleRate, spatialAudio);
|
|
1932
|
+
if (streamInfo.stream) {
|
|
1933
|
+
streamInfo.stream.groupId = globalGroupId;
|
|
1934
|
+
}
|
|
1935
|
+
if (this.groupIdToStreamInfosMap_.has(globalGroupId)) {
|
|
1936
|
+
this.groupIdToStreamInfosMap_.get(globalGroupId).push(streamInfo);
|
|
1877
1937
|
} else {
|
|
1878
|
-
this.groupIdToStreamInfosMap_.set(
|
|
1938
|
+
this.groupIdToStreamInfosMap_.set(globalGroupId, [streamInfo]);
|
|
1879
1939
|
}
|
|
1880
1940
|
|
|
1881
|
-
|
|
1882
|
-
// calls to createStreamInfoFromMediaTag_ before either has resolved.
|
|
1883
|
-
if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
|
|
1884
|
-
return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
|
|
1885
|
-
}
|
|
1886
|
-
this.uriToStreamInfosMap_.set(verbatimMediaPlaylistUri, streamInfo);
|
|
1941
|
+
this.uriToStreamInfosMap_.set(key, streamInfo);
|
|
1887
1942
|
return streamInfo;
|
|
1888
1943
|
}
|
|
1889
1944
|
|
|
1890
1945
|
/**
|
|
1891
|
-
* Parse EXT-X-
|
|
1946
|
+
* Parse EXT-X-IMAGE-STREAM-INF media tag into a Stream object.
|
|
1892
1947
|
*
|
|
1893
1948
|
* @param {shaka.hls.Tag} tag
|
|
1894
1949
|
* @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
|
|
@@ -1900,8 +1955,7 @@ shaka.hls.HlsParser = class {
|
|
|
1900
1955
|
/** @type {string} */
|
|
1901
1956
|
const type = shaka.util.ManifestParserUtils.ContentType.IMAGE;
|
|
1902
1957
|
|
|
1903
|
-
const verbatimImagePlaylistUri =
|
|
1904
|
-
tag.getRequiredAttrValue('URI'), this.globalVariables_);
|
|
1958
|
+
const verbatimImagePlaylistUri = tag.getRequiredAttrValue('URI');
|
|
1905
1959
|
const codecs = tag.getAttributeValue('CODECS', 'jpeg') || '';
|
|
1906
1960
|
|
|
1907
1961
|
// Check if the stream has already been created as part of another Variant
|
|
@@ -1916,16 +1970,10 @@ shaka.hls.HlsParser = class {
|
|
|
1916
1970
|
const characteristics = tag.getAttributeValue('CHARACTERISTICS');
|
|
1917
1971
|
|
|
1918
1972
|
const streamInfo = this.createStreamInfo_(
|
|
1919
|
-
verbatimImagePlaylistUri, codecs, type, language,
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
/* spatialAudio= */ false);
|
|
1923
|
-
|
|
1924
|
-
// TODO: This check is necessary because of the possibility of multiple
|
|
1925
|
-
// calls to createStreamInfoFromImageTag_ before either has resolved.
|
|
1926
|
-
if (this.uriToStreamInfosMap_.has(verbatimImagePlaylistUri)) {
|
|
1927
|
-
return this.uriToStreamInfosMap_.get(verbatimImagePlaylistUri);
|
|
1928
|
-
}
|
|
1973
|
+
this.globalId_++, [verbatimImagePlaylistUri], codecs, type, language,
|
|
1974
|
+
/* primary= */ false, name, /* channelsCount= */ null,
|
|
1975
|
+
/* closedCaptions= */ null, characteristics, /* forced= */ false,
|
|
1976
|
+
/* sampleRate= */ null, /* spatialAudio= */ false);
|
|
1929
1977
|
|
|
1930
1978
|
// Parse misc attributes.
|
|
1931
1979
|
const resolution = tag.getAttributeValue('RESOLUTION');
|
|
@@ -1958,45 +2006,104 @@ shaka.hls.HlsParser = class {
|
|
|
1958
2006
|
return streamInfo;
|
|
1959
2007
|
}
|
|
1960
2008
|
|
|
2009
|
+
/**
|
|
2010
|
+
* Parse EXT-X-I-FRAME-STREAM-INF media tag into a Stream object.
|
|
2011
|
+
*
|
|
2012
|
+
* @param {shaka.hls.Tag} tag
|
|
2013
|
+
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
2014
|
+
* @private
|
|
2015
|
+
*/
|
|
2016
|
+
createStreamInfoFromIframeTag_(tag) {
|
|
2017
|
+
goog.asserts.assert(tag.name == 'EXT-X-I-FRAME-STREAM-INF',
|
|
2018
|
+
'Should only be called on iframe tags!');
|
|
2019
|
+
/** @type {string} */
|
|
2020
|
+
const type = shaka.util.ManifestParserUtils.ContentType.IMAGE;
|
|
2021
|
+
|
|
2022
|
+
const verbatimIFramePlaylistUri = tag.getRequiredAttrValue('URI');
|
|
2023
|
+
const codecs = tag.getAttributeValue('CODECS') || '';
|
|
2024
|
+
|
|
2025
|
+
// Check if the stream has already been created as part of another Variant
|
|
2026
|
+
// and return it if it has.
|
|
2027
|
+
if (this.uriToStreamInfosMap_.has(verbatimIFramePlaylistUri)) {
|
|
2028
|
+
return this.uriToStreamInfosMap_.get(verbatimIFramePlaylistUri);
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
const language = tag.getAttributeValue('LANGUAGE');
|
|
2032
|
+
const name = tag.getAttributeValue('NAME');
|
|
2033
|
+
|
|
2034
|
+
const characteristics = tag.getAttributeValue('CHARACTERISTICS');
|
|
2035
|
+
|
|
2036
|
+
const streamInfo = this.createStreamInfo_(
|
|
2037
|
+
this.globalId_++, [verbatimIFramePlaylistUri], codecs, type, language,
|
|
2038
|
+
/* primary= */ false, name, /* channelsCount= */ null,
|
|
2039
|
+
/* closedCaptions= */ null, characteristics, /* forced= */ false,
|
|
2040
|
+
/* sampleRate= */ null, /* spatialAudio= */ false);
|
|
2041
|
+
|
|
2042
|
+
// Parse misc attributes.
|
|
2043
|
+
const resolution = tag.getAttributeValue('RESOLUTION');
|
|
2044
|
+
const [width, height] = resolution ? resolution.split('x') : [null, null];
|
|
2045
|
+
streamInfo.stream.width = Number(width) || undefined;
|
|
2046
|
+
streamInfo.stream.height = Number(height) || undefined;
|
|
2047
|
+
const bandwidth = tag.getAttributeValue('BANDWIDTH');
|
|
2048
|
+
if (bandwidth) {
|
|
2049
|
+
streamInfo.stream.bandwidth = Number(bandwidth);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
this.uriToStreamInfosMap_.set(verbatimIFramePlaylistUri, streamInfo);
|
|
2053
|
+
return streamInfo;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1961
2056
|
/**
|
|
1962
2057
|
* Parse an EXT-X-STREAM-INF media tag into a Stream object.
|
|
1963
2058
|
*
|
|
1964
|
-
* @param {!shaka.hls.Tag}
|
|
2059
|
+
* @param {!Array.<!shaka.hls.Tag>} tags
|
|
1965
2060
|
* @param {!Array.<string>} allCodecs
|
|
1966
2061
|
* @param {string} type
|
|
2062
|
+
* @param {?string} language
|
|
2063
|
+
* @param {?string} name
|
|
2064
|
+
* @param {?number} channelsCount
|
|
2065
|
+
* @param {?string} characteristics
|
|
2066
|
+
* @param {?number} sampleRate
|
|
2067
|
+
* @param {boolean} spatialAudio
|
|
1967
2068
|
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1968
2069
|
* @private
|
|
1969
2070
|
*/
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
const
|
|
1974
|
-
|
|
2071
|
+
createStreamInfoFromVariantTags_(tags, allCodecs, type, language, name,
|
|
2072
|
+
channelsCount, characteristics, sampleRate, spatialAudio) {
|
|
2073
|
+
const streamId = this.globalId_++;
|
|
2074
|
+
const verbatimMediaPlaylistUris = [];
|
|
2075
|
+
for (const tag of tags) {
|
|
2076
|
+
goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
|
|
2077
|
+
'Should only be called on variant tags!');
|
|
2078
|
+
const uri = tag.getRequiredAttrValue('URI');
|
|
2079
|
+
const pathwayId = tag.getAttributeValue('PATHWAY-ID');
|
|
2080
|
+
if (this.contentSteeringManager_ && pathwayId) {
|
|
2081
|
+
this.contentSteeringManager_.addLocation(streamId, pathwayId, uri);
|
|
2082
|
+
}
|
|
2083
|
+
verbatimMediaPlaylistUris.push(uri);
|
|
2084
|
+
}
|
|
1975
2085
|
|
|
1976
|
-
|
|
1977
|
-
|
|
2086
|
+
const key = verbatimMediaPlaylistUris.sort().join(',');
|
|
2087
|
+
if (this.uriToStreamInfosMap_.has(key)) {
|
|
2088
|
+
return this.uriToStreamInfosMap_.get(key);
|
|
1978
2089
|
}
|
|
1979
2090
|
|
|
1980
|
-
const closedCaptions = this.getClosedCaptions_(
|
|
2091
|
+
const closedCaptions = this.getClosedCaptions_(tags[0], type);
|
|
1981
2092
|
const codecs = shaka.util.ManifestParserUtils.guessCodecs(type, allCodecs);
|
|
1982
|
-
const streamInfo = this.createStreamInfo_(
|
|
1983
|
-
codecs, type,
|
|
1984
|
-
/*
|
|
1985
|
-
|
|
1986
|
-
/*
|
|
1987
|
-
// TODO: This check is necessary because of the possibility of multiple
|
|
1988
|
-
// calls to createStreamInfoFromVariantTag_ before either has resolved.
|
|
1989
|
-
if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
|
|
1990
|
-
return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
|
|
1991
|
-
}
|
|
2093
|
+
const streamInfo = this.createStreamInfo_(
|
|
2094
|
+
streamId, verbatimMediaPlaylistUris, codecs, type, language,
|
|
2095
|
+
/* primary= */ false, name, channelsCount, closedCaptions,
|
|
2096
|
+
characteristics, /* forced= */ false, sampleRate,
|
|
2097
|
+
/* spatialAudio= */ false);
|
|
1992
2098
|
|
|
1993
|
-
this.uriToStreamInfosMap_.set(
|
|
2099
|
+
this.uriToStreamInfosMap_.set(key, streamInfo);
|
|
1994
2100
|
return streamInfo;
|
|
1995
2101
|
}
|
|
1996
2102
|
|
|
1997
2103
|
|
|
1998
2104
|
/**
|
|
1999
|
-
* @param {
|
|
2105
|
+
* @param {number} streamId
|
|
2106
|
+
* @param {!Array.<string>} verbatimMediaPlaylistUris
|
|
2000
2107
|
* @param {string} codecs
|
|
2001
2108
|
* @param {string} type
|
|
2002
2109
|
* @param {?string} languageValue
|
|
@@ -2011,25 +2118,34 @@ shaka.hls.HlsParser = class {
|
|
|
2011
2118
|
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
2012
2119
|
* @private
|
|
2013
2120
|
*/
|
|
2014
|
-
createStreamInfo_(
|
|
2015
|
-
primary, name, channelsCount, closedCaptions,
|
|
2016
|
-
sampleRate, spatialAudio) {
|
|
2121
|
+
createStreamInfo_(streamId, verbatimMediaPlaylistUris, codecs, type,
|
|
2122
|
+
languageValue, primary, name, channelsCount, closedCaptions,
|
|
2123
|
+
characteristics, forced, sampleRate, spatialAudio) {
|
|
2017
2124
|
// TODO: Refactor, too many parameters
|
|
2018
|
-
const initialMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
|
|
2019
|
-
this.masterPlaylistUri_, verbatimMediaPlaylistUri);
|
|
2020
2125
|
|
|
2021
2126
|
// This stream is lazy-loaded inside the createSegmentIndex function.
|
|
2022
2127
|
// So we start out with a stream object that does not contain the actual
|
|
2023
2128
|
// segment index, then download when createSegmentIndex is called.
|
|
2024
|
-
const stream = this.makeStreamObject_(codecs, type,
|
|
2025
|
-
|
|
2026
|
-
sampleRate, spatialAudio);
|
|
2129
|
+
const stream = this.makeStreamObject_(streamId, codecs, type,
|
|
2130
|
+
languageValue, primary, name, channelsCount, closedCaptions,
|
|
2131
|
+
characteristics, forced, sampleRate, spatialAudio);
|
|
2132
|
+
|
|
2133
|
+
const redirectUris = [];
|
|
2134
|
+
const getUris = () => {
|
|
2135
|
+
if (this.contentSteeringManager_ &&
|
|
2136
|
+
verbatimMediaPlaylistUris.length > 1) {
|
|
2137
|
+
return this.contentSteeringManager_.getLocations(streamId);
|
|
2138
|
+
}
|
|
2139
|
+
return redirectUris.concat(shaka.hls.Utils.constructUris(
|
|
2140
|
+
[this.masterPlaylistUri_], verbatimMediaPlaylistUris,
|
|
2141
|
+
this.globalVariables_));
|
|
2142
|
+
};
|
|
2027
2143
|
const streamInfo = {
|
|
2028
2144
|
stream,
|
|
2029
2145
|
type,
|
|
2030
|
-
|
|
2146
|
+
redirectUris,
|
|
2147
|
+
getUris,
|
|
2031
2148
|
// These values are filled out or updated after lazy-loading:
|
|
2032
|
-
absoluteMediaPlaylistUri: initialMediaPlaylistUri,
|
|
2033
2149
|
minTimestamp: 0,
|
|
2034
2150
|
maxTimestamp: 0,
|
|
2035
2151
|
mediaSequenceToStartTime: new Map(),
|
|
@@ -2044,27 +2160,45 @@ shaka.hls.HlsParser = class {
|
|
|
2044
2160
|
|
|
2045
2161
|
/** @param {!AbortSignal} abortSignal */
|
|
2046
2162
|
const downloadSegmentIndex = async (abortSignal) => {
|
|
2163
|
+
const uris = streamInfo.getUris();
|
|
2047
2164
|
// Download the actual manifest.
|
|
2048
2165
|
const response = await this.requestManifest_(
|
|
2049
|
-
streamInfo.
|
|
2166
|
+
streamInfo.getUris(), /* isPlaylist= */ true);
|
|
2050
2167
|
if (abortSignal.aborted) {
|
|
2051
2168
|
return;
|
|
2052
2169
|
}
|
|
2053
2170
|
|
|
2054
2171
|
// Record the final URI after redirects.
|
|
2055
|
-
const
|
|
2172
|
+
const responseUri = response.uri;
|
|
2173
|
+
if (responseUri != response.originalUri && !uris.includes(responseUri)) {
|
|
2174
|
+
redirectUris.push(responseUri);
|
|
2175
|
+
}
|
|
2056
2176
|
|
|
2057
2177
|
// Record the redirected, final URI of this media playlist when we parse
|
|
2058
2178
|
// it.
|
|
2059
2179
|
/** @type {!shaka.hls.Playlist} */
|
|
2060
|
-
const playlist = this.manifestTextParser_.parsePlaylist(
|
|
2061
|
-
|
|
2180
|
+
const playlist = this.manifestTextParser_.parsePlaylist(response.data);
|
|
2181
|
+
|
|
2182
|
+
let mimeType = undefined;
|
|
2183
|
+
|
|
2184
|
+
// If no codec info was provided in the manifest and codec guessing is
|
|
2185
|
+
// disabled we try to get necessary info from the media data.
|
|
2186
|
+
if (!this.codecInfoInManifest_ && this.config_.hls.disableCodecGuessing) {
|
|
2187
|
+
const basicInfo =
|
|
2188
|
+
await this.getMediaPlaylistBasicInfo_(playlist, getUris);
|
|
2189
|
+
|
|
2190
|
+
goog.asserts.assert(
|
|
2191
|
+
type === basicInfo.type, 'Media types should match!');
|
|
2192
|
+
|
|
2193
|
+
mimeType = basicInfo.mimeType;
|
|
2194
|
+
codecs = basicInfo.codecs;
|
|
2195
|
+
}
|
|
2062
2196
|
|
|
2063
2197
|
const wasLive = this.isLive_();
|
|
2064
2198
|
const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
|
|
2065
|
-
playlist,
|
|
2199
|
+
streamId, playlist, getUris, responseUri, codecs,
|
|
2066
2200
|
type, languageValue, primary, name, channelsCount, closedCaptions,
|
|
2067
|
-
characteristics, forced, sampleRate, spatialAudio);
|
|
2201
|
+
characteristics, forced, sampleRate, spatialAudio, mimeType);
|
|
2068
2202
|
if (abortSignal.aborted) {
|
|
2069
2203
|
return;
|
|
2070
2204
|
}
|
|
@@ -2078,7 +2212,6 @@ shaka.hls.HlsParser = class {
|
|
|
2078
2212
|
}
|
|
2079
2213
|
|
|
2080
2214
|
// Copy values from the real stream info to our initial one.
|
|
2081
|
-
streamInfo.absoluteMediaPlaylistUri = absoluteMediaPlaylistUri;
|
|
2082
2215
|
streamInfo.minTimestamp = realStreamInfo.minTimestamp;
|
|
2083
2216
|
streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
|
|
2084
2217
|
streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
|
|
@@ -2095,6 +2228,7 @@ shaka.hls.HlsParser = class {
|
|
|
2095
2228
|
stream.keyIds = realStream.keyIds;
|
|
2096
2229
|
stream.mimeType = realStream.mimeType;
|
|
2097
2230
|
stream.bandwidth = realStream.bandwidth;
|
|
2231
|
+
stream.codecs = realStream.codecs || stream.codecs;
|
|
2098
2232
|
|
|
2099
2233
|
// Since we lazy-loaded this content, the player may need to create new
|
|
2100
2234
|
// sessions for the DRM info in this stream.
|
|
@@ -2296,9 +2430,10 @@ shaka.hls.HlsParser = class {
|
|
|
2296
2430
|
}
|
|
2297
2431
|
|
|
2298
2432
|
/**
|
|
2433
|
+
* @param {number} streamId
|
|
2299
2434
|
* @param {!shaka.hls.Playlist} playlist
|
|
2300
|
-
* @param {string}
|
|
2301
|
-
* @param {string}
|
|
2435
|
+
* @param {function():!Array.<string>} getUris
|
|
2436
|
+
* @param {string} responseUri
|
|
2302
2437
|
* @param {string} codecs
|
|
2303
2438
|
* @param {string} type
|
|
2304
2439
|
* @param {?string} languageValue
|
|
@@ -2314,8 +2449,8 @@ shaka.hls.HlsParser = class {
|
|
|
2314
2449
|
* @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
|
|
2315
2450
|
* @private
|
|
2316
2451
|
*/
|
|
2317
|
-
async convertParsedPlaylistIntoStreamInfo_(
|
|
2318
|
-
|
|
2452
|
+
async convertParsedPlaylistIntoStreamInfo_(streamId, playlist,
|
|
2453
|
+
getUris, responseUri, codecs, type, languageValue, primary, name,
|
|
2319
2454
|
channelsCount, closedCaptions, characteristics, forced, sampleRate,
|
|
2320
2455
|
spatialAudio, mimeType = undefined) {
|
|
2321
2456
|
if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
|
|
@@ -2331,7 +2466,8 @@ shaka.hls.HlsParser = class {
|
|
|
2331
2466
|
const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
|
|
2332
2467
|
'EXT-X-DEFINE');
|
|
2333
2468
|
|
|
2334
|
-
const mediaVariables =
|
|
2469
|
+
const mediaVariables =
|
|
2470
|
+
this.parseMediaVariables_(variablesTags, responseUri);
|
|
2335
2471
|
|
|
2336
2472
|
goog.asserts.assert(playlist.segments != null,
|
|
2337
2473
|
'Media playlist should have segments!');
|
|
@@ -2344,7 +2480,7 @@ shaka.hls.HlsParser = class {
|
|
|
2344
2480
|
|
|
2345
2481
|
if (!mimeType) {
|
|
2346
2482
|
mimeType = await this.guessMimeType_(type, codecs, playlist,
|
|
2347
|
-
mediaVariables);
|
|
2483
|
+
mediaVariables, getUris);
|
|
2348
2484
|
}
|
|
2349
2485
|
|
|
2350
2486
|
const {drmInfos, keyIds, encrypted, aesEncrypted} =
|
|
@@ -2357,11 +2493,20 @@ shaka.hls.HlsParser = class {
|
|
|
2357
2493
|
shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
|
|
2358
2494
|
}
|
|
2359
2495
|
|
|
2496
|
+
const stream = this.makeStreamObject_(streamId, codecs, type,
|
|
2497
|
+
languageValue, primary, name, channelsCount, closedCaptions,
|
|
2498
|
+
characteristics, forced, sampleRate, spatialAudio);
|
|
2499
|
+
stream.encrypted = encrypted;
|
|
2500
|
+
stream.drmInfos = drmInfos;
|
|
2501
|
+
stream.keyIds = keyIds;
|
|
2502
|
+
stream.mimeType = mimeType;
|
|
2503
|
+
|
|
2360
2504
|
const mediaSequenceToStartTime = this.isLive_() ?
|
|
2361
2505
|
this.mediaSequenceToStartTimeByType_.get(type) : new Map();
|
|
2506
|
+
|
|
2362
2507
|
const {segments, bandwidth} = this.createSegments_(
|
|
2363
|
-
|
|
2364
|
-
|
|
2508
|
+
playlist, stream, mediaSequenceToStartTime, mediaVariables, getUris);
|
|
2509
|
+
stream.bandwidth = bandwidth;
|
|
2365
2510
|
|
|
2366
2511
|
// This new calculation is necessary for Low Latency streams.
|
|
2367
2512
|
if (this.isLive_()) {
|
|
@@ -2373,6 +2518,7 @@ shaka.hls.HlsParser = class {
|
|
|
2373
2518
|
const lastEndTime = lastSegment.endTime;
|
|
2374
2519
|
/** @type {!shaka.media.SegmentIndex} */
|
|
2375
2520
|
const segmentIndex = new shaka.media.SegmentIndex(segments);
|
|
2521
|
+
stream.segmentIndex = segmentIndex;
|
|
2376
2522
|
|
|
2377
2523
|
const serverControlTag = shaka.hls.Utils.getFirstTagWithName(
|
|
2378
2524
|
playlist.tags, 'EXT-X-SERVER-CONTROL');
|
|
@@ -2387,21 +2533,11 @@ shaka.hls.HlsParser = class {
|
|
|
2387
2533
|
const {nextMediaSequence, nextPart} =
|
|
2388
2534
|
this.getNextMediaSequenceAndPart_(mediaSequenceNumber, segments);
|
|
2389
2535
|
|
|
2390
|
-
const stream = this.makeStreamObject_(codecs, type, languageValue, primary,
|
|
2391
|
-
name, channelsCount, closedCaptions, characteristics, forced,
|
|
2392
|
-
sampleRate, spatialAudio);
|
|
2393
|
-
stream.segmentIndex = segmentIndex;
|
|
2394
|
-
stream.encrypted = encrypted;
|
|
2395
|
-
stream.drmInfos = drmInfos;
|
|
2396
|
-
stream.keyIds = keyIds;
|
|
2397
|
-
stream.mimeType = mimeType;
|
|
2398
|
-
stream.bandwidth = bandwidth;
|
|
2399
|
-
|
|
2400
2536
|
return {
|
|
2401
2537
|
stream,
|
|
2402
2538
|
type,
|
|
2403
|
-
|
|
2404
|
-
|
|
2539
|
+
redirectUris: [],
|
|
2540
|
+
getUris,
|
|
2405
2541
|
minTimestamp: firstStartTime,
|
|
2406
2542
|
maxTimestamp: lastEndTime,
|
|
2407
2543
|
canSkipSegments,
|
|
@@ -2465,6 +2601,7 @@ shaka.hls.HlsParser = class {
|
|
|
2465
2601
|
* The parameters that are passed into here are only the things that can be
|
|
2466
2602
|
* known without downloading the media playlist; other values must be set
|
|
2467
2603
|
* manually on the object after creation.
|
|
2604
|
+
* @param {number} id
|
|
2468
2605
|
* @param {string} codecs
|
|
2469
2606
|
* @param {string} type
|
|
2470
2607
|
* @param {?string} languageValue
|
|
@@ -2479,8 +2616,9 @@ shaka.hls.HlsParser = class {
|
|
|
2479
2616
|
* @return {!shaka.extern.Stream}
|
|
2480
2617
|
* @private
|
|
2481
2618
|
*/
|
|
2482
|
-
makeStreamObject_(codecs, type, languageValue, primary, name,
|
|
2483
|
-
closedCaptions, characteristics, forced, sampleRate,
|
|
2619
|
+
makeStreamObject_(id, codecs, type, languageValue, primary, name,
|
|
2620
|
+
channelsCount, closedCaptions, characteristics, forced, sampleRate,
|
|
2621
|
+
spatialAudio) {
|
|
2484
2622
|
// Fill out a "best-guess" mimeType, for now. It will be replaced once the
|
|
2485
2623
|
// stream is lazy-loaded.
|
|
2486
2624
|
const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
|
|
@@ -2492,8 +2630,21 @@ shaka.hls.HlsParser = class {
|
|
|
2492
2630
|
}
|
|
2493
2631
|
}
|
|
2494
2632
|
|
|
2495
|
-
|
|
2496
|
-
|
|
2633
|
+
let kind = undefined;
|
|
2634
|
+
let accessibilityPurpose = null;
|
|
2635
|
+
if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
|
|
2636
|
+
if (roles.includes('public.accessibility.transcribes-spoken-dialog') &&
|
|
2637
|
+
roles.includes('public.accessibility.describes-music-and-sound')) {
|
|
2638
|
+
kind = shaka.util.ManifestParserUtils.TextStreamKind.CLOSED_CAPTION;
|
|
2639
|
+
} else {
|
|
2640
|
+
kind = shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE;
|
|
2641
|
+
}
|
|
2642
|
+
} else {
|
|
2643
|
+
if (roles.includes('public.accessibility.describes-video')) {
|
|
2644
|
+
accessibilityPurpose =
|
|
2645
|
+
shaka.media.ManifestParser.AccessibilityPurpose.VISUALLY_IMPAIRED;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2497
2648
|
|
|
2498
2649
|
// If there are no roles, and we have defaulted to the subtitle "kind" for
|
|
2499
2650
|
// this track, add the implied subtitle role.
|
|
@@ -2535,9 +2686,11 @@ shaka.hls.HlsParser = class {
|
|
|
2535
2686
|
spatialAudio,
|
|
2536
2687
|
closedCaptions,
|
|
2537
2688
|
hdr: undefined,
|
|
2689
|
+
videoLayout: undefined,
|
|
2538
2690
|
tilesLayout: undefined,
|
|
2539
|
-
accessibilityPurpose:
|
|
2691
|
+
accessibilityPurpose: accessibilityPurpose,
|
|
2540
2692
|
external: false,
|
|
2693
|
+
fastSwitching: false,
|
|
2541
2694
|
};
|
|
2542
2695
|
}
|
|
2543
2696
|
|
|
@@ -2608,10 +2761,12 @@ shaka.hls.HlsParser = class {
|
|
|
2608
2761
|
/**
|
|
2609
2762
|
* @param {!shaka.hls.Tag} drmTag
|
|
2610
2763
|
* @param {!shaka.hls.Playlist} playlist
|
|
2764
|
+
* @param {function():!Array.<string>} getUris
|
|
2765
|
+
* @param {?Map.<string, string>=} variables
|
|
2611
2766
|
* @return {!shaka.extern.aes128Key}
|
|
2612
2767
|
* @private
|
|
2613
2768
|
*/
|
|
2614
|
-
parseAES128DrmTag_(drmTag, playlist) {
|
|
2769
|
+
parseAES128DrmTag_(drmTag, playlist, getUris, variables) {
|
|
2615
2770
|
// Check if the Web Crypto API is available.
|
|
2616
2771
|
if (!window.crypto || !window.crypto.subtle) {
|
|
2617
2772
|
shaka.log.alwaysWarn('Web Crypto API is not available to decrypt ' +
|
|
@@ -2646,18 +2801,17 @@ shaka.hls.HlsParser = class {
|
|
|
2646
2801
|
}
|
|
2647
2802
|
}
|
|
2648
2803
|
|
|
2649
|
-
const keyUri = shaka.hls.Utils.constructAbsoluteUri(
|
|
2650
|
-
playlist.absoluteUri, drmTag.getRequiredAttrValue('URI'));
|
|
2651
|
-
|
|
2652
|
-
const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
|
|
2653
|
-
const request = shaka.net.NetworkingEngine.makeRequest(
|
|
2654
|
-
[keyUri], this.config_.retryParameters);
|
|
2655
|
-
|
|
2656
2804
|
const keyInfo = {method: 'AES-128', iv, firstMediaSequenceNumber};
|
|
2657
2805
|
|
|
2658
2806
|
// Don't download the key object until the segment is parsed, to avoid a
|
|
2659
2807
|
// startup delay for long manifests with lots of keys.
|
|
2660
2808
|
keyInfo.fetchKey = async () => {
|
|
2809
|
+
const keyUri = shaka.hls.Utils.constructSegmentUris(
|
|
2810
|
+
getUris(), drmTag.getRequiredAttrValue('URI'), variables);
|
|
2811
|
+
|
|
2812
|
+
const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
|
|
2813
|
+
const request = shaka.net.NetworkingEngine.makeRequest(
|
|
2814
|
+
[keyUri], this.config_.retryParameters);
|
|
2661
2815
|
const keyResponse = await this.makeNetworkRequest_(request, requestType);
|
|
2662
2816
|
|
|
2663
2817
|
// keyResponse.status is undefined when URI is "data:text/plain;base64,"
|
|
@@ -2834,11 +2988,12 @@ shaka.hls.HlsParser = class {
|
|
|
2834
2988
|
* Get the InitSegmentReference for a segment if it has a EXT-X-MAP tag.
|
|
2835
2989
|
* @param {!shaka.hls.Playlist} playlist
|
|
2836
2990
|
* @param {!Array.<!shaka.hls.Tag>} tags Segment tags
|
|
2837
|
-
* @param {
|
|
2991
|
+
* @param {function():!Array.<string>} getUris
|
|
2992
|
+
* @param {?Map.<string, string>=} variables
|
|
2838
2993
|
* @return {shaka.media.InitSegmentReference}
|
|
2839
2994
|
* @private
|
|
2840
2995
|
*/
|
|
2841
|
-
getInitSegmentReference_(playlist, tags, variables) {
|
|
2996
|
+
getInitSegmentReference_(playlist, tags, getUris, variables) {
|
|
2842
2997
|
/** @type {?shaka.hls.Tag} */
|
|
2843
2998
|
const mapTag = shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-MAP');
|
|
2844
2999
|
|
|
@@ -2847,28 +3002,30 @@ shaka.hls.HlsParser = class {
|
|
|
2847
3002
|
}
|
|
2848
3003
|
// Map tag example: #EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"
|
|
2849
3004
|
const verbatimInitSegmentUri = mapTag.getRequiredAttrValue('URI');
|
|
2850
|
-
const
|
|
2851
|
-
|
|
2852
|
-
playlist.absoluteUri, verbatimInitSegmentUri),
|
|
2853
|
-
variables);
|
|
3005
|
+
const absoluteInitSegmentUris = shaka.hls.Utils.constructSegmentUris(
|
|
3006
|
+
getUris(), verbatimInitSegmentUri, variables);
|
|
2854
3007
|
|
|
2855
3008
|
const mapTagKey = [
|
|
2856
|
-
|
|
3009
|
+
absoluteInitSegmentUris.toString(),
|
|
2857
3010
|
mapTag.getAttributeValue('BYTERANGE', ''),
|
|
2858
3011
|
].join('-');
|
|
2859
3012
|
if (!this.mapTagToInitSegmentRefMap_.has(mapTagKey)) {
|
|
2860
3013
|
/** @type {shaka.extern.aes128Key|undefined} */
|
|
2861
3014
|
let aes128Key = undefined;
|
|
3015
|
+
let byteRangeTag = null;
|
|
2862
3016
|
for (const tag of tags) {
|
|
2863
3017
|
if (tag.name == 'EXT-X-KEY') {
|
|
2864
3018
|
if (tag.getRequiredAttrValue('METHOD') == 'AES-128' &&
|
|
2865
3019
|
tag.id < mapTag.id) {
|
|
2866
|
-
aes128Key =
|
|
3020
|
+
aes128Key =
|
|
3021
|
+
this.parseAES128DrmTag_(tag, playlist, getUris, variables);
|
|
2867
3022
|
}
|
|
3023
|
+
} else if (tag.name == 'EXT-X-BYTERANGE' && tag.id < mapTag.id) {
|
|
3024
|
+
byteRangeTag = tag;
|
|
2868
3025
|
}
|
|
2869
3026
|
}
|
|
2870
3027
|
const initSegmentRef = this.createInitSegmentReference_(
|
|
2871
|
-
|
|
3028
|
+
absoluteInitSegmentUris, mapTag, byteRangeTag, aes128Key);
|
|
2872
3029
|
this.mapTagToInitSegmentRefMap_.set(mapTagKey, initSegmentRef);
|
|
2873
3030
|
}
|
|
2874
3031
|
return this.mapTagToInitSegmentRefMap_.get(mapTagKey);
|
|
@@ -2877,16 +3034,21 @@ shaka.hls.HlsParser = class {
|
|
|
2877
3034
|
/**
|
|
2878
3035
|
* Create an InitSegmentReference object for the EXT-X-MAP tag in the media
|
|
2879
3036
|
* playlist.
|
|
2880
|
-
* @param {string}
|
|
3037
|
+
* @param {!Array.<string>} absoluteInitSegmentUris
|
|
2881
3038
|
* @param {!shaka.hls.Tag} mapTag EXT-X-MAP
|
|
3039
|
+
* @param {shaka.hls.Tag=} byteRangeTag EXT-X-BYTERANGE
|
|
2882
3040
|
* @param {shaka.extern.aes128Key=} aes128Key
|
|
2883
3041
|
* @return {!shaka.media.InitSegmentReference}
|
|
2884
3042
|
* @private
|
|
2885
3043
|
*/
|
|
2886
|
-
createInitSegmentReference_(
|
|
3044
|
+
createInitSegmentReference_(absoluteInitSegmentUris, mapTag, byteRangeTag,
|
|
3045
|
+
aes128Key) {
|
|
2887
3046
|
let startByte = 0;
|
|
2888
3047
|
let endByte = null;
|
|
2889
|
-
|
|
3048
|
+
let byterange = mapTag.getAttributeValue('BYTERANGE');
|
|
3049
|
+
if (!byterange && byteRangeTag) {
|
|
3050
|
+
byterange = byteRangeTag.value;
|
|
3051
|
+
}
|
|
2890
3052
|
// If a BYTERANGE attribute is not specified, the segment consists
|
|
2891
3053
|
// of the entire resource.
|
|
2892
3054
|
if (byterange) {
|
|
@@ -2907,7 +3069,7 @@ shaka.hls.HlsParser = class {
|
|
|
2907
3069
|
}
|
|
2908
3070
|
|
|
2909
3071
|
const initSegmentRef = new shaka.media.InitSegmentReference(
|
|
2910
|
-
() =>
|
|
3072
|
+
() => absoluteInitSegmentUris,
|
|
2911
3073
|
startByte,
|
|
2912
3074
|
endByte,
|
|
2913
3075
|
/* mediaQuality= */ null,
|
|
@@ -2925,16 +3087,16 @@ shaka.hls.HlsParser = class {
|
|
|
2925
3087
|
* @param {!shaka.hls.Segment} hlsSegment
|
|
2926
3088
|
* @param {number} startTime
|
|
2927
3089
|
* @param {!Map.<string, string>} variables
|
|
2928
|
-
* @param {
|
|
2929
|
-
* @param {
|
|
2930
|
-
* @param {string}
|
|
3090
|
+
* @param {!shaka.hls.Playlist} playlist
|
|
3091
|
+
* @param {shaka.extern.Stream} stream
|
|
3092
|
+
* @param {function():!Array.<string>} getUris
|
|
2931
3093
|
* @param {shaka.extern.aes128Key=} aes128Key
|
|
2932
3094
|
* @return {shaka.media.SegmentReference}
|
|
2933
3095
|
* @private
|
|
2934
3096
|
*/
|
|
2935
3097
|
createSegmentReference_(
|
|
2936
3098
|
initSegmentReference, previousReference, hlsSegment, startTime,
|
|
2937
|
-
variables,
|
|
3099
|
+
variables, playlist, stream, getUris, aes128Key) {
|
|
2938
3100
|
const tags = hlsSegment.tags;
|
|
2939
3101
|
const extinfTag =
|
|
2940
3102
|
shaka.hls.Utils.getFirstTagWithName(tags, 'EXTINF');
|
|
@@ -2991,8 +3153,9 @@ shaka.hls.HlsParser = class {
|
|
|
2991
3153
|
let isPreloadSegment = false;
|
|
2992
3154
|
|
|
2993
3155
|
if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) {
|
|
2994
|
-
const byterangeOptimizationSupport = (mimeType == 'video/mp4' ||
|
|
2995
|
-
mimeType == 'audio/mp4') && window.ReadableStream
|
|
3156
|
+
const byterangeOptimizationSupport = (stream.mimeType == 'video/mp4' ||
|
|
3157
|
+
stream.mimeType == 'audio/mp4') && window.ReadableStream &&
|
|
3158
|
+
this.config_.hls.allowLowLatencyByteRangeOptimization;
|
|
2996
3159
|
|
|
2997
3160
|
let partialSyncTime = syncTime;
|
|
2998
3161
|
for (let i = 0; i < hlsSegment.partialSegments.length; i++) {
|
|
@@ -3048,14 +3211,14 @@ shaka.hls.HlsParser = class {
|
|
|
3048
3211
|
somePartialSegmentWithGap = true;
|
|
3049
3212
|
}
|
|
3050
3213
|
|
|
3051
|
-
let
|
|
3214
|
+
let uris = null;
|
|
3052
3215
|
const getPartialUris = () => {
|
|
3053
|
-
if (
|
|
3216
|
+
if (uris == null) {
|
|
3054
3217
|
goog.asserts.assert(pUri, 'Partial uri should be defined!');
|
|
3055
|
-
|
|
3056
|
-
|
|
3218
|
+
uris = shaka.hls.Utils.constructSegmentUris(
|
|
3219
|
+
getUris(), pUri, variables);
|
|
3057
3220
|
}
|
|
3058
|
-
return
|
|
3221
|
+
return uris;
|
|
3059
3222
|
};
|
|
3060
3223
|
|
|
3061
3224
|
if (byterangeOptimizationSupport &&
|
|
@@ -3150,7 +3313,7 @@ shaka.hls.HlsParser = class {
|
|
|
3150
3313
|
|
|
3151
3314
|
let tilesLayout = '';
|
|
3152
3315
|
let tileDuration = null;
|
|
3153
|
-
if (type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
|
|
3316
|
+
if (stream.type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
|
|
3154
3317
|
// By default in HLS the tilesLayout is 1x1
|
|
3155
3318
|
tilesLayout = '1x1';
|
|
3156
3319
|
const tilesTag =
|
|
@@ -3164,22 +3327,25 @@ shaka.hls.HlsParser = class {
|
|
|
3164
3327
|
}
|
|
3165
3328
|
}
|
|
3166
3329
|
|
|
3167
|
-
let
|
|
3168
|
-
const
|
|
3330
|
+
let uris = null;
|
|
3331
|
+
const getSegmentUris = () => {
|
|
3169
3332
|
if (getUrisOptimization) {
|
|
3170
3333
|
return getUrisOptimization();
|
|
3171
3334
|
}
|
|
3172
|
-
if (
|
|
3173
|
-
|
|
3174
|
-
hlsSegment.
|
|
3335
|
+
if (uris == null) {
|
|
3336
|
+
uris = shaka.hls.Utils.constructSegmentUris(getUris(),
|
|
3337
|
+
hlsSegment.verbatimSegmentUri, variables);
|
|
3175
3338
|
}
|
|
3176
|
-
return
|
|
3339
|
+
return uris || [];
|
|
3177
3340
|
};
|
|
3178
3341
|
|
|
3342
|
+
const allPartialSegments = partialSegmentRefs.length > 0 &&
|
|
3343
|
+
!!hlsSegment.verbatimSegmentUri;
|
|
3344
|
+
|
|
3179
3345
|
const reference = new shaka.media.SegmentReference(
|
|
3180
3346
|
startTime,
|
|
3181
3347
|
endTime,
|
|
3182
|
-
|
|
3348
|
+
getSegmentUris,
|
|
3183
3349
|
startByte,
|
|
3184
3350
|
endByte,
|
|
3185
3351
|
initSegmentReference,
|
|
@@ -3192,6 +3358,7 @@ shaka.hls.HlsParser = class {
|
|
|
3192
3358
|
syncTime,
|
|
3193
3359
|
status,
|
|
3194
3360
|
aes128Key,
|
|
3361
|
+
allPartialSegments,
|
|
3195
3362
|
);
|
|
3196
3363
|
|
|
3197
3364
|
if (segmentWithByteRangeOptimization) {
|
|
@@ -3239,18 +3406,17 @@ shaka.hls.HlsParser = class {
|
|
|
3239
3406
|
* get the bandwidth necessary for this segments If it's defined in the
|
|
3240
3407
|
* playlist.
|
|
3241
3408
|
*
|
|
3242
|
-
* @param {string} verbatimMediaPlaylistUri
|
|
3243
3409
|
* @param {!shaka.hls.Playlist} playlist
|
|
3244
|
-
* @param {
|
|
3245
|
-
* @param {string} mimeType
|
|
3410
|
+
* @param {shaka.extern.Stream} stream
|
|
3246
3411
|
* @param {!Map.<number, number>} mediaSequenceToStartTime
|
|
3247
3412
|
* @param {!Map.<string, string>} variables
|
|
3413
|
+
* @param {function():!Array.<string>} getUris
|
|
3248
3414
|
* @return {{segments: !Array.<!shaka.media.SegmentReference>,
|
|
3249
3415
|
* bandwidth: (number|undefined)}}
|
|
3250
3416
|
* @private
|
|
3251
3417
|
*/
|
|
3252
|
-
createSegments_(
|
|
3253
|
-
|
|
3418
|
+
createSegments_(playlist, stream, mediaSequenceToStartTime, variables,
|
|
3419
|
+
getUris) {
|
|
3254
3420
|
/** @type {Array.<!shaka.hls.Segment>} */
|
|
3255
3421
|
const hlsSegments = playlist.segments;
|
|
3256
3422
|
goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
|
|
@@ -3302,7 +3468,8 @@ shaka.hls.HlsParser = class {
|
|
|
3302
3468
|
for (const drmTag of item.tags) {
|
|
3303
3469
|
if (drmTag.name == 'EXT-X-KEY') {
|
|
3304
3470
|
if (drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
|
|
3305
|
-
aes128Key =
|
|
3471
|
+
aes128Key =
|
|
3472
|
+
this.parseAES128DrmTag_(drmTag, playlist, getUris, variables);
|
|
3306
3473
|
} else {
|
|
3307
3474
|
aes128Key = undefined;
|
|
3308
3475
|
}
|
|
@@ -3312,7 +3479,7 @@ shaka.hls.HlsParser = class {
|
|
|
3312
3479
|
mediaSequenceToStartTime.set(position, startTime);
|
|
3313
3480
|
|
|
3314
3481
|
initSegmentRef = this.getInitSegmentReference_(playlist,
|
|
3315
|
-
item.tags, variables);
|
|
3482
|
+
item.tags, getUris, variables);
|
|
3316
3483
|
|
|
3317
3484
|
// If the stream is low latency and the user has not configured the
|
|
3318
3485
|
// lowLatencyMode, but if it has been configured to activate the
|
|
@@ -3332,9 +3499,9 @@ shaka.hls.HlsParser = class {
|
|
|
3332
3499
|
item,
|
|
3333
3500
|
startTime,
|
|
3334
3501
|
variables,
|
|
3335
|
-
playlist
|
|
3336
|
-
|
|
3337
|
-
|
|
3502
|
+
playlist,
|
|
3503
|
+
stream,
|
|
3504
|
+
getUris,
|
|
3338
3505
|
aes128Key);
|
|
3339
3506
|
|
|
3340
3507
|
if (reference) {
|
|
@@ -3468,42 +3635,6 @@ shaka.hls.HlsParser = class {
|
|
|
3468
3635
|
};
|
|
3469
3636
|
}
|
|
3470
3637
|
|
|
3471
|
-
/**
|
|
3472
|
-
* Replaces the variables of a given URI.
|
|
3473
|
-
*
|
|
3474
|
-
* @param {string} uri
|
|
3475
|
-
* @param {!Map.<string, string>} variables
|
|
3476
|
-
* @return {string}
|
|
3477
|
-
* @private
|
|
3478
|
-
*/
|
|
3479
|
-
variableSubstitution_(uri, variables) {
|
|
3480
|
-
if (!variables.size) {
|
|
3481
|
-
return uri;
|
|
3482
|
-
}
|
|
3483
|
-
let newUri = String(uri).replace(/%7B/g, '{').replace(/%7D/g, '}');
|
|
3484
|
-
|
|
3485
|
-
const uriVariables = newUri.match(/{\$\w*}/g);
|
|
3486
|
-
if (uriVariables) {
|
|
3487
|
-
for (const variable of uriVariables) {
|
|
3488
|
-
// Note: All variables have the structure {$...}
|
|
3489
|
-
const variableName = variable.slice(2, variable.length - 1);
|
|
3490
|
-
const replaceValue = variables.get(variableName);
|
|
3491
|
-
if (replaceValue) {
|
|
3492
|
-
newUri = newUri.replace(variable, replaceValue);
|
|
3493
|
-
} else {
|
|
3494
|
-
shaka.log.error('A variable has been found that is not declared',
|
|
3495
|
-
variableName);
|
|
3496
|
-
throw new shaka.util.Error(
|
|
3497
|
-
shaka.util.Error.Severity.CRITICAL,
|
|
3498
|
-
shaka.util.Error.Category.MANIFEST,
|
|
3499
|
-
shaka.util.Error.Code.HLS_VARIABLE_NOT_FOUND,
|
|
3500
|
-
variableName);
|
|
3501
|
-
}
|
|
3502
|
-
}
|
|
3503
|
-
}
|
|
3504
|
-
return newUri;
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
3638
|
/**
|
|
3508
3639
|
* Attempts to guess stream's mime type based on content type and URI.
|
|
3509
3640
|
*
|
|
@@ -3537,6 +3668,10 @@ shaka.hls.HlsParser = class {
|
|
|
3537
3668
|
}
|
|
3538
3669
|
}
|
|
3539
3670
|
|
|
3671
|
+
if (codecs == 'mjpg') {
|
|
3672
|
+
return 'application/mp4';
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3540
3675
|
// Not enough information to guess from the content type and codecs.
|
|
3541
3676
|
return null;
|
|
3542
3677
|
}
|
|
@@ -3569,10 +3704,11 @@ shaka.hls.HlsParser = class {
|
|
|
3569
3704
|
* @param {string} codecs
|
|
3570
3705
|
* @param {!shaka.hls.Playlist} playlist
|
|
3571
3706
|
* @param {!Map.<string, string>} variables
|
|
3707
|
+
* @param {function():!Array.<string>} getUris
|
|
3572
3708
|
* @return {!Promise.<string>}
|
|
3573
3709
|
* @private
|
|
3574
3710
|
*/
|
|
3575
|
-
async guessMimeType_(contentType, codecs, playlist, variables) {
|
|
3711
|
+
async guessMimeType_(contentType, codecs, playlist, variables, getUris) {
|
|
3576
3712
|
const HlsParser = shaka.hls.HlsParser;
|
|
3577
3713
|
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
3578
3714
|
|
|
@@ -3582,10 +3718,13 @@ shaka.hls.HlsParser = class {
|
|
|
3582
3718
|
goog.asserts.assert(playlist.segments.length,
|
|
3583
3719
|
'Playlist should have segments!');
|
|
3584
3720
|
const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2);
|
|
3585
|
-
const middleSegmentUri = this.variableSubstitution_(
|
|
3586
|
-
playlist.segments[middleSegmentIdx].absoluteUri, variables);
|
|
3587
3721
|
|
|
3588
|
-
const
|
|
3722
|
+
const middleSegmentUris = shaka.hls.Utils.constructSegmentUris(
|
|
3723
|
+
getUris(),
|
|
3724
|
+
playlist.segments[middleSegmentIdx].verbatimSegmentUri,
|
|
3725
|
+
variables);
|
|
3726
|
+
|
|
3727
|
+
const parsedUri = new goog.Uri(middleSegmentUris[0]);
|
|
3589
3728
|
const extension = parsedUri.getPath().split('.').pop();
|
|
3590
3729
|
const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
|
|
3591
3730
|
|
|
@@ -3608,7 +3747,7 @@ shaka.hls.HlsParser = class {
|
|
|
3608
3747
|
// If unable to guess mime type, request a segment and try getting it
|
|
3609
3748
|
// from the response.
|
|
3610
3749
|
const headRequest = shaka.net.NetworkingEngine.makeRequest(
|
|
3611
|
-
|
|
3750
|
+
middleSegmentUris, this.config_.retryParameters);
|
|
3612
3751
|
headRequest.method = 'HEAD';
|
|
3613
3752
|
const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
|
|
3614
3753
|
const response = await this.makeNetworkRequest_(
|
|
@@ -3650,14 +3789,17 @@ shaka.hls.HlsParser = class {
|
|
|
3650
3789
|
* @param {?string} height
|
|
3651
3790
|
* @param {?string} frameRate
|
|
3652
3791
|
* @param {?string} videoRange
|
|
3792
|
+
* @param {?string} videoLayout
|
|
3653
3793
|
* @private
|
|
3654
3794
|
*/
|
|
3655
|
-
addVideoAttributes_(stream, width, height, frameRate, videoRange
|
|
3795
|
+
addVideoAttributes_(stream, width, height, frameRate, videoRange,
|
|
3796
|
+
videoLayout) {
|
|
3656
3797
|
if (stream) {
|
|
3657
3798
|
stream.width = Number(width) || undefined;
|
|
3658
3799
|
stream.height = Number(height) || undefined;
|
|
3659
3800
|
stream.frameRate = Number(frameRate) || undefined;
|
|
3660
3801
|
stream.hdr = videoRange || undefined;
|
|
3802
|
+
stream.videoLayout = videoLayout || undefined;
|
|
3661
3803
|
}
|
|
3662
3804
|
}
|
|
3663
3805
|
|
|
@@ -3665,16 +3807,16 @@ shaka.hls.HlsParser = class {
|
|
|
3665
3807
|
* Makes a network request for the manifest and returns a Promise
|
|
3666
3808
|
* with the resulting data.
|
|
3667
3809
|
*
|
|
3668
|
-
* @param {string}
|
|
3810
|
+
* @param {!Array.<string>} uris
|
|
3669
3811
|
* @param {boolean=} isPlaylist
|
|
3670
3812
|
* @return {!Promise.<!shaka.extern.Response>}
|
|
3671
3813
|
* @private
|
|
3672
3814
|
*/
|
|
3673
|
-
requestManifest_(
|
|
3815
|
+
requestManifest_(uris, isPlaylist) {
|
|
3674
3816
|
const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
|
|
3675
3817
|
|
|
3676
3818
|
const request = shaka.net.NetworkingEngine.makeRequest(
|
|
3677
|
-
|
|
3819
|
+
uris, this.config_.retryParameters);
|
|
3678
3820
|
|
|
3679
3821
|
const type = isPlaylist ?
|
|
3680
3822
|
shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_PLAYLIST :
|
|
@@ -3960,8 +4102,8 @@ shaka.hls.HlsParser = class {
|
|
|
3960
4102
|
* @typedef {{
|
|
3961
4103
|
* stream: !shaka.extern.Stream,
|
|
3962
4104
|
* type: string,
|
|
3963
|
-
*
|
|
3964
|
-
*
|
|
4105
|
+
* redirectUris: !Array.<string>,
|
|
4106
|
+
* getUris: function():!Array.<string>,
|
|
3965
4107
|
* minTimestamp: number,
|
|
3966
4108
|
* maxTimestamp: number,
|
|
3967
4109
|
* mediaSequenceToStartTime: !Map.<number, number>,
|
|
@@ -3981,14 +4123,10 @@ shaka.hls.HlsParser = class {
|
|
|
3981
4123
|
* The Stream itself.
|
|
3982
4124
|
* @property {string} type
|
|
3983
4125
|
* The type value. Could be 'video', 'audio', 'text', or 'image'.
|
|
3984
|
-
* @property {string}
|
|
3985
|
-
* The
|
|
3986
|
-
*
|
|
3987
|
-
*
|
|
3988
|
-
* from different origins each time.
|
|
3989
|
-
* @property {string} absoluteMediaPlaylistUri
|
|
3990
|
-
* The absolute media playlist URI, resolved relative to the master playlist
|
|
3991
|
-
* and updated to reflect any redirects.
|
|
4126
|
+
* @property {!Array.<string>} redirectUris
|
|
4127
|
+
* The redirect URIs.
|
|
4128
|
+
* @property {function():!Array.<string>} getUris
|
|
4129
|
+
* The verbatim media playlist URIs, as it appeared in the master playlist.
|
|
3992
4130
|
* @property {number} minTimestamp
|
|
3993
4131
|
* The minimum timestamp found in the stream.
|
|
3994
4132
|
* @property {number} maxTimestamp
|
|
@@ -4030,30 +4168,6 @@ shaka.hls.HlsParser.StreamInfo;
|
|
|
4030
4168
|
shaka.hls.HlsParser.StreamInfos;
|
|
4031
4169
|
|
|
4032
4170
|
|
|
4033
|
-
/**
|
|
4034
|
-
* @typedef {{
|
|
4035
|
-
* type: string,
|
|
4036
|
-
* mimeType: string,
|
|
4037
|
-
* codecs: string,
|
|
4038
|
-
* language: ?string,
|
|
4039
|
-
* height: ?string,
|
|
4040
|
-
* width: ?string,
|
|
4041
|
-
* channelCount: ?number,
|
|
4042
|
-
* sampleRate: ?number
|
|
4043
|
-
* }}
|
|
4044
|
-
*
|
|
4045
|
-
* @property {string} type
|
|
4046
|
-
* @property {string} mimeType
|
|
4047
|
-
* @property {string} codecs
|
|
4048
|
-
* @property {?string} language
|
|
4049
|
-
* @property {?string} height
|
|
4050
|
-
* @property {?string} width
|
|
4051
|
-
* @property {?number} channelCount
|
|
4052
|
-
* @property {?number} sampleRate
|
|
4053
|
-
*/
|
|
4054
|
-
shaka.hls.HlsParser.BasicInfo;
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
4171
|
/**
|
|
4058
4172
|
* @const {!Object.<string, string>}
|
|
4059
4173
|
* @private
|
|
@@ -4175,8 +4289,6 @@ shaka.hls.HlsParser.PresentationType_ = {
|
|
|
4175
4289
|
LIVE: 'LIVE',
|
|
4176
4290
|
};
|
|
4177
4291
|
|
|
4178
|
-
shaka.media.ManifestParser.registerParserByExtension(
|
|
4179
|
-
'm3u8', () => new shaka.hls.HlsParser());
|
|
4180
4292
|
shaka.media.ManifestParser.registerParserByMime(
|
|
4181
4293
|
'application/x-mpegurl', () => new shaka.hls.HlsParser());
|
|
4182
4294
|
shaka.media.ManifestParser.registerParserByMime(
|