128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
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()
|