import asyncio import hashlib import logging import os import subprocess import tempfile from pathlib import Path from aiohttp import web from .config import Config from .ffprobe import ffprobe LOG = logging.getLogger("player.segments") class Segments: def __init__(self, download_path: str, index: int, duration: float, vconvert: bool, aconvert: bool): config = Config.get_instance() self.download_path = download_path self.index = int(index) self.duration = float(duration) self.vconvert = bool(vconvert) self.aconvert = bool(aconvert) self.vcodec = config.streamer_vcodec self.acodec = config.streamer_acodec # sadly due to unforeseen circumstances, we have to convert the video for now. self.vconvert = True self.aconvert = True async def build_ffmpeg_args(self, file: Path) -> list[str]: try: ff = await ffprobe(file) except UnicodeDecodeError: pass tmpFile = Path(tempfile.gettempdir()).joinpath( f"ytptube_stream.{hashlib.sha256(str(file).encode()).hexdigest()}" ) if not tmpFile.exists(): tmpFile.symlink_to(file, target_is_directory=False) startTime: str = f"{0:.6f}" if self.index == 0 else f"{self.duration * self.index:.6f}" fargs = [ "-xerror", "-hide_banner", "-loglevel", "error", "-ss", str(startTime), "-t", str(f"{self.duration:.6f}"), "-copyts", "-i", f"file:{tmpFile}", "-map_metadata", "-1", ] if ff and ff.has_video(): fargs += [ "-pix_fmt", "yuv420p", "-g", "52", "-map", "0:v:0", "-strict", "-2", "-codec:v", self.vcodec if self.vconvert else "copy", ] if ff and ff.has_audio(): fargs += ["-map", "0:a:0", "-codec:a", self.acodec if self.aconvert else "copy"] fargs += ["-sn", "-muxdelay", "0", "-f", "mpegts", "pipe:1"] return fargs async def stream(self, file: Path, resp: web.StreamResponse): ffmpeg_args: list[str] = await self.build_ffmpeg_args(file) proc = await asyncio.create_subprocess_exec( "ffmpeg", *ffmpeg_args, stdin=asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, ) client_disconnected = False LOG.debug(f"Streaming '{file}' segment '{self.index}'. ffmpeg: {' '.join(ffmpeg_args)}") try: while True: chunk: bytes = await proc.stdout.read(1024 * 64) if not chunk: break try: await resp.write(chunk) except (asyncio.CancelledError, ConnectionResetError, BrokenPipeError, ConnectionError): LOG.warning("Client disconnected or connection reset while writing.") client_disconnected = True break except asyncio.CancelledError: LOG.warning("Client disconnected. Terminating ffmpeg.") client_disconnected = True proc.terminate() try: await asyncio.wait_for(proc.wait(), timeout=5) except TimeoutError: LOG.error("ffmpeg process did not terminate in time. Killing it.") proc.kill() raise except ConnectionResetError: LOG.warning("Connection reset by peer. Skipping further writes.") client_disconnected = True finally: if not client_disconnected: try: await resp.write_eof() except (ConnectionResetError, RuntimeError): LOG.warning("Failed to write EOF; client already disconnected.") await proc.wait()