shaka-player 4.5.0 → 4.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
- package/AUTHORS +2 -0
- package/CHANGELOG.md +109 -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 +34 -30
- 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 +789 -738
- package/dist/shaka-player.compiled.debug.map +4 -4
- package/dist/shaka-player.compiled.externs.js +162 -77
- package/dist/shaka-player.compiled.js +1147 -1113
- 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 +857 -791
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +167 -77
- package/dist/shaka-player.ui.js +1359 -1314
- 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 +251 -84
- 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 +92 -29
- package/lib/hls/hls_classes.js +28 -43
- package/lib/hls/hls_parser.js +770 -652
- 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 +72 -44
- 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 +998 -1503
- 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 +417 -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');
|
|
1406
1350
|
|
|
1407
|
-
|
|
1408
|
-
|
|
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';
|
|
1365
|
+
|
|
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*/);
|
|
@@ -1643,9 +1672,15 @@ shaka.hls.HlsParser = class {
|
|
|
1643
1672
|
// the value NONE. If the value is a quoted-string, it MUST match the
|
|
1644
1673
|
// value of the GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the
|
|
1645
1674
|
// Playlist whose TYPE attribute is CLOSED-CAPTIONS.
|
|
1646
|
-
if (type == ContentType.VIDEO
|
|
1647
|
-
|
|
1648
|
-
|
|
1675
|
+
if (type == ContentType.VIDEO) {
|
|
1676
|
+
if (closedCaptionsAttr && closedCaptionsAttr != 'NONE') {
|
|
1677
|
+
return this.groupIdToClosedCaptionsMap_.get(closedCaptionsAttr);
|
|
1678
|
+
} else if (!closedCaptionsAttr &&
|
|
1679
|
+
this.groupIdToClosedCaptionsMap_.size) {
|
|
1680
|
+
for (const key of this.groupIdToClosedCaptionsMap_.keys()) {
|
|
1681
|
+
return this.groupIdToClosedCaptionsMap_.get(key);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1649
1684
|
}
|
|
1650
1685
|
return null;
|
|
1651
1686
|
}
|
|
@@ -1686,6 +1721,7 @@ shaka.hls.HlsParser = class {
|
|
|
1686
1721
|
* @param {?string} height
|
|
1687
1722
|
* @param {?string} frameRate
|
|
1688
1723
|
* @param {?string} videoRange
|
|
1724
|
+
* @param {?string} videoLayout
|
|
1689
1725
|
* @param {!Array.<shaka.extern.DrmInfo>} drmInfos
|
|
1690
1726
|
* @param {!Set.<string>} keyIds
|
|
1691
1727
|
* @return {!Array.<!shaka.extern.Variant>}
|
|
@@ -1693,13 +1729,13 @@ shaka.hls.HlsParser = class {
|
|
|
1693
1729
|
*/
|
|
1694
1730
|
createVariants_(
|
|
1695
1731
|
audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
|
|
1696
|
-
drmInfos, keyIds) {
|
|
1732
|
+
videoLayout, drmInfos, keyIds) {
|
|
1697
1733
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
1698
1734
|
const DrmEngine = shaka.media.DrmEngine;
|
|
1699
1735
|
|
|
1700
1736
|
for (const info of videoInfos) {
|
|
1701
1737
|
this.addVideoAttributes_(
|
|
1702
|
-
info.stream, width, height, frameRate, videoRange);
|
|
1738
|
+
info.stream, width, height, frameRate, videoRange, videoLayout);
|
|
1703
1739
|
}
|
|
1704
1740
|
|
|
1705
1741
|
// In case of audio-only or video-only content or the audio/video is
|
|
@@ -1731,9 +1767,9 @@ shaka.hls.HlsParser = class {
|
|
|
1731
1767
|
const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
|
|
1732
1768
|
const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
|
|
1733
1769
|
const videoStreamUri =
|
|
1734
|
-
videoInfo ? videoInfo.
|
|
1770
|
+
videoInfo ? videoInfo.getUris().sort().join(',') : '';
|
|
1735
1771
|
const audioStreamUri =
|
|
1736
|
-
audioInfo ? audioInfo.
|
|
1772
|
+
audioInfo ? audioInfo.getUris().sort().join(',') : '';
|
|
1737
1773
|
const variantUriKey = videoStreamUri + ' - ' + audioStreamUri;
|
|
1738
1774
|
|
|
1739
1775
|
if (audioStream && videoStream) {
|
|
@@ -1821,34 +1857,59 @@ shaka.hls.HlsParser = class {
|
|
|
1821
1857
|
/**
|
|
1822
1858
|
* Parse EXT-X-MEDIA media tag into a Stream object.
|
|
1823
1859
|
*
|
|
1824
|
-
* @param {shaka.hls.Tag}
|
|
1860
|
+
* @param {!Array.<!shaka.hls.Tag>} tags
|
|
1861
|
+
* @param {!Map.<string, string>} groupIdPathwayIdMapping
|
|
1825
1862
|
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1826
1863
|
* @private
|
|
1827
1864
|
*/
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
const
|
|
1865
|
+
createStreamInfoFromMediaTags_(tags, groupIdPathwayIdMapping) {
|
|
1866
|
+
const verbatimMediaPlaylistUris = [];
|
|
1867
|
+
const globalGroupIds = [];
|
|
1868
|
+
const groupIdUriMappping = new Map();
|
|
1869
|
+
for (const tag of tags) {
|
|
1870
|
+
goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
|
|
1871
|
+
'Should only be called on media tags!');
|
|
1872
|
+
const uri = tag.getRequiredAttrValue('URI');
|
|
1873
|
+
const groupId = tag.getRequiredAttrValue('GROUP-ID');
|
|
1874
|
+
verbatimMediaPlaylistUris.push(uri);
|
|
1875
|
+
globalGroupIds.push(groupId);
|
|
1876
|
+
groupIdUriMappping.set(groupId, uri);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
const globalGroupId = globalGroupIds.sort().join(',');
|
|
1880
|
+
const firstTag = tags[0];
|
|
1832
1881
|
let codecs = '';
|
|
1833
1882
|
/** @type {string} */
|
|
1834
|
-
const type = this.getType_(
|
|
1883
|
+
const type = this.getType_(firstTag);
|
|
1835
1884
|
if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
|
|
1836
|
-
codecs =
|
|
1837
|
-
} else
|
|
1838
|
-
|
|
1885
|
+
codecs = firstTag.getAttributeValue('CODECS') || '';
|
|
1886
|
+
} else {
|
|
1887
|
+
for (const groupId of globalGroupIds) {
|
|
1888
|
+
if (this.groupIdToCodecsMap_.has(groupId)) {
|
|
1889
|
+
codecs = this.groupIdToCodecsMap_.get(groupId);
|
|
1890
|
+
break;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1839
1893
|
}
|
|
1840
1894
|
|
|
1841
|
-
const verbatimMediaPlaylistUri = this.variableSubstitution_(
|
|
1842
|
-
tag.getRequiredAttrValue('URI'), this.globalVariables_);
|
|
1843
|
-
|
|
1844
1895
|
// Check if the stream has already been created as part of another Variant
|
|
1845
1896
|
// and return it if it has.
|
|
1846
|
-
|
|
1847
|
-
|
|
1897
|
+
const key = verbatimMediaPlaylistUris.sort().join(',');
|
|
1898
|
+
if (this.uriToStreamInfosMap_.has(key)) {
|
|
1899
|
+
return this.uriToStreamInfosMap_.get(key);
|
|
1900
|
+
}
|
|
1901
|
+
const streamId = this.globalId_++;
|
|
1902
|
+
if (this.contentSteeringManager_) {
|
|
1903
|
+
for (const [groupId, uri] of groupIdUriMappping) {
|
|
1904
|
+
const pathwayId = groupIdPathwayIdMapping.get(groupId);
|
|
1905
|
+
if (pathwayId) {
|
|
1906
|
+
this.contentSteeringManager_.addLocation(streamId, pathwayId, uri);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1848
1909
|
}
|
|
1849
1910
|
|
|
1850
|
-
const language =
|
|
1851
|
-
const name =
|
|
1911
|
+
const language = firstTag.getAttributeValue('LANGUAGE');
|
|
1912
|
+
const name = firstTag.getAttributeValue('NAME');
|
|
1852
1913
|
|
|
1853
1914
|
// NOTE: According to the HLS spec, "DEFAULT=YES" requires "AUTOSELECT=YES".
|
|
1854
1915
|
// However, we don't bother to validate "AUTOSELECT", since we don't
|
|
@@ -1856,39 +1917,39 @@ shaka.hls.HlsParser = class {
|
|
|
1856
1917
|
// "AUTOSELECT=YES". A value of "AUTOSELECT=NO" would imply that it may
|
|
1857
1918
|
// only be selected explicitly by the user, and we don't have a way to
|
|
1858
1919
|
// represent that in our model.
|
|
1859
|
-
const defaultAttrValue =
|
|
1920
|
+
const defaultAttrValue = firstTag.getAttributeValue('DEFAULT');
|
|
1860
1921
|
const primary = defaultAttrValue == 'YES';
|
|
1861
1922
|
|
|
1862
|
-
const channelsCount =
|
|
1863
|
-
|
|
1864
|
-
const
|
|
1923
|
+
const channelsCount =
|
|
1924
|
+
type == 'audio' ? this.getChannelsCount_(firstTag) : null;
|
|
1925
|
+
const spatialAudio =
|
|
1926
|
+
type == 'audio' ? this.isSpatialAudio_(firstTag) : false;
|
|
1927
|
+
const characteristics = firstTag.getAttributeValue('CHARACTERISTICS');
|
|
1865
1928
|
|
|
1866
|
-
const forcedAttrValue =
|
|
1929
|
+
const forcedAttrValue = firstTag.getAttributeValue('FORCED');
|
|
1867
1930
|
const forced = forcedAttrValue == 'YES';
|
|
1868
|
-
const sampleRate = type == 'audio' ? this.getSampleRate_(
|
|
1931
|
+
const sampleRate = type == 'audio' ? this.getSampleRate_(firstTag) : null;
|
|
1869
1932
|
// TODO: Should we take into account some of the currently ignored
|
|
1870
1933
|
// attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj
|
|
1871
1934
|
const streamInfo = this.createStreamInfo_(
|
|
1872
|
-
|
|
1873
|
-
channelsCount, /* closedCaptions= */ null,
|
|
1874
|
-
sampleRate, spatialAudio);
|
|
1875
|
-
if (
|
|
1876
|
-
|
|
1935
|
+
streamId, verbatimMediaPlaylistUris, codecs, type, language,
|
|
1936
|
+
primary, name, channelsCount, /* closedCaptions= */ null,
|
|
1937
|
+
characteristics, forced, sampleRate, spatialAudio);
|
|
1938
|
+
if (streamInfo.stream) {
|
|
1939
|
+
streamInfo.stream.groupId = globalGroupId;
|
|
1940
|
+
}
|
|
1941
|
+
if (this.groupIdToStreamInfosMap_.has(globalGroupId)) {
|
|
1942
|
+
this.groupIdToStreamInfosMap_.get(globalGroupId).push(streamInfo);
|
|
1877
1943
|
} else {
|
|
1878
|
-
this.groupIdToStreamInfosMap_.set(
|
|
1944
|
+
this.groupIdToStreamInfosMap_.set(globalGroupId, [streamInfo]);
|
|
1879
1945
|
}
|
|
1880
1946
|
|
|
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);
|
|
1947
|
+
this.uriToStreamInfosMap_.set(key, streamInfo);
|
|
1887
1948
|
return streamInfo;
|
|
1888
1949
|
}
|
|
1889
1950
|
|
|
1890
1951
|
/**
|
|
1891
|
-
* Parse EXT-X-
|
|
1952
|
+
* Parse EXT-X-IMAGE-STREAM-INF media tag into a Stream object.
|
|
1892
1953
|
*
|
|
1893
1954
|
* @param {shaka.hls.Tag} tag
|
|
1894
1955
|
* @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
|
|
@@ -1900,8 +1961,7 @@ shaka.hls.HlsParser = class {
|
|
|
1900
1961
|
/** @type {string} */
|
|
1901
1962
|
const type = shaka.util.ManifestParserUtils.ContentType.IMAGE;
|
|
1902
1963
|
|
|
1903
|
-
const verbatimImagePlaylistUri =
|
|
1904
|
-
tag.getRequiredAttrValue('URI'), this.globalVariables_);
|
|
1964
|
+
const verbatimImagePlaylistUri = tag.getRequiredAttrValue('URI');
|
|
1905
1965
|
const codecs = tag.getAttributeValue('CODECS', 'jpeg') || '';
|
|
1906
1966
|
|
|
1907
1967
|
// Check if the stream has already been created as part of another Variant
|
|
@@ -1916,16 +1976,10 @@ shaka.hls.HlsParser = class {
|
|
|
1916
1976
|
const characteristics = tag.getAttributeValue('CHARACTERISTICS');
|
|
1917
1977
|
|
|
1918
1978
|
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
|
-
}
|
|
1979
|
+
this.globalId_++, [verbatimImagePlaylistUri], codecs, type, language,
|
|
1980
|
+
/* primary= */ false, name, /* channelsCount= */ null,
|
|
1981
|
+
/* closedCaptions= */ null, characteristics, /* forced= */ false,
|
|
1982
|
+
/* sampleRate= */ null, /* spatialAudio= */ false);
|
|
1929
1983
|
|
|
1930
1984
|
// Parse misc attributes.
|
|
1931
1985
|
const resolution = tag.getAttributeValue('RESOLUTION');
|
|
@@ -1958,45 +2012,104 @@ shaka.hls.HlsParser = class {
|
|
|
1958
2012
|
return streamInfo;
|
|
1959
2013
|
}
|
|
1960
2014
|
|
|
2015
|
+
/**
|
|
2016
|
+
* Parse EXT-X-I-FRAME-STREAM-INF media tag into a Stream object.
|
|
2017
|
+
*
|
|
2018
|
+
* @param {shaka.hls.Tag} tag
|
|
2019
|
+
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
2020
|
+
* @private
|
|
2021
|
+
*/
|
|
2022
|
+
createStreamInfoFromIframeTag_(tag) {
|
|
2023
|
+
goog.asserts.assert(tag.name == 'EXT-X-I-FRAME-STREAM-INF',
|
|
2024
|
+
'Should only be called on iframe tags!');
|
|
2025
|
+
/** @type {string} */
|
|
2026
|
+
const type = shaka.util.ManifestParserUtils.ContentType.IMAGE;
|
|
2027
|
+
|
|
2028
|
+
const verbatimIFramePlaylistUri = tag.getRequiredAttrValue('URI');
|
|
2029
|
+
const codecs = tag.getAttributeValue('CODECS') || '';
|
|
2030
|
+
|
|
2031
|
+
// Check if the stream has already been created as part of another Variant
|
|
2032
|
+
// and return it if it has.
|
|
2033
|
+
if (this.uriToStreamInfosMap_.has(verbatimIFramePlaylistUri)) {
|
|
2034
|
+
return this.uriToStreamInfosMap_.get(verbatimIFramePlaylistUri);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
const language = tag.getAttributeValue('LANGUAGE');
|
|
2038
|
+
const name = tag.getAttributeValue('NAME');
|
|
2039
|
+
|
|
2040
|
+
const characteristics = tag.getAttributeValue('CHARACTERISTICS');
|
|
2041
|
+
|
|
2042
|
+
const streamInfo = this.createStreamInfo_(
|
|
2043
|
+
this.globalId_++, [verbatimIFramePlaylistUri], codecs, type, language,
|
|
2044
|
+
/* primary= */ false, name, /* channelsCount= */ null,
|
|
2045
|
+
/* closedCaptions= */ null, characteristics, /* forced= */ false,
|
|
2046
|
+
/* sampleRate= */ null, /* spatialAudio= */ false);
|
|
2047
|
+
|
|
2048
|
+
// Parse misc attributes.
|
|
2049
|
+
const resolution = tag.getAttributeValue('RESOLUTION');
|
|
2050
|
+
const [width, height] = resolution ? resolution.split('x') : [null, null];
|
|
2051
|
+
streamInfo.stream.width = Number(width) || undefined;
|
|
2052
|
+
streamInfo.stream.height = Number(height) || undefined;
|
|
2053
|
+
const bandwidth = tag.getAttributeValue('BANDWIDTH');
|
|
2054
|
+
if (bandwidth) {
|
|
2055
|
+
streamInfo.stream.bandwidth = Number(bandwidth);
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
this.uriToStreamInfosMap_.set(verbatimIFramePlaylistUri, streamInfo);
|
|
2059
|
+
return streamInfo;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
1961
2062
|
/**
|
|
1962
2063
|
* Parse an EXT-X-STREAM-INF media tag into a Stream object.
|
|
1963
2064
|
*
|
|
1964
|
-
* @param {!shaka.hls.Tag}
|
|
2065
|
+
* @param {!Array.<!shaka.hls.Tag>} tags
|
|
1965
2066
|
* @param {!Array.<string>} allCodecs
|
|
1966
2067
|
* @param {string} type
|
|
2068
|
+
* @param {?string} language
|
|
2069
|
+
* @param {?string} name
|
|
2070
|
+
* @param {?number} channelsCount
|
|
2071
|
+
* @param {?string} characteristics
|
|
2072
|
+
* @param {?number} sampleRate
|
|
2073
|
+
* @param {boolean} spatialAudio
|
|
1967
2074
|
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1968
2075
|
* @private
|
|
1969
2076
|
*/
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
const
|
|
1974
|
-
|
|
2077
|
+
createStreamInfoFromVariantTags_(tags, allCodecs, type, language, name,
|
|
2078
|
+
channelsCount, characteristics, sampleRate, spatialAudio) {
|
|
2079
|
+
const streamId = this.globalId_++;
|
|
2080
|
+
const verbatimMediaPlaylistUris = [];
|
|
2081
|
+
for (const tag of tags) {
|
|
2082
|
+
goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
|
|
2083
|
+
'Should only be called on variant tags!');
|
|
2084
|
+
const uri = tag.getRequiredAttrValue('URI');
|
|
2085
|
+
const pathwayId = tag.getAttributeValue('PATHWAY-ID');
|
|
2086
|
+
if (this.contentSteeringManager_ && pathwayId) {
|
|
2087
|
+
this.contentSteeringManager_.addLocation(streamId, pathwayId, uri);
|
|
2088
|
+
}
|
|
2089
|
+
verbatimMediaPlaylistUris.push(uri);
|
|
2090
|
+
}
|
|
1975
2091
|
|
|
1976
|
-
|
|
1977
|
-
|
|
2092
|
+
const key = verbatimMediaPlaylistUris.sort().join(',');
|
|
2093
|
+
if (this.uriToStreamInfosMap_.has(key)) {
|
|
2094
|
+
return this.uriToStreamInfosMap_.get(key);
|
|
1978
2095
|
}
|
|
1979
2096
|
|
|
1980
|
-
const closedCaptions = this.getClosedCaptions_(
|
|
2097
|
+
const closedCaptions = this.getClosedCaptions_(tags[0], type);
|
|
1981
2098
|
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
|
-
}
|
|
2099
|
+
const streamInfo = this.createStreamInfo_(
|
|
2100
|
+
streamId, verbatimMediaPlaylistUris, codecs, type, language,
|
|
2101
|
+
/* primary= */ false, name, channelsCount, closedCaptions,
|
|
2102
|
+
characteristics, /* forced= */ false, sampleRate,
|
|
2103
|
+
/* spatialAudio= */ false);
|
|
1992
2104
|
|
|
1993
|
-
this.uriToStreamInfosMap_.set(
|
|
2105
|
+
this.uriToStreamInfosMap_.set(key, streamInfo);
|
|
1994
2106
|
return streamInfo;
|
|
1995
2107
|
}
|
|
1996
2108
|
|
|
1997
2109
|
|
|
1998
2110
|
/**
|
|
1999
|
-
* @param {
|
|
2111
|
+
* @param {number} streamId
|
|
2112
|
+
* @param {!Array.<string>} verbatimMediaPlaylistUris
|
|
2000
2113
|
* @param {string} codecs
|
|
2001
2114
|
* @param {string} type
|
|
2002
2115
|
* @param {?string} languageValue
|
|
@@ -2011,25 +2124,34 @@ shaka.hls.HlsParser = class {
|
|
|
2011
2124
|
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
2012
2125
|
* @private
|
|
2013
2126
|
*/
|
|
2014
|
-
createStreamInfo_(
|
|
2015
|
-
primary, name, channelsCount, closedCaptions,
|
|
2016
|
-
sampleRate, spatialAudio) {
|
|
2127
|
+
createStreamInfo_(streamId, verbatimMediaPlaylistUris, codecs, type,
|
|
2128
|
+
languageValue, primary, name, channelsCount, closedCaptions,
|
|
2129
|
+
characteristics, forced, sampleRate, spatialAudio) {
|
|
2017
2130
|
// TODO: Refactor, too many parameters
|
|
2018
|
-
const initialMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
|
|
2019
|
-
this.masterPlaylistUri_, verbatimMediaPlaylistUri);
|
|
2020
2131
|
|
|
2021
2132
|
// This stream is lazy-loaded inside the createSegmentIndex function.
|
|
2022
2133
|
// So we start out with a stream object that does not contain the actual
|
|
2023
2134
|
// segment index, then download when createSegmentIndex is called.
|
|
2024
|
-
const stream = this.makeStreamObject_(codecs, type,
|
|
2025
|
-
|
|
2026
|
-
sampleRate, spatialAudio);
|
|
2135
|
+
const stream = this.makeStreamObject_(streamId, codecs, type,
|
|
2136
|
+
languageValue, primary, name, channelsCount, closedCaptions,
|
|
2137
|
+
characteristics, forced, sampleRate, spatialAudio);
|
|
2138
|
+
|
|
2139
|
+
const redirectUris = [];
|
|
2140
|
+
const getUris = () => {
|
|
2141
|
+
if (this.contentSteeringManager_ &&
|
|
2142
|
+
verbatimMediaPlaylistUris.length > 1) {
|
|
2143
|
+
return this.contentSteeringManager_.getLocations(streamId);
|
|
2144
|
+
}
|
|
2145
|
+
return redirectUris.concat(shaka.hls.Utils.constructUris(
|
|
2146
|
+
[this.masterPlaylistUri_], verbatimMediaPlaylistUris,
|
|
2147
|
+
this.globalVariables_));
|
|
2148
|
+
};
|
|
2027
2149
|
const streamInfo = {
|
|
2028
2150
|
stream,
|
|
2029
2151
|
type,
|
|
2030
|
-
|
|
2152
|
+
redirectUris,
|
|
2153
|
+
getUris,
|
|
2031
2154
|
// These values are filled out or updated after lazy-loading:
|
|
2032
|
-
absoluteMediaPlaylistUri: initialMediaPlaylistUri,
|
|
2033
2155
|
minTimestamp: 0,
|
|
2034
2156
|
maxTimestamp: 0,
|
|
2035
2157
|
mediaSequenceToStartTime: new Map(),
|
|
@@ -2044,27 +2166,45 @@ shaka.hls.HlsParser = class {
|
|
|
2044
2166
|
|
|
2045
2167
|
/** @param {!AbortSignal} abortSignal */
|
|
2046
2168
|
const downloadSegmentIndex = async (abortSignal) => {
|
|
2169
|
+
const uris = streamInfo.getUris();
|
|
2047
2170
|
// Download the actual manifest.
|
|
2048
2171
|
const response = await this.requestManifest_(
|
|
2049
|
-
streamInfo.
|
|
2172
|
+
streamInfo.getUris(), /* isPlaylist= */ true);
|
|
2050
2173
|
if (abortSignal.aborted) {
|
|
2051
2174
|
return;
|
|
2052
2175
|
}
|
|
2053
2176
|
|
|
2054
2177
|
// Record the final URI after redirects.
|
|
2055
|
-
const
|
|
2178
|
+
const responseUri = response.uri;
|
|
2179
|
+
if (responseUri != response.originalUri && !uris.includes(responseUri)) {
|
|
2180
|
+
redirectUris.push(responseUri);
|
|
2181
|
+
}
|
|
2056
2182
|
|
|
2057
2183
|
// Record the redirected, final URI of this media playlist when we parse
|
|
2058
2184
|
// it.
|
|
2059
2185
|
/** @type {!shaka.hls.Playlist} */
|
|
2060
|
-
const playlist = this.manifestTextParser_.parsePlaylist(
|
|
2061
|
-
|
|
2186
|
+
const playlist = this.manifestTextParser_.parsePlaylist(response.data);
|
|
2187
|
+
|
|
2188
|
+
let mimeType = undefined;
|
|
2189
|
+
|
|
2190
|
+
// If no codec info was provided in the manifest and codec guessing is
|
|
2191
|
+
// disabled we try to get necessary info from the media data.
|
|
2192
|
+
if (!this.codecInfoInManifest_ && this.config_.hls.disableCodecGuessing) {
|
|
2193
|
+
const basicInfo =
|
|
2194
|
+
await this.getMediaPlaylistBasicInfo_(playlist, getUris);
|
|
2195
|
+
|
|
2196
|
+
goog.asserts.assert(
|
|
2197
|
+
type === basicInfo.type, 'Media types should match!');
|
|
2198
|
+
|
|
2199
|
+
mimeType = basicInfo.mimeType;
|
|
2200
|
+
codecs = basicInfo.codecs;
|
|
2201
|
+
}
|
|
2062
2202
|
|
|
2063
2203
|
const wasLive = this.isLive_();
|
|
2064
2204
|
const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
|
|
2065
|
-
playlist,
|
|
2205
|
+
streamId, playlist, getUris, responseUri, codecs,
|
|
2066
2206
|
type, languageValue, primary, name, channelsCount, closedCaptions,
|
|
2067
|
-
characteristics, forced, sampleRate, spatialAudio);
|
|
2207
|
+
characteristics, forced, sampleRate, spatialAudio, mimeType);
|
|
2068
2208
|
if (abortSignal.aborted) {
|
|
2069
2209
|
return;
|
|
2070
2210
|
}
|
|
@@ -2078,7 +2218,6 @@ shaka.hls.HlsParser = class {
|
|
|
2078
2218
|
}
|
|
2079
2219
|
|
|
2080
2220
|
// Copy values from the real stream info to our initial one.
|
|
2081
|
-
streamInfo.absoluteMediaPlaylistUri = absoluteMediaPlaylistUri;
|
|
2082
2221
|
streamInfo.minTimestamp = realStreamInfo.minTimestamp;
|
|
2083
2222
|
streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
|
|
2084
2223
|
streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
|
|
@@ -2095,6 +2234,7 @@ shaka.hls.HlsParser = class {
|
|
|
2095
2234
|
stream.keyIds = realStream.keyIds;
|
|
2096
2235
|
stream.mimeType = realStream.mimeType;
|
|
2097
2236
|
stream.bandwidth = realStream.bandwidth;
|
|
2237
|
+
stream.codecs = realStream.codecs || stream.codecs;
|
|
2098
2238
|
|
|
2099
2239
|
// Since we lazy-loaded this content, the player may need to create new
|
|
2100
2240
|
// sessions for the DRM info in this stream.
|
|
@@ -2296,9 +2436,10 @@ shaka.hls.HlsParser = class {
|
|
|
2296
2436
|
}
|
|
2297
2437
|
|
|
2298
2438
|
/**
|
|
2439
|
+
* @param {number} streamId
|
|
2299
2440
|
* @param {!shaka.hls.Playlist} playlist
|
|
2300
|
-
* @param {string}
|
|
2301
|
-
* @param {string}
|
|
2441
|
+
* @param {function():!Array.<string>} getUris
|
|
2442
|
+
* @param {string} responseUri
|
|
2302
2443
|
* @param {string} codecs
|
|
2303
2444
|
* @param {string} type
|
|
2304
2445
|
* @param {?string} languageValue
|
|
@@ -2314,8 +2455,8 @@ shaka.hls.HlsParser = class {
|
|
|
2314
2455
|
* @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
|
|
2315
2456
|
* @private
|
|
2316
2457
|
*/
|
|
2317
|
-
async convertParsedPlaylistIntoStreamInfo_(
|
|
2318
|
-
|
|
2458
|
+
async convertParsedPlaylistIntoStreamInfo_(streamId, playlist,
|
|
2459
|
+
getUris, responseUri, codecs, type, languageValue, primary, name,
|
|
2319
2460
|
channelsCount, closedCaptions, characteristics, forced, sampleRate,
|
|
2320
2461
|
spatialAudio, mimeType = undefined) {
|
|
2321
2462
|
if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
|
|
@@ -2331,7 +2472,8 @@ shaka.hls.HlsParser = class {
|
|
|
2331
2472
|
const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
|
|
2332
2473
|
'EXT-X-DEFINE');
|
|
2333
2474
|
|
|
2334
|
-
const mediaVariables =
|
|
2475
|
+
const mediaVariables =
|
|
2476
|
+
this.parseMediaVariables_(variablesTags, responseUri);
|
|
2335
2477
|
|
|
2336
2478
|
goog.asserts.assert(playlist.segments != null,
|
|
2337
2479
|
'Media playlist should have segments!');
|
|
@@ -2344,7 +2486,7 @@ shaka.hls.HlsParser = class {
|
|
|
2344
2486
|
|
|
2345
2487
|
if (!mimeType) {
|
|
2346
2488
|
mimeType = await this.guessMimeType_(type, codecs, playlist,
|
|
2347
|
-
mediaVariables);
|
|
2489
|
+
mediaVariables, getUris);
|
|
2348
2490
|
}
|
|
2349
2491
|
|
|
2350
2492
|
const {drmInfos, keyIds, encrypted, aesEncrypted} =
|
|
@@ -2357,11 +2499,20 @@ shaka.hls.HlsParser = class {
|
|
|
2357
2499
|
shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
|
|
2358
2500
|
}
|
|
2359
2501
|
|
|
2502
|
+
const stream = this.makeStreamObject_(streamId, codecs, type,
|
|
2503
|
+
languageValue, primary, name, channelsCount, closedCaptions,
|
|
2504
|
+
characteristics, forced, sampleRate, spatialAudio);
|
|
2505
|
+
stream.encrypted = encrypted;
|
|
2506
|
+
stream.drmInfos = drmInfos;
|
|
2507
|
+
stream.keyIds = keyIds;
|
|
2508
|
+
stream.mimeType = mimeType;
|
|
2509
|
+
|
|
2360
2510
|
const mediaSequenceToStartTime = this.isLive_() ?
|
|
2361
2511
|
this.mediaSequenceToStartTimeByType_.get(type) : new Map();
|
|
2512
|
+
|
|
2362
2513
|
const {segments, bandwidth} = this.createSegments_(
|
|
2363
|
-
|
|
2364
|
-
|
|
2514
|
+
playlist, stream, mediaSequenceToStartTime, mediaVariables, getUris);
|
|
2515
|
+
stream.bandwidth = bandwidth;
|
|
2365
2516
|
|
|
2366
2517
|
// This new calculation is necessary for Low Latency streams.
|
|
2367
2518
|
if (this.isLive_()) {
|
|
@@ -2373,6 +2524,7 @@ shaka.hls.HlsParser = class {
|
|
|
2373
2524
|
const lastEndTime = lastSegment.endTime;
|
|
2374
2525
|
/** @type {!shaka.media.SegmentIndex} */
|
|
2375
2526
|
const segmentIndex = new shaka.media.SegmentIndex(segments);
|
|
2527
|
+
stream.segmentIndex = segmentIndex;
|
|
2376
2528
|
|
|
2377
2529
|
const serverControlTag = shaka.hls.Utils.getFirstTagWithName(
|
|
2378
2530
|
playlist.tags, 'EXT-X-SERVER-CONTROL');
|
|
@@ -2387,21 +2539,11 @@ shaka.hls.HlsParser = class {
|
|
|
2387
2539
|
const {nextMediaSequence, nextPart} =
|
|
2388
2540
|
this.getNextMediaSequenceAndPart_(mediaSequenceNumber, segments);
|
|
2389
2541
|
|
|
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
2542
|
return {
|
|
2401
2543
|
stream,
|
|
2402
2544
|
type,
|
|
2403
|
-
|
|
2404
|
-
|
|
2545
|
+
redirectUris: [],
|
|
2546
|
+
getUris,
|
|
2405
2547
|
minTimestamp: firstStartTime,
|
|
2406
2548
|
maxTimestamp: lastEndTime,
|
|
2407
2549
|
canSkipSegments,
|
|
@@ -2465,6 +2607,7 @@ shaka.hls.HlsParser = class {
|
|
|
2465
2607
|
* The parameters that are passed into here are only the things that can be
|
|
2466
2608
|
* known without downloading the media playlist; other values must be set
|
|
2467
2609
|
* manually on the object after creation.
|
|
2610
|
+
* @param {number} id
|
|
2468
2611
|
* @param {string} codecs
|
|
2469
2612
|
* @param {string} type
|
|
2470
2613
|
* @param {?string} languageValue
|
|
@@ -2479,8 +2622,9 @@ shaka.hls.HlsParser = class {
|
|
|
2479
2622
|
* @return {!shaka.extern.Stream}
|
|
2480
2623
|
* @private
|
|
2481
2624
|
*/
|
|
2482
|
-
makeStreamObject_(codecs, type, languageValue, primary, name,
|
|
2483
|
-
closedCaptions, characteristics, forced, sampleRate,
|
|
2625
|
+
makeStreamObject_(id, codecs, type, languageValue, primary, name,
|
|
2626
|
+
channelsCount, closedCaptions, characteristics, forced, sampleRate,
|
|
2627
|
+
spatialAudio) {
|
|
2484
2628
|
// Fill out a "best-guess" mimeType, for now. It will be replaced once the
|
|
2485
2629
|
// stream is lazy-loaded.
|
|
2486
2630
|
const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
|
|
@@ -2492,8 +2636,21 @@ shaka.hls.HlsParser = class {
|
|
|
2492
2636
|
}
|
|
2493
2637
|
}
|
|
2494
2638
|
|
|
2495
|
-
|
|
2496
|
-
|
|
2639
|
+
let kind = undefined;
|
|
2640
|
+
let accessibilityPurpose = null;
|
|
2641
|
+
if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
|
|
2642
|
+
if (roles.includes('public.accessibility.transcribes-spoken-dialog') &&
|
|
2643
|
+
roles.includes('public.accessibility.describes-music-and-sound')) {
|
|
2644
|
+
kind = shaka.util.ManifestParserUtils.TextStreamKind.CLOSED_CAPTION;
|
|
2645
|
+
} else {
|
|
2646
|
+
kind = shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE;
|
|
2647
|
+
}
|
|
2648
|
+
} else {
|
|
2649
|
+
if (roles.includes('public.accessibility.describes-video')) {
|
|
2650
|
+
accessibilityPurpose =
|
|
2651
|
+
shaka.media.ManifestParser.AccessibilityPurpose.VISUALLY_IMPAIRED;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2497
2654
|
|
|
2498
2655
|
// If there are no roles, and we have defaulted to the subtitle "kind" for
|
|
2499
2656
|
// this track, add the implied subtitle role.
|
|
@@ -2535,9 +2692,11 @@ shaka.hls.HlsParser = class {
|
|
|
2535
2692
|
spatialAudio,
|
|
2536
2693
|
closedCaptions,
|
|
2537
2694
|
hdr: undefined,
|
|
2695
|
+
videoLayout: undefined,
|
|
2538
2696
|
tilesLayout: undefined,
|
|
2539
|
-
accessibilityPurpose:
|
|
2697
|
+
accessibilityPurpose: accessibilityPurpose,
|
|
2540
2698
|
external: false,
|
|
2699
|
+
fastSwitching: false,
|
|
2541
2700
|
};
|
|
2542
2701
|
}
|
|
2543
2702
|
|
|
@@ -2608,10 +2767,12 @@ shaka.hls.HlsParser = class {
|
|
|
2608
2767
|
/**
|
|
2609
2768
|
* @param {!shaka.hls.Tag} drmTag
|
|
2610
2769
|
* @param {!shaka.hls.Playlist} playlist
|
|
2770
|
+
* @param {function():!Array.<string>} getUris
|
|
2771
|
+
* @param {?Map.<string, string>=} variables
|
|
2611
2772
|
* @return {!shaka.extern.aes128Key}
|
|
2612
2773
|
* @private
|
|
2613
2774
|
*/
|
|
2614
|
-
parseAES128DrmTag_(drmTag, playlist) {
|
|
2775
|
+
parseAES128DrmTag_(drmTag, playlist, getUris, variables) {
|
|
2615
2776
|
// Check if the Web Crypto API is available.
|
|
2616
2777
|
if (!window.crypto || !window.crypto.subtle) {
|
|
2617
2778
|
shaka.log.alwaysWarn('Web Crypto API is not available to decrypt ' +
|
|
@@ -2646,18 +2807,17 @@ shaka.hls.HlsParser = class {
|
|
|
2646
2807
|
}
|
|
2647
2808
|
}
|
|
2648
2809
|
|
|
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
2810
|
const keyInfo = {method: 'AES-128', iv, firstMediaSequenceNumber};
|
|
2657
2811
|
|
|
2658
2812
|
// Don't download the key object until the segment is parsed, to avoid a
|
|
2659
2813
|
// startup delay for long manifests with lots of keys.
|
|
2660
2814
|
keyInfo.fetchKey = async () => {
|
|
2815
|
+
const keyUri = shaka.hls.Utils.constructSegmentUris(
|
|
2816
|
+
getUris(), drmTag.getRequiredAttrValue('URI'), variables);
|
|
2817
|
+
|
|
2818
|
+
const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
|
|
2819
|
+
const request = shaka.net.NetworkingEngine.makeRequest(
|
|
2820
|
+
[keyUri], this.config_.retryParameters);
|
|
2661
2821
|
const keyResponse = await this.makeNetworkRequest_(request, requestType);
|
|
2662
2822
|
|
|
2663
2823
|
// keyResponse.status is undefined when URI is "data:text/plain;base64,"
|
|
@@ -2834,11 +2994,12 @@ shaka.hls.HlsParser = class {
|
|
|
2834
2994
|
* Get the InitSegmentReference for a segment if it has a EXT-X-MAP tag.
|
|
2835
2995
|
* @param {!shaka.hls.Playlist} playlist
|
|
2836
2996
|
* @param {!Array.<!shaka.hls.Tag>} tags Segment tags
|
|
2837
|
-
* @param {
|
|
2997
|
+
* @param {function():!Array.<string>} getUris
|
|
2998
|
+
* @param {?Map.<string, string>=} variables
|
|
2838
2999
|
* @return {shaka.media.InitSegmentReference}
|
|
2839
3000
|
* @private
|
|
2840
3001
|
*/
|
|
2841
|
-
getInitSegmentReference_(playlist, tags, variables) {
|
|
3002
|
+
getInitSegmentReference_(playlist, tags, getUris, variables) {
|
|
2842
3003
|
/** @type {?shaka.hls.Tag} */
|
|
2843
3004
|
const mapTag = shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-MAP');
|
|
2844
3005
|
|
|
@@ -2847,28 +3008,30 @@ shaka.hls.HlsParser = class {
|
|
|
2847
3008
|
}
|
|
2848
3009
|
// Map tag example: #EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"
|
|
2849
3010
|
const verbatimInitSegmentUri = mapTag.getRequiredAttrValue('URI');
|
|
2850
|
-
const
|
|
2851
|
-
|
|
2852
|
-
playlist.absoluteUri, verbatimInitSegmentUri),
|
|
2853
|
-
variables);
|
|
3011
|
+
const absoluteInitSegmentUris = shaka.hls.Utils.constructSegmentUris(
|
|
3012
|
+
getUris(), verbatimInitSegmentUri, variables);
|
|
2854
3013
|
|
|
2855
3014
|
const mapTagKey = [
|
|
2856
|
-
|
|
3015
|
+
absoluteInitSegmentUris.toString(),
|
|
2857
3016
|
mapTag.getAttributeValue('BYTERANGE', ''),
|
|
2858
3017
|
].join('-');
|
|
2859
3018
|
if (!this.mapTagToInitSegmentRefMap_.has(mapTagKey)) {
|
|
2860
3019
|
/** @type {shaka.extern.aes128Key|undefined} */
|
|
2861
3020
|
let aes128Key = undefined;
|
|
3021
|
+
let byteRangeTag = null;
|
|
2862
3022
|
for (const tag of tags) {
|
|
2863
3023
|
if (tag.name == 'EXT-X-KEY') {
|
|
2864
3024
|
if (tag.getRequiredAttrValue('METHOD') == 'AES-128' &&
|
|
2865
3025
|
tag.id < mapTag.id) {
|
|
2866
|
-
aes128Key =
|
|
3026
|
+
aes128Key =
|
|
3027
|
+
this.parseAES128DrmTag_(tag, playlist, getUris, variables);
|
|
2867
3028
|
}
|
|
3029
|
+
} else if (tag.name == 'EXT-X-BYTERANGE' && tag.id < mapTag.id) {
|
|
3030
|
+
byteRangeTag = tag;
|
|
2868
3031
|
}
|
|
2869
3032
|
}
|
|
2870
3033
|
const initSegmentRef = this.createInitSegmentReference_(
|
|
2871
|
-
|
|
3034
|
+
absoluteInitSegmentUris, mapTag, byteRangeTag, aes128Key);
|
|
2872
3035
|
this.mapTagToInitSegmentRefMap_.set(mapTagKey, initSegmentRef);
|
|
2873
3036
|
}
|
|
2874
3037
|
return this.mapTagToInitSegmentRefMap_.get(mapTagKey);
|
|
@@ -2877,16 +3040,21 @@ shaka.hls.HlsParser = class {
|
|
|
2877
3040
|
/**
|
|
2878
3041
|
* Create an InitSegmentReference object for the EXT-X-MAP tag in the media
|
|
2879
3042
|
* playlist.
|
|
2880
|
-
* @param {string}
|
|
3043
|
+
* @param {!Array.<string>} absoluteInitSegmentUris
|
|
2881
3044
|
* @param {!shaka.hls.Tag} mapTag EXT-X-MAP
|
|
3045
|
+
* @param {shaka.hls.Tag=} byteRangeTag EXT-X-BYTERANGE
|
|
2882
3046
|
* @param {shaka.extern.aes128Key=} aes128Key
|
|
2883
3047
|
* @return {!shaka.media.InitSegmentReference}
|
|
2884
3048
|
* @private
|
|
2885
3049
|
*/
|
|
2886
|
-
createInitSegmentReference_(
|
|
3050
|
+
createInitSegmentReference_(absoluteInitSegmentUris, mapTag, byteRangeTag,
|
|
3051
|
+
aes128Key) {
|
|
2887
3052
|
let startByte = 0;
|
|
2888
3053
|
let endByte = null;
|
|
2889
|
-
|
|
3054
|
+
let byterange = mapTag.getAttributeValue('BYTERANGE');
|
|
3055
|
+
if (!byterange && byteRangeTag) {
|
|
3056
|
+
byterange = byteRangeTag.value;
|
|
3057
|
+
}
|
|
2890
3058
|
// If a BYTERANGE attribute is not specified, the segment consists
|
|
2891
3059
|
// of the entire resource.
|
|
2892
3060
|
if (byterange) {
|
|
@@ -2907,7 +3075,7 @@ shaka.hls.HlsParser = class {
|
|
|
2907
3075
|
}
|
|
2908
3076
|
|
|
2909
3077
|
const initSegmentRef = new shaka.media.InitSegmentReference(
|
|
2910
|
-
() =>
|
|
3078
|
+
() => absoluteInitSegmentUris,
|
|
2911
3079
|
startByte,
|
|
2912
3080
|
endByte,
|
|
2913
3081
|
/* mediaQuality= */ null,
|
|
@@ -2925,16 +3093,16 @@ shaka.hls.HlsParser = class {
|
|
|
2925
3093
|
* @param {!shaka.hls.Segment} hlsSegment
|
|
2926
3094
|
* @param {number} startTime
|
|
2927
3095
|
* @param {!Map.<string, string>} variables
|
|
2928
|
-
* @param {
|
|
2929
|
-
* @param {
|
|
2930
|
-
* @param {string}
|
|
3096
|
+
* @param {!shaka.hls.Playlist} playlist
|
|
3097
|
+
* @param {shaka.extern.Stream} stream
|
|
3098
|
+
* @param {function():!Array.<string>} getUris
|
|
2931
3099
|
* @param {shaka.extern.aes128Key=} aes128Key
|
|
2932
3100
|
* @return {shaka.media.SegmentReference}
|
|
2933
3101
|
* @private
|
|
2934
3102
|
*/
|
|
2935
3103
|
createSegmentReference_(
|
|
2936
3104
|
initSegmentReference, previousReference, hlsSegment, startTime,
|
|
2937
|
-
variables,
|
|
3105
|
+
variables, playlist, stream, getUris, aes128Key) {
|
|
2938
3106
|
const tags = hlsSegment.tags;
|
|
2939
3107
|
const extinfTag =
|
|
2940
3108
|
shaka.hls.Utils.getFirstTagWithName(tags, 'EXTINF');
|
|
@@ -2991,8 +3159,9 @@ shaka.hls.HlsParser = class {
|
|
|
2991
3159
|
let isPreloadSegment = false;
|
|
2992
3160
|
|
|
2993
3161
|
if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) {
|
|
2994
|
-
const byterangeOptimizationSupport = (mimeType == 'video/mp4' ||
|
|
2995
|
-
mimeType == 'audio/mp4') && window.ReadableStream
|
|
3162
|
+
const byterangeOptimizationSupport = (stream.mimeType == 'video/mp4' ||
|
|
3163
|
+
stream.mimeType == 'audio/mp4') && window.ReadableStream &&
|
|
3164
|
+
this.config_.hls.allowLowLatencyByteRangeOptimization;
|
|
2996
3165
|
|
|
2997
3166
|
let partialSyncTime = syncTime;
|
|
2998
3167
|
for (let i = 0; i < hlsSegment.partialSegments.length; i++) {
|
|
@@ -3048,14 +3217,14 @@ shaka.hls.HlsParser = class {
|
|
|
3048
3217
|
somePartialSegmentWithGap = true;
|
|
3049
3218
|
}
|
|
3050
3219
|
|
|
3051
|
-
let
|
|
3220
|
+
let uris = null;
|
|
3052
3221
|
const getPartialUris = () => {
|
|
3053
|
-
if (
|
|
3222
|
+
if (uris == null) {
|
|
3054
3223
|
goog.asserts.assert(pUri, 'Partial uri should be defined!');
|
|
3055
|
-
|
|
3056
|
-
|
|
3224
|
+
uris = shaka.hls.Utils.constructSegmentUris(
|
|
3225
|
+
getUris(), pUri, variables);
|
|
3057
3226
|
}
|
|
3058
|
-
return
|
|
3227
|
+
return uris;
|
|
3059
3228
|
};
|
|
3060
3229
|
|
|
3061
3230
|
if (byterangeOptimizationSupport &&
|
|
@@ -3150,7 +3319,7 @@ shaka.hls.HlsParser = class {
|
|
|
3150
3319
|
|
|
3151
3320
|
let tilesLayout = '';
|
|
3152
3321
|
let tileDuration = null;
|
|
3153
|
-
if (type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
|
|
3322
|
+
if (stream.type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
|
|
3154
3323
|
// By default in HLS the tilesLayout is 1x1
|
|
3155
3324
|
tilesLayout = '1x1';
|
|
3156
3325
|
const tilesTag =
|
|
@@ -3164,22 +3333,25 @@ shaka.hls.HlsParser = class {
|
|
|
3164
3333
|
}
|
|
3165
3334
|
}
|
|
3166
3335
|
|
|
3167
|
-
let
|
|
3168
|
-
const
|
|
3336
|
+
let uris = null;
|
|
3337
|
+
const getSegmentUris = () => {
|
|
3169
3338
|
if (getUrisOptimization) {
|
|
3170
3339
|
return getUrisOptimization();
|
|
3171
3340
|
}
|
|
3172
|
-
if (
|
|
3173
|
-
|
|
3174
|
-
hlsSegment.
|
|
3341
|
+
if (uris == null) {
|
|
3342
|
+
uris = shaka.hls.Utils.constructSegmentUris(getUris(),
|
|
3343
|
+
hlsSegment.verbatimSegmentUri, variables);
|
|
3175
3344
|
}
|
|
3176
|
-
return
|
|
3345
|
+
return uris || [];
|
|
3177
3346
|
};
|
|
3178
3347
|
|
|
3348
|
+
const allPartialSegments = partialSegmentRefs.length > 0 &&
|
|
3349
|
+
!!hlsSegment.verbatimSegmentUri;
|
|
3350
|
+
|
|
3179
3351
|
const reference = new shaka.media.SegmentReference(
|
|
3180
3352
|
startTime,
|
|
3181
3353
|
endTime,
|
|
3182
|
-
|
|
3354
|
+
getSegmentUris,
|
|
3183
3355
|
startByte,
|
|
3184
3356
|
endByte,
|
|
3185
3357
|
initSegmentReference,
|
|
@@ -3192,6 +3364,7 @@ shaka.hls.HlsParser = class {
|
|
|
3192
3364
|
syncTime,
|
|
3193
3365
|
status,
|
|
3194
3366
|
aes128Key,
|
|
3367
|
+
allPartialSegments,
|
|
3195
3368
|
);
|
|
3196
3369
|
|
|
3197
3370
|
if (segmentWithByteRangeOptimization) {
|
|
@@ -3239,18 +3412,17 @@ shaka.hls.HlsParser = class {
|
|
|
3239
3412
|
* get the bandwidth necessary for this segments If it's defined in the
|
|
3240
3413
|
* playlist.
|
|
3241
3414
|
*
|
|
3242
|
-
* @param {string} verbatimMediaPlaylistUri
|
|
3243
3415
|
* @param {!shaka.hls.Playlist} playlist
|
|
3244
|
-
* @param {
|
|
3245
|
-
* @param {string} mimeType
|
|
3416
|
+
* @param {shaka.extern.Stream} stream
|
|
3246
3417
|
* @param {!Map.<number, number>} mediaSequenceToStartTime
|
|
3247
3418
|
* @param {!Map.<string, string>} variables
|
|
3419
|
+
* @param {function():!Array.<string>} getUris
|
|
3248
3420
|
* @return {{segments: !Array.<!shaka.media.SegmentReference>,
|
|
3249
3421
|
* bandwidth: (number|undefined)}}
|
|
3250
3422
|
* @private
|
|
3251
3423
|
*/
|
|
3252
|
-
createSegments_(
|
|
3253
|
-
|
|
3424
|
+
createSegments_(playlist, stream, mediaSequenceToStartTime, variables,
|
|
3425
|
+
getUris) {
|
|
3254
3426
|
/** @type {Array.<!shaka.hls.Segment>} */
|
|
3255
3427
|
const hlsSegments = playlist.segments;
|
|
3256
3428
|
goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
|
|
@@ -3302,7 +3474,8 @@ shaka.hls.HlsParser = class {
|
|
|
3302
3474
|
for (const drmTag of item.tags) {
|
|
3303
3475
|
if (drmTag.name == 'EXT-X-KEY') {
|
|
3304
3476
|
if (drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
|
|
3305
|
-
aes128Key =
|
|
3477
|
+
aes128Key =
|
|
3478
|
+
this.parseAES128DrmTag_(drmTag, playlist, getUris, variables);
|
|
3306
3479
|
} else {
|
|
3307
3480
|
aes128Key = undefined;
|
|
3308
3481
|
}
|
|
@@ -3312,7 +3485,7 @@ shaka.hls.HlsParser = class {
|
|
|
3312
3485
|
mediaSequenceToStartTime.set(position, startTime);
|
|
3313
3486
|
|
|
3314
3487
|
initSegmentRef = this.getInitSegmentReference_(playlist,
|
|
3315
|
-
item.tags, variables);
|
|
3488
|
+
item.tags, getUris, variables);
|
|
3316
3489
|
|
|
3317
3490
|
// If the stream is low latency and the user has not configured the
|
|
3318
3491
|
// lowLatencyMode, but if it has been configured to activate the
|
|
@@ -3332,9 +3505,9 @@ shaka.hls.HlsParser = class {
|
|
|
3332
3505
|
item,
|
|
3333
3506
|
startTime,
|
|
3334
3507
|
variables,
|
|
3335
|
-
playlist
|
|
3336
|
-
|
|
3337
|
-
|
|
3508
|
+
playlist,
|
|
3509
|
+
stream,
|
|
3510
|
+
getUris,
|
|
3338
3511
|
aes128Key);
|
|
3339
3512
|
|
|
3340
3513
|
if (reference) {
|
|
@@ -3468,42 +3641,6 @@ shaka.hls.HlsParser = class {
|
|
|
3468
3641
|
};
|
|
3469
3642
|
}
|
|
3470
3643
|
|
|
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
3644
|
/**
|
|
3508
3645
|
* Attempts to guess stream's mime type based on content type and URI.
|
|
3509
3646
|
*
|
|
@@ -3537,6 +3674,10 @@ shaka.hls.HlsParser = class {
|
|
|
3537
3674
|
}
|
|
3538
3675
|
}
|
|
3539
3676
|
|
|
3677
|
+
if (codecs == 'mjpg') {
|
|
3678
|
+
return 'application/mp4';
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3540
3681
|
// Not enough information to guess from the content type and codecs.
|
|
3541
3682
|
return null;
|
|
3542
3683
|
}
|
|
@@ -3569,10 +3710,11 @@ shaka.hls.HlsParser = class {
|
|
|
3569
3710
|
* @param {string} codecs
|
|
3570
3711
|
* @param {!shaka.hls.Playlist} playlist
|
|
3571
3712
|
* @param {!Map.<string, string>} variables
|
|
3713
|
+
* @param {function():!Array.<string>} getUris
|
|
3572
3714
|
* @return {!Promise.<string>}
|
|
3573
3715
|
* @private
|
|
3574
3716
|
*/
|
|
3575
|
-
async guessMimeType_(contentType, codecs, playlist, variables) {
|
|
3717
|
+
async guessMimeType_(contentType, codecs, playlist, variables, getUris) {
|
|
3576
3718
|
const HlsParser = shaka.hls.HlsParser;
|
|
3577
3719
|
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
3578
3720
|
|
|
@@ -3582,10 +3724,13 @@ shaka.hls.HlsParser = class {
|
|
|
3582
3724
|
goog.asserts.assert(playlist.segments.length,
|
|
3583
3725
|
'Playlist should have segments!');
|
|
3584
3726
|
const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2);
|
|
3585
|
-
const middleSegmentUri = this.variableSubstitution_(
|
|
3586
|
-
playlist.segments[middleSegmentIdx].absoluteUri, variables);
|
|
3587
3727
|
|
|
3588
|
-
const
|
|
3728
|
+
const middleSegmentUris = shaka.hls.Utils.constructSegmentUris(
|
|
3729
|
+
getUris(),
|
|
3730
|
+
playlist.segments[middleSegmentIdx].verbatimSegmentUri,
|
|
3731
|
+
variables);
|
|
3732
|
+
|
|
3733
|
+
const parsedUri = new goog.Uri(middleSegmentUris[0]);
|
|
3589
3734
|
const extension = parsedUri.getPath().split('.').pop();
|
|
3590
3735
|
const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
|
|
3591
3736
|
|
|
@@ -3608,7 +3753,7 @@ shaka.hls.HlsParser = class {
|
|
|
3608
3753
|
// If unable to guess mime type, request a segment and try getting it
|
|
3609
3754
|
// from the response.
|
|
3610
3755
|
const headRequest = shaka.net.NetworkingEngine.makeRequest(
|
|
3611
|
-
|
|
3756
|
+
middleSegmentUris, this.config_.retryParameters);
|
|
3612
3757
|
headRequest.method = 'HEAD';
|
|
3613
3758
|
const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
|
|
3614
3759
|
const response = await this.makeNetworkRequest_(
|
|
@@ -3650,14 +3795,17 @@ shaka.hls.HlsParser = class {
|
|
|
3650
3795
|
* @param {?string} height
|
|
3651
3796
|
* @param {?string} frameRate
|
|
3652
3797
|
* @param {?string} videoRange
|
|
3798
|
+
* @param {?string} videoLayout
|
|
3653
3799
|
* @private
|
|
3654
3800
|
*/
|
|
3655
|
-
addVideoAttributes_(stream, width, height, frameRate, videoRange
|
|
3801
|
+
addVideoAttributes_(stream, width, height, frameRate, videoRange,
|
|
3802
|
+
videoLayout) {
|
|
3656
3803
|
if (stream) {
|
|
3657
3804
|
stream.width = Number(width) || undefined;
|
|
3658
3805
|
stream.height = Number(height) || undefined;
|
|
3659
3806
|
stream.frameRate = Number(frameRate) || undefined;
|
|
3660
3807
|
stream.hdr = videoRange || undefined;
|
|
3808
|
+
stream.videoLayout = videoLayout || undefined;
|
|
3661
3809
|
}
|
|
3662
3810
|
}
|
|
3663
3811
|
|
|
@@ -3665,16 +3813,16 @@ shaka.hls.HlsParser = class {
|
|
|
3665
3813
|
* Makes a network request for the manifest and returns a Promise
|
|
3666
3814
|
* with the resulting data.
|
|
3667
3815
|
*
|
|
3668
|
-
* @param {string}
|
|
3816
|
+
* @param {!Array.<string>} uris
|
|
3669
3817
|
* @param {boolean=} isPlaylist
|
|
3670
3818
|
* @return {!Promise.<!shaka.extern.Response>}
|
|
3671
3819
|
* @private
|
|
3672
3820
|
*/
|
|
3673
|
-
requestManifest_(
|
|
3821
|
+
requestManifest_(uris, isPlaylist) {
|
|
3674
3822
|
const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
|
|
3675
3823
|
|
|
3676
3824
|
const request = shaka.net.NetworkingEngine.makeRequest(
|
|
3677
|
-
|
|
3825
|
+
uris, this.config_.retryParameters);
|
|
3678
3826
|
|
|
3679
3827
|
const type = isPlaylist ?
|
|
3680
3828
|
shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_PLAYLIST :
|
|
@@ -3960,8 +4108,8 @@ shaka.hls.HlsParser = class {
|
|
|
3960
4108
|
* @typedef {{
|
|
3961
4109
|
* stream: !shaka.extern.Stream,
|
|
3962
4110
|
* type: string,
|
|
3963
|
-
*
|
|
3964
|
-
*
|
|
4111
|
+
* redirectUris: !Array.<string>,
|
|
4112
|
+
* getUris: function():!Array.<string>,
|
|
3965
4113
|
* minTimestamp: number,
|
|
3966
4114
|
* maxTimestamp: number,
|
|
3967
4115
|
* mediaSequenceToStartTime: !Map.<number, number>,
|
|
@@ -3981,14 +4129,10 @@ shaka.hls.HlsParser = class {
|
|
|
3981
4129
|
* The Stream itself.
|
|
3982
4130
|
* @property {string} type
|
|
3983
4131
|
* 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.
|
|
4132
|
+
* @property {!Array.<string>} redirectUris
|
|
4133
|
+
* The redirect URIs.
|
|
4134
|
+
* @property {function():!Array.<string>} getUris
|
|
4135
|
+
* The verbatim media playlist URIs, as it appeared in the master playlist.
|
|
3992
4136
|
* @property {number} minTimestamp
|
|
3993
4137
|
* The minimum timestamp found in the stream.
|
|
3994
4138
|
* @property {number} maxTimestamp
|
|
@@ -4030,30 +4174,6 @@ shaka.hls.HlsParser.StreamInfo;
|
|
|
4030
4174
|
shaka.hls.HlsParser.StreamInfos;
|
|
4031
4175
|
|
|
4032
4176
|
|
|
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
4177
|
/**
|
|
4058
4178
|
* @const {!Object.<string, string>}
|
|
4059
4179
|
* @private
|
|
@@ -4175,8 +4295,6 @@ shaka.hls.HlsParser.PresentationType_ = {
|
|
|
4175
4295
|
LIVE: 'LIVE',
|
|
4176
4296
|
};
|
|
4177
4297
|
|
|
4178
|
-
shaka.media.ManifestParser.registerParserByExtension(
|
|
4179
|
-
'm3u8', () => new shaka.hls.HlsParser());
|
|
4180
4298
|
shaka.media.ManifestParser.registerParserByMime(
|
|
4181
4299
|
'application/x-mpegurl', () => new shaka.hls.HlsParser());
|
|
4182
4300
|
shaka.media.ManifestParser.registerParserByMime(
|