Add option to map HLS Stream Languages from m3u8 Playlist instead of segment files
Checklist
- [x] This is a plugin issue and not a different kind of issue
- [x] I have read the contribution guidelines
- [x] I have checked the list of open and recently closed plugin issues
- [x] I have checked the commit log of the master branch
Collaboration
Streamlink version
Your Streamlink version (7.1.3) is up to date!
Description
Many TV Providers provide HLS Streams with an m3u8 Playlist.
Some providers have an invalid language in the segment files of each audio and video tracks and Streamlink does not rewrite them with the correct language from the m3u8 Playlist file. VLC, Kodi, and FFMPEG show all audio and video tracks of the Stream segments in English, when using streamlink.
FFMPEG seems to read the playlist and write the correct language in the mpegts pipe output. But streamlink just copies the segments and does not change the info to the correct language.
An option to let Streamlink decide, to choose the languages from the playlist and also modify the pipe output to the correct language if the segments language is different than in the m3u8 Playlist description.
--hls-audio-select "*" should read the playlist too and correct the segments language with the specification of the playlist before pipe to output.
Example Playlist:
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="deutsch",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="deu",URI="https://zba6-4-hls7-live.zahs.tv/HD_sf1/t_track_audio_bw_256000_num_1_tid_3_p_20_l_de_nd_1600_mbr_8000.m3u8?z32="
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="english",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="eng",URI="https://zba6-4-hls7-live.zahs.tv/HD_sf1/t_track_audio_bw_128000000_num_2_tid_4_p_10_l_en_nd_1600_mbr_8000.m3u8?z32="
#EXT-X-STREAM-INF:BANDWIDTH=8000000,CODECS="avc1.4d402a,mp4a.40.2,ec-3",RESOLUTION=1920x1080,FRAME-RATE=50,AUDIO="audio",CLOSED-CAPTIONS=NONE
https://zba6-4-hls7-live.zahs.tv/HD_sf1/t_track_video_bw_7800000_num_0_tid_1_nd_1600_mbr_8000.m3u8?z32=
Debug log
2025-03-11 23:04:55.193 spawn: Executing "//usr/bin/streamlink"
2025-03-11 23:04:56.007 spawn: [session][debug] Loading plugin: hls
2025-03-11 23:04:56.010 spawn: [cli][debug] OS: Linux-6.8.12-5-pve-x86_64-with-glibc2.35
2025-03-11 23:04:56.010 spawn: [cli][debug] Python: 3.10.12
2025-03-11 23:04:56.010 spawn: [cli][debug] OpenSSL: OpenSSL 3.0.2 15 Mar 2022
2025-03-11 23:04:56.010 spawn: [cli][debug] Streamlink: 7.1.3
2025-03-11 23:04:56.010 spawn: [cli][debug] Dependencies:
2025-03-11 23:04:56.017 spawn: [cli][debug] certifi: 2020.6.20
2025-03-11 23:04:56.018 spawn: [cli][debug] exceptiongroup: 1.2.2
2025-03-11 23:04:56.020 spawn: [cli][debug] isodate: 0.6.1
2025-03-11 23:04:56.021 spawn: [cli][debug] lxml: 4.8.0
2025-03-11 23:04:56.023 spawn: [cli][debug] pycountry: 20.7.3
2025-03-11 23:04:56.024 spawn: [cli][debug] pycryptodome: 3.20.0
2025-03-11 23:04:56.028 spawn: [cli][debug] PySocks: 1.7.1
2025-03-11 23:04:56.030 spawn: [cli][debug] requests: 2.32.3
2025-03-11 23:04:56.031 spawn: [cli][debug] trio: 0.29.0
2025-03-11 23:04:56.032 spawn: [cli][debug] trio-websocket: 0.12.2
2025-03-11 23:04:56.035 spawn: [cli][debug] urllib3: 1.26.5
2025-03-11 23:04:56.036 spawn: [cli][debug] websocket-client: 1.2.3
2025-03-11 23:04:56.036 spawn: [cli][debug] Arguments:
2025-03-11 23:04:56.036 spawn: [cli][debug] url=hls://http://yourdomain.com:5000/api/tby/live/38?code=xxxxxxxxxxxxxxx
2025-03-11 23:04:56.037 spawn: [cli][debug] --locale=de_DE
2025-03-11 23:04:56.037 spawn: [cli][debug] --loglevel=debug
2025-03-11 23:04:56.037 spawn: [cli][debug] --stdout=True
2025-03-11 23:04:56.037 spawn: [cli][debug] --default-stream=['best']
2025-03-11 23:04:56.037 spawn: [cli][debug] --retry-streams=1.0
2025-03-11 23:04:56.037 spawn: [cli][debug] --retry-max=10
2025-03-11 23:04:56.037 spawn: [cli][debug] --retry-open=5
2025-03-11 23:04:56.037 spawn: [cli][debug] --ringbuffer-size=67108864
2025-03-11 23:04:56.037 spawn: [cli][debug] --stream-segment-threads=2
2025-03-11 23:04:56.037 spawn: [cli][debug] --mux-subtitles=True
2025-03-11 23:04:56.037 spawn: [cli][debug] --hls-live-edge=3
2025-03-11 23:04:56.037 spawn: [cli][debug] --hls-playlist-reload-attempts=99
2025-03-11 23:04:56.038 spawn: [cli][debug] --hls-playlist-reload-time=segment
2025-03-11 23:04:56.038 spawn: [cli][debug] --hls-segment-queue-threshold=0.0
2025-03-11 23:04:56.038 spawn: [cli][debug] --hls-audio-select=['ger']
2025-03-11 23:04:56.038 spawn: [cli][debug] --hls-start-offset=10.0
2025-03-11 23:04:56.038 spawn: [cli][debug] --http-proxy=http://username:password@proxyip:22540
2025-03-11 23:04:56.039 spawn: [cli][info] Found matching plugin hls for URL hls://http://yourdomain.com:5000/api/tby/live/38?code=xxxxxxxxxxxxxxx
2025-03-11 23:04:56.039 spawn: [plugins.hls][debug] URL=http://yourdomain.com:5000/api/tby/live/38?code=xxxxxxxxxxxxxxx; params={}
2025-03-11 23:04:56.093 spawn: [utils.l10n][debug] Language code: de_DE
2025-03-11 23:04:56.790 spawn: [stream.ffmpegmux][debug] ffmpeg version 4.4.2-0ubuntu0.22.04.1 Copyright (c) 2000-2021 the FFmpeg developers
2025-03-11 23:04:56.790 spawn: built with gcc 11 (Ubuntu 11.2.0-19ubuntu1)
2025-03-11 23:04:56.790 spawn: configuration: --prefix=/usr --extra-version=0ubuntu0.22.04.1 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzimg --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-o
2025-03-11 23:04:56.790 spawn: libavutil 56. 70.100 / 56. 70.100
2025-03-11 23:04:56.790 spawn: libavcodec 58.134.100 / 58.134.100
2025-03-11 23:04:56.790 spawn: libavformat 58. 76.100 / 58. 76.100
2025-03-11 23:04:56.790 spawn: libavdevice 58. 13.100 / 58. 13.100
2025-03-11 23:04:56.790 spawn: libavfilter 7.110.100 / 7.110.100
2025-03-11 23:04:56.790 spawn: libswscale 5. 9.100 / 5. 9.100
2025-03-11 23:04:56.790 spawn: libswresample 3. 9.100 / 3. 9.100
2025-03-11 23:04:56.790 spawn: libpostproc 55. 9.100 / 55. 9.100
2025-03-11 23:04:56.790 spawn: [stream.hls][debug] Using external audio tracks for stream 1080p (language=deu, name=deutsch)
2025-03-11 23:04:56.791 spawn: [cli][info] Available streams: 1080p (worst, best)
2025-03-11 23:04:56.792 spawn: [cli][info] Opening stream: 1080p (hls-multi)
2025-03-11 23:04:56.792 spawn: [stream.ffmpegmux][debug] Opening hls substream
2025-03-11 23:04:56.792 spawn: [stream.hls][debug] Reloading playlist
2025-03-11 23:04:56.795 spawn: [stream.ffmpegmux][debug] Opening hls substream
2025-03-11 23:04:56.796 spawn: [stream.hls][debug] Reloading playlist
2025-03-11 23:04:56.800 spawn: [utils.named_pipe][info] Creating pipe streamlinkpipe-754949-1-1192
2025-03-11 23:04:56.801 spawn: [utils.named_pipe][info] Creating pipe streamlinkpipe-754949-2-1858
2025-03-11 23:04:56.801 spawn: [stream.ffmpegmux][debug] ffmpeg command: ['/usr/bin/ffmpeg', '-y', '-nostats', '-loglevel', 'info', '-i', '/tmp/streamlinkpipe-754949-1-1192', '-i', '/tmp/streamlinkpipe-754949-2-1858', '-c:v', 'copy', '-c:a', 'copy', '-map', '0:v?', '-map', '0:a?', '-map', '1:a', '-f', 'mpegts', 'pipe:1']
2025-03-11 23:04:56.801 spawn: [stream.ffmpegmux][debug] Starting copy to pipe: /tmp/streamlinkpipe-754949-1-1192
2025-03-11 23:04:56.801 spawn: [stream.ffmpegmux][debug] Starting copy to pipe: /tmp/streamlinkpipe-754949-2-1858
2025-03-11 23:04:56.802 spawn: [cli][debug] Pre-buffering 8192 bytes
2025-03-11 23:04:57.145 spawn: [stream.hls][debug] Time offsets negative for live streams, skipping back 10 seconds
2025-03-11 23:04:57.145 spawn: [stream.hls][debug] First Sequence: 1088583883; Last Sequence: 1088583932
2025-03-11 23:04:57.145 spawn: [stream.hls][debug] Start offset: -10; Duration: None; Start Sequence: 1088583925; End Sequence: None
2025-03-11 23:04:57.145 spawn: [stream.hls][debug] Adding segment 1088583925 to queue
2025-03-11 23:04:57.148 spawn: [stream.hls][debug] Adding segment 1088583926 to queue
2025-03-11 23:04:57.149 spawn: [stream.hls][debug] Adding segment 1088583927 to queue
2025-03-11 23:04:57.149 spawn: [stream.hls][debug] Adding segment 1088583928 to queue
2025-03-11 23:04:57.149 spawn: [stream.hls][debug] Adding segment 1088583929 to queue
2025-03-11 23:04:57.149 spawn: [stream.hls][debug] Adding segment 1088583930 to queue
2025-03-11 23:04:57.149 spawn: [stream.hls][debug] Adding segment 1088583931 to queue
2025-03-11 23:04:57.149 spawn: [stream.hls][debug] Adding segment 1088583932 to queue
2025-03-11 23:04:57.156 spawn: [stream.hls][debug] Time offsets negative for live streams, skipping back 10 seconds
2025-03-11 23:04:57.157 spawn: [stream.hls][debug] First Sequence: 1088583883; Last Sequence: 1088583932
2025-03-11 23:04:57.157 spawn: [stream.hls][debug] Start offset: -10; Duration: None; Start Sequence: 1088583925; End Sequence: None
2025-03-11 23:04:57.157 spawn: [stream.hls][debug] Adding segment 1088583925 to queue
2025-03-11 23:04:57.160 spawn: [stream.hls][debug] Adding segment 1088583926 to queue
2025-03-11 23:04:57.161 spawn: [stream.hls][debug] Adding segment 1088583927 to queue
2025-03-11 23:04:57.161 spawn: [stream.hls][debug] Adding segment 1088583928 to queue
2025-03-11 23:04:57.161 spawn: [stream.hls][debug] Adding segment 1088583929 to queue
2025-03-11 23:04:57.161 spawn: [stream.hls][debug] Adding segment 1088583930 to queue
2025-03-11 23:04:57.161 spawn: [stream.hls][debug] Adding segment 1088583931 to queue
2025-03-11 23:04:57.161 spawn: [stream.hls][debug] Adding segment 1088583932 to queue
2025-03-11 23:04:57.199 spawn: [stream.hls][debug] Writing segment 1088583925 to output
2025-03-11 23:04:57.199 spawn: [stream.hls][debug] Segment initialization 1088583925 complete
2025-03-11 23:04:57.209 spawn: [stream.hls][debug] Writing segment 1088583925 to output
2025-03-11 23:04:57.211 spawn: [stream.hls][debug] Segment initialization 1088583925 complete
2025-03-11 23:04:57.527 spawn: [stream.hls][debug] Writing segment 1088583925 to output
2025-03-11 23:04:57.530 spawn: [stream.hls][debug] Segment 1088583925 complete
2025-03-11 23:04:57.530 spawn: [stream.hls][debug] Writing segment 1088583926 to output
2025-03-11 23:04:57.530 spawn: [stream.hls][debug] Segment 1088583926 complete
2025-03-11 23:04:57.531 spawn: [stream.hls][debug] Writing segment 1088583927 to output
2025-03-11 23:04:57.531 spawn: [stream.hls][debug] Segment 1088583927 complete
2025-03-11 23:04:57.531 spawn: [stream.hls][debug] Writing segment 1088583928 to output
2025-03-11 23:04:57.533 spawn: [stream.hls][debug] Segment 1088583928 complete
2025-03-11 23:04:57.534 spawn: [stream.hls][debug] Writing segment 1088583929 to output
2025-03-11 23:04:57.534 spawn: [stream.hls][debug] Segment 1088583929 complete
2025-03-11 23:04:57.536 spawn: [stream.hls][debug] Writing segment 1088583930 to output
2025-03-11 23:04:57.537 spawn: [stream.hls][debug] Segment 1088583930 complete
2025-03-11 23:04:57.592 spawn: [stream.hls][debug] Writing segment 1088583931 to output
2025-03-11 23:04:57.593 spawn: [stream.hls][debug] Segment 1088583931 complete
2025-03-11 23:04:57.619 spawn: [stream.hls][debug] Writing segment 1088583932 to output
2025-03-11 23:04:57.619 spawn: [stream.hls][debug] Segment 1088583932 complete
2025-03-11 23:04:57.806 spawn: [stream.hls][debug] Writing segment 1088583925 to output
2025-03-11 23:04:57.811 spawn: [stream.hls][debug] Segment 1088583925 complete
2025-03-11 23:04:57.811 spawn: [stream.hls][debug] Writing segment 1088583926 to output
2025-03-11 23:04:57.815 spawn: [stream.hls][debug] Segment 1088583926 complete
2025-03-11 23:04:57.815 spawn: [stream.hls][debug] Writing segment 1088583927 to output
2025-03-11 23:04:57.818 spawn: [stream.hls][debug] Segment 1088583927 complete
2025-03-11 23:04:57.818 spawn: [stream.hls][debug] Writing segment 1088583928 to output
2025-03-11 23:04:57.821 spawn: [stream.hls][debug] Segment 1088583928 complete
2025-03-11 23:04:57.849 spawn: [cli][debug] Writing stream to output
2025-03-11 23:04:57.890 spawn: [stream.hls][debug] Writing segment 1088583929 to output
2025-03-11 23:04:57.897 spawn: [stream.hls][debug] Segment 1088583929 complete
2025-03-11 23:04:57.995 spawn: [stream.hls][debug] Writing segment 1088583930 to output
2025-03-11 23:04:58.001 spawn: [stream.hls][debug] Segment 1088583930 complete
2025-03-11 23:04:58.008 spawn: [stream.hls][debug] Writing segment 1088583931 to output
2025-03-11 23:04:58.010 spawn: [stream.hls][debug] Segment 1088583931 complete
2025-03-11 23:04:58.136 spawn: [stream.hls][debug] Writing segment 1088583932 to output
2025-03-11 23:04:58.139 spawn: [stream.hls][debug] Segment 1088583932 complete
2025-03-11 23:04:58.392 spawn: [stream.hls][debug] Reloading playlist
2025-03-11 23:04:58.396 spawn: [stream.hls][debug] Reloading playlist
2025-03-11 23:04:58.444 spawn: [stream.hls][debug] Adding segment 1088583933 to queue
2025-03-11 23:04:58.452 spawn: [stream.hls][debug] Adding segment 1088583933 to queue
2025-03-11 23:04:58.511 spawn: [stream.hls][debug] Writing segment 1088583933 to output
2025-03-11 23:04:58.511 spawn: [stream.hls][debug] Segment 1088583933 complete
2025-03-11 23:04:58.603 spawn: [stream.hls][debug] Writing segment 1088583933 to output
2025-03-11 23:04:58.606 spawn: [stream.hls][debug] Segment 1088583933 complete
2025-03-11 23:04:59.992 spawn: [stream.hls][debug] Reloading playlist
2025-03-11 23:04:59.996 spawn: [stream.hls][debug] Reloading playlist
2025-03-11 23:05:00.043 spawn: [stream.hls][debug] Adding segment 1088583934 to queue
2025-03-11 23:05:00.048 spawn: [stream.hls][debug] Adding segment 1088583934 to queue
2025-03-11 23:05:00.138 spawn: [stream.hls][debug] Writing segment 1088583934 to output
2025-03-11 23:05:00.138 spawn: [stream.hls][debug] Segment 1088583934 complete
2025-03-11 23:05:00.368 spawn: [stream.hls][debug] Writing segment 1088583934 to output
2025-03-11 23:05:00.371 spawn: [stream.hls][debug] Segment 1088583934 complete
Streamlink currently only sets the language metadata for subtitle tracks when muxing.
This could of course be extended for audio streams. I don't think a CLI argument for choosing whether to use metadata from the HLS multivariant playlist or whether keeping the metadata from the stream tracks is necessary, because the multivariant playlist's metadata is what makes Streamlink choose the streams in the first palce when different media streams are available, so it should always take precedence.
However, I'm not entirely sure if it makes sense implementing this in the current state, because this requires some further modifications of HLSStream.parse_variant_playlist, MuxedHLSStream and MuxedStream which would all need to be rewritten anyway in the future because of #4902.
A solution right now would require changing the type of audio stream data that is passed to MuxedHLSStream, which is currently a simple str | list[str] of URL(s). Metadata could be made optional by replacing str with str | tuple[str, TAudioStreamMetadata] where TAudioStreamMetadata could be a simple str for the language right now.
Btw, --hls-audio-select=ger doesn't do the thing you're expecting here, which is why it's using the English audio stream, despite your de_DE locale value...
ger is not a valid ISO 639-2 language code for German. It's deu.
Streamlink simply compares the direct values you set in --hls-audio-select with the language attribute of all available audio streams. The German audio stream is properly annotated as deu. And since "deu" in ["ger"] is obviously False, it skips this audio stream.
https://github.com/streamlink/streamlink/blob/7.1.3/src/streamlink/stream/hls/hls.py#L770
This could be considered a bug though, because Streamlink should resolve the user's input with a proper Language object from pycountry's language database that Streamlink relies on in its l10n implementation.
>>> from streamlink.utils.l10n import Language, Localization
>>> deutsch = Language.get("ger")
>>> str(deutsch)
"Language('de', 'deu', 'German', bibliographic='ger')"
>>> locale = Localization("de_DE")
>>> locale.equivalent(language="ger")
True