shaka-player 4.4.3 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
- package/.github/workflows/release-please.yaml +2 -27
- package/AUTHORS +2 -0
- package/CHANGELOG.md +156 -39
- package/CODE_OF_CONDUCT.md +1 -1
- package/CONTRIBUTORS +2 -0
- package/README.md +54 -42
- package/demo/asset_card.js +18 -21
- package/demo/cast_receiver/index.html +0 -1
- package/demo/close_button.js +0 -3
- package/demo/common/asset.js +33 -22
- package/demo/common/assets.js +249 -60
- package/demo/config.js +219 -253
- package/demo/custom.js +88 -133
- package/demo/front.js +7 -10
- package/demo/index.html +19 -22
- package/demo/input.js +6 -11
- package/demo/input_container.js +8 -9
- package/demo/main.js +62 -126
- package/demo/search.js +29 -34
- package/demo/service_worker.js +0 -3
- package/demo/tooltip.js +3 -5
- package/demo/visualizer.js +3 -5
- package/demo/visualizer_button.js +1 -20
- package/dist/controls.css +1 -1
- package/dist/controls.css.map +1 -1
- package/dist/demo.compiled.debug.js +211 -228
- package/dist/demo.compiled.debug.map +4 -4
- package/dist/demo.compiled.js +211 -228
- package/dist/demo.compiled.map +4 -4
- package/dist/deps.js +42 -37
- package/dist/locales.js +34 -30
- package/dist/receiver.compiled.debug.js +64 -77
- package/dist/receiver.compiled.debug.map +4 -4
- package/dist/receiver.compiled.js +64 -77
- package/dist/receiver.compiled.map +4 -4
- package/dist/shaka-player.compiled.d.ts +343 -151
- package/dist/shaka-player.compiled.debug.d.ts +343 -151
- package/dist/shaka-player.compiled.debug.externs.js +356 -117
- package/dist/shaka-player.compiled.debug.js +952 -811
- package/dist/shaka-player.compiled.debug.map +4 -4
- package/dist/shaka-player.compiled.externs.js +356 -117
- package/dist/shaka-player.compiled.js +1165 -1078
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.d.ts +359 -153
- package/dist/shaka-player.ui.debug.d.ts +359 -153
- package/dist/shaka-player.ui.debug.externs.js +382 -117
- package/dist/shaka-player.ui.debug.js +1084 -918
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +382 -117
- package/dist/shaka-player.ui.js +1377 -1274
- package/dist/shaka-player.ui.map +4 -4
- package/docs/design/lcevc-integration.md +14 -14
- 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/faq.md +27 -55
- package/docs/tutorials/index.json +2 -1
- package/docs/tutorials/lcevc.md +118 -0
- package/docs/tutorials/offline.md +10 -8
- package/docs/tutorials/plugins.md +5 -0
- package/docs/tutorials/ui-customization.md +17 -0
- package/docs/tutorials/ui.md +2 -1
- package/docs/tutorials/upgrade.md +3 -0
- package/externs/css_style_declaration.js +19 -0
- package/externs/lcevc.js +37 -13
- package/externs/managedmediasource.js +24 -0
- package/externs/media_remote.js +68 -0
- package/externs/mediatailor.js +193 -0
- package/externs/pictureinpicture.js +11 -0
- package/externs/shaka/abr_manager.js +6 -2
- package/externs/shaka/ads.js +27 -0
- package/externs/shaka/cea.js +7 -0
- package/externs/shaka/codecs.js +5 -3
- package/externs/shaka/manifest.js +31 -10
- package/externs/shaka/manifest_parser.js +24 -1
- package/externs/shaka/net.js +2 -2
- package/externs/shaka/offline.js +13 -2
- package/externs/shaka/player.js +65 -8
- package/externs/shaka/text.js +3 -1
- package/externs/shaka/transmuxer.js +1 -1
- package/karma.conf.js +7 -3
- package/lib/abr/simple_abr_manager.js +66 -9
- package/lib/ads/ad_manager.js +104 -50
- package/lib/ads/media_tailor_ad.js +297 -0
- package/lib/ads/media_tailor_ad_manager.js +839 -0
- 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/cea/ts_cea_parser.js +15 -2
- package/lib/config/codec_switching_strategy.js +24 -0
- package/lib/dash/content_protection.js +120 -2
- package/lib/dash/dash_parser.js +322 -93
- package/lib/dash/mpd_utils.js +62 -37
- package/lib/dash/segment_base.js +17 -10
- package/lib/dash/segment_list.js +18 -9
- package/lib/dash/segment_template.js +122 -107
- package/lib/dependencies/all.js +8 -7
- package/lib/hls/hls_classes.js +28 -43
- package/lib/hls/hls_parser.js +798 -658
- package/lib/hls/hls_utils.js +64 -5
- package/lib/hls/manifest_text_parser.js +7 -11
- package/lib/lcevc/lcevc_dec.js +220 -0
- package/lib/media/adaptation_set.js +32 -14
- package/lib/media/adaptation_set_criteria.js +86 -16
- 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 +286 -44
- 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 +6 -6
- package/lib/media/segment_prefetch.js +92 -24
- package/lib/media/segment_reference.js +73 -7
- package/lib/media/segment_utils.js +343 -0
- package/lib/media/stall_detector.js +0 -3
- package/lib/media/streaming_engine.js +73 -38
- package/lib/mss/mss_parser.js +19 -2
- package/lib/net/networking_engine.js +67 -12
- package/lib/net/networking_utils.js +95 -0
- package/lib/offline/indexeddb/v1_storage_cell.js +3 -0
- package/lib/offline/indexeddb/v2_storage_cell.js +3 -0
- package/lib/offline/manifest_converter.js +3 -0
- package/lib/offline/offline_manifest_parser.js +10 -0
- package/lib/offline/storage.js +14 -7
- package/lib/player.js +1098 -1537
- package/lib/polyfill/mediasource.js +22 -1
- package/lib/text/cue.js +15 -0
- 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 +45 -5
- package/lib/text/ui_text_displayer.js +8 -0
- package/lib/text/vtt_text_parser.js +40 -22
- 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 +863 -0
- package/lib/transmuxer/mp3_transmuxer.js +1 -1
- package/lib/transmuxer/mpeg_ts_transmuxer.js +4 -3
- package/lib/transmuxer/mss_transmuxer.js +1 -1
- package/lib/transmuxer/ts_transmuxer.js +187 -22
- package/lib/util/buffer_utils.js +26 -4
- package/lib/util/content_steering_manager.js +358 -0
- package/lib/util/error.js +30 -15
- package/lib/util/exp_golomb.js +30 -2
- package/lib/util/fairplay_utils.js +1 -1
- package/lib/util/fake_event.js +8 -1
- package/lib/util/id3_utils.js +50 -1
- package/lib/util/manifest_parser_utils.js +12 -8
- package/lib/util/mp4_generator.js +6 -8
- package/lib/util/mutex.js +59 -0
- package/lib/util/object_utils.js +1 -1
- package/lib/util/periods.js +156 -179
- package/lib/util/platform.js +44 -3
- package/lib/util/player_configuration.js +43 -6
- package/lib/util/state_history.js +6 -2
- package/lib/util/stream_utils.js +206 -283
- package/lib/util/ts_parser.js +335 -98
- package/lib/util/uint8array_utils.js +5 -1
- package/lib/util/xml_utils.js +5 -0
- package/maintained-branches.md +3 -2
- package/package.json +4 -6
- package/roadmap.md +25 -5
- package/shaka-player.uncompiled.js +2 -2
- package/support.html +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 +50 -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 +182 -0
- package/test/dash/dash_parser_content_protection_unit.js +2 -0
- package/test/dash/dash_parser_integration.js +74 -0
- package/test/dash/dash_parser_live_unit.js +2 -19
- package/test/dash/dash_parser_manifest_unit.js +536 -2
- package/test/dash/dash_parser_segment_base_unit.js +2 -0
- package/test/dash/dash_parser_segment_list_unit.js +2 -0
- package/test/dash/dash_parser_segment_template_unit.js +2 -0
- package/test/dash/mpd_utils_unit.js +61 -30
- package/test/demo/demo_unit.js +0 -16
- package/test/hls/hls_live_unit.js +14 -3
- package/test/hls/hls_parser_unit.js +385 -33
- package/test/hls/manifest_text_parser_unit.js +32 -99
- package/test/media/adaptation_set_criteria_unit.js +284 -15
- package/test/media/adaptation_set_unit.js +2 -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 +166 -2
- package/test/media/play_rate_controller_unit.js +0 -48
- package/test/media/segment_index_unit.js +5 -4
- package/test/media/segment_prefetch_unit.js +1 -1
- package/test/media/streaming_engine_unit.js +17 -17
- package/test/mss/mss_parser_unit.js +2 -0
- 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 +14 -0
- package/test/offline/offline_integration.js +2 -1
- package/test/offline/storage_integration.js +22 -13
- 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 +182 -158
- package/test/test/assets/dash-aes-128/Fragments(video=0,format=mpd-time-csf) +0 -0
- package/test/test/assets/dash-aes-128/Fragments(video=20000000,format=mpd-time-csf) +0 -0
- package/test/test/assets/dash-aes-128/Fragments(video=40000000,format=mpd-time-csf) +0 -0
- package/test/test/assets/dash-aes-128/Fragments(video=60000000,format=mpd-time-csf) +0 -0
- package/test/test/assets/dash-aes-128/Fragments(video=i,format=mpd-time-csf) +0 -0
- package/test/test/assets/dash-aes-128/dash.mpd +18 -0
- package/test/test/assets/dash-aes-128/license +1 -0
- package/test/test/assets/dash-multi-codec/audio_en_2c_128k_aac.mp4 +0 -0
- package/test/test/assets/dash-multi-codec/audio_es_2c_64k_opus.webm +0 -0
- package/test/test/assets/dash-multi-codec/dash.mpd +32 -0
- package/test/test/assets/dash-multi-codec/video_144p_108k_h264.mp4 +0 -0
- package/test/test/assets/hls-ts-h265/0.ts +0 -0
- package/test/test/assets/hls-ts-h265/hevc.m3u8 +6 -0
- package/test/test/assets/hls-ts-h265/index.m3u8 +7 -0
- package/test/test/assets/hls-ts-muxed-aac-h265/0.ts +0 -0
- package/test/test/assets/hls-ts-muxed-aac-h265/1.ts +0 -0
- package/test/test/assets/hls-ts-muxed-aac-h265/media.m3u8 +9 -0
- package/test/test/assets/hls-ts-muxed-ac3-h264/media.m3u8 +24 -0
- package/test/test/assets/hls-ts-muxed-ac3-h264/s_00000.ts +0 -0
- package/test/test/assets/hls-ts-muxed-ac3-h264/s_00001.ts +0 -0
- package/test/test/assets/hls-ts-muxed-ac3-h264/s_00002.ts +0 -0
- package/test/test/assets/hls-ts-muxed-ac3-h264/s_00003.ts +0 -0
- package/test/test/assets/hls-ts-muxed-ac3-h264/s_00004.ts +0 -0
- package/test/test/assets/hls-ts-muxed-ac3-h264/s_00005.ts +0 -0
- package/test/test/assets/hls-ts-muxed-ec3-h264/ChID_voices_1280x720p_25fps_h264_6ch_256kbps_ddp_sub_0000.ts +0 -0
- package/test/test/assets/hls-ts-muxed-ec3-h264/ChID_voices_1280x720p_25fps_h264_6ch_256kbps_ddp_sub_0001.ts +0 -0
- package/test/test/assets/hls-ts-muxed-ec3-h264/ChID_voices_1280x720p_25fps_h264_6ch_256kbps_ddp_sub_0002.ts +0 -0
- package/test/test/assets/hls-ts-muxed-ec3-h264/ChID_voices_1280x720p_25fps_h264_6ch_256kbps_ddp_sub_0003.ts +0 -0
- package/test/test/assets/hls-ts-muxed-ec3-h264/prog_index.m3u8 +14 -0
- package/test/test/assets/hls-ts-muxed-mp3-h264/index.m3u8 +23 -0
- package/test/test/assets/hls-ts-muxed-mp3-h264/seg-1-v1-a1.ts +0 -0
- package/test/test/assets/hls-ts-muxed-mp3-h264/seg-2-v1-a1.ts +0 -0
- package/test/test/assets/hls-ts-muxed-mp3-h264/seg-3-v1-a1.ts +0 -0
- package/test/test/assets/hls-ts-muxed-mp3-h264/seg-4-v1-a1.ts +0 -0
- package/test/test/assets/hls-ts-muxed-mp3-h264/seg-5-v1-a1.ts +0 -0
- package/test/test/assets/hls-ts-muxed-mp3-h264/seg-6-v1-a1.ts +0 -0
- package/test/test/assets/hls-ts-muxed-mp3-h264/seg-7-v1-a1.ts +0 -0
- package/test/test/assets/hls-ts-muxed-mp3-h264/seg-8-v1-a1.ts +0 -0
- package/test/test/util/dash_parser_util.js +4 -0
- package/test/test/util/fake_ad_manager.js +20 -0
- package/test/test/util/fake_media_source_engine.js +13 -2
- 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 +11 -3
- package/test/test/util/manifest_parser_util.js +14 -2
- package/test/test/util/mss_parser_util.js +4 -0
- package/test/test/util/offline_utils.js +2 -0
- package/test/test/util/simple_fakes.js +2 -2
- package/test/test/util/streaming_engine_util.js +10 -4
- 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 +85 -2
- package/test/text/vtt_text_parser_unit.js +103 -31
- package/test/transmuxer/transmuxer_engine_integration.js +9 -0
- package/test/transmuxer/transmuxer_integration.js +150 -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 +28 -29
- 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 +207 -169
- 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/controls.js +125 -0
- package/ui/externs/ui.js +24 -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/pip_button.js +22 -120
- package/ui/remote_button.js +203 -0
- 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 +11 -4
- package/demo/common/message_ids.js +0 -309
- package/demo/locales/en.json +0 -288
- package/demo/locales/source.json +0 -1146
- package/externs/mux.js +0 -69
- package/lib/lcevc/lcevc_dil.js +0 -152
- package/lib/routing/node.js +0 -23
- package/lib/routing/payload.js +0 -43
- package/lib/routing/walker.js +0 -587
- package/lib/transmuxer/muxjs_transmuxer.js +0 -242
- package/test/routing/walker_unit.js +0 -504
- package/test/transmuxer/muxjs_transmuxer_integration.js +0 -177
package/lib/hls/hls_parser.js
CHANGED
|
@@ -9,6 +9,7 @@ goog.provide('shaka.hls.HlsParser');
|
|
|
9
9
|
|
|
10
10
|
goog.require('goog.Uri');
|
|
11
11
|
goog.require('goog.asserts');
|
|
12
|
+
goog.require('shaka.abr.Ewma');
|
|
12
13
|
goog.require('shaka.hls.ManifestTextParser');
|
|
13
14
|
goog.require('shaka.hls.Playlist');
|
|
14
15
|
goog.require('shaka.hls.PlaylistType');
|
|
@@ -25,18 +26,16 @@ goog.require('shaka.net.DataUriPlugin');
|
|
|
25
26
|
goog.require('shaka.net.NetworkingEngine');
|
|
26
27
|
goog.require('shaka.util.ArrayUtils');
|
|
27
28
|
goog.require('shaka.util.BufferUtils');
|
|
29
|
+
goog.require('shaka.util.ContentSteeringManager');
|
|
28
30
|
goog.require('shaka.util.Error');
|
|
29
31
|
goog.require('shaka.util.FakeEvent');
|
|
30
|
-
goog.require('shaka.util.Functional');
|
|
31
32
|
goog.require('shaka.util.LanguageUtils');
|
|
32
33
|
goog.require('shaka.util.ManifestParserUtils');
|
|
33
34
|
goog.require('shaka.util.MimeUtils');
|
|
34
|
-
goog.require('shaka.util.Mp4BoxParsers');
|
|
35
|
-
goog.require('shaka.util.Mp4Parser');
|
|
36
35
|
goog.require('shaka.util.OperationManager');
|
|
37
36
|
goog.require('shaka.util.Pssh');
|
|
37
|
+
goog.require('shaka.media.SegmentUtils');
|
|
38
38
|
goog.require('shaka.util.Timer');
|
|
39
|
-
goog.require('shaka.util.TsParser');
|
|
40
39
|
goog.require('shaka.util.Platform');
|
|
41
40
|
goog.require('shaka.util.Uint8ArrayUtils');
|
|
42
41
|
goog.require('shaka.util.XmlUtils');
|
|
@@ -111,8 +110,8 @@ shaka.hls.HlsParser = class {
|
|
|
111
110
|
* timestamps, offsets, and to handle TS rollover.
|
|
112
111
|
*
|
|
113
112
|
* During parsing, used to avoid duplicates in the async methods
|
|
114
|
-
*
|
|
115
|
-
*
|
|
113
|
+
* createStreamInfoFromMediaTags_, createStreamInfoFromImageTag_ and
|
|
114
|
+
* createStreamInfoFromVariantTags_.
|
|
116
115
|
*
|
|
117
116
|
* @private {!Map.<string, shaka.hls.HlsParser.StreamInfo>}
|
|
118
117
|
*/
|
|
@@ -156,6 +155,13 @@ shaka.hls.HlsParser = class {
|
|
|
156
155
|
*/
|
|
157
156
|
this.streamsFinalized_ = false;
|
|
158
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Whether the manifest informs about the codec to use.
|
|
160
|
+
*
|
|
161
|
+
* @private
|
|
162
|
+
*/
|
|
163
|
+
this.codecInfoInManifest_ = false;
|
|
164
|
+
|
|
159
165
|
/**
|
|
160
166
|
* This timer is used to trigger the start of a manifest update. A manifest
|
|
161
167
|
* update is async. Once the update is finished, the timer will be restarted
|
|
@@ -215,6 +221,16 @@ shaka.hls.HlsParser = class {
|
|
|
215
221
|
|
|
216
222
|
/** @private {boolean} */
|
|
217
223
|
this.lowLatencyByterangeOptimization_ = false;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* An ewma that tracks how long updates take.
|
|
227
|
+
* This is to mitigate issues caused by slow parsing on embedded devices.
|
|
228
|
+
* @private {!shaka.abr.Ewma}
|
|
229
|
+
*/
|
|
230
|
+
this.averageUpdateDuration_ = new shaka.abr.Ewma(5);
|
|
231
|
+
|
|
232
|
+
/** @private {?shaka.util.ContentSteeringManager} */
|
|
233
|
+
this.contentSteeringManager_ = null;
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
|
|
@@ -224,6 +240,10 @@ shaka.hls.HlsParser = class {
|
|
|
224
240
|
*/
|
|
225
241
|
configure(config) {
|
|
226
242
|
this.config_ = config;
|
|
243
|
+
|
|
244
|
+
if (this.contentSteeringManager_) {
|
|
245
|
+
this.contentSteeringManager_.configure(this.config_);
|
|
246
|
+
}
|
|
227
247
|
}
|
|
228
248
|
|
|
229
249
|
/**
|
|
@@ -235,7 +255,7 @@ shaka.hls.HlsParser = class {
|
|
|
235
255
|
this.playerInterface_ = playerInterface;
|
|
236
256
|
this.lowLatencyMode_ = playerInterface.isLowLatencyMode();
|
|
237
257
|
|
|
238
|
-
const response = await this.requestManifest_(uri);
|
|
258
|
+
const response = await this.requestManifest_([uri]);
|
|
239
259
|
|
|
240
260
|
// Record the master playlist URI after redirects.
|
|
241
261
|
this.masterPlaylistUri_ = response.uri;
|
|
@@ -276,6 +296,10 @@ shaka.hls.HlsParser = class {
|
|
|
276
296
|
this.groupIdToCodecsMap_.clear();
|
|
277
297
|
this.globalVariables_.clear();
|
|
278
298
|
|
|
299
|
+
if (this.contentSteeringManager_) {
|
|
300
|
+
this.contentSteeringManager_.destroy();
|
|
301
|
+
}
|
|
302
|
+
|
|
279
303
|
return Promise.all(pending);
|
|
280
304
|
}
|
|
281
305
|
|
|
@@ -346,40 +370,42 @@ shaka.hls.HlsParser = class {
|
|
|
346
370
|
* @private
|
|
347
371
|
*/
|
|
348
372
|
async updateStream_(streamInfo) {
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (streamInfo.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
+
}
|
|
357
389
|
}
|
|
358
|
-
if (streamInfo.
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
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');
|
|
363
394
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
queryData.add('_HLS_skip', 'YES');
|
|
369
|
-
}
|
|
370
|
-
if (queryData.getCount()) {
|
|
371
|
-
uriObj.setQueryData(queryData);
|
|
395
|
+
if (queryData.getCount()) {
|
|
396
|
+
uriObj.setQueryData(queryData);
|
|
397
|
+
}
|
|
398
|
+
manifestUris.push(uriObj.toString());
|
|
372
399
|
}
|
|
373
400
|
const response =
|
|
374
|
-
await this.requestManifest_(
|
|
401
|
+
await this.requestManifest_(manifestUris, /* isPlaylist= */ true);
|
|
375
402
|
if (!streamInfo.stream.segmentIndex) {
|
|
376
403
|
// The stream was closed since the update was first requested.
|
|
377
404
|
return;
|
|
378
405
|
}
|
|
379
406
|
|
|
380
407
|
/** @type {shaka.hls.Playlist} */
|
|
381
|
-
const playlist = this.manifestTextParser_.parsePlaylist(
|
|
382
|
-
response.data, response.uri);
|
|
408
|
+
const playlist = this.manifestTextParser_.parsePlaylist(response.data);
|
|
383
409
|
|
|
384
410
|
if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
|
|
385
411
|
throw new shaka.util.Error(
|
|
@@ -388,11 +414,19 @@ shaka.hls.HlsParser = class {
|
|
|
388
414
|
shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
|
|
389
415
|
}
|
|
390
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
|
+
|
|
391
424
|
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
392
425
|
const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
|
|
393
426
|
'EXT-X-DEFINE');
|
|
394
427
|
|
|
395
|
-
const mediaVariables = this.parseMediaVariables_(
|
|
428
|
+
const mediaVariables = this.parseMediaVariables_(
|
|
429
|
+
variablesTags, responseUri);
|
|
396
430
|
|
|
397
431
|
const stream = streamInfo.stream;
|
|
398
432
|
|
|
@@ -410,8 +444,8 @@ shaka.hls.HlsParser = class {
|
|
|
410
444
|
}
|
|
411
445
|
|
|
412
446
|
const {segments, bandwidth} = this.createSegments_(
|
|
413
|
-
|
|
414
|
-
|
|
447
|
+
playlist, stream, mediaSequenceToStartTime, mediaVariables,
|
|
448
|
+
streamInfo.getUris);
|
|
415
449
|
|
|
416
450
|
stream.bandwidth = bandwidth;
|
|
417
451
|
|
|
@@ -466,6 +500,24 @@ shaka.hls.HlsParser = class {
|
|
|
466
500
|
// No-op
|
|
467
501
|
}
|
|
468
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
|
+
|
|
469
521
|
/**
|
|
470
522
|
* Align the streams by sequence number by dropping early segments. Then
|
|
471
523
|
* offset the streams to begin at presentation time 0.
|
|
@@ -591,7 +643,7 @@ shaka.hls.HlsParser = class {
|
|
|
591
643
|
const segment0 = segmentIndex.earliestReference();
|
|
592
644
|
if (segment0.syncTime == null) {
|
|
593
645
|
shaka.log.alwaysError('Missing EXT-X-PROGRAM-DATE-TIME for stream',
|
|
594
|
-
streamInfo.
|
|
646
|
+
streamInfo.getUris(),
|
|
595
647
|
'Expect AV sync issues!');
|
|
596
648
|
} else {
|
|
597
649
|
// Stream metadata are offset by a fixed amount based on the
|
|
@@ -632,7 +684,7 @@ shaka.hls.HlsParser = class {
|
|
|
632
684
|
}
|
|
633
685
|
|
|
634
686
|
shaka.log.debug('Offset', offset, 'applied to',
|
|
635
|
-
streamInfo.
|
|
687
|
+
streamInfo.getUris());
|
|
636
688
|
}
|
|
637
689
|
|
|
638
690
|
/**
|
|
@@ -649,14 +701,11 @@ shaka.hls.HlsParser = class {
|
|
|
649
701
|
goog.asserts.assert(this.masterPlaylistUri_,
|
|
650
702
|
'Master playlist URI must be set before calling parseManifest_!');
|
|
651
703
|
|
|
652
|
-
const playlist = this.manifestTextParser_.parsePlaylist(
|
|
653
|
-
data, this.masterPlaylistUri_);
|
|
704
|
+
const playlist = this.manifestTextParser_.parsePlaylist(data);
|
|
654
705
|
|
|
655
706
|
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
656
707
|
const variablesTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-DEFINE');
|
|
657
708
|
|
|
658
|
-
this.parseMasterVariables_(variablesTags);
|
|
659
|
-
|
|
660
709
|
/** @type {!Array.<!shaka.extern.Variant>} */
|
|
661
710
|
let variants = [];
|
|
662
711
|
/** @type {!Array.<!shaka.extern.Stream>} */
|
|
@@ -666,9 +715,14 @@ shaka.hls.HlsParser = class {
|
|
|
666
715
|
|
|
667
716
|
// Parsing a media playlist results in a single-variant stream.
|
|
668
717
|
if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
|
|
718
|
+
const getUris = () => {
|
|
719
|
+
return [uri];
|
|
720
|
+
};
|
|
721
|
+
|
|
669
722
|
// Get necessary info for this stream. These are things we would normally
|
|
670
723
|
// find from the master playlist (e.g. from values on EXT-X-MEDIA tags).
|
|
671
|
-
const basicInfo =
|
|
724
|
+
const basicInfo =
|
|
725
|
+
await this.getMediaPlaylistBasicInfo_(playlist, getUris);
|
|
672
726
|
const type = basicInfo.type;
|
|
673
727
|
const mimeType = basicInfo.mimeType;
|
|
674
728
|
const codecs = basicInfo.codecs;
|
|
@@ -677,6 +731,7 @@ shaka.hls.HlsParser = class {
|
|
|
677
731
|
const width = basicInfo.width;
|
|
678
732
|
const channelsCount = basicInfo.channelCount;
|
|
679
733
|
const sampleRate = basicInfo.sampleRate;
|
|
734
|
+
const closedCaptions = basicInfo.closedCaptions;
|
|
680
735
|
|
|
681
736
|
// Some values we cannot figure out, and aren't important enough to ask
|
|
682
737
|
// the user to provide through config values. A lot of these are only
|
|
@@ -684,21 +739,21 @@ shaka.hls.HlsParser = class {
|
|
|
684
739
|
// So these unknowns should be set to false or null, largely.
|
|
685
740
|
const spatialAudio = false;
|
|
686
741
|
const characteristics = null;
|
|
687
|
-
const closedCaptions = new Map();
|
|
688
742
|
const forced = false; // Only relevant for text.
|
|
689
743
|
const primary = true; // This is the only stream!
|
|
690
744
|
const name = 'Media Playlist';
|
|
691
745
|
|
|
692
746
|
// Make the stream info, with those values.
|
|
693
747
|
const streamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
|
|
694
|
-
playlist,
|
|
695
|
-
|
|
696
|
-
spatialAudio, mimeType);
|
|
748
|
+
this.globalId_++, playlist, getUris, uri, codecs, type,
|
|
749
|
+
languageValue, primary, name, channelsCount, closedCaptions,
|
|
750
|
+
characteristics, forced, sampleRate, spatialAudio, mimeType);
|
|
697
751
|
this.uriToStreamInfosMap_.set(uri, streamInfo);
|
|
698
752
|
|
|
699
753
|
if (type == 'video') {
|
|
700
754
|
this.addVideoAttributes_(streamInfo.stream, width, height,
|
|
701
|
-
/* frameRate= */ null, /* videoRange= */ null
|
|
755
|
+
/* frameRate= */ null, /* videoRange= */ null,
|
|
756
|
+
/* videoLayout= */ null);
|
|
702
757
|
}
|
|
703
758
|
|
|
704
759
|
// Wrap the stream from that stream info with a variant.
|
|
@@ -715,8 +770,11 @@ shaka.hls.HlsParser = class {
|
|
|
715
770
|
decodingInfos: [],
|
|
716
771
|
});
|
|
717
772
|
} else {
|
|
773
|
+
this.parseMasterVariables_(variablesTags);
|
|
774
|
+
|
|
718
775
|
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
719
|
-
const mediaTags = Utils.filterTagsByName(
|
|
776
|
+
const mediaTags = Utils.filterTagsByName(
|
|
777
|
+
playlist.tags, 'EXT-X-MEDIA');
|
|
720
778
|
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
721
779
|
const variantTags = Utils.filterTagsByName(
|
|
722
780
|
playlist.tags, 'EXT-X-STREAM-INF');
|
|
@@ -724,43 +782,28 @@ shaka.hls.HlsParser = class {
|
|
|
724
782
|
const imageTags = Utils.filterTagsByName(
|
|
725
783
|
playlist.tags, 'EXT-X-IMAGE-STREAM-INF');
|
|
726
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>} */
|
|
727
788
|
const sessionKeyTags = Utils.filterTagsByName(
|
|
728
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');
|
|
729
796
|
|
|
730
|
-
this.
|
|
797
|
+
this.processSessionData_(sessionDataTags);
|
|
798
|
+
await this.processContentSteering_(contentSteeringTags);
|
|
731
799
|
|
|
732
|
-
|
|
733
|
-
const sesionDataTags =
|
|
734
|
-
Utils.filterTagsByName(playlist.tags, 'EXT-X-SESSION-DATA');
|
|
735
|
-
for (const tag of sesionDataTags) {
|
|
736
|
-
const id = tag.getAttributeValue('DATA-ID');
|
|
737
|
-
const uri = tag.getAttributeValue('URI');
|
|
738
|
-
const language = tag.getAttributeValue('LANGUAGE');
|
|
739
|
-
const value = tag.getAttributeValue('VALUE');
|
|
740
|
-
const data = (new Map()).set('id', id);
|
|
741
|
-
if (uri) {
|
|
742
|
-
data.set('uri', shaka.hls.Utils.constructAbsoluteUri(
|
|
743
|
-
this.masterPlaylistUri_, uri));
|
|
744
|
-
}
|
|
745
|
-
if (language) {
|
|
746
|
-
data.set('language', language);
|
|
747
|
-
}
|
|
748
|
-
if (value) {
|
|
749
|
-
data.set('value', value);
|
|
750
|
-
}
|
|
751
|
-
const event = new shaka.util.FakeEvent('sessiondata', data);
|
|
752
|
-
if (this.playerInterface_) {
|
|
753
|
-
this.playerInterface_.onEvent(event);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
800
|
+
this.parseCodecs_(variantTags);
|
|
756
801
|
|
|
757
|
-
// Parse audio and video media tags first, so that we can extract segment
|
|
758
|
-
// start time from audio/video streams and reuse for text streams.
|
|
759
|
-
this.createStreamInfosFromMediaTags_(mediaTags);
|
|
760
802
|
this.parseClosedCaptions_(mediaTags);
|
|
761
|
-
variants =
|
|
803
|
+
variants =
|
|
804
|
+
this.createVariantsForTags_(variantTags, sessionKeyTags, mediaTags);
|
|
762
805
|
textStreams = this.parseTexts_(mediaTags);
|
|
763
|
-
imageStreams = await this.parseImages_(imageTags);
|
|
806
|
+
imageStreams = await this.parseImages_(imageTags, iFrameTags);
|
|
764
807
|
}
|
|
765
808
|
|
|
766
809
|
// Make sure that the parser has not been destroyed.
|
|
@@ -809,64 +852,72 @@ shaka.hls.HlsParser = class {
|
|
|
809
852
|
type: shaka.media.ManifestParser.HLS,
|
|
810
853
|
serviceDescription: null,
|
|
811
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
|
+
|
|
812
873
|
this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
|
|
813
874
|
}
|
|
814
875
|
|
|
815
876
|
/**
|
|
816
877
|
* @param {shaka.hls.Playlist} playlist
|
|
817
|
-
* @
|
|
878
|
+
* @param {function():!Array.<string>} getUris
|
|
879
|
+
* @return {!Promise.<shaka.media.SegmentUtils.BasicInfo>}
|
|
818
880
|
* @private
|
|
819
881
|
*/
|
|
820
|
-
async getMediaPlaylistBasicInfo_(playlist) {
|
|
882
|
+
async getMediaPlaylistBasicInfo_(playlist, getUris) {
|
|
821
883
|
const HlsParser = shaka.hls.HlsParser;
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
shaka.util.MimeUtils.getBasicType(defaultFullMimeType);
|
|
825
|
-
const defaultType = defaultMimeType.split('/')[0];
|
|
826
|
-
const defaultCodecs = shaka.util.MimeUtils.getCodecs(defaultFullMimeType);
|
|
827
|
-
const defaultBasicInfo = {
|
|
828
|
-
type: defaultType,
|
|
829
|
-
mimeType: defaultMimeType,
|
|
830
|
-
codecs: defaultCodecs,
|
|
831
|
-
language: null,
|
|
832
|
-
height: null,
|
|
833
|
-
width: null,
|
|
834
|
-
channelCount: null,
|
|
835
|
-
sampleRate: null,
|
|
836
|
-
};
|
|
884
|
+
const defaultBasicInfo = shaka.media.SegmentUtils.getBasicInfoFromMimeType(
|
|
885
|
+
this.config_.hls.mediaPlaylistFullMimeType);
|
|
837
886
|
if (!playlist.segments.length) {
|
|
838
887
|
return defaultBasicInfo;
|
|
839
888
|
}
|
|
840
|
-
const
|
|
841
|
-
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);
|
|
842
896
|
const extension = parsedUri.getPath().split('.').pop();
|
|
843
897
|
const rawMimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension];
|
|
844
898
|
if (rawMimeType) {
|
|
845
|
-
return
|
|
846
|
-
|
|
847
|
-
mimeType: rawMimeType,
|
|
848
|
-
codecs: '',
|
|
849
|
-
language: null,
|
|
850
|
-
height: null,
|
|
851
|
-
width: null,
|
|
852
|
-
channelCount: null,
|
|
853
|
-
sampleRate: null,
|
|
854
|
-
};
|
|
899
|
+
return shaka.media.SegmentUtils.getBasicInfoFromMimeType(
|
|
900
|
+
rawMimeType);
|
|
855
901
|
}
|
|
856
902
|
|
|
857
|
-
|
|
903
|
+
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
904
|
+
|
|
905
|
+
let initData = null;
|
|
858
906
|
const initSegmentRef = this.getInitSegmentReference_(
|
|
859
|
-
playlist,
|
|
907
|
+
playlist, middleSegment.tags, getUris);
|
|
860
908
|
if (initSegmentRef) {
|
|
861
|
-
|
|
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;
|
|
862
916
|
}
|
|
863
917
|
|
|
864
|
-
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
865
918
|
const segmentRequest = shaka.net.NetworkingEngine.makeRequest(
|
|
866
|
-
|
|
867
|
-
const type =
|
|
868
|
-
shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT :
|
|
869
|
-
shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
|
|
919
|
+
middleSegmentUris, this.config_.retryParameters);
|
|
920
|
+
const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
|
|
870
921
|
const response = await this.makeNetworkRequest_(
|
|
871
922
|
segmentRequest, requestType, {type});
|
|
872
923
|
|
|
@@ -877,13 +928,16 @@ shaka.hls.HlsParser = class {
|
|
|
877
928
|
}
|
|
878
929
|
|
|
879
930
|
if (extension == 'ts' || contentMimeType == 'video/mp2t') {
|
|
880
|
-
const basicInfo =
|
|
931
|
+
const basicInfo =
|
|
932
|
+
shaka.media.SegmentUtils.getBasicInfoFromTs(response.data);
|
|
881
933
|
if (basicInfo) {
|
|
882
934
|
return basicInfo;
|
|
883
935
|
}
|
|
884
|
-
} else if (extension == 'mp4' ||
|
|
885
|
-
contentMimeType == 'video/mp4' || contentMimeType == 'audio/mp4'
|
|
886
|
-
|
|
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);
|
|
887
941
|
if (basicInfo) {
|
|
888
942
|
return basicInfo;
|
|
889
943
|
}
|
|
@@ -891,232 +945,6 @@ shaka.hls.HlsParser = class {
|
|
|
891
945
|
return defaultBasicInfo;
|
|
892
946
|
}
|
|
893
947
|
|
|
894
|
-
/**
|
|
895
|
-
* @param {shaka.extern.Response} response
|
|
896
|
-
* @return {?shaka.hls.HlsParser.BasicInfo}
|
|
897
|
-
* @private
|
|
898
|
-
*/
|
|
899
|
-
getBasicInfoFromTs_(response) {
|
|
900
|
-
const uint8ArrayData = shaka.util.BufferUtils.toUint8(response.data);
|
|
901
|
-
const tsParser = new shaka.util.TsParser().parse(uint8ArrayData);
|
|
902
|
-
const tsCodecs = tsParser.getCodecs();
|
|
903
|
-
const videoInfo = tsParser.getVideoInfo();
|
|
904
|
-
const codecs = [];
|
|
905
|
-
let hasAudio = false;
|
|
906
|
-
let hasVideo = false;
|
|
907
|
-
switch (tsCodecs.audio) {
|
|
908
|
-
case 'aac':
|
|
909
|
-
codecs.push('mp4a.40.2');
|
|
910
|
-
hasAudio = true;
|
|
911
|
-
break;
|
|
912
|
-
case 'mp3':
|
|
913
|
-
codecs.push('mp4a.40.34');
|
|
914
|
-
hasAudio = true;
|
|
915
|
-
break;
|
|
916
|
-
case 'ac3':
|
|
917
|
-
codecs.push('ac-3');
|
|
918
|
-
hasAudio = true;
|
|
919
|
-
break;
|
|
920
|
-
case 'ec3':
|
|
921
|
-
codecs.push('ec-3');
|
|
922
|
-
hasAudio = true;
|
|
923
|
-
break;
|
|
924
|
-
}
|
|
925
|
-
switch (tsCodecs.video) {
|
|
926
|
-
case 'avc':
|
|
927
|
-
if (videoInfo.codec) {
|
|
928
|
-
codecs.push(videoInfo.codec);
|
|
929
|
-
} else {
|
|
930
|
-
codecs.push('avc1.42E01E');
|
|
931
|
-
}
|
|
932
|
-
hasVideo = true;
|
|
933
|
-
break;
|
|
934
|
-
case 'hvc':
|
|
935
|
-
codecs.push('hvc1.1.6.L93.90');
|
|
936
|
-
hasVideo = true;
|
|
937
|
-
break;
|
|
938
|
-
}
|
|
939
|
-
if (!codecs.length) {
|
|
940
|
-
return null;
|
|
941
|
-
}
|
|
942
|
-
const onlyAudio = hasAudio && !hasVideo;
|
|
943
|
-
return {
|
|
944
|
-
type: onlyAudio ? 'audio' : 'video',
|
|
945
|
-
mimeType: 'video/mp2t',
|
|
946
|
-
codecs: codecs.join(', '),
|
|
947
|
-
language: null,
|
|
948
|
-
height: videoInfo.height,
|
|
949
|
-
width: videoInfo.width,
|
|
950
|
-
channelCount: null,
|
|
951
|
-
sampleRate: null,
|
|
952
|
-
};
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
/**
|
|
956
|
-
* @param {shaka.extern.Response} response
|
|
957
|
-
* @return {?shaka.hls.HlsParser.BasicInfo}
|
|
958
|
-
* @private
|
|
959
|
-
*/
|
|
960
|
-
getBasicInfoFromMp4_(response) {
|
|
961
|
-
const Mp4Parser = shaka.util.Mp4Parser;
|
|
962
|
-
|
|
963
|
-
const codecs = [];
|
|
964
|
-
|
|
965
|
-
let hasAudio = false;
|
|
966
|
-
let hasVideo = false;
|
|
967
|
-
|
|
968
|
-
const addCodec = (codec) => {
|
|
969
|
-
const codecLC = codec.toLowerCase();
|
|
970
|
-
switch (codecLC) {
|
|
971
|
-
case 'avc1':
|
|
972
|
-
case 'avc3':
|
|
973
|
-
codecs.push(codecLC + '.42E01E');
|
|
974
|
-
hasVideo = true;
|
|
975
|
-
break;
|
|
976
|
-
case 'hev1':
|
|
977
|
-
case 'hvc1':
|
|
978
|
-
codecs.push(codecLC + '.1.6.L93.90');
|
|
979
|
-
hasVideo = true;
|
|
980
|
-
break;
|
|
981
|
-
case 'dvh1':
|
|
982
|
-
case 'dvhe':
|
|
983
|
-
codecs.push(codecLC + '.05.04');
|
|
984
|
-
hasVideo = true;
|
|
985
|
-
break;
|
|
986
|
-
case 'vp09':
|
|
987
|
-
codecs.push(codecLC + '.00.10.08');
|
|
988
|
-
hasVideo = true;
|
|
989
|
-
break;
|
|
990
|
-
case 'av01':
|
|
991
|
-
codecs.push(codecLC + '.0.01M.08');
|
|
992
|
-
hasVideo = true;
|
|
993
|
-
break;
|
|
994
|
-
case 'mp4a':
|
|
995
|
-
// We assume AAC, but this can be wrong since mp4a supports
|
|
996
|
-
// others codecs
|
|
997
|
-
codecs.push('mp4a.40.2');
|
|
998
|
-
hasAudio = true;
|
|
999
|
-
break;
|
|
1000
|
-
case 'ac-3':
|
|
1001
|
-
case 'ec-3':
|
|
1002
|
-
case 'opus':
|
|
1003
|
-
case 'flac':
|
|
1004
|
-
codecs.push(codecLC);
|
|
1005
|
-
hasAudio = true;
|
|
1006
|
-
break;
|
|
1007
|
-
}
|
|
1008
|
-
};
|
|
1009
|
-
|
|
1010
|
-
const codecBoxParser = (box) => addCodec(box.name);
|
|
1011
|
-
|
|
1012
|
-
/** @type {?string} */
|
|
1013
|
-
let language = null;
|
|
1014
|
-
/** @type {?string} */
|
|
1015
|
-
let height = null;
|
|
1016
|
-
/** @type {?string} */
|
|
1017
|
-
let width = null;
|
|
1018
|
-
/** @type {?number} */
|
|
1019
|
-
let channelCount = null;
|
|
1020
|
-
/** @type {?number} */
|
|
1021
|
-
let sampleRate = null;
|
|
1022
|
-
|
|
1023
|
-
new Mp4Parser()
|
|
1024
|
-
.box('moov', Mp4Parser.children)
|
|
1025
|
-
.box('trak', Mp4Parser.children)
|
|
1026
|
-
.fullBox('tkhd', (box) => {
|
|
1027
|
-
goog.asserts.assert(
|
|
1028
|
-
box.version != null,
|
|
1029
|
-
'TKHD is a full box and should have a valid version.');
|
|
1030
|
-
const parsedTKHDBox = shaka.util.Mp4BoxParsers.parseTKHD(
|
|
1031
|
-
box.reader, box.version);
|
|
1032
|
-
height = String(parsedTKHDBox.height);
|
|
1033
|
-
width = String(parsedTKHDBox.width);
|
|
1034
|
-
})
|
|
1035
|
-
.box('mdia', Mp4Parser.children)
|
|
1036
|
-
.fullBox('mdhd', (box) => {
|
|
1037
|
-
goog.asserts.assert(
|
|
1038
|
-
box.version != null,
|
|
1039
|
-
'MDHD is a full box and should have a valid version.');
|
|
1040
|
-
const parsedMDHDBox = shaka.util.Mp4BoxParsers.parseMDHD(
|
|
1041
|
-
box.reader, box.version);
|
|
1042
|
-
language = parsedMDHDBox.language;
|
|
1043
|
-
})
|
|
1044
|
-
.box('minf', Mp4Parser.children)
|
|
1045
|
-
.box('stbl', Mp4Parser.children)
|
|
1046
|
-
.fullBox('stsd', Mp4Parser.sampleDescription)
|
|
1047
|
-
|
|
1048
|
-
// AUDIO
|
|
1049
|
-
// These are the various boxes that signal a codec.
|
|
1050
|
-
.box('mp4a', (box) => {
|
|
1051
|
-
const parsedMP4ABox = shaka.util.Mp4BoxParsers.parseMP4A(box.reader);
|
|
1052
|
-
channelCount = parsedMP4ABox.channelCount;
|
|
1053
|
-
sampleRate = parsedMP4ABox.sampleRate;
|
|
1054
|
-
if (box.reader.hasMoreData()) {
|
|
1055
|
-
Mp4Parser.children(box);
|
|
1056
|
-
} else {
|
|
1057
|
-
codecBoxParser(box);
|
|
1058
|
-
}
|
|
1059
|
-
})
|
|
1060
|
-
.box('esds', (box) => {
|
|
1061
|
-
const parsedESDSBox = shaka.util.Mp4BoxParsers.parseESDS(box.reader);
|
|
1062
|
-
codecs.push(parsedESDSBox.codec);
|
|
1063
|
-
hasAudio = true;
|
|
1064
|
-
})
|
|
1065
|
-
.box('ac-3', codecBoxParser)
|
|
1066
|
-
.box('ec-3', codecBoxParser)
|
|
1067
|
-
.box('opus', codecBoxParser)
|
|
1068
|
-
.box('Opus', codecBoxParser)
|
|
1069
|
-
.box('fLaC', codecBoxParser)
|
|
1070
|
-
|
|
1071
|
-
// VIDEO
|
|
1072
|
-
// These are the various boxes that signal a codec.
|
|
1073
|
-
.box('avc1', (box) => {
|
|
1074
|
-
const parsedAVCBox =
|
|
1075
|
-
shaka.util.Mp4BoxParsers.parseAVC(box.reader, box.name);
|
|
1076
|
-
codecs.push(parsedAVCBox.codec);
|
|
1077
|
-
hasVideo = true;
|
|
1078
|
-
})
|
|
1079
|
-
.box('avc3', (box) => {
|
|
1080
|
-
const parsedAVCBox =
|
|
1081
|
-
shaka.util.Mp4BoxParsers.parseAVC(box.reader, box.name);
|
|
1082
|
-
codecs.push(parsedAVCBox.codec);
|
|
1083
|
-
hasVideo = true;
|
|
1084
|
-
})
|
|
1085
|
-
.box('hev1', codecBoxParser)
|
|
1086
|
-
.box('hvc1', codecBoxParser)
|
|
1087
|
-
.box('dvh1', codecBoxParser)
|
|
1088
|
-
.box('dvhe', codecBoxParser)
|
|
1089
|
-
.box('vp09', codecBoxParser)
|
|
1090
|
-
.box('av01', codecBoxParser)
|
|
1091
|
-
|
|
1092
|
-
// This signals an encrypted sample, which we can go inside of to
|
|
1093
|
-
// find the codec used.
|
|
1094
|
-
// Note: If encrypted, you can only have audio or video, not both.
|
|
1095
|
-
.box('enca', Mp4Parser.visualSampleEntry)
|
|
1096
|
-
.box('encv', Mp4Parser.visualSampleEntry)
|
|
1097
|
-
.box('sinf', Mp4Parser.children)
|
|
1098
|
-
.box('frma', (box) => {
|
|
1099
|
-
const {codec} = shaka.util.Mp4BoxParsers.parseFRMA(box.reader);
|
|
1100
|
-
addCodec(codec);
|
|
1101
|
-
})
|
|
1102
|
-
|
|
1103
|
-
.parse(response.data, /* partialOkay= */ true);
|
|
1104
|
-
if (!codecs.length) {
|
|
1105
|
-
return null;
|
|
1106
|
-
}
|
|
1107
|
-
const onlyAudio = hasAudio && !hasVideo;
|
|
1108
|
-
return {
|
|
1109
|
-
type: onlyAudio ? 'audio' : 'video',
|
|
1110
|
-
mimeType: onlyAudio ? 'audio/mp4' : 'video/mp4',
|
|
1111
|
-
codecs: this.filterDuplicateCodecs_(codecs).join(', '),
|
|
1112
|
-
language: language,
|
|
1113
|
-
height: height,
|
|
1114
|
-
width: width,
|
|
1115
|
-
channelCount: channelCount,
|
|
1116
|
-
sampleRate: sampleRate,
|
|
1117
|
-
};
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
948
|
/** @private */
|
|
1121
949
|
determineDuration_() {
|
|
1122
950
|
goog.asserts.assert(this.presentationTimeline_,
|
|
@@ -1171,31 +999,50 @@ shaka.hls.HlsParser = class {
|
|
|
1171
999
|
* @private
|
|
1172
1000
|
*/
|
|
1173
1001
|
parseMasterVariables_(tags) {
|
|
1002
|
+
const queryParams = new goog.Uri(this.masterPlaylistUri_).getQueryData();
|
|
1174
1003
|
for (const variableTag of tags) {
|
|
1175
1004
|
const name = variableTag.getAttributeValue('NAME');
|
|
1176
1005
|
const value = variableTag.getAttributeValue('VALUE');
|
|
1006
|
+
const queryParam = variableTag.getAttributeValue('QUERYPARAM');
|
|
1177
1007
|
if (name && value) {
|
|
1178
1008
|
if (!this.globalVariables_.has(name)) {
|
|
1179
1009
|
this.globalVariables_.set(name, value);
|
|
1180
1010
|
}
|
|
1181
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
|
+
}
|
|
1182
1018
|
}
|
|
1183
1019
|
}
|
|
1184
1020
|
|
|
1185
1021
|
/**
|
|
1186
1022
|
* Get the variables of each variant tag, and store in a map.
|
|
1187
1023
|
* @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
|
|
1024
|
+
* @param {string} uri Media playlist URI.
|
|
1188
1025
|
* @return {!Map.<string, string>}
|
|
1189
1026
|
* @private
|
|
1190
1027
|
*/
|
|
1191
|
-
parseMediaVariables_(tags) {
|
|
1028
|
+
parseMediaVariables_(tags, uri) {
|
|
1029
|
+
const queryParams = new goog.Uri(uri).getQueryData();
|
|
1192
1030
|
const mediaVariables = new Map();
|
|
1193
1031
|
for (const variableTag of tags) {
|
|
1194
1032
|
const name = variableTag.getAttributeValue('NAME');
|
|
1195
1033
|
const value = variableTag.getAttributeValue('VALUE');
|
|
1034
|
+
const queryParam = variableTag.getAttributeValue('QUERYPARAM');
|
|
1196
1035
|
const mediaImport = variableTag.getAttributeValue('IMPORT');
|
|
1197
1036
|
if (name && value) {
|
|
1198
|
-
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
|
+
}
|
|
1199
1046
|
}
|
|
1200
1047
|
if (mediaImport) {
|
|
1201
1048
|
const globalValue = this.globalVariables_.get(mediaImport);
|
|
@@ -1248,6 +1095,68 @@ shaka.hls.HlsParser = class {
|
|
|
1248
1095
|
}
|
|
1249
1096
|
}
|
|
1250
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
|
+
|
|
1251
1160
|
/**
|
|
1252
1161
|
* Parse Subtitles and Closed Captions from 'EXT-X-MEDIA' tags.
|
|
1253
1162
|
* Create text streams for Subtitles, but not Closed Captions.
|
|
@@ -1266,7 +1175,7 @@ shaka.hls.HlsParser = class {
|
|
|
1266
1175
|
return null;
|
|
1267
1176
|
}
|
|
1268
1177
|
try {
|
|
1269
|
-
return this.
|
|
1178
|
+
return this.createStreamInfoFromMediaTags_([tag], new Map()).stream;
|
|
1270
1179
|
} catch (e) {
|
|
1271
1180
|
if (this.config_.hls.ignoreTextStreamFailures) {
|
|
1272
1181
|
return null;
|
|
@@ -1300,10 +1209,11 @@ shaka.hls.HlsParser = class {
|
|
|
1300
1209
|
|
|
1301
1210
|
/**
|
|
1302
1211
|
* @param {!Array.<!shaka.hls.Tag>} imageTags from the playlist.
|
|
1212
|
+
* @param {!Array.<!shaka.hls.Tag>} iFrameTags from the playlist.
|
|
1303
1213
|
* @return {!Promise.<!Array.<!shaka.extern.Stream>>}
|
|
1304
1214
|
* @private
|
|
1305
1215
|
*/
|
|
1306
|
-
async parseImages_(imageTags) {
|
|
1216
|
+
async parseImages_(imageTags, iFrameTags) {
|
|
1307
1217
|
// Create image stream for each image tag.
|
|
1308
1218
|
const imageStreamPromises = imageTags.map(async (tag) => {
|
|
1309
1219
|
const disableThumbnails = this.config_.disableThumbnails;
|
|
@@ -1319,16 +1229,34 @@ shaka.hls.HlsParser = class {
|
|
|
1319
1229
|
}
|
|
1320
1230
|
throw e;
|
|
1321
1231
|
}
|
|
1322
|
-
})
|
|
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
|
+
}));
|
|
1323
1250
|
const imageStreams = await Promise.all(imageStreamPromises);
|
|
1324
1251
|
return imageStreams.filter((s) => s);
|
|
1325
1252
|
}
|
|
1326
1253
|
|
|
1327
1254
|
/**
|
|
1328
1255
|
* @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
|
|
1256
|
+
* @param {!Map.<string, string>} groupIdPathwayIdMapping
|
|
1329
1257
|
* @private
|
|
1330
1258
|
*/
|
|
1331
|
-
createStreamInfosFromMediaTags_(mediaTags) {
|
|
1259
|
+
createStreamInfosFromMediaTags_(mediaTags, groupIdPathwayIdMapping) {
|
|
1332
1260
|
// Filter out subtitles and media tags without uri.
|
|
1333
1261
|
mediaTags = mediaTags.filter((tag) => {
|
|
1334
1262
|
const uri = tag.getAttributeValue('URI') || '';
|
|
@@ -1336,9 +1264,20 @@ shaka.hls.HlsParser = class {
|
|
|
1336
1264
|
return type != 'SUBTITLES' && uri != '';
|
|
1337
1265
|
});
|
|
1338
1266
|
|
|
1339
|
-
|
|
1267
|
+
const groupedTags = {};
|
|
1340
1268
|
for (const tag of mediaTags) {
|
|
1341
|
-
|
|
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);
|
|
1342
1281
|
}
|
|
1343
1282
|
}
|
|
1344
1283
|
|
|
@@ -1346,10 +1285,12 @@ shaka.hls.HlsParser = class {
|
|
|
1346
1285
|
* @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
|
|
1347
1286
|
* @param {!Array.<!shaka.hls.Tag>} sessionKeyTags EXT-X-SESSION-KEY tags
|
|
1348
1287
|
* from the playlist.
|
|
1288
|
+
* @param {!Array.<!shaka.hls.Tag>} mediaTags EXT-X-MEDIA tags from the
|
|
1289
|
+
* playlist.
|
|
1349
1290
|
* @return {!Array.<!shaka.extern.Variant>}
|
|
1350
1291
|
* @private
|
|
1351
1292
|
*/
|
|
1352
|
-
createVariantsForTags_(tags, sessionKeyTags) {
|
|
1293
|
+
createVariantsForTags_(tags, sessionKeyTags, mediaTags) {
|
|
1353
1294
|
// EXT-X-SESSION-KEY processing
|
|
1354
1295
|
const drmInfos = [];
|
|
1355
1296
|
const keyIds = new Set();
|
|
@@ -1381,24 +1322,54 @@ shaka.hls.HlsParser = class {
|
|
|
1381
1322
|
}
|
|
1382
1323
|
}
|
|
1383
1324
|
|
|
1384
|
-
|
|
1385
|
-
const
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
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
|
+
}
|
|
1389
1334
|
|
|
1390
|
-
|
|
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'));
|
|
1345
|
+
|
|
1346
|
+
const resolution = firstTag.getAttributeValue('RESOLUTION');
|
|
1391
1347
|
const [width, height] = resolution ? resolution.split('x') : [null, null];
|
|
1392
1348
|
|
|
1393
|
-
const videoRange =
|
|
1349
|
+
const videoRange = firstTag.getAttributeValue('VIDEO-RANGE');
|
|
1394
1350
|
|
|
1395
|
-
|
|
1396
|
-
|
|
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);
|
|
1397
1368
|
|
|
1398
1369
|
goog.asserts.assert(streamInfos.audio.length ||
|
|
1399
1370
|
streamInfos.video.length, 'We should have created a stream!');
|
|
1400
1371
|
|
|
1401
|
-
|
|
1372
|
+
allVariants.push(...this.createVariants_(
|
|
1402
1373
|
streamInfos.audio,
|
|
1403
1374
|
streamInfos.video,
|
|
1404
1375
|
bandwidth,
|
|
@@ -1406,47 +1377,78 @@ shaka.hls.HlsParser = class {
|
|
|
1406
1377
|
height,
|
|
1407
1378
|
frameRate,
|
|
1408
1379
|
videoRange,
|
|
1380
|
+
videoLayout,
|
|
1409
1381
|
drmInfos,
|
|
1410
|
-
keyIds);
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
// Filter out null variants.
|
|
1414
|
-
variants = variants.filter((variant) => variant != null);
|
|
1415
|
-
return variants;
|
|
1382
|
+
keyIds));
|
|
1383
|
+
}
|
|
1384
|
+
return allVariants.filter((variant) => variant != null);
|
|
1416
1385
|
}
|
|
1417
1386
|
|
|
1418
1387
|
/**
|
|
1419
1388
|
* Create audio and video streamInfos from an 'EXT-X-STREAM-INF' tag and its
|
|
1420
1389
|
* related media tags.
|
|
1421
1390
|
*
|
|
1422
|
-
* @param {!shaka.hls.Tag}
|
|
1391
|
+
* @param {!Array.<!shaka.hls.Tag>} tags
|
|
1392
|
+
* @param {!Array.<!shaka.hls.Tag>} mediaTags
|
|
1423
1393
|
* @param {?string} resolution
|
|
1424
1394
|
* @param {?string} frameRate
|
|
1425
1395
|
* @return {!shaka.hls.HlsParser.StreamInfos}
|
|
1426
1396
|
* @private
|
|
1427
1397
|
*/
|
|
1428
|
-
|
|
1398
|
+
createStreamInfosForVariantTags_(tags, mediaTags, resolution, frameRate) {
|
|
1429
1399
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
1430
|
-
/** @type {!Array.<string>} */
|
|
1431
|
-
let allCodecs = this.getCodecsForVariantTag_(tag);
|
|
1432
|
-
const audioGroupId = tag.getAttributeValue('AUDIO');
|
|
1433
|
-
const videoGroupId = tag.getAttributeValue('VIDEO');
|
|
1434
|
-
goog.asserts.assert(audioGroupId == null || videoGroupId == null,
|
|
1435
|
-
'Unexpected: both video and audio described by media tags!');
|
|
1436
|
-
|
|
1437
|
-
const groupId = audioGroupId || videoGroupId;
|
|
1438
|
-
const streamInfos =
|
|
1439
|
-
(groupId && this.groupIdToStreamInfosMap_.has(groupId)) ?
|
|
1440
|
-
this.groupIdToStreamInfosMap_.get(groupId) : [];
|
|
1441
1400
|
|
|
1442
1401
|
/** @type {shaka.hls.HlsParser.StreamInfos} */
|
|
1443
1402
|
const res = {
|
|
1444
|
-
audio:
|
|
1445
|
-
video:
|
|
1403
|
+
audio: [],
|
|
1404
|
+
video: [],
|
|
1446
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
|
+
}
|
|
1447
1451
|
|
|
1448
|
-
// Make an educated guess about the stream type.
|
|
1449
|
-
shaka.log.debug('Guessing stream type for', tag.toString());
|
|
1450
1452
|
let type;
|
|
1451
1453
|
let ignoreStream = false;
|
|
1452
1454
|
|
|
@@ -1455,11 +1457,15 @@ shaka.hls.HlsParser = class {
|
|
|
1455
1457
|
// In that case, ignore the local URI and use the version in the
|
|
1456
1458
|
// AUDIO tag, so you inherit its language.
|
|
1457
1459
|
// As an example, see the manifest linked in issue #860.
|
|
1458
|
-
const
|
|
1460
|
+
const allStreamUris = tags.map((tag) => tag.getRequiredAttrValue('URI'));
|
|
1459
1461
|
const hasSameUri = res.audio.find((audio) => {
|
|
1460
|
-
return audio && audio.
|
|
1462
|
+
return audio && audio.getUris().find((uri) => {
|
|
1463
|
+
return allStreamUris.includes(uri);
|
|
1464
|
+
});
|
|
1461
1465
|
});
|
|
1462
1466
|
|
|
1467
|
+
/** @type {!Array.<string>} */
|
|
1468
|
+
let allCodecs = this.getCodecsForVariantTag_(tags[0]);
|
|
1463
1469
|
const videoCodecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
|
|
1464
1470
|
ContentType.VIDEO, allCodecs);
|
|
1465
1471
|
const audioCodecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
|
|
@@ -1471,7 +1477,8 @@ shaka.hls.HlsParser = class {
|
|
|
1471
1477
|
type = ContentType.AUDIO;
|
|
1472
1478
|
shaka.log.debug('Guessing audio-only.');
|
|
1473
1479
|
ignoreStream = res.audio.length > 0;
|
|
1474
|
-
} else if (!
|
|
1480
|
+
} else if (!res.audio.length && !res.video.length &&
|
|
1481
|
+
audioCodecs && videoCodecs) {
|
|
1475
1482
|
// There are both audio and video codecs, so assume multiplexed content.
|
|
1476
1483
|
// Note that the default used when CODECS is missing assumes multiple
|
|
1477
1484
|
// (and therefore multiplexed).
|
|
@@ -1494,8 +1501,35 @@ shaka.hls.HlsParser = class {
|
|
|
1494
1501
|
}
|
|
1495
1502
|
|
|
1496
1503
|
if (!ignoreStream) {
|
|
1497
|
-
|
|
1498
|
-
|
|
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
|
+
}
|
|
1499
1533
|
res[streamInfo.stream.type] = [streamInfo];
|
|
1500
1534
|
}
|
|
1501
1535
|
return res;
|
|
@@ -1510,17 +1544,24 @@ shaka.hls.HlsParser = class {
|
|
|
1510
1544
|
* @private
|
|
1511
1545
|
*/
|
|
1512
1546
|
getCodecsForVariantTag_(tag) {
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
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(',');
|
|
1520
1563
|
}
|
|
1521
|
-
const defaultCodecs = defaultCodecsArray.join(',');
|
|
1522
1564
|
|
|
1523
|
-
const codecsString = tag.getAttributeValue('CODECS', defaultCodecs);
|
|
1524
1565
|
// Strip out internal whitespace while splitting on commas:
|
|
1525
1566
|
/** @type {!Array.<string>} */
|
|
1526
1567
|
const codecs = codecsString.split(/\s*,\s*/);
|
|
@@ -1674,6 +1715,7 @@ shaka.hls.HlsParser = class {
|
|
|
1674
1715
|
* @param {?string} height
|
|
1675
1716
|
* @param {?string} frameRate
|
|
1676
1717
|
* @param {?string} videoRange
|
|
1718
|
+
* @param {?string} videoLayout
|
|
1677
1719
|
* @param {!Array.<shaka.extern.DrmInfo>} drmInfos
|
|
1678
1720
|
* @param {!Set.<string>} keyIds
|
|
1679
1721
|
* @return {!Array.<!shaka.extern.Variant>}
|
|
@@ -1681,13 +1723,13 @@ shaka.hls.HlsParser = class {
|
|
|
1681
1723
|
*/
|
|
1682
1724
|
createVariants_(
|
|
1683
1725
|
audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
|
|
1684
|
-
drmInfos, keyIds) {
|
|
1726
|
+
videoLayout, drmInfos, keyIds) {
|
|
1685
1727
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
1686
1728
|
const DrmEngine = shaka.media.DrmEngine;
|
|
1687
1729
|
|
|
1688
1730
|
for (const info of videoInfos) {
|
|
1689
1731
|
this.addVideoAttributes_(
|
|
1690
|
-
info.stream, width, height, frameRate, videoRange);
|
|
1732
|
+
info.stream, width, height, frameRate, videoRange, videoLayout);
|
|
1691
1733
|
}
|
|
1692
1734
|
|
|
1693
1735
|
// In case of audio-only or video-only content or the audio/video is
|
|
@@ -1719,9 +1761,9 @@ shaka.hls.HlsParser = class {
|
|
|
1719
1761
|
const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
|
|
1720
1762
|
const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
|
|
1721
1763
|
const videoStreamUri =
|
|
1722
|
-
videoInfo ? videoInfo.
|
|
1764
|
+
videoInfo ? videoInfo.getUris().sort().join(',') : '';
|
|
1723
1765
|
const audioStreamUri =
|
|
1724
|
-
audioInfo ? audioInfo.
|
|
1766
|
+
audioInfo ? audioInfo.getUris().sort().join(',') : '';
|
|
1725
1767
|
const variantUriKey = videoStreamUri + ' - ' + audioStreamUri;
|
|
1726
1768
|
|
|
1727
1769
|
if (audioStream && videoStream) {
|
|
@@ -1809,34 +1851,59 @@ shaka.hls.HlsParser = class {
|
|
|
1809
1851
|
/**
|
|
1810
1852
|
* Parse EXT-X-MEDIA media tag into a Stream object.
|
|
1811
1853
|
*
|
|
1812
|
-
* @param {shaka.hls.Tag}
|
|
1854
|
+
* @param {!Array.<!shaka.hls.Tag>} tags
|
|
1855
|
+
* @param {!Map.<string, string>} groupIdPathwayIdMapping
|
|
1813
1856
|
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1814
1857
|
* @private
|
|
1815
1858
|
*/
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
const
|
|
1859
|
+
createStreamInfoFromMediaTags_(tags, groupIdPathwayIdMapping) {
|
|
1860
|
+
const verbatimMediaPlaylistUris = [];
|
|
1861
|
+
const globalGroupIds = [];
|
|
1862
|
+
const groupIdUriMappping = new Map();
|
|
1863
|
+
for (const tag of tags) {
|
|
1864
|
+
goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
|
|
1865
|
+
'Should only be called on media tags!');
|
|
1866
|
+
const uri = tag.getRequiredAttrValue('URI');
|
|
1867
|
+
const groupId = tag.getRequiredAttrValue('GROUP-ID');
|
|
1868
|
+
verbatimMediaPlaylistUris.push(uri);
|
|
1869
|
+
globalGroupIds.push(groupId);
|
|
1870
|
+
groupIdUriMappping.set(groupId, uri);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
const globalGroupId = globalGroupIds.sort().join(',');
|
|
1874
|
+
const firstTag = tags[0];
|
|
1820
1875
|
let codecs = '';
|
|
1821
1876
|
/** @type {string} */
|
|
1822
|
-
const type = this.getType_(
|
|
1877
|
+
const type = this.getType_(firstTag);
|
|
1823
1878
|
if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
|
|
1824
|
-
codecs =
|
|
1825
|
-
} else
|
|
1826
|
-
|
|
1879
|
+
codecs = firstTag.getAttributeValue('CODECS') || '';
|
|
1880
|
+
} else {
|
|
1881
|
+
for (const groupId of globalGroupIds) {
|
|
1882
|
+
if (this.groupIdToCodecsMap_.has(groupId)) {
|
|
1883
|
+
codecs = this.groupIdToCodecsMap_.get(groupId);
|
|
1884
|
+
break;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1827
1887
|
}
|
|
1828
1888
|
|
|
1829
|
-
const verbatimMediaPlaylistUri = this.variableSubstitution_(
|
|
1830
|
-
tag.getRequiredAttrValue('URI'), this.globalVariables_);
|
|
1831
|
-
|
|
1832
1889
|
// Check if the stream has already been created as part of another Variant
|
|
1833
1890
|
// and return it if it has.
|
|
1834
|
-
|
|
1835
|
-
|
|
1891
|
+
const key = verbatimMediaPlaylistUris.sort().join(',');
|
|
1892
|
+
if (this.uriToStreamInfosMap_.has(key)) {
|
|
1893
|
+
return this.uriToStreamInfosMap_.get(key);
|
|
1894
|
+
}
|
|
1895
|
+
const streamId = this.globalId_++;
|
|
1896
|
+
if (this.contentSteeringManager_) {
|
|
1897
|
+
for (const [groupId, uri] of groupIdUriMappping) {
|
|
1898
|
+
const pathwayId = groupIdPathwayIdMapping.get(groupId);
|
|
1899
|
+
if (pathwayId) {
|
|
1900
|
+
this.contentSteeringManager_.addLocation(streamId, pathwayId, uri);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1836
1903
|
}
|
|
1837
1904
|
|
|
1838
|
-
const language =
|
|
1839
|
-
const name =
|
|
1905
|
+
const language = firstTag.getAttributeValue('LANGUAGE');
|
|
1906
|
+
const name = firstTag.getAttributeValue('NAME');
|
|
1840
1907
|
|
|
1841
1908
|
// NOTE: According to the HLS spec, "DEFAULT=YES" requires "AUTOSELECT=YES".
|
|
1842
1909
|
// However, we don't bother to validate "AUTOSELECT", since we don't
|
|
@@ -1844,39 +1911,39 @@ shaka.hls.HlsParser = class {
|
|
|
1844
1911
|
// "AUTOSELECT=YES". A value of "AUTOSELECT=NO" would imply that it may
|
|
1845
1912
|
// only be selected explicitly by the user, and we don't have a way to
|
|
1846
1913
|
// represent that in our model.
|
|
1847
|
-
const defaultAttrValue =
|
|
1914
|
+
const defaultAttrValue = firstTag.getAttributeValue('DEFAULT');
|
|
1848
1915
|
const primary = defaultAttrValue == 'YES';
|
|
1849
1916
|
|
|
1850
|
-
const channelsCount =
|
|
1851
|
-
|
|
1852
|
-
const
|
|
1917
|
+
const channelsCount =
|
|
1918
|
+
type == 'audio' ? this.getChannelsCount_(firstTag) : null;
|
|
1919
|
+
const spatialAudio =
|
|
1920
|
+
type == 'audio' ? this.isSpatialAudio_(firstTag) : false;
|
|
1921
|
+
const characteristics = firstTag.getAttributeValue('CHARACTERISTICS');
|
|
1853
1922
|
|
|
1854
|
-
const forcedAttrValue =
|
|
1923
|
+
const forcedAttrValue = firstTag.getAttributeValue('FORCED');
|
|
1855
1924
|
const forced = forcedAttrValue == 'YES';
|
|
1856
|
-
const sampleRate = type == 'audio' ? this.getSampleRate_(
|
|
1925
|
+
const sampleRate = type == 'audio' ? this.getSampleRate_(firstTag) : null;
|
|
1857
1926
|
// TODO: Should we take into account some of the currently ignored
|
|
1858
1927
|
// attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj
|
|
1859
1928
|
const streamInfo = this.createStreamInfo_(
|
|
1860
|
-
|
|
1861
|
-
channelsCount, /* closedCaptions= */ null,
|
|
1862
|
-
sampleRate, spatialAudio);
|
|
1863
|
-
if (
|
|
1864
|
-
|
|
1929
|
+
streamId, verbatimMediaPlaylistUris, codecs, type, language,
|
|
1930
|
+
primary, name, channelsCount, /* closedCaptions= */ null,
|
|
1931
|
+
characteristics, forced, sampleRate, spatialAudio);
|
|
1932
|
+
if (streamInfo.stream) {
|
|
1933
|
+
streamInfo.stream.groupId = globalGroupId;
|
|
1934
|
+
}
|
|
1935
|
+
if (this.groupIdToStreamInfosMap_.has(globalGroupId)) {
|
|
1936
|
+
this.groupIdToStreamInfosMap_.get(globalGroupId).push(streamInfo);
|
|
1865
1937
|
} else {
|
|
1866
|
-
this.groupIdToStreamInfosMap_.set(
|
|
1938
|
+
this.groupIdToStreamInfosMap_.set(globalGroupId, [streamInfo]);
|
|
1867
1939
|
}
|
|
1868
1940
|
|
|
1869
|
-
|
|
1870
|
-
// calls to createStreamInfoFromMediaTag_ before either has resolved.
|
|
1871
|
-
if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
|
|
1872
|
-
return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
|
|
1873
|
-
}
|
|
1874
|
-
this.uriToStreamInfosMap_.set(verbatimMediaPlaylistUri, streamInfo);
|
|
1941
|
+
this.uriToStreamInfosMap_.set(key, streamInfo);
|
|
1875
1942
|
return streamInfo;
|
|
1876
1943
|
}
|
|
1877
1944
|
|
|
1878
1945
|
/**
|
|
1879
|
-
* Parse EXT-X-
|
|
1946
|
+
* Parse EXT-X-IMAGE-STREAM-INF media tag into a Stream object.
|
|
1880
1947
|
*
|
|
1881
1948
|
* @param {shaka.hls.Tag} tag
|
|
1882
1949
|
* @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
|
|
@@ -1888,8 +1955,7 @@ shaka.hls.HlsParser = class {
|
|
|
1888
1955
|
/** @type {string} */
|
|
1889
1956
|
const type = shaka.util.ManifestParserUtils.ContentType.IMAGE;
|
|
1890
1957
|
|
|
1891
|
-
const verbatimImagePlaylistUri =
|
|
1892
|
-
tag.getRequiredAttrValue('URI'), this.globalVariables_);
|
|
1958
|
+
const verbatimImagePlaylistUri = tag.getRequiredAttrValue('URI');
|
|
1893
1959
|
const codecs = tag.getAttributeValue('CODECS', 'jpeg') || '';
|
|
1894
1960
|
|
|
1895
1961
|
// Check if the stream has already been created as part of another Variant
|
|
@@ -1904,16 +1970,10 @@ shaka.hls.HlsParser = class {
|
|
|
1904
1970
|
const characteristics = tag.getAttributeValue('CHARACTERISTICS');
|
|
1905
1971
|
|
|
1906
1972
|
const streamInfo = this.createStreamInfo_(
|
|
1907
|
-
verbatimImagePlaylistUri, codecs, type, language,
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
/* spatialAudio= */ false);
|
|
1911
|
-
|
|
1912
|
-
// TODO: This check is necessary because of the possibility of multiple
|
|
1913
|
-
// calls to createStreamInfoFromImageTag_ before either has resolved.
|
|
1914
|
-
if (this.uriToStreamInfosMap_.has(verbatimImagePlaylistUri)) {
|
|
1915
|
-
return this.uriToStreamInfosMap_.get(verbatimImagePlaylistUri);
|
|
1916
|
-
}
|
|
1973
|
+
this.globalId_++, [verbatimImagePlaylistUri], codecs, type, language,
|
|
1974
|
+
/* primary= */ false, name, /* channelsCount= */ null,
|
|
1975
|
+
/* closedCaptions= */ null, characteristics, /* forced= */ false,
|
|
1976
|
+
/* sampleRate= */ null, /* spatialAudio= */ false);
|
|
1917
1977
|
|
|
1918
1978
|
// Parse misc attributes.
|
|
1919
1979
|
const resolution = tag.getAttributeValue('RESOLUTION');
|
|
@@ -1946,45 +2006,104 @@ shaka.hls.HlsParser = class {
|
|
|
1946
2006
|
return streamInfo;
|
|
1947
2007
|
}
|
|
1948
2008
|
|
|
2009
|
+
/**
|
|
2010
|
+
* Parse EXT-X-I-FRAME-STREAM-INF media tag into a Stream object.
|
|
2011
|
+
*
|
|
2012
|
+
* @param {shaka.hls.Tag} tag
|
|
2013
|
+
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
2014
|
+
* @private
|
|
2015
|
+
*/
|
|
2016
|
+
createStreamInfoFromIframeTag_(tag) {
|
|
2017
|
+
goog.asserts.assert(tag.name == 'EXT-X-I-FRAME-STREAM-INF',
|
|
2018
|
+
'Should only be called on iframe tags!');
|
|
2019
|
+
/** @type {string} */
|
|
2020
|
+
const type = shaka.util.ManifestParserUtils.ContentType.IMAGE;
|
|
2021
|
+
|
|
2022
|
+
const verbatimIFramePlaylistUri = tag.getRequiredAttrValue('URI');
|
|
2023
|
+
const codecs = tag.getAttributeValue('CODECS') || '';
|
|
2024
|
+
|
|
2025
|
+
// Check if the stream has already been created as part of another Variant
|
|
2026
|
+
// and return it if it has.
|
|
2027
|
+
if (this.uriToStreamInfosMap_.has(verbatimIFramePlaylistUri)) {
|
|
2028
|
+
return this.uriToStreamInfosMap_.get(verbatimIFramePlaylistUri);
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
const language = tag.getAttributeValue('LANGUAGE');
|
|
2032
|
+
const name = tag.getAttributeValue('NAME');
|
|
2033
|
+
|
|
2034
|
+
const characteristics = tag.getAttributeValue('CHARACTERISTICS');
|
|
2035
|
+
|
|
2036
|
+
const streamInfo = this.createStreamInfo_(
|
|
2037
|
+
this.globalId_++, [verbatimIFramePlaylistUri], codecs, type, language,
|
|
2038
|
+
/* primary= */ false, name, /* channelsCount= */ null,
|
|
2039
|
+
/* closedCaptions= */ null, characteristics, /* forced= */ false,
|
|
2040
|
+
/* sampleRate= */ null, /* spatialAudio= */ false);
|
|
2041
|
+
|
|
2042
|
+
// Parse misc attributes.
|
|
2043
|
+
const resolution = tag.getAttributeValue('RESOLUTION');
|
|
2044
|
+
const [width, height] = resolution ? resolution.split('x') : [null, null];
|
|
2045
|
+
streamInfo.stream.width = Number(width) || undefined;
|
|
2046
|
+
streamInfo.stream.height = Number(height) || undefined;
|
|
2047
|
+
const bandwidth = tag.getAttributeValue('BANDWIDTH');
|
|
2048
|
+
if (bandwidth) {
|
|
2049
|
+
streamInfo.stream.bandwidth = Number(bandwidth);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
this.uriToStreamInfosMap_.set(verbatimIFramePlaylistUri, streamInfo);
|
|
2053
|
+
return streamInfo;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1949
2056
|
/**
|
|
1950
2057
|
* Parse an EXT-X-STREAM-INF media tag into a Stream object.
|
|
1951
2058
|
*
|
|
1952
|
-
* @param {!shaka.hls.Tag}
|
|
2059
|
+
* @param {!Array.<!shaka.hls.Tag>} tags
|
|
1953
2060
|
* @param {!Array.<string>} allCodecs
|
|
1954
2061
|
* @param {string} type
|
|
2062
|
+
* @param {?string} language
|
|
2063
|
+
* @param {?string} name
|
|
2064
|
+
* @param {?number} channelsCount
|
|
2065
|
+
* @param {?string} characteristics
|
|
2066
|
+
* @param {?number} sampleRate
|
|
2067
|
+
* @param {boolean} spatialAudio
|
|
1955
2068
|
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
1956
2069
|
* @private
|
|
1957
2070
|
*/
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
const
|
|
1962
|
-
|
|
2071
|
+
createStreamInfoFromVariantTags_(tags, allCodecs, type, language, name,
|
|
2072
|
+
channelsCount, characteristics, sampleRate, spatialAudio) {
|
|
2073
|
+
const streamId = this.globalId_++;
|
|
2074
|
+
const verbatimMediaPlaylistUris = [];
|
|
2075
|
+
for (const tag of tags) {
|
|
2076
|
+
goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
|
|
2077
|
+
'Should only be called on variant tags!');
|
|
2078
|
+
const uri = tag.getRequiredAttrValue('URI');
|
|
2079
|
+
const pathwayId = tag.getAttributeValue('PATHWAY-ID');
|
|
2080
|
+
if (this.contentSteeringManager_ && pathwayId) {
|
|
2081
|
+
this.contentSteeringManager_.addLocation(streamId, pathwayId, uri);
|
|
2082
|
+
}
|
|
2083
|
+
verbatimMediaPlaylistUris.push(uri);
|
|
2084
|
+
}
|
|
1963
2085
|
|
|
1964
|
-
|
|
1965
|
-
|
|
2086
|
+
const key = verbatimMediaPlaylistUris.sort().join(',');
|
|
2087
|
+
if (this.uriToStreamInfosMap_.has(key)) {
|
|
2088
|
+
return this.uriToStreamInfosMap_.get(key);
|
|
1966
2089
|
}
|
|
1967
2090
|
|
|
1968
|
-
const closedCaptions = this.getClosedCaptions_(
|
|
2091
|
+
const closedCaptions = this.getClosedCaptions_(tags[0], type);
|
|
1969
2092
|
const codecs = shaka.util.ManifestParserUtils.guessCodecs(type, allCodecs);
|
|
1970
|
-
const streamInfo = this.createStreamInfo_(
|
|
1971
|
-
codecs, type,
|
|
1972
|
-
/*
|
|
1973
|
-
|
|
1974
|
-
/*
|
|
1975
|
-
// TODO: This check is necessary because of the possibility of multiple
|
|
1976
|
-
// calls to createStreamInfoFromVariantTag_ before either has resolved.
|
|
1977
|
-
if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
|
|
1978
|
-
return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
|
|
1979
|
-
}
|
|
2093
|
+
const streamInfo = this.createStreamInfo_(
|
|
2094
|
+
streamId, verbatimMediaPlaylistUris, codecs, type, language,
|
|
2095
|
+
/* primary= */ false, name, channelsCount, closedCaptions,
|
|
2096
|
+
characteristics, /* forced= */ false, sampleRate,
|
|
2097
|
+
/* spatialAudio= */ false);
|
|
1980
2098
|
|
|
1981
|
-
this.uriToStreamInfosMap_.set(
|
|
2099
|
+
this.uriToStreamInfosMap_.set(key, streamInfo);
|
|
1982
2100
|
return streamInfo;
|
|
1983
2101
|
}
|
|
1984
2102
|
|
|
1985
2103
|
|
|
1986
2104
|
/**
|
|
1987
|
-
* @param {
|
|
2105
|
+
* @param {number} streamId
|
|
2106
|
+
* @param {!Array.<string>} verbatimMediaPlaylistUris
|
|
1988
2107
|
* @param {string} codecs
|
|
1989
2108
|
* @param {string} type
|
|
1990
2109
|
* @param {?string} languageValue
|
|
@@ -1999,25 +2118,34 @@ shaka.hls.HlsParser = class {
|
|
|
1999
2118
|
* @return {!shaka.hls.HlsParser.StreamInfo}
|
|
2000
2119
|
* @private
|
|
2001
2120
|
*/
|
|
2002
|
-
createStreamInfo_(
|
|
2003
|
-
primary, name, channelsCount, closedCaptions,
|
|
2004
|
-
sampleRate, spatialAudio) {
|
|
2121
|
+
createStreamInfo_(streamId, verbatimMediaPlaylistUris, codecs, type,
|
|
2122
|
+
languageValue, primary, name, channelsCount, closedCaptions,
|
|
2123
|
+
characteristics, forced, sampleRate, spatialAudio) {
|
|
2005
2124
|
// TODO: Refactor, too many parameters
|
|
2006
|
-
const initialMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
|
|
2007
|
-
this.masterPlaylistUri_, verbatimMediaPlaylistUri);
|
|
2008
2125
|
|
|
2009
2126
|
// This stream is lazy-loaded inside the createSegmentIndex function.
|
|
2010
2127
|
// So we start out with a stream object that does not contain the actual
|
|
2011
2128
|
// segment index, then download when createSegmentIndex is called.
|
|
2012
|
-
const stream = this.makeStreamObject_(codecs, type,
|
|
2013
|
-
|
|
2014
|
-
sampleRate, spatialAudio);
|
|
2129
|
+
const stream = this.makeStreamObject_(streamId, codecs, type,
|
|
2130
|
+
languageValue, primary, name, channelsCount, closedCaptions,
|
|
2131
|
+
characteristics, forced, sampleRate, spatialAudio);
|
|
2132
|
+
|
|
2133
|
+
const redirectUris = [];
|
|
2134
|
+
const getUris = () => {
|
|
2135
|
+
if (this.contentSteeringManager_ &&
|
|
2136
|
+
verbatimMediaPlaylistUris.length > 1) {
|
|
2137
|
+
return this.contentSteeringManager_.getLocations(streamId);
|
|
2138
|
+
}
|
|
2139
|
+
return redirectUris.concat(shaka.hls.Utils.constructUris(
|
|
2140
|
+
[this.masterPlaylistUri_], verbatimMediaPlaylistUris,
|
|
2141
|
+
this.globalVariables_));
|
|
2142
|
+
};
|
|
2015
2143
|
const streamInfo = {
|
|
2016
2144
|
stream,
|
|
2017
2145
|
type,
|
|
2018
|
-
|
|
2146
|
+
redirectUris,
|
|
2147
|
+
getUris,
|
|
2019
2148
|
// These values are filled out or updated after lazy-loading:
|
|
2020
|
-
absoluteMediaPlaylistUri: initialMediaPlaylistUri,
|
|
2021
2149
|
minTimestamp: 0,
|
|
2022
2150
|
maxTimestamp: 0,
|
|
2023
2151
|
mediaSequenceToStartTime: new Map(),
|
|
@@ -2032,27 +2160,45 @@ shaka.hls.HlsParser = class {
|
|
|
2032
2160
|
|
|
2033
2161
|
/** @param {!AbortSignal} abortSignal */
|
|
2034
2162
|
const downloadSegmentIndex = async (abortSignal) => {
|
|
2163
|
+
const uris = streamInfo.getUris();
|
|
2035
2164
|
// Download the actual manifest.
|
|
2036
2165
|
const response = await this.requestManifest_(
|
|
2037
|
-
streamInfo.
|
|
2166
|
+
streamInfo.getUris(), /* isPlaylist= */ true);
|
|
2038
2167
|
if (abortSignal.aborted) {
|
|
2039
2168
|
return;
|
|
2040
2169
|
}
|
|
2041
2170
|
|
|
2042
2171
|
// Record the final URI after redirects.
|
|
2043
|
-
const
|
|
2172
|
+
const responseUri = response.uri;
|
|
2173
|
+
if (responseUri != response.originalUri && !uris.includes(responseUri)) {
|
|
2174
|
+
redirectUris.push(responseUri);
|
|
2175
|
+
}
|
|
2044
2176
|
|
|
2045
2177
|
// Record the redirected, final URI of this media playlist when we parse
|
|
2046
2178
|
// it.
|
|
2047
2179
|
/** @type {!shaka.hls.Playlist} */
|
|
2048
|
-
const playlist = this.manifestTextParser_.parsePlaylist(
|
|
2049
|
-
|
|
2180
|
+
const playlist = this.manifestTextParser_.parsePlaylist(response.data);
|
|
2181
|
+
|
|
2182
|
+
let mimeType = undefined;
|
|
2183
|
+
|
|
2184
|
+
// If no codec info was provided in the manifest and codec guessing is
|
|
2185
|
+
// disabled we try to get necessary info from the media data.
|
|
2186
|
+
if (!this.codecInfoInManifest_ && this.config_.hls.disableCodecGuessing) {
|
|
2187
|
+
const basicInfo =
|
|
2188
|
+
await this.getMediaPlaylistBasicInfo_(playlist, getUris);
|
|
2189
|
+
|
|
2190
|
+
goog.asserts.assert(
|
|
2191
|
+
type === basicInfo.type, 'Media types should match!');
|
|
2192
|
+
|
|
2193
|
+
mimeType = basicInfo.mimeType;
|
|
2194
|
+
codecs = basicInfo.codecs;
|
|
2195
|
+
}
|
|
2050
2196
|
|
|
2051
2197
|
const wasLive = this.isLive_();
|
|
2052
2198
|
const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
|
|
2053
|
-
playlist,
|
|
2199
|
+
streamId, playlist, getUris, responseUri, codecs,
|
|
2054
2200
|
type, languageValue, primary, name, channelsCount, closedCaptions,
|
|
2055
|
-
characteristics, forced, sampleRate, spatialAudio);
|
|
2201
|
+
characteristics, forced, sampleRate, spatialAudio, mimeType);
|
|
2056
2202
|
if (abortSignal.aborted) {
|
|
2057
2203
|
return;
|
|
2058
2204
|
}
|
|
@@ -2066,7 +2212,6 @@ shaka.hls.HlsParser = class {
|
|
|
2066
2212
|
}
|
|
2067
2213
|
|
|
2068
2214
|
// Copy values from the real stream info to our initial one.
|
|
2069
|
-
streamInfo.absoluteMediaPlaylistUri = absoluteMediaPlaylistUri;
|
|
2070
2215
|
streamInfo.minTimestamp = realStreamInfo.minTimestamp;
|
|
2071
2216
|
streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
|
|
2072
2217
|
streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
|
|
@@ -2083,6 +2228,7 @@ shaka.hls.HlsParser = class {
|
|
|
2083
2228
|
stream.keyIds = realStream.keyIds;
|
|
2084
2229
|
stream.mimeType = realStream.mimeType;
|
|
2085
2230
|
stream.bandwidth = realStream.bandwidth;
|
|
2231
|
+
stream.codecs = realStream.codecs || stream.codecs;
|
|
2086
2232
|
|
|
2087
2233
|
// Since we lazy-loaded this content, the player may need to create new
|
|
2088
2234
|
// sessions for the DRM info in this stream.
|
|
@@ -2284,9 +2430,10 @@ shaka.hls.HlsParser = class {
|
|
|
2284
2430
|
}
|
|
2285
2431
|
|
|
2286
2432
|
/**
|
|
2433
|
+
* @param {number} streamId
|
|
2287
2434
|
* @param {!shaka.hls.Playlist} playlist
|
|
2288
|
-
* @param {string}
|
|
2289
|
-
* @param {string}
|
|
2435
|
+
* @param {function():!Array.<string>} getUris
|
|
2436
|
+
* @param {string} responseUri
|
|
2290
2437
|
* @param {string} codecs
|
|
2291
2438
|
* @param {string} type
|
|
2292
2439
|
* @param {?string} languageValue
|
|
@@ -2302,8 +2449,8 @@ shaka.hls.HlsParser = class {
|
|
|
2302
2449
|
* @return {!Promise.<!shaka.hls.HlsParser.StreamInfo>}
|
|
2303
2450
|
* @private
|
|
2304
2451
|
*/
|
|
2305
|
-
async convertParsedPlaylistIntoStreamInfo_(
|
|
2306
|
-
|
|
2452
|
+
async convertParsedPlaylistIntoStreamInfo_(streamId, playlist,
|
|
2453
|
+
getUris, responseUri, codecs, type, languageValue, primary, name,
|
|
2307
2454
|
channelsCount, closedCaptions, characteristics, forced, sampleRate,
|
|
2308
2455
|
spatialAudio, mimeType = undefined) {
|
|
2309
2456
|
if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
|
|
@@ -2319,7 +2466,8 @@ shaka.hls.HlsParser = class {
|
|
|
2319
2466
|
const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
|
|
2320
2467
|
'EXT-X-DEFINE');
|
|
2321
2468
|
|
|
2322
|
-
const mediaVariables =
|
|
2469
|
+
const mediaVariables =
|
|
2470
|
+
this.parseMediaVariables_(variablesTags, responseUri);
|
|
2323
2471
|
|
|
2324
2472
|
goog.asserts.assert(playlist.segments != null,
|
|
2325
2473
|
'Media playlist should have segments!');
|
|
@@ -2332,7 +2480,7 @@ shaka.hls.HlsParser = class {
|
|
|
2332
2480
|
|
|
2333
2481
|
if (!mimeType) {
|
|
2334
2482
|
mimeType = await this.guessMimeType_(type, codecs, playlist,
|
|
2335
|
-
mediaVariables);
|
|
2483
|
+
mediaVariables, getUris);
|
|
2336
2484
|
}
|
|
2337
2485
|
|
|
2338
2486
|
const {drmInfos, keyIds, encrypted, aesEncrypted} =
|
|
@@ -2345,11 +2493,20 @@ shaka.hls.HlsParser = class {
|
|
|
2345
2493
|
shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
|
|
2346
2494
|
}
|
|
2347
2495
|
|
|
2496
|
+
const stream = this.makeStreamObject_(streamId, codecs, type,
|
|
2497
|
+
languageValue, primary, name, channelsCount, closedCaptions,
|
|
2498
|
+
characteristics, forced, sampleRate, spatialAudio);
|
|
2499
|
+
stream.encrypted = encrypted;
|
|
2500
|
+
stream.drmInfos = drmInfos;
|
|
2501
|
+
stream.keyIds = keyIds;
|
|
2502
|
+
stream.mimeType = mimeType;
|
|
2503
|
+
|
|
2348
2504
|
const mediaSequenceToStartTime = this.isLive_() ?
|
|
2349
2505
|
this.mediaSequenceToStartTimeByType_.get(type) : new Map();
|
|
2506
|
+
|
|
2350
2507
|
const {segments, bandwidth} = this.createSegments_(
|
|
2351
|
-
|
|
2352
|
-
|
|
2508
|
+
playlist, stream, mediaSequenceToStartTime, mediaVariables, getUris);
|
|
2509
|
+
stream.bandwidth = bandwidth;
|
|
2353
2510
|
|
|
2354
2511
|
// This new calculation is necessary for Low Latency streams.
|
|
2355
2512
|
if (this.isLive_()) {
|
|
@@ -2361,6 +2518,7 @@ shaka.hls.HlsParser = class {
|
|
|
2361
2518
|
const lastEndTime = lastSegment.endTime;
|
|
2362
2519
|
/** @type {!shaka.media.SegmentIndex} */
|
|
2363
2520
|
const segmentIndex = new shaka.media.SegmentIndex(segments);
|
|
2521
|
+
stream.segmentIndex = segmentIndex;
|
|
2364
2522
|
|
|
2365
2523
|
const serverControlTag = shaka.hls.Utils.getFirstTagWithName(
|
|
2366
2524
|
playlist.tags, 'EXT-X-SERVER-CONTROL');
|
|
@@ -2375,21 +2533,11 @@ shaka.hls.HlsParser = class {
|
|
|
2375
2533
|
const {nextMediaSequence, nextPart} =
|
|
2376
2534
|
this.getNextMediaSequenceAndPart_(mediaSequenceNumber, segments);
|
|
2377
2535
|
|
|
2378
|
-
const stream = this.makeStreamObject_(codecs, type, languageValue, primary,
|
|
2379
|
-
name, channelsCount, closedCaptions, characteristics, forced,
|
|
2380
|
-
sampleRate, spatialAudio);
|
|
2381
|
-
stream.segmentIndex = segmentIndex;
|
|
2382
|
-
stream.encrypted = encrypted;
|
|
2383
|
-
stream.drmInfos = drmInfos;
|
|
2384
|
-
stream.keyIds = keyIds;
|
|
2385
|
-
stream.mimeType = mimeType;
|
|
2386
|
-
stream.bandwidth = bandwidth;
|
|
2387
|
-
|
|
2388
2536
|
return {
|
|
2389
2537
|
stream,
|
|
2390
2538
|
type,
|
|
2391
|
-
|
|
2392
|
-
|
|
2539
|
+
redirectUris: [],
|
|
2540
|
+
getUris,
|
|
2393
2541
|
minTimestamp: firstStartTime,
|
|
2394
2542
|
maxTimestamp: lastEndTime,
|
|
2395
2543
|
canSkipSegments,
|
|
@@ -2453,6 +2601,7 @@ shaka.hls.HlsParser = class {
|
|
|
2453
2601
|
* The parameters that are passed into here are only the things that can be
|
|
2454
2602
|
* known without downloading the media playlist; other values must be set
|
|
2455
2603
|
* manually on the object after creation.
|
|
2604
|
+
* @param {number} id
|
|
2456
2605
|
* @param {string} codecs
|
|
2457
2606
|
* @param {string} type
|
|
2458
2607
|
* @param {?string} languageValue
|
|
@@ -2467,8 +2616,9 @@ shaka.hls.HlsParser = class {
|
|
|
2467
2616
|
* @return {!shaka.extern.Stream}
|
|
2468
2617
|
* @private
|
|
2469
2618
|
*/
|
|
2470
|
-
makeStreamObject_(codecs, type, languageValue, primary, name,
|
|
2471
|
-
closedCaptions, characteristics, forced, sampleRate,
|
|
2619
|
+
makeStreamObject_(id, codecs, type, languageValue, primary, name,
|
|
2620
|
+
channelsCount, closedCaptions, characteristics, forced, sampleRate,
|
|
2621
|
+
spatialAudio) {
|
|
2472
2622
|
// Fill out a "best-guess" mimeType, for now. It will be replaced once the
|
|
2473
2623
|
// stream is lazy-loaded.
|
|
2474
2624
|
const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
|
|
@@ -2480,8 +2630,21 @@ shaka.hls.HlsParser = class {
|
|
|
2480
2630
|
}
|
|
2481
2631
|
}
|
|
2482
2632
|
|
|
2483
|
-
|
|
2484
|
-
|
|
2633
|
+
let kind = undefined;
|
|
2634
|
+
let accessibilityPurpose = null;
|
|
2635
|
+
if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
|
|
2636
|
+
if (roles.includes('public.accessibility.transcribes-spoken-dialog') &&
|
|
2637
|
+
roles.includes('public.accessibility.describes-music-and-sound')) {
|
|
2638
|
+
kind = shaka.util.ManifestParserUtils.TextStreamKind.CLOSED_CAPTION;
|
|
2639
|
+
} else {
|
|
2640
|
+
kind = shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE;
|
|
2641
|
+
}
|
|
2642
|
+
} else {
|
|
2643
|
+
if (roles.includes('public.accessibility.describes-video')) {
|
|
2644
|
+
accessibilityPurpose =
|
|
2645
|
+
shaka.media.ManifestParser.AccessibilityPurpose.VISUALLY_IMPAIRED;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2485
2648
|
|
|
2486
2649
|
// If there are no roles, and we have defaulted to the subtitle "kind" for
|
|
2487
2650
|
// this track, add the implied subtitle role.
|
|
@@ -2493,6 +2656,7 @@ shaka.hls.HlsParser = class {
|
|
|
2493
2656
|
return {
|
|
2494
2657
|
id: this.globalId_++,
|
|
2495
2658
|
originalId: name,
|
|
2659
|
+
groupId: null,
|
|
2496
2660
|
createSegmentIndex: () => Promise.resolve(),
|
|
2497
2661
|
segmentIndex: null,
|
|
2498
2662
|
mimeType,
|
|
@@ -2522,9 +2686,11 @@ shaka.hls.HlsParser = class {
|
|
|
2522
2686
|
spatialAudio,
|
|
2523
2687
|
closedCaptions,
|
|
2524
2688
|
hdr: undefined,
|
|
2689
|
+
videoLayout: undefined,
|
|
2525
2690
|
tilesLayout: undefined,
|
|
2526
|
-
accessibilityPurpose:
|
|
2691
|
+
accessibilityPurpose: accessibilityPurpose,
|
|
2527
2692
|
external: false,
|
|
2693
|
+
fastSwitching: false,
|
|
2528
2694
|
};
|
|
2529
2695
|
}
|
|
2530
2696
|
|
|
@@ -2595,10 +2761,12 @@ shaka.hls.HlsParser = class {
|
|
|
2595
2761
|
/**
|
|
2596
2762
|
* @param {!shaka.hls.Tag} drmTag
|
|
2597
2763
|
* @param {!shaka.hls.Playlist} playlist
|
|
2598
|
-
* @
|
|
2764
|
+
* @param {function():!Array.<string>} getUris
|
|
2765
|
+
* @param {?Map.<string, string>=} variables
|
|
2766
|
+
* @return {!shaka.extern.aes128Key}
|
|
2599
2767
|
* @private
|
|
2600
2768
|
*/
|
|
2601
|
-
parseAES128DrmTag_(drmTag, playlist) {
|
|
2769
|
+
parseAES128DrmTag_(drmTag, playlist, getUris, variables) {
|
|
2602
2770
|
// Check if the Web Crypto API is available.
|
|
2603
2771
|
if (!window.crypto || !window.crypto.subtle) {
|
|
2604
2772
|
shaka.log.alwaysWarn('Web Crypto API is not available to decrypt ' +
|
|
@@ -2629,22 +2797,21 @@ shaka.hls.HlsParser = class {
|
|
|
2629
2797
|
throw new shaka.util.Error(
|
|
2630
2798
|
shaka.util.Error.Severity.CRITICAL,
|
|
2631
2799
|
shaka.util.Error.Category.MANIFEST,
|
|
2632
|
-
shaka.util.Error.Code.
|
|
2800
|
+
shaka.util.Error.Code.AES_128_INVALID_IV_LENGTH);
|
|
2633
2801
|
}
|
|
2634
2802
|
}
|
|
2635
2803
|
|
|
2636
|
-
const keyUri = shaka.hls.Utils.constructAbsoluteUri(
|
|
2637
|
-
playlist.absoluteUri, drmTag.getRequiredAttrValue('URI'));
|
|
2638
|
-
|
|
2639
|
-
const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
|
|
2640
|
-
const request = shaka.net.NetworkingEngine.makeRequest(
|
|
2641
|
-
[keyUri], this.config_.retryParameters);
|
|
2642
|
-
|
|
2643
2804
|
const keyInfo = {method: 'AES-128', iv, firstMediaSequenceNumber};
|
|
2644
2805
|
|
|
2645
2806
|
// Don't download the key object until the segment is parsed, to avoid a
|
|
2646
2807
|
// startup delay for long manifests with lots of keys.
|
|
2647
2808
|
keyInfo.fetchKey = async () => {
|
|
2809
|
+
const keyUri = shaka.hls.Utils.constructSegmentUris(
|
|
2810
|
+
getUris(), drmTag.getRequiredAttrValue('URI'), variables);
|
|
2811
|
+
|
|
2812
|
+
const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
|
|
2813
|
+
const request = shaka.net.NetworkingEngine.makeRequest(
|
|
2814
|
+
[keyUri], this.config_.retryParameters);
|
|
2648
2815
|
const keyResponse = await this.makeNetworkRequest_(request, requestType);
|
|
2649
2816
|
|
|
2650
2817
|
// keyResponse.status is undefined when URI is "data:text/plain;base64,"
|
|
@@ -2652,7 +2819,7 @@ shaka.hls.HlsParser = class {
|
|
|
2652
2819
|
throw new shaka.util.Error(
|
|
2653
2820
|
shaka.util.Error.Severity.CRITICAL,
|
|
2654
2821
|
shaka.util.Error.Category.MANIFEST,
|
|
2655
|
-
shaka.util.Error.Code.
|
|
2822
|
+
shaka.util.Error.Code.AES_128_INVALID_KEY_LENGTH);
|
|
2656
2823
|
}
|
|
2657
2824
|
|
|
2658
2825
|
const algorithm = {
|
|
@@ -2821,11 +2988,12 @@ shaka.hls.HlsParser = class {
|
|
|
2821
2988
|
* Get the InitSegmentReference for a segment if it has a EXT-X-MAP tag.
|
|
2822
2989
|
* @param {!shaka.hls.Playlist} playlist
|
|
2823
2990
|
* @param {!Array.<!shaka.hls.Tag>} tags Segment tags
|
|
2824
|
-
* @param {
|
|
2991
|
+
* @param {function():!Array.<string>} getUris
|
|
2992
|
+
* @param {?Map.<string, string>=} variables
|
|
2825
2993
|
* @return {shaka.media.InitSegmentReference}
|
|
2826
2994
|
* @private
|
|
2827
2995
|
*/
|
|
2828
|
-
getInitSegmentReference_(playlist, tags, variables) {
|
|
2996
|
+
getInitSegmentReference_(playlist, tags, getUris, variables) {
|
|
2829
2997
|
/** @type {?shaka.hls.Tag} */
|
|
2830
2998
|
const mapTag = shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-MAP');
|
|
2831
2999
|
|
|
@@ -2834,28 +3002,30 @@ shaka.hls.HlsParser = class {
|
|
|
2834
3002
|
}
|
|
2835
3003
|
// Map tag example: #EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"
|
|
2836
3004
|
const verbatimInitSegmentUri = mapTag.getRequiredAttrValue('URI');
|
|
2837
|
-
const
|
|
2838
|
-
|
|
2839
|
-
playlist.absoluteUri, verbatimInitSegmentUri),
|
|
2840
|
-
variables);
|
|
3005
|
+
const absoluteInitSegmentUris = shaka.hls.Utils.constructSegmentUris(
|
|
3006
|
+
getUris(), verbatimInitSegmentUri, variables);
|
|
2841
3007
|
|
|
2842
3008
|
const mapTagKey = [
|
|
2843
|
-
|
|
3009
|
+
absoluteInitSegmentUris.toString(),
|
|
2844
3010
|
mapTag.getAttributeValue('BYTERANGE', ''),
|
|
2845
3011
|
].join('-');
|
|
2846
3012
|
if (!this.mapTagToInitSegmentRefMap_.has(mapTagKey)) {
|
|
2847
|
-
/** @type {shaka.extern.
|
|
3013
|
+
/** @type {shaka.extern.aes128Key|undefined} */
|
|
2848
3014
|
let aes128Key = undefined;
|
|
3015
|
+
let byteRangeTag = null;
|
|
2849
3016
|
for (const tag of tags) {
|
|
2850
3017
|
if (tag.name == 'EXT-X-KEY') {
|
|
2851
3018
|
if (tag.getRequiredAttrValue('METHOD') == 'AES-128' &&
|
|
2852
3019
|
tag.id < mapTag.id) {
|
|
2853
|
-
aes128Key =
|
|
3020
|
+
aes128Key =
|
|
3021
|
+
this.parseAES128DrmTag_(tag, playlist, getUris, variables);
|
|
2854
3022
|
}
|
|
3023
|
+
} else if (tag.name == 'EXT-X-BYTERANGE' && tag.id < mapTag.id) {
|
|
3024
|
+
byteRangeTag = tag;
|
|
2855
3025
|
}
|
|
2856
3026
|
}
|
|
2857
3027
|
const initSegmentRef = this.createInitSegmentReference_(
|
|
2858
|
-
|
|
3028
|
+
absoluteInitSegmentUris, mapTag, byteRangeTag, aes128Key);
|
|
2859
3029
|
this.mapTagToInitSegmentRefMap_.set(mapTagKey, initSegmentRef);
|
|
2860
3030
|
}
|
|
2861
3031
|
return this.mapTagToInitSegmentRefMap_.get(mapTagKey);
|
|
@@ -2864,16 +3034,21 @@ shaka.hls.HlsParser = class {
|
|
|
2864
3034
|
/**
|
|
2865
3035
|
* Create an InitSegmentReference object for the EXT-X-MAP tag in the media
|
|
2866
3036
|
* playlist.
|
|
2867
|
-
* @param {string}
|
|
3037
|
+
* @param {!Array.<string>} absoluteInitSegmentUris
|
|
2868
3038
|
* @param {!shaka.hls.Tag} mapTag EXT-X-MAP
|
|
2869
|
-
* @param {shaka.
|
|
3039
|
+
* @param {shaka.hls.Tag=} byteRangeTag EXT-X-BYTERANGE
|
|
3040
|
+
* @param {shaka.extern.aes128Key=} aes128Key
|
|
2870
3041
|
* @return {!shaka.media.InitSegmentReference}
|
|
2871
3042
|
* @private
|
|
2872
3043
|
*/
|
|
2873
|
-
createInitSegmentReference_(
|
|
3044
|
+
createInitSegmentReference_(absoluteInitSegmentUris, mapTag, byteRangeTag,
|
|
3045
|
+
aes128Key) {
|
|
2874
3046
|
let startByte = 0;
|
|
2875
3047
|
let endByte = null;
|
|
2876
|
-
|
|
3048
|
+
let byterange = mapTag.getAttributeValue('BYTERANGE');
|
|
3049
|
+
if (!byterange && byteRangeTag) {
|
|
3050
|
+
byterange = byteRangeTag.value;
|
|
3051
|
+
}
|
|
2877
3052
|
// If a BYTERANGE attribute is not specified, the segment consists
|
|
2878
3053
|
// of the entire resource.
|
|
2879
3054
|
if (byterange) {
|
|
@@ -2894,7 +3069,7 @@ shaka.hls.HlsParser = class {
|
|
|
2894
3069
|
}
|
|
2895
3070
|
|
|
2896
3071
|
const initSegmentRef = new shaka.media.InitSegmentReference(
|
|
2897
|
-
() =>
|
|
3072
|
+
() => absoluteInitSegmentUris,
|
|
2898
3073
|
startByte,
|
|
2899
3074
|
endByte,
|
|
2900
3075
|
/* mediaQuality= */ null,
|
|
@@ -2912,16 +3087,16 @@ shaka.hls.HlsParser = class {
|
|
|
2912
3087
|
* @param {!shaka.hls.Segment} hlsSegment
|
|
2913
3088
|
* @param {number} startTime
|
|
2914
3089
|
* @param {!Map.<string, string>} variables
|
|
2915
|
-
* @param {
|
|
2916
|
-
* @param {
|
|
2917
|
-
* @param {string}
|
|
2918
|
-
* @param {shaka.extern.
|
|
3090
|
+
* @param {!shaka.hls.Playlist} playlist
|
|
3091
|
+
* @param {shaka.extern.Stream} stream
|
|
3092
|
+
* @param {function():!Array.<string>} getUris
|
|
3093
|
+
* @param {shaka.extern.aes128Key=} aes128Key
|
|
2919
3094
|
* @return {shaka.media.SegmentReference}
|
|
2920
3095
|
* @private
|
|
2921
3096
|
*/
|
|
2922
3097
|
createSegmentReference_(
|
|
2923
3098
|
initSegmentReference, previousReference, hlsSegment, startTime,
|
|
2924
|
-
variables,
|
|
3099
|
+
variables, playlist, stream, getUris, aes128Key) {
|
|
2925
3100
|
const tags = hlsSegment.tags;
|
|
2926
3101
|
const extinfTag =
|
|
2927
3102
|
shaka.hls.Utils.getFirstTagWithName(tags, 'EXTINF');
|
|
@@ -2978,8 +3153,9 @@ shaka.hls.HlsParser = class {
|
|
|
2978
3153
|
let isPreloadSegment = false;
|
|
2979
3154
|
|
|
2980
3155
|
if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) {
|
|
2981
|
-
const byterangeOptimizationSupport = (mimeType == 'video/mp4' ||
|
|
2982
|
-
mimeType == 'audio/mp4') && window.ReadableStream
|
|
3156
|
+
const byterangeOptimizationSupport = (stream.mimeType == 'video/mp4' ||
|
|
3157
|
+
stream.mimeType == 'audio/mp4') && window.ReadableStream &&
|
|
3158
|
+
this.config_.hls.allowLowLatencyByteRangeOptimization;
|
|
2983
3159
|
|
|
2984
3160
|
let partialSyncTime = syncTime;
|
|
2985
3161
|
for (let i = 0; i < hlsSegment.partialSegments.length; i++) {
|
|
@@ -3035,14 +3211,14 @@ shaka.hls.HlsParser = class {
|
|
|
3035
3211
|
somePartialSegmentWithGap = true;
|
|
3036
3212
|
}
|
|
3037
3213
|
|
|
3038
|
-
let
|
|
3214
|
+
let uris = null;
|
|
3039
3215
|
const getPartialUris = () => {
|
|
3040
|
-
if (
|
|
3216
|
+
if (uris == null) {
|
|
3041
3217
|
goog.asserts.assert(pUri, 'Partial uri should be defined!');
|
|
3042
|
-
|
|
3043
|
-
|
|
3218
|
+
uris = shaka.hls.Utils.constructSegmentUris(
|
|
3219
|
+
getUris(), pUri, variables);
|
|
3044
3220
|
}
|
|
3045
|
-
return
|
|
3221
|
+
return uris;
|
|
3046
3222
|
};
|
|
3047
3223
|
|
|
3048
3224
|
if (byterangeOptimizationSupport &&
|
|
@@ -3066,7 +3242,7 @@ shaka.hls.HlsParser = class {
|
|
|
3066
3242
|
/* tileDuration= */ null,
|
|
3067
3243
|
partialSyncTime,
|
|
3068
3244
|
partialStatus,
|
|
3069
|
-
|
|
3245
|
+
aes128Key);
|
|
3070
3246
|
if (item.name == 'EXT-X-PRELOAD-HINT') {
|
|
3071
3247
|
partial.markAsPreload();
|
|
3072
3248
|
isPreloadSegment = true;
|
|
@@ -3137,7 +3313,7 @@ shaka.hls.HlsParser = class {
|
|
|
3137
3313
|
|
|
3138
3314
|
let tilesLayout = '';
|
|
3139
3315
|
let tileDuration = null;
|
|
3140
|
-
if (type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
|
|
3316
|
+
if (stream.type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
|
|
3141
3317
|
// By default in HLS the tilesLayout is 1x1
|
|
3142
3318
|
tilesLayout = '1x1';
|
|
3143
3319
|
const tilesTag =
|
|
@@ -3151,22 +3327,25 @@ shaka.hls.HlsParser = class {
|
|
|
3151
3327
|
}
|
|
3152
3328
|
}
|
|
3153
3329
|
|
|
3154
|
-
let
|
|
3155
|
-
const
|
|
3330
|
+
let uris = null;
|
|
3331
|
+
const getSegmentUris = () => {
|
|
3156
3332
|
if (getUrisOptimization) {
|
|
3157
3333
|
return getUrisOptimization();
|
|
3158
3334
|
}
|
|
3159
|
-
if (
|
|
3160
|
-
|
|
3161
|
-
hlsSegment.
|
|
3335
|
+
if (uris == null) {
|
|
3336
|
+
uris = shaka.hls.Utils.constructSegmentUris(getUris(),
|
|
3337
|
+
hlsSegment.verbatimSegmentUri, variables);
|
|
3162
3338
|
}
|
|
3163
|
-
return
|
|
3339
|
+
return uris || [];
|
|
3164
3340
|
};
|
|
3165
3341
|
|
|
3342
|
+
const allPartialSegments = partialSegmentRefs.length > 0 &&
|
|
3343
|
+
!!hlsSegment.verbatimSegmentUri;
|
|
3344
|
+
|
|
3166
3345
|
const reference = new shaka.media.SegmentReference(
|
|
3167
3346
|
startTime,
|
|
3168
3347
|
endTime,
|
|
3169
|
-
|
|
3348
|
+
getSegmentUris,
|
|
3170
3349
|
startByte,
|
|
3171
3350
|
endByte,
|
|
3172
3351
|
initSegmentReference,
|
|
@@ -3178,7 +3357,8 @@ shaka.hls.HlsParser = class {
|
|
|
3178
3357
|
tileDuration,
|
|
3179
3358
|
syncTime,
|
|
3180
3359
|
status,
|
|
3181
|
-
|
|
3360
|
+
aes128Key,
|
|
3361
|
+
allPartialSegments,
|
|
3182
3362
|
);
|
|
3183
3363
|
|
|
3184
3364
|
if (segmentWithByteRangeOptimization) {
|
|
@@ -3226,18 +3406,17 @@ shaka.hls.HlsParser = class {
|
|
|
3226
3406
|
* get the bandwidth necessary for this segments If it's defined in the
|
|
3227
3407
|
* playlist.
|
|
3228
3408
|
*
|
|
3229
|
-
* @param {string} verbatimMediaPlaylistUri
|
|
3230
3409
|
* @param {!shaka.hls.Playlist} playlist
|
|
3231
|
-
* @param {
|
|
3232
|
-
* @param {string} mimeType
|
|
3410
|
+
* @param {shaka.extern.Stream} stream
|
|
3233
3411
|
* @param {!Map.<number, number>} mediaSequenceToStartTime
|
|
3234
3412
|
* @param {!Map.<string, string>} variables
|
|
3413
|
+
* @param {function():!Array.<string>} getUris
|
|
3235
3414
|
* @return {{segments: !Array.<!shaka.media.SegmentReference>,
|
|
3236
3415
|
* bandwidth: (number|undefined)}}
|
|
3237
3416
|
* @private
|
|
3238
3417
|
*/
|
|
3239
|
-
createSegments_(
|
|
3240
|
-
|
|
3418
|
+
createSegments_(playlist, stream, mediaSequenceToStartTime, variables,
|
|
3419
|
+
getUris) {
|
|
3241
3420
|
/** @type {Array.<!shaka.hls.Segment>} */
|
|
3242
3421
|
const hlsSegments = playlist.segments;
|
|
3243
3422
|
goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
|
|
@@ -3245,8 +3424,8 @@ shaka.hls.HlsParser = class {
|
|
|
3245
3424
|
/** @type {shaka.media.InitSegmentReference} */
|
|
3246
3425
|
let initSegmentRef;
|
|
3247
3426
|
|
|
3248
|
-
/** @type {shaka.extern.
|
|
3249
|
-
let
|
|
3427
|
+
/** @type {shaka.extern.aes128Key|undefined} */
|
|
3428
|
+
let aes128Key = undefined;
|
|
3250
3429
|
|
|
3251
3430
|
let discontinuitySequence = shaka.hls.Utils.getFirstTagWithNameAsNumber(
|
|
3252
3431
|
playlist.tags, 'EXT-X-DISCONTINUITY-SEQUENCE', 0);
|
|
@@ -3289,9 +3468,10 @@ shaka.hls.HlsParser = class {
|
|
|
3289
3468
|
for (const drmTag of item.tags) {
|
|
3290
3469
|
if (drmTag.name == 'EXT-X-KEY') {
|
|
3291
3470
|
if (drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
|
|
3292
|
-
|
|
3471
|
+
aes128Key =
|
|
3472
|
+
this.parseAES128DrmTag_(drmTag, playlist, getUris, variables);
|
|
3293
3473
|
} else {
|
|
3294
|
-
|
|
3474
|
+
aes128Key = undefined;
|
|
3295
3475
|
}
|
|
3296
3476
|
}
|
|
3297
3477
|
}
|
|
@@ -3299,7 +3479,7 @@ shaka.hls.HlsParser = class {
|
|
|
3299
3479
|
mediaSequenceToStartTime.set(position, startTime);
|
|
3300
3480
|
|
|
3301
3481
|
initSegmentRef = this.getInitSegmentReference_(playlist,
|
|
3302
|
-
item.tags, variables);
|
|
3482
|
+
item.tags, getUris, variables);
|
|
3303
3483
|
|
|
3304
3484
|
// If the stream is low latency and the user has not configured the
|
|
3305
3485
|
// lowLatencyMode, but if it has been configured to activate the
|
|
@@ -3319,10 +3499,10 @@ shaka.hls.HlsParser = class {
|
|
|
3319
3499
|
item,
|
|
3320
3500
|
startTime,
|
|
3321
3501
|
variables,
|
|
3322
|
-
playlist
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3502
|
+
playlist,
|
|
3503
|
+
stream,
|
|
3504
|
+
getUris,
|
|
3505
|
+
aes128Key);
|
|
3326
3506
|
|
|
3327
3507
|
if (reference) {
|
|
3328
3508
|
const bitrate = shaka.hls.Utils.getFirstTagWithNameAsNumber(
|
|
@@ -3455,42 +3635,6 @@ shaka.hls.HlsParser = class {
|
|
|
3455
3635
|
};
|
|
3456
3636
|
}
|
|
3457
3637
|
|
|
3458
|
-
/**
|
|
3459
|
-
* Replaces the variables of a given URI.
|
|
3460
|
-
*
|
|
3461
|
-
* @param {string} uri
|
|
3462
|
-
* @param {!Map.<string, string>} variables
|
|
3463
|
-
* @return {string}
|
|
3464
|
-
* @private
|
|
3465
|
-
*/
|
|
3466
|
-
variableSubstitution_(uri, variables) {
|
|
3467
|
-
if (!variables.size) {
|
|
3468
|
-
return uri;
|
|
3469
|
-
}
|
|
3470
|
-
let newUri = String(uri).replace(/%7B/g, '{').replace(/%7D/g, '}');
|
|
3471
|
-
|
|
3472
|
-
const uriVariables = newUri.match(/{\$\w*}/g);
|
|
3473
|
-
if (uriVariables) {
|
|
3474
|
-
for (const variable of uriVariables) {
|
|
3475
|
-
// Note: All variables have the structure {$...}
|
|
3476
|
-
const variableName = variable.slice(2, variable.length - 1);
|
|
3477
|
-
const replaceValue = variables.get(variableName);
|
|
3478
|
-
if (replaceValue) {
|
|
3479
|
-
newUri = newUri.replace(variable, replaceValue);
|
|
3480
|
-
} else {
|
|
3481
|
-
shaka.log.error('A variable has been found that is not declared',
|
|
3482
|
-
variableName);
|
|
3483
|
-
throw new shaka.util.Error(
|
|
3484
|
-
shaka.util.Error.Severity.CRITICAL,
|
|
3485
|
-
shaka.util.Error.Category.MANIFEST,
|
|
3486
|
-
shaka.util.Error.Code.HLS_VARIABLE_NOT_FOUND,
|
|
3487
|
-
variableName);
|
|
3488
|
-
}
|
|
3489
|
-
}
|
|
3490
|
-
}
|
|
3491
|
-
return newUri;
|
|
3492
|
-
}
|
|
3493
|
-
|
|
3494
3638
|
/**
|
|
3495
3639
|
* Attempts to guess stream's mime type based on content type and URI.
|
|
3496
3640
|
*
|
|
@@ -3524,6 +3668,10 @@ shaka.hls.HlsParser = class {
|
|
|
3524
3668
|
}
|
|
3525
3669
|
}
|
|
3526
3670
|
|
|
3671
|
+
if (codecs == 'mjpg') {
|
|
3672
|
+
return 'application/mp4';
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3527
3675
|
// Not enough information to guess from the content type and codecs.
|
|
3528
3676
|
return null;
|
|
3529
3677
|
}
|
|
@@ -3556,10 +3704,11 @@ shaka.hls.HlsParser = class {
|
|
|
3556
3704
|
* @param {string} codecs
|
|
3557
3705
|
* @param {!shaka.hls.Playlist} playlist
|
|
3558
3706
|
* @param {!Map.<string, string>} variables
|
|
3707
|
+
* @param {function():!Array.<string>} getUris
|
|
3559
3708
|
* @return {!Promise.<string>}
|
|
3560
3709
|
* @private
|
|
3561
3710
|
*/
|
|
3562
|
-
async guessMimeType_(contentType, codecs, playlist, variables) {
|
|
3711
|
+
async guessMimeType_(contentType, codecs, playlist, variables, getUris) {
|
|
3563
3712
|
const HlsParser = shaka.hls.HlsParser;
|
|
3564
3713
|
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
3565
3714
|
|
|
@@ -3569,10 +3718,13 @@ shaka.hls.HlsParser = class {
|
|
|
3569
3718
|
goog.asserts.assert(playlist.segments.length,
|
|
3570
3719
|
'Playlist should have segments!');
|
|
3571
3720
|
const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2);
|
|
3572
|
-
const middleSegmentUri = this.variableSubstitution_(
|
|
3573
|
-
playlist.segments[middleSegmentIdx].absoluteUri, variables);
|
|
3574
3721
|
|
|
3575
|
-
const
|
|
3722
|
+
const middleSegmentUris = shaka.hls.Utils.constructSegmentUris(
|
|
3723
|
+
getUris(),
|
|
3724
|
+
playlist.segments[middleSegmentIdx].verbatimSegmentUri,
|
|
3725
|
+
variables);
|
|
3726
|
+
|
|
3727
|
+
const parsedUri = new goog.Uri(middleSegmentUris[0]);
|
|
3576
3728
|
const extension = parsedUri.getPath().split('.').pop();
|
|
3577
3729
|
const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
|
|
3578
3730
|
|
|
@@ -3595,7 +3747,7 @@ shaka.hls.HlsParser = class {
|
|
|
3595
3747
|
// If unable to guess mime type, request a segment and try getting it
|
|
3596
3748
|
// from the response.
|
|
3597
3749
|
const headRequest = shaka.net.NetworkingEngine.makeRequest(
|
|
3598
|
-
|
|
3750
|
+
middleSegmentUris, this.config_.retryParameters);
|
|
3599
3751
|
headRequest.method = 'HEAD';
|
|
3600
3752
|
const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
|
|
3601
3753
|
const response = await this.makeNetworkRequest_(
|
|
@@ -3637,14 +3789,17 @@ shaka.hls.HlsParser = class {
|
|
|
3637
3789
|
* @param {?string} height
|
|
3638
3790
|
* @param {?string} frameRate
|
|
3639
3791
|
* @param {?string} videoRange
|
|
3792
|
+
* @param {?string} videoLayout
|
|
3640
3793
|
* @private
|
|
3641
3794
|
*/
|
|
3642
|
-
addVideoAttributes_(stream, width, height, frameRate, videoRange
|
|
3795
|
+
addVideoAttributes_(stream, width, height, frameRate, videoRange,
|
|
3796
|
+
videoLayout) {
|
|
3643
3797
|
if (stream) {
|
|
3644
3798
|
stream.width = Number(width) || undefined;
|
|
3645
3799
|
stream.height = Number(height) || undefined;
|
|
3646
3800
|
stream.frameRate = Number(frameRate) || undefined;
|
|
3647
3801
|
stream.hdr = videoRange || undefined;
|
|
3802
|
+
stream.videoLayout = videoLayout || undefined;
|
|
3648
3803
|
}
|
|
3649
3804
|
}
|
|
3650
3805
|
|
|
@@ -3652,16 +3807,16 @@ shaka.hls.HlsParser = class {
|
|
|
3652
3807
|
* Makes a network request for the manifest and returns a Promise
|
|
3653
3808
|
* with the resulting data.
|
|
3654
3809
|
*
|
|
3655
|
-
* @param {string}
|
|
3810
|
+
* @param {!Array.<string>} uris
|
|
3656
3811
|
* @param {boolean=} isPlaylist
|
|
3657
3812
|
* @return {!Promise.<!shaka.extern.Response>}
|
|
3658
3813
|
* @private
|
|
3659
3814
|
*/
|
|
3660
|
-
requestManifest_(
|
|
3815
|
+
requestManifest_(uris, isPlaylist) {
|
|
3661
3816
|
const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
|
|
3662
3817
|
|
|
3663
3818
|
const request = shaka.net.NetworkingEngine.makeRequest(
|
|
3664
|
-
|
|
3819
|
+
uris, this.config_.retryParameters);
|
|
3665
3820
|
|
|
3666
3821
|
const type = isPlaylist ?
|
|
3667
3822
|
shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_PLAYLIST :
|
|
@@ -3690,12 +3845,20 @@ shaka.hls.HlsParser = class {
|
|
|
3690
3845
|
}
|
|
3691
3846
|
|
|
3692
3847
|
try {
|
|
3848
|
+
const startTime = Date.now();
|
|
3693
3849
|
await this.update();
|
|
3694
3850
|
|
|
3851
|
+
// Keep track of how long the longest manifest update took.
|
|
3852
|
+
const endTime = Date.now();
|
|
3853
|
+
|
|
3695
3854
|
// This may have converted to VOD, in which case we stop updating.
|
|
3696
3855
|
if (this.isLive_()) {
|
|
3856
|
+
const updateDuration = (endTime - startTime) / 1000.0;
|
|
3857
|
+
this.averageUpdateDuration_.sample(1, updateDuration);
|
|
3697
3858
|
const delay = this.getUpdatePlaylistDelay_();
|
|
3698
|
-
|
|
3859
|
+
const finalDelay = Math.max(0,
|
|
3860
|
+
delay - this.averageUpdateDuration_.getEstimate());
|
|
3861
|
+
this.updatePlaylistTimer_.tickAfter(/* seconds= */ finalDelay);
|
|
3699
3862
|
}
|
|
3700
3863
|
} catch (error) {
|
|
3701
3864
|
// Detect a call to stop() during this.update()
|
|
@@ -3718,6 +3881,13 @@ shaka.hls.HlsParser = class {
|
|
|
3718
3881
|
// Try again very soon.
|
|
3719
3882
|
this.updatePlaylistTimer_.tickAfter(/* seconds= */ 0.1);
|
|
3720
3883
|
}
|
|
3884
|
+
|
|
3885
|
+
// Detect a call to stop()
|
|
3886
|
+
if (!this.playerInterface_) {
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
this.playerInterface_.onManifestUpdated();
|
|
3721
3891
|
}
|
|
3722
3892
|
|
|
3723
3893
|
|
|
@@ -3932,8 +4102,8 @@ shaka.hls.HlsParser = class {
|
|
|
3932
4102
|
* @typedef {{
|
|
3933
4103
|
* stream: !shaka.extern.Stream,
|
|
3934
4104
|
* type: string,
|
|
3935
|
-
*
|
|
3936
|
-
*
|
|
4105
|
+
* redirectUris: !Array.<string>,
|
|
4106
|
+
* getUris: function():!Array.<string>,
|
|
3937
4107
|
* minTimestamp: number,
|
|
3938
4108
|
* maxTimestamp: number,
|
|
3939
4109
|
* mediaSequenceToStartTime: !Map.<number, number>,
|
|
@@ -3953,14 +4123,10 @@ shaka.hls.HlsParser = class {
|
|
|
3953
4123
|
* The Stream itself.
|
|
3954
4124
|
* @property {string} type
|
|
3955
4125
|
* The type value. Could be 'video', 'audio', 'text', or 'image'.
|
|
3956
|
-
* @property {string}
|
|
3957
|
-
* The
|
|
3958
|
-
*
|
|
3959
|
-
*
|
|
3960
|
-
* from different origins each time.
|
|
3961
|
-
* @property {string} absoluteMediaPlaylistUri
|
|
3962
|
-
* The absolute media playlist URI, resolved relative to the master playlist
|
|
3963
|
-
* and updated to reflect any redirects.
|
|
4126
|
+
* @property {!Array.<string>} redirectUris
|
|
4127
|
+
* The redirect URIs.
|
|
4128
|
+
* @property {function():!Array.<string>} getUris
|
|
4129
|
+
* The verbatim media playlist URIs, as it appeared in the master playlist.
|
|
3964
4130
|
* @property {number} minTimestamp
|
|
3965
4131
|
* The minimum timestamp found in the stream.
|
|
3966
4132
|
* @property {number} maxTimestamp
|
|
@@ -4002,30 +4168,6 @@ shaka.hls.HlsParser.StreamInfo;
|
|
|
4002
4168
|
shaka.hls.HlsParser.StreamInfos;
|
|
4003
4169
|
|
|
4004
4170
|
|
|
4005
|
-
/**
|
|
4006
|
-
* @typedef {{
|
|
4007
|
-
* type: string,
|
|
4008
|
-
* mimeType: string,
|
|
4009
|
-
* codecs: string,
|
|
4010
|
-
* language: ?string,
|
|
4011
|
-
* height: ?string,
|
|
4012
|
-
* width: ?string,
|
|
4013
|
-
* channelCount: ?number,
|
|
4014
|
-
* sampleRate: ?number
|
|
4015
|
-
* }}
|
|
4016
|
-
*
|
|
4017
|
-
* @property {string} type
|
|
4018
|
-
* @property {string} mimeType
|
|
4019
|
-
* @property {string} codecs
|
|
4020
|
-
* @property {?string} language
|
|
4021
|
-
* @property {?string} height
|
|
4022
|
-
* @property {?string} width
|
|
4023
|
-
* @property {?number} channelCount
|
|
4024
|
-
* @property {?number} sampleRate
|
|
4025
|
-
*/
|
|
4026
|
-
shaka.hls.HlsParser.BasicInfo;
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
4171
|
/**
|
|
4030
4172
|
* @const {!Object.<string, string>}
|
|
4031
4173
|
* @private
|
|
@@ -4147,8 +4289,6 @@ shaka.hls.HlsParser.PresentationType_ = {
|
|
|
4147
4289
|
LIVE: 'LIVE',
|
|
4148
4290
|
};
|
|
4149
4291
|
|
|
4150
|
-
shaka.media.ManifestParser.registerParserByExtension(
|
|
4151
|
-
'm3u8', () => new shaka.hls.HlsParser());
|
|
4152
4292
|
shaka.media.ManifestParser.registerParserByMime(
|
|
4153
4293
|
'application/x-mpegurl', () => new shaka.hls.HlsParser());
|
|
4154
4294
|
shaka.media.ManifestParser.registerParserByMime(
|