Skip to content

Commit

Permalink
fix: proxify streams to use plugins session parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
vzhd1701 committed Jul 1, 2022
1 parent 8c33a6f commit 021e61a
Show file tree
Hide file tree
Showing 16 changed files with 1,097 additions and 197 deletions.
89 changes: 89 additions & 0 deletions gridplayer/models/stream.py
@@ -0,0 +1,89 @@
import re
from dataclasses import dataclass
from typing import Dict, Iterable, Optional, Tuple


class HashableDict(dict): # noqa: WPS600
def __hash__(self):
return hash(frozenset(self.items()))


@dataclass(frozen=True)
class StreamSessionOpts(object):
service: str
session_headers: Optional[HashableDict]


@dataclass(frozen=True)
class Stream(object):
url: str
protocol: str
session: Optional[StreamSessionOpts] = None
audio_tracks: Optional["Streams"] = None


class Streams(object):
def __init__(self, streams: Optional[Dict[str, Stream]] = None):
if streams:
self.streams = HashableDict(streams)
else:
self.streams = HashableDict()

def __hash__(self):
return hash(self.streams)

def __getitem__(self, key):
return self.streams[key]

def __setitem__(self, key, value): # noqa: WPS110
self.streams[key] = value

def __len__(self):
return len(self.streams)

def __iter__(self):
return iter(self.streams)

def __reversed__(self):
return reversed(self.streams)

def items(self) -> Iterable[Tuple[str, Stream]]: # noqa: WPS110
return self.streams.items()

@property
def best(self) -> Tuple[str, Stream]:
return list(self.streams.items())[-1]

@property
def worst(self) -> Tuple[str, Stream]:
return list(self.streams.items())[0]

def by_quality(self, quality: str) -> Tuple[str, Stream]:
if quality == "best":
return self.best

if quality == "worst":
return self.worst

if self.streams.get(quality):
return quality, self.streams[quality]

return self._guess_quality(quality)

def _guess_quality(self, quality: str) -> Tuple[str, Stream]:
quality_lines = re.search(r"^(\d+)", quality)
if not quality_lines:
return self.best

quality_lines = int(quality_lines.group(1))
for quality_code, stream_url in reversed(self.streams.items()):
stream_lines = re.search(r"^(\d+)", quality_code)
if not stream_lines:
continue

stream_lines = int(stream_lines.group(1))

if stream_lines <= quality_lines:
return quality_code, stream_url

return self.best
36 changes: 36 additions & 0 deletions gridplayer/player/managers/stream_proxy.py
@@ -0,0 +1,36 @@
from threading import Lock

from gridplayer.models.stream import Stream
from gridplayer.player.managers.base import ManagerBase
from gridplayer.utils.stream_proxy.stream_proxy import StreamProxy


class StreamProxyManager(ManagerBase):
def __init__(self, **kwargs):
super().__init__(**kwargs)

self._stream_proxy_lock = Lock()
self._stream_proxy = None

@property
def stream_proxy(self):
with self._stream_proxy_lock:
if self._stream_proxy is None:
self._stream_proxy = StreamProxy(parent=self)
self._stream_proxy.start()

return self._stream_proxy

@property
def commands(self):
return {
"add_stream": self.cmd_add_stream,
}

def cmd_add_stream(self, stream: Stream) -> str:
return self.stream_proxy.add_stream(stream)

def cleanup(self):
if self._stream_proxy is not None:
self._stream_proxy.cleanup()
self._stream_proxy = None
2 changes: 2 additions & 0 deletions gridplayer/player/player.py
Expand Up @@ -19,6 +19,7 @@
from gridplayer.player.managers.settings import SettingsManager
from gridplayer.player.managers.single_mode import SingleModeManager
from gridplayer.player.managers.snapshots import SnapshotsManager
from gridplayer.player.managers.stream_proxy import StreamProxyManager
from gridplayer.player.managers.video_blocks import VideoBlocksManager
from gridplayer.player.managers.video_driver import VideoDriverManager
from gridplayer.player.managers.window_state import WindowStateManager
Expand Down Expand Up @@ -46,6 +47,7 @@ def __init__(self, **kwargs):
"drag_n_drop": DragNDropManager,
"single_mode": SingleModeManager,
"log": LogManager,
"stream_proxy": StreamProxyManager,
"add_videos": AddVideosManager,
"dialogs": DialogsManager,
"settings": SettingsManager,
Expand Down
Empty file.
64 changes: 64 additions & 0 deletions gridplayer/utils/stream_proxy/m3u8.py
@@ -0,0 +1,64 @@
from typing import List

from streamlink.stream.hls_playlist import M3U8, ByteRange, Segment

LIVESTREAM_EDGE = 16


def m3u8_to_str(hls_playlist: M3U8):
res = ["#EXTM3U"]
res += [f"#EXT-X-VERSION:{hls_playlist.version}"]
res += [f"#EXT-X-TARGETDURATION:{hls_playlist.target_duration}"]
if hls_playlist.playlist_type:
res += [f"#EXT-X-PLAYLIST-TYPE:{hls_playlist.playlist_type}"]
res += [f"#EXT-X-MEDIA-SEQUENCE:{hls_playlist.media_sequence}"]

# grab only the edge if it's a livestream
if hls_playlist.media_sequence:
segments = hls_playlist.segments[-LIVESTREAM_EDGE:]
else:
segments = hls_playlist.segments

res += sum([_segment_to_str(s) for s in segments], [])

if hls_playlist.is_endlist:
res += ["#EXT-X-ENDLIST"]

return "\n".join(res)


def _segment_to_str(segment: Segment) -> List[str]:
res = []

if segment.date:
timestamp = segment.date.isoformat(timespec="seconds")
timestamp = timestamp.replace("+00:00", "Z")
res += [f"#EXT-X-PROGRAM-DATE-TIME:{timestamp}"]
if segment.discontinuity:
res += ["#EXT-X-DISCONTINUITY"]
if segment.byterange:
res += ["#EXT-X-BYTERANGE:{0}".format(_byterange_to_str(segment.byterange))]
if segment.map:
res += [
'#EXT-X-MAP:URI="{0}"{1}'.format(
segment.map.uri,
',BYTERANGE="{0}"'.format(_byterange_to_str(segment.map.byterange))
if segment.map.byterange
else "",
)
]

res += [
"#EXTINF:{0},{1}".format(
segment.duration, segment.title if segment.title else ""
)
]

res += [segment.uri]

return res


def _byterange_to_str(byterange: ByteRange) -> str:
offset_txt = f"@{byterange.offset}" if byterange.offset else ""
return f"{byterange.range}{offset_txt}"

0 comments on commit 021e61a

Please sign in to comment.