def reload_playlist(self): if self.closed: return self.reader.buffer.wait_free() self.logger.debug("Reloading playlist") res = self.session.http.get(self.stream.url, exception=StreamError, retries=self.playlist_reload_retries, **self.reader.request_params) try: playlist = hls_playlist.load(res.text, res.url) except ValueError as err: raise StreamError(err) if playlist.is_master: raise StreamError("Attempted to play a variant playlist, use " "'hlsvariant://{0}' instead".format( self.stream.url)) if playlist.iframes_only: raise StreamError( "Streams containing I-frames only is not playable") media_sequence = playlist.media_sequence or 0 sequences = [ Sequence(media_sequence + i, s) for i, s in enumerate(playlist.segments) ] if sequences: self.process_sequences(playlist, sequences)
def reload_playlist(self): if self.closed: return self.reader.buffer.wait_free() log.debug("Reloading playlist") res = self.session.http.get(self.stream.url, exception=StreamError, retries=self.playlist_reload_retries, **self.reader.request_params) try: playlist = hls_playlist.load(res.text, res.url) except ValueError as err: raise StreamError(err) if playlist.is_master: raise StreamError("Attempted to play a variant playlist, use " "'hls://{0}' instead".format(self.stream.url)) if playlist.iframes_only: raise StreamError("Streams containing I-frames only is not playable") media_sequence = playlist.media_sequence or 0 sequences = [Sequence(media_sequence + i, s) for i, s in enumerate(playlist.segments)] if sequences: self.process_sequences(playlist, sequences)
def test_load(self): with text("hls/test_1.m3u8") as m3u8_fh: playlist = load(m3u8_fh.read(), "http://test.se/") self.assertEqual( playlist.media, [ Media(uri='http://test.se/audio/stereo/en/128kbit.m3u8', type='AUDIO', group_id='stereo', language='en', name='English', default=True, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/audio/stereo/none/128kbit.m3u8', type='AUDIO', group_id='stereo', language='dubbing', name='Dubbing', default=False, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/audio/surround/en/320kbit.m3u8', type='AUDIO', group_id='surround', language='en', name='English', default=True, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/audio/stereo/none/128kbit.m3u8', type='AUDIO', group_id='surround', language='dubbing', name='Dubbing', default=False, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/subtitles_de.m3u8', type='SUBTITLES', group_id='subs', language='de', name='Deutsch', default=False, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/subtitles_en.m3u8', type='SUBTITLES', group_id='subs', language='en', name='English', default=True, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/subtitles_es.m3u8', type='SUBTITLES', group_id='subs', language='es', name='Espanol', default=False, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/subtitles_fr.m3u8', type='SUBTITLES', group_id='subs', language='fr', name='Français', default=False, autoselect=True, forced=False, characteristics=None) ] ) self.assertEqual( [p.stream_info for p in playlist.playlists], [ StreamInfo(bandwidth=258157.0, program_id='1', codecs=['avc1.4d400d', 'mp4a.40.2'], resolution=Resolution(width=422, height=180), audio='stereo', video=None, subtitles='subs'), StreamInfo(bandwidth=520929.0, program_id='1', codecs=['avc1.4d4015', 'mp4a.40.2'], resolution=Resolution(width=638, height=272), audio='stereo', video=None, subtitles='subs'), StreamInfo(bandwidth=831270.0, program_id='1', codecs=['avc1.4d4015', 'mp4a.40.2'], resolution=Resolution(width=638, height=272), audio='stereo', video=None, subtitles='subs'), StreamInfo(bandwidth=1144430.0, program_id='1', codecs=['avc1.4d401f', 'mp4a.40.2'], resolution=Resolution(width=958, height=408), audio='surround', video=None, subtitles='subs'), StreamInfo(bandwidth=1558322.0, program_id='1', codecs=['avc1.4d401f', 'mp4a.40.2'], resolution=Resolution(width=1277, height=554), audio='surround', video=None, subtitles='subs'), StreamInfo(bandwidth=4149264.0, program_id='1', codecs=['avc1.4d4028', 'mp4a.40.2'], resolution=Resolution(width=1921, height=818), audio='surround', video=None, subtitles='subs'), StreamInfo(bandwidth=6214307.0, program_id='1', codecs=['avc1.4d4028', 'mp4a.40.2'], resolution=Resolution(width=1921, height=818), audio='surround', video=None, subtitles='subs'), StreamInfo(bandwidth=10285391.0, program_id='1', codecs=['avc1.4d4033', 'mp4a.40.2'], resolution=Resolution(width=4096, height=1744), audio='surround', video=None, subtitles='subs') ] )
def reload_playlist(self): if self.closed: return self.reader.buffer.wait_free() log.debug("Reloading playlist") if self.stream.channel: parsed = urlparse(self.stream.url) if self.stream._first_netloc is None: # save the first netloc self.stream._first_netloc = parsed.netloc # always use the first saved netloc new_stream_url = parsed._replace( netloc=self.stream._first_netloc).geturl() else: new_stream_url = self.stream.url try: res = self.session.http.get(new_stream_url, exception=StreamError, retries=self.playlist_reload_retries, **self.reader.request_params) except StreamError as err: if (hasattr(self.stream, "watch_timeout") and any( x in str(err) for x in ("403 Client Error", "502 Server Error"))): self.stream.watch_timeout = 0 self.playlist_reload_time = 0 log.debug( f"Force reloading the channel playlist on error: {err}") return raise err try: playlist = hls_playlist.load(res.text, res.url) except ValueError as err: raise StreamError(err) if playlist.is_master: raise StreamError("Attempted to play a variant playlist, use " "'hls://{0}' instead".format(self.stream.url)) if playlist.iframes_only: raise StreamError( "Streams containing I-frames only is not playable") media_sequence = playlist.media_sequence or 0 sequences = [ Sequence(media_sequence + i, s) for i, s in enumerate(playlist.segments) ] if sequences: self.process_sequences(playlist, sequences)
def reload_playlist(self): if self.closed: return self.reader.buffer.wait_free() log.debug("Reloading playlist") if self.stream.channel: parsed = urlparse(self.stream.url) if self.stream._first_netloc is None: # save the first netloc self.stream._first_netloc = parsed.netloc # always use the first saved netloc new_stream_url = parsed._replace(netloc=self.stream._first_netloc).geturl() else: new_stream_url = self.stream.url try: res = self.session.http.get( new_stream_url, exception=StreamError, retries=self.playlist_reload_retries, **self.reader.request_params) except StreamError as err: if (hasattr(self.stream, "watch_timeout") and any(x in str(err) for x in ("403 Client Error", "502 Server Error"))): self.stream.watch_timeout = 0 self.playlist_reload_time = 0 log.debug("Force reloading the channel playlist on error: {0}", err) return raise err try: playlist = hls_playlist.load(res.text, res.url) except ValueError as err: raise StreamError(err) if playlist.is_master: raise StreamError("Attempted to play a variant playlist, use " "'hls://{0}' instead".format(self.stream.url)) if playlist.iframes_only: raise StreamError("Streams containing I-frames only is not playable") media_sequence = playlist.media_sequence or 0 sequences = [Sequence(media_sequence + i, s) for i, s in enumerate(playlist.segments)] if sequences: self.process_sequences(playlist, sequences)
def test_load(self): with text("hls/test_1.m3u8") as m3u8_fh: playlist = load(m3u8_fh.read(), "http://test.se/") self.assertEqual(playlist.media, [Media(uri='http://test.se/audio/stereo/en/128kbit.m3u8', type='AUDIO', group_id='stereo', language='en', name='English', default=True, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/audio/stereo/none/128kbit.m3u8', type='AUDIO', group_id='stereo', language='dubbing', name='Dubbing', default=False, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/audio/surround/en/320kbit.m3u8', type='AUDIO', group_id='surround', language='en', name='English', default=True, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/audio/stereo/none/128kbit.m3u8', type='AUDIO', group_id='surround', language='dubbing', name='Dubbing', default=False, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/subtitles_de.m3u8', type='SUBTITLES', group_id='subs', language='de', name='Deutsch', default=False, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/subtitles_en.m3u8', type='SUBTITLES', group_id='subs', language='en', name='English', default=True, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/subtitles_es.m3u8', type='SUBTITLES', group_id='subs', language='es', name='Espanol', default=False, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/subtitles_fr.m3u8', type='SUBTITLES', group_id='subs', language='fr', name='Français', default=False, autoselect=True, forced=False, characteristics=None)]) self.assertEqual([p.stream_info for p in playlist.playlists], [StreamInfo(bandwidth=258157.0, program_id='1', codecs=['avc1.4d400d', 'mp4a.40.2'], resolution=Resolution(width=422, height=180), audio='stereo', video=None, subtitles='subs'), StreamInfo(bandwidth=520929.0, program_id='1', codecs=['avc1.4d4015', 'mp4a.40.2'], resolution=Resolution(width=638, height=272), audio='stereo', video=None, subtitles='subs'), StreamInfo(bandwidth=831270.0, program_id='1', codecs=['avc1.4d4015', 'mp4a.40.2'], resolution=Resolution(width=638, height=272), audio='stereo', video=None, subtitles='subs'), StreamInfo(bandwidth=1144430.0, program_id='1', codecs=['avc1.4d401f', 'mp4a.40.2'], resolution=Resolution(width=958, height=408), audio='surround', video=None, subtitles='subs'), StreamInfo(bandwidth=1558322.0, program_id='1', codecs=['avc1.4d401f', 'mp4a.40.2'], resolution=Resolution(width=1277, height=554), audio='surround', video=None, subtitles='subs'), StreamInfo(bandwidth=4149264.0, program_id='1', codecs=['avc1.4d4028', 'mp4a.40.2'], resolution=Resolution(width=1921, height=818), audio='surround', video=None, subtitles='subs'), StreamInfo(bandwidth=6214307.0, program_id='1', codecs=['avc1.4d4028', 'mp4a.40.2'], resolution=Resolution(width=1921, height=818), audio='surround', video=None, subtitles='subs'), StreamInfo(bandwidth=10285391.0, program_id='1', codecs=['avc1.4d4033', 'mp4a.40.2'], resolution=Resolution(width=4096, height=1744), audio='surround', video=None, subtitles='subs')])
def _get_variant_playlist(cls, res): return hls_playlist.load(res.text, base_uri=res.url)
def _reload_playlist(self, text, url): return hls_playlist.load(text, url)
def test_parse_date(self): with text("hls/test_date.m3u8") as m3u8_fh: playlist = load(m3u8_fh.read(), "http://test.se/") start_date = datetime(year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0, tzinfo=tzinfo.UTC) end_date = datetime(year=2000, month=1, day=1, hour=0, minute=1, second=0, microsecond=0, tzinfo=tzinfo.UTC) delta_15 = timedelta(seconds=15) delta_30 = timedelta(seconds=30, milliseconds=500) delta_60 = timedelta(seconds=60) self.assertEqual(playlist.target_duration, 120) self.assertEqual([daterange for daterange in playlist.dateranges], [ DateRange(id="start-invalid", start_date=None, classname=None, end_date=None, duration=None, planned_duration=None, end_on_next=False, x={}), DateRange(id="start-no-frac", start_date=start_date, classname=None, end_date=None, duration=None, planned_duration=None, end_on_next=False, x={}), DateRange(id="start-with-frac", start_date=start_date, classname=None, end_date=None, duration=None, planned_duration=None, end_on_next=False, x={}), DateRange(id="with-class", start_date=start_date, classname="bar", end_date=None, duration=None, planned_duration=None, end_on_next=False, x={}), DateRange(id="duration", start_date=start_date, duration=delta_30, classname=None, end_date=None, planned_duration=None, end_on_next=False, x={}), DateRange(id="planned-duration", start_date=start_date, planned_duration=delta_15, classname=None, end_date=None, duration=None, end_on_next=False, x={}), DateRange(id="duration-precedence", start_date=start_date, duration=delta_30, planned_duration=delta_15, classname=None, end_date=None, end_on_next=False, x={}), DateRange(id="end", start_date=start_date, end_date=end_date, classname=None, duration=None, planned_duration=None, end_on_next=False, x={}), DateRange(id="end-precedence", start_date=start_date, end_date=end_date, duration=delta_30, classname=None, planned_duration=None, end_on_next=False, x={}), DateRange(x={"X-CUSTOM": "value"}, id=None, start_date=None, end_date=None, duration=None, classname=None, planned_duration=None, end_on_next=False) ]) self.assertEqual([segment for segment in playlist.segments], [ Segment(uri="http://test.se/segment0-15.ts", duration=15.0, title="live", date=start_date, key=None, discontinuity=False, byterange=None, map=None), Segment(uri="http://test.se/segment15-30.5.ts", duration=15.5, title="live", date=start_date + delta_15, key=None, discontinuity=False, byterange=None, map=None), Segment(uri="http://test.se/segment30.5-60.ts", duration=29.5, title="live", date=start_date + delta_30, key=None, discontinuity=False, byterange=None, map=None), Segment(uri="http://test.se/segment60-.ts", duration=60.0, title="live", date=start_date + delta_60, key=None, discontinuity=False, byterange=None, map=None) ]) self.assertEqual([ playlist.is_date_in_daterange(playlist.segments[0].date, daterange) for daterange in playlist.dateranges ], [None, True, True, True, True, True, True, True, True, None]) self.assertEqual([ playlist.is_date_in_daterange(playlist.segments[1].date, daterange) for daterange in playlist.dateranges ], [None, True, True, True, True, False, True, True, True, None]) self.assertEqual([ playlist.is_date_in_daterange(playlist.segments[2].date, daterange) for daterange in playlist.dateranges ], [None, True, True, True, False, False, False, True, True, None]) self.assertEqual([ playlist.is_date_in_daterange(playlist.segments[3].date, daterange) for daterange in playlist.dateranges ], [None, True, True, True, False, False, False, False, False, None])
def parse_variant_playlist(cls, session_, url, name_key="name", name_prefix="", check_streams=False, force_restart=False, **request_params): """Attempts to parse a variant playlist and return its streams. :param url: The URL of the variant playlist. :param name_key: Prefer to use this key as stream name, valid keys are: name, pixels, bitrate. :param name_prefix: Add this prefix to the stream names. :param force_restart: Start at the first segment even for a live stream :param check_streams: Only allow streams that are accesible. """ logger = session_.logger.new_module("hls.parse_variant_playlist") locale = session_.localization # Backwards compatibility with "namekey" and "nameprefix" params. name_key = request_params.pop("namekey", name_key) name_prefix = request_params.pop("nameprefix", name_prefix) res = session_.http.get(url, exception=IOError, **request_params) try: parser = hls_playlist.load(res.text, base_uri=res.url) except ValueError as err: raise IOError("Failed to parse playlist: {0}".format(err)) streams = {} for playlist in filter(lambda p: not p.is_iframe, parser.playlists): names = dict(name=None, pixels=None, bitrate=None) fallback_audio = None default_audio = None preferred_audio = None for media in playlist.media: if media.type == "VIDEO" and media.name: names["name"] = media.name elif media.type == "AUDIO": if not fallback_audio and media.default: fallback_audio = media # if the media is "audoselect" and it better matches the users preferences, use that # instead of default if not default_audio and ( media.autoselect and locale.equivalent(language=media.language)): default_audio = media # select the first audio stream that matches the users explict language selection if (not preferred_audio or media.default ) and locale.explicit and locale.equivalent( language=media.language): preferred_audio = media if playlist.stream_info.resolution: width, height = playlist.stream_info.resolution names["pixels"] = "{0}p".format(height) if playlist.stream_info.bandwidth: bw = playlist.stream_info.bandwidth if bw >= 1000: names["bitrate"] = "{0}k".format(int(bw / 1000.0)) else: names["bitrate"] = "{0}k".format(bw / 1000.0) stream_name = (names.get(name_key) or names.get("name") or names.get("pixels") or names.get("bitrate")) if not stream_name: continue if stream_name in streams: # rename duplicate streams stream_name = "{0}_alt".format(stream_name) num_alts = len( list( filter(lambda n: n.startswith(stream_name), streams.keys()))) # We shouldn't need more than 2 alt streams if num_alts >= 2: continue elif num_alts > 0: stream_name = "{0}{1}".format(stream_name, num_alts + 1) if check_streams: try: session_.http.get(playlist.uri, **request_params) except KeyboardInterrupt: raise except Exception: continue external_audio = preferred_audio or default_audio or fallback_audio if external_audio and external_audio.uri and FFMPEGMuxer.is_usable( session_): logger.debug( "Using external audio track for stream {0} (language={1}, name={2})" .format(name_prefix + stream_name, external_audio.language, external_audio.name or "N/A")) stream = MuxedHLSStream(session_, video=playlist.uri, audio=external_audio and external_audio.uri, force_restart=force_restart, **request_params) else: stream = HLSStream(session_, playlist.uri, force_restart=force_restart, **request_params) streams[name_prefix + stream_name] = stream return streams
def parse_variant_playlist(cls, session_, url, name_key="name", name_prefix="", check_streams=False, force_restart=False, name_fmt=None, start_offset=0, duration=None, **request_params): """Attempts to parse a variant playlist and return its streams. :param url: The URL of the variant playlist. :param name_key: Prefer to use this key as stream name, valid keys are: name, pixels, bitrate. :param name_prefix: Add this prefix to the stream names. :param check_streams: Only allow streams that are accessible. :param force_restart: Start at the first segment even for a live stream :param name_fmt: A format string for the name, allowed format keys are name, pixels, bitrate. """ locale = session_.localization # Backwards compatibility with "namekey" and "nameprefix" params. name_key = request_params.pop("namekey", name_key) name_prefix = request_params.pop("nameprefix", name_prefix) audio_select = session_.options.get("hls-audio-select") or [] res = session_.http.get(url, exception=IOError, **request_params) try: parser = hls_playlist.load(res.text, base_uri=res.url) except ValueError as err: raise IOError("Failed to parse playlist: {0}".format(err)) streams = {} for playlist in filter(lambda p: not p.is_iframe, parser.playlists): names = dict(name=None, pixels=None, bitrate=None) audio_streams = [] fallback_audio = [] default_audio = [] preferred_audio = [] for media in playlist.media: if media.type == "VIDEO" and media.name: names["name"] = media.name elif media.type == "AUDIO": audio_streams.append(media) for media in audio_streams: # Media without a uri is not relevant as external audio if not media.uri: continue if not fallback_audio and media.default: fallback_audio = [media] # if the media is "audoselect" and it better matches the users preferences, use that # instead of default if not default_audio and ( media.autoselect and locale.equivalent(language=media.language)): default_audio = [media] # select the first audio stream that matches the users explict language selection if (('*' in audio_select or media.language in audio_select or media.name in audio_select) or ((not preferred_audio or media.default) and locale.explicit and locale.equivalent(language=media.language))): preferred_audio.append(media) # final fallback on the first audio stream listed fallback_audio = fallback_audio or (len(audio_streams) and audio_streams[0].uri and [audio_streams[0]]) if playlist.stream_info.resolution: width, height = playlist.stream_info.resolution names["pixels"] = "{0}p".format(height) if playlist.stream_info.bandwidth: bw = playlist.stream_info.bandwidth if bw >= 1000: names["bitrate"] = "{0}k".format(int(bw / 1000.0)) else: names["bitrate"] = "{0}k".format(bw / 1000.0) if name_fmt: stream_name = name_fmt.format(**names) else: stream_name = (names.get(name_key) or names.get("name") or names.get("pixels") or names.get("bitrate")) if not stream_name: continue if stream_name in streams: # rename duplicate streams stream_name = "{0}_alt".format(stream_name) num_alts = len( list( filter(lambda n: n.startswith(stream_name), streams.keys()))) # We shouldn't need more than 2 alt streams if num_alts >= 2: continue elif num_alts > 0: stream_name = "{0}{1}".format(stream_name, num_alts + 1) if check_streams: try: session_.http.get(playlist.uri, **request_params) except KeyboardInterrupt: raise except Exception: continue external_audio = preferred_audio or default_audio or fallback_audio if external_audio and FFMPEGMuxer.is_usable(session_): external_audio_msg = ", ".join([ "(language={0}, name={1})".format(x.language, (x.name or "N/A")) for x in external_audio ]) log.debug("Using external audio tracks for stream {0} {1}", name_prefix + stream_name, external_audio_msg) stream = KTCityMuxedHLSStream( session_, video=playlist.uri, audio=[x.uri for x in external_audio if x.uri], force_restart=force_restart, start_offset=start_offset, duration=duration, **request_params) else: stream = KTCityHLS(session_, playlist.uri, force_restart=force_restart, start_offset=start_offset, duration=duration, **request_params) streams[name_prefix + stream_name] = stream return streams
def parse_variant_playlist(cls, session_, url, name_key="name", name_prefix="", check_streams=False, force_restart=False, name_fmt=None, start_offset=0, duration=None, **request_params): """Attempts to parse a variant playlist and return its streams. :param url: The URL of the variant playlist. :param name_key: Prefer to use this key as stream name, valid keys are: name, pixels, bitrate. :param name_prefix: Add this prefix to the stream names. :param check_streams: Only allow streams that are accessible. :param force_restart: Start at the first segment even for a live stream :param name_fmt: A format string for the name, allowed format keys are name, pixels, bitrate. """ locale = session_.localization # Backwards compatibility with "namekey" and "nameprefix" params. name_key = request_params.pop("namekey", name_key) name_prefix = request_params.pop("nameprefix", name_prefix) audio_select = session_.options.get("hls-audio-select") or [] res = session_.http.get(url, exception=IOError, **request_params) try: parser = hls_playlist.load(res.text, base_uri=res.url) except ValueError as err: raise IOError("Failed to parse playlist: {0}".format(err)) streams = {} for playlist in filter(lambda p: not p.is_iframe, parser.playlists): names = dict(name=None, pixels=None, bitrate=None) audio_streams = [] fallback_audio = [] default_audio = [] preferred_audio = [] for media in playlist.media: if media.type == "VIDEO" and media.name: names["name"] = media.name elif media.type == "AUDIO": audio_streams.append(media) for media in audio_streams: # Media without a uri is not relevant as external audio if not media.uri: continue if not fallback_audio and media.default: fallback_audio = [media] # if the media is "audoselect" and it better matches the users preferences, use that # instead of default if not default_audio and (media.autoselect and locale.equivalent(language=media.language)): default_audio = [media] # select the first audio stream that matches the users explict language selection if (('*' in audio_select or media.language in audio_select or media.name in audio_select) or ((not preferred_audio or media.default) and locale.explicit and locale.equivalent( language=media.language))): preferred_audio.append(media) # final fallback on the first audio stream listed fallback_audio = fallback_audio or (len(audio_streams) and audio_streams[0].uri and [audio_streams[0]]) if playlist.stream_info.resolution: width, height = playlist.stream_info.resolution names["pixels"] = "{0}p".format(height) if playlist.stream_info.bandwidth: bw = playlist.stream_info.bandwidth if bw >= 1000: names["bitrate"] = "{0}k".format(int(bw / 1000.0)) else: names["bitrate"] = "{0}k".format(bw / 1000.0) if name_fmt: stream_name = name_fmt.format(**names) else: stream_name = (names.get(name_key) or names.get("name") or names.get("pixels") or names.get("bitrate")) if not stream_name: continue if stream_name in streams: # rename duplicate streams stream_name = "{0}_alt".format(stream_name) num_alts = len(list(filter(lambda n: n.startswith(stream_name), streams.keys()))) # We shouldn't need more than 2 alt streams if num_alts >= 2: continue elif num_alts > 0: stream_name = "{0}{1}".format(stream_name, num_alts + 1) if check_streams: try: session_.http.get(playlist.uri, **request_params) except KeyboardInterrupt: raise except Exception: continue external_audio = preferred_audio or default_audio or fallback_audio if external_audio and FFMPEGMuxer.is_usable(session_): external_audio_msg = ", ".join([ "(language={0}, name={1})".format(x.language, (x.name or "N/A")) for x in external_audio ]) log.debug("Using external audio tracks for stream {0} {1}", name_prefix + stream_name, external_audio_msg) stream = MuxedHLSStream(session_, video=playlist.uri, audio=[x.uri for x in external_audio if x.uri], force_restart=force_restart, start_offset=start_offset, duration=duration, **request_params) else: stream = HLSStream(session_, playlist.uri, force_restart=force_restart, start_offset=start_offset, duration=duration, **request_params) streams[name_prefix + stream_name] = stream return streams