Exemple #1
0
    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)
Exemple #2
0
    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)
Exemple #3
0
    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')
            ]
        )
Exemple #4
0
    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)
Exemple #5
0
    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')])
Exemple #7
0
 def _get_variant_playlist(cls, res):
     return hls_playlist.load(res.text, base_uri=res.url)
Exemple #8
0
 def _reload_playlist(self, text, url):
     return hls_playlist.load(text, url)
Exemple #9
0
    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])
Exemple #10
0
    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
Exemple #11
0
    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
Exemple #12
0
    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