def test_ffmpeg_open_vcodec_acodec(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): f = FFMPEGMuxer(session, acodec="mp3", vcodec="avc") with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', 'avc', '-c:a', 'mp3', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY)
def test_ffmpeg_open_copyts(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): f = FFMPEGMuxer(session, copyts=True) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-copyts', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY)
def test_ffmpeg_open_metadata_title(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): f = FFMPEGMuxer(session, metadata={None: ["title=test"]}) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-metadata', 'title=test', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY)
def test_ffmpeg_open_format(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): session.options.set("ffmpeg-fout", "avi") f = FFMPEGMuxer(session, format="mpegts") with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-f', 'avi', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY)
def test_ffmpeg_open_vcodec_acodec_user_override(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): session.options.set("ffmpeg-video-transcode", "divx") session.options.set("ffmpeg-audio-transcode", "ogg") f = FFMPEGMuxer(session, acodec="mp3", vcodec="avc") with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', 'divx', '-c:a', 'ogg', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY)
def test_ffmpeg_open_copyts_enable_start_at_zero_user_override(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): session.options.set("ffmpeg-copyts", True) f = FFMPEGMuxer(session, copyts=False, start_at_zero=True) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-copyts', '-start_at_zero', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY)
def open(self): if self.video_representation: video = DASHStreamReader(self, self.video_representation.id) video.open() if self.audio_representation: audio = DASHStreamReader(self, self.audio_representation.id) audio.open() if self.video_representation and self.audio_representation: return FFMPEGMuxer(self.session, video, audio, copyts=True).open() elif self.video_representation: return video elif self.audio_representation: return audio
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 = cls._get_variant_playlist(res) except ValueError as err: raise IOError("Failed to parse playlist: {0}".format(err)) streams = OrderedDict() 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 name_prefix: stream_name = "{0}{1}".format(name_prefix, stream_name) 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 = u", ".join([ u"(language={0}, name={1})".format(x.language, (x.name or "N/A")) for x in external_audio ]) log.debug(u"Using external audio tracks for stream {0} {1}".format( 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 = cls(session_, playlist.uri, force_restart=force_restart, start_offset=start_offset, duration=duration, **request_params) streams[stream_name] = stream return streams
def parse_variant_playlist( cls, session_, url: str, name_key: str = "name", name_prefix: str = "", check_streams: bool = False, force_restart: bool = False, name_fmt: Optional[str] = None, start_offset: float = 0, duration: Optional[float] = None, **request_params ) -> Dict[str, Union["HLSStream", "MuxedHLSStream"]]: """ Parse a variant playlist and return its streams. :param streamlink.Streamlink session_: Streamlink session instance :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 :param start_offset: Number of seconds to be skipped from the beginning :param duration: Number of second until ending the stream :param request_params: Additional keyword arguments passed to :class:`HLSStream`, :class:`MuxedHLSStream`, or :py:meth:`requests.Session.request` """ locale = session_.localization audio_select = session_.options.get("hls-audio-select") or [] res = session_.http.get(url, exception=IOError, **request_params) try: parser = cls._get_variant_playlist(res) except ValueError as err: raise OSError("Failed to parse playlist: {0}".format(err)) streams = OrderedDict() 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 name_prefix: stream_name = "{0}{1}".format(name_prefix, stream_name) 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([ f"(language={x.language}, name={x.name or 'N/A'})" for x in external_audio ]) log.debug( f"Using external audio tracks for stream {stream_name} {external_audio_msg}" ) stream = MuxedHLSStream( session_, video=playlist.uri, audio=[x.uri for x in external_audio if x.uri], url_master=url, force_restart=force_restart, start_offset=start_offset, duration=duration, **request_params) else: stream = cls(session_, playlist.uri, url_master=url, force_restart=force_restart, start_offset=start_offset, duration=duration, **request_params) streams[stream_name] = stream return streams
def test_ffmpeg_command(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): assert FFMPEGMuxer.command(session) == "ffmpeg"
def _get_streams(self): page = http.get(self.url, schema=_schema) if not page: return pubkey_pem = get_public_key(self.cache, urljoin(self.url, page["clientlibs"])) if not pubkey_pem: raise PluginError("Unable to get public key") flashvars = page["flashvars"] params = { "cashPath": int(time.time() * 1000) } res = http.get(urljoin(self.url, flashvars["country"]), params=params) if not res: return language = http.xml(res, schema=_language_schema) api_params = {} for key in ("ss_id", "mv_id", "device_cd", "ss1_prm", "ss2_prm", "ss3_prm"): if flashvars.get(key, ""): api_params[key] = flashvars[key] aeskey = number.long_to_bytes(random.getrandbits(8 * 32), 32) params = { "s": flashvars["s"], "c": language, "e": self.url, "d": aes_encrypt(aeskey, json.dumps(api_params)), "a": rsa_encrypt(pubkey_pem, aeskey) } res = http.get(urljoin(self.url, flashvars["init"]), params=params) if not res: return rtn = http.json(res, schema=_init_schema) if not rtn: return init_data = parse_json(aes_decrypt(aeskey, rtn)) parsed = urlparse(init_data["play_url"]) if parsed.scheme != "https" or not parsed.path.startswith("/i/") or not parsed.path.endswith("/master.m3u8"): return hlsstream_url = init_data["play_url"] streams = HLSStream.parse_variant_playlist(self.session, hlsstream_url) if "caption_url" in init_data: if self.get_option("mux_subtitles") and FFMPEGMuxer.is_usable(self.session): res = http.get(init_data["caption_url"]) srt = http.xml(res, ignore_ns=True, schema=_xml_to_srt_schema) subfiles = [] metadata = {} for i, lang, srt in ((i, s[0], s[1]) for i, s in enumerate(srt)): subfile = tempfile.TemporaryFile() subfile.write(srt.encode("utf8")) subfile.seek(0) subfiles.append(FileStream(self.session, fileobj=subfile)) metadata["s:s:{0}".format(i)] = ["language={0}".format(lang)] for n, s in streams.items(): yield n, MuxedStream(self.session, s, *subfiles, maps=list(range(0, len(metadata) + 1)), metadata=metadata) return else: self.logger.info("Subtitles: {0}".format(init_data["caption_url"])) for s in streams.items(): yield s
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 = 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