Refactor streaming logic to be more efficient

This commit is contained in:
arabcoders 2025-06-01 18:22:19 +03:00
parent 890f2e99ef
commit d6aa711667
2 changed files with 89 additions and 72 deletions

View file

@ -1383,16 +1383,8 @@ class HttpAPI(Common):
lastMod = time.strftime("%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(mtime, tz=UTC).timetuple()) lastMod = time.strftime("%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(mtime, tz=UTC).timetuple())
return web.Response(status=web.HTTPNotModified.status_code, headers={"Last-Modified": lastMod}) return web.Response(status=web.HTTPNotModified.status_code, headers={"Last-Modified": lastMod})
segmenter = Segments( resp = web.StreamResponse(
download_path=self.config.download_path, status=web.HTTPOk.status_code,
index=int(segment),
duration=float(f"{float(sd if sd else M3u8.duration):.6f}"),
vconvert=vc == 1,
aconvert=ac == 1,
)
return web.Response(
body=await segmenter.stream(file=realFile),
headers={ headers={
"Content-Type": "video/mpegts", "Content-Type": "video/mpegts",
"X-Accel-Buffering": "no", "X-Accel-Buffering": "no",
@ -1406,9 +1398,20 @@ class HttpAPI(Common):
"%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(time.time() + 31536000, tz=UTC).timetuple() "%a, %d %b %Y %H:%M:%S GMT", datetime.fromtimestamp(time.time() + 31536000, tz=UTC).timetuple()
), ),
}, },
status=web.HTTPOk.status_code,
) )
await resp.prepare(request)
await Segments(
download_path=self.config.download_path,
index=int(segment),
duration=float(f"{float(sd if sd else M3u8.duration):.6f}"),
vconvert=vc == 1,
aconvert=ac == 1,
).stream(realFile, resp)
return resp
@route("GET", "api/player/subtitle/{file:.*}.vtt") @route("GET", "api/player/subtitle/{file:.*}.vtt")
async def subtitles(self, request: Request) -> Response: async def subtitles(self, request: Request) -> Response:
""" """

View file

@ -5,9 +5,10 @@ import os
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from aiohttp import web
from .config import Config from .config import Config
from .ffprobe import ffprobe from .ffprobe import ffprobe
from .Utils import StreamingError
LOG = logging.getLogger("player.segments") LOG = logging.getLogger("player.segments")
@ -26,86 +27,99 @@ class Segments:
self.vconvert = True self.vconvert = True
self.aconvert = True self.aconvert = True
async def stream(self, file: Path) -> bytes: async def build_ffmpeg_args(self, file: Path) -> list[str]:
try: try:
ff = await ffprobe(file) ff = await ffprobe(file)
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
tmpDir: str = tempfile.gettempdir() tmpDir = tempfile.gettempdir()
tmpFile = os.path.join(tmpDir, f'ytptube_stream.{hashlib.sha256(str(file).encode("utf-8")).hexdigest()}') tmpFile = os.path.join(tmpDir, f"ytptube_stream.{hashlib.sha256(str(file).encode()).hexdigest()}")
if not os.path.exists(tmpFile): if not os.path.exists(tmpFile):
os.symlink(file, tmpFile) os.symlink(file, tmpFile)
if self.index == 0: startTime = f"{0:.6f}" if self.index == 0 else f"{self.duration * self.index:.6f}"
startTime: float = f"{0:.6f}"
else:
startTime: float = f"{self.duration * self.index:.6f}"
fargs = [] fargs = [
fargs.append("-xerror") "-xerror",
fargs.append("-hide_banner") "-hide_banner",
fargs.append("-loglevel") "-loglevel",
fargs.append("error") "error",
"-ss",
str(startTime),
"-t",
str(f"{self.duration:.6f}"),
"-copyts",
"-i",
f"file:{tmpFile}",
"-map_metadata",
"-1",
]
fargs.append("-ss") if ff and ff.has_video():
fargs.append(str(startTime if startTime else "0.00000")) fargs += [
fargs.append("-t") "-pix_fmt",
fargs.append(str(f"{self.duration:.6f}")) "yuv420p",
"-g",
"52",
"-map",
"0:v:0",
"-strict",
"-2",
"-codec:v",
self.vcodec if self.vconvert else "copy",
]
fargs.append("-copyts") if ff and ff.has_audio():
fargs += ["-map", "0:a:0", "-codec:a", self.acodec if self.aconvert else "copy"]
fargs.append("-i") fargs += ["-sn", "-muxdelay", "0", "-f", "mpegts", "pipe:1"]
fargs.append(f"file:{tmpFile}") return fargs
fargs.append("-map_metadata")
fargs.append("-1")
# video section. async def stream(self, file: Path, resp: web.StreamResponse):
if ff.has_video(): ffmpeg_args = await self.build_ffmpeg_args(file)
fargs.append("-pix_fmt")
fargs.append("yuv420p")
fargs.append("-g")
fargs.append("52")
fargs.append("-map") proc = await asyncio.create_subprocess_exec(
fargs.append("0:v:0")
fargs.append("-strict")
fargs.append("-2")
fargs.append("-codec:v")
fargs.append(self.vcodec if self.vconvert else "copy")
# audio section.
if ff.has_audio():
fargs.append("-map")
fargs.append("0:a:0")
fargs.append("-codec:a")
fargs.append(self.acodec if self.aconvert else "copy")
fargs.append("-sn")
fargs.append("-muxdelay")
fargs.append("0")
fargs.append("-f")
fargs.append("mpegts")
fargs.append("pipe:1")
LOG.debug(f"Streaming '{file}' segment '{self.index}'. ffmpeg: {' '.join(fargs)}")
proc = await asyncio.subprocess.create_subprocess_exec(
"ffmpeg", "ffmpeg",
*fargs, *ffmpeg_args,
stdin=asyncio.subprocess.DEVNULL, stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
) )
data, err = await proc.communicate() client_disconnected = False
if 0 != proc.returncode: LOG.debug(f"Streaming '{file}' segment '{self.index}'. ffmpeg: {' '.join(ffmpeg_args)}")
LOG.error(f"Failed to stream '{file}' segment '{self.index}'. {err.decode('utf-8')}.")
msg = f"Failed to stream '{file}' segment '{self.index}'."
raise StreamingError(msg)
return data try:
while True:
chunk = 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()