Fixed audio only streaming

This commit is contained in:
ArabCoders 2025-01-02 19:11:22 +03:00
parent 62c457df65
commit d8f73a618a
5 changed files with 135 additions and 94 deletions

2
.vscode/launch.json vendored
View file

@ -30,6 +30,7 @@
"console": "internalConsole",
"justMyCode": true,
"env": {
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
"YTP_CONFIG_PATH": "${workspaceFolder}/var/config",
"YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads",
"YTP_TEMP_PATH": "${workspaceFolder}/var/tmp",
@ -39,6 +40,7 @@
"YTP_YTDL_DEBUG": "true",
"YTP_MAX_WORKERS": "2",
"YTP_IGNORE_UI": "true",
"YTP_PIP_IGNORE_UPDATES": "true",
}
},
{

View file

@ -2,7 +2,7 @@ import math
import os
from urllib.parse import quote
from .Utils import calcDownloadPath, StreamingError
from .ffprobe import FFProbe
from .ffprobe import ffprobe
class M3u8:
@ -30,15 +30,14 @@ class M3u8:
raise StreamingError(f"File '{realFile}' does not exist.")
try:
ffprobe = FFProbe(realFile)
await ffprobe.run()
ff = await ffprobe(realFile)
except UnicodeDecodeError:
pass
if "duration" not in ffprobe.metadata:
if "duration" not in ff.metadata:
raise StreamingError(f"Unable to get '{realFile}' play duration.")
duration: float = float(ffprobe.metadata.get("duration"))
duration: float = float(ff.metadata.get("duration"))
m3u8 = []
@ -53,7 +52,7 @@ class M3u8:
segmentParams: dict = {}
for stream in ffprobe.streams:
for stream in ff.streams():
if stream.is_video():
if stream.codec_name not in self.ok_vcodecs:
segmentParams["vc"] = 1

View file

@ -2,10 +2,8 @@ import glob
import re
from pathlib import Path
from urllib.parse import quote
from aiohttp.web import Response
from .ffprobe import FFProbe
from .ffprobe import ffprobe
from .Subtitle import Subtitle
from .Utils import calcDownloadPath, checkId, StreamingError
@ -32,15 +30,14 @@ class Playlist:
)
try:
ffprobe = FFProbe(rFile)
await ffprobe.run()
ff = await ffprobe(rFile)
except UnicodeDecodeError:
pass
if "duration" not in ffprobe.metadata:
if "duration" not in ff.metadata:
raise StreamingError(f"Unable to get '{rFile}' duration.")
duration: float = float(ffprobe.metadata.get("duration"))
duration: float = float(ff.metadata.get("duration"))
playlist = []
playlist.append("#EXTM3U")

View file

@ -4,6 +4,7 @@ import logging
import os
import tempfile
from .ffprobe import ffprobe
from .config import Config
from .Utils import calcDownloadPath, StreamingError
@ -21,6 +22,7 @@ class Segments:
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 stream(self, path: str, file: str) -> bytes:
realFile: str = calcDownloadPath(basePath=path, folder=file, createPath=False)
@ -28,6 +30,11 @@ class Segments:
if not os.path.exists(realFile):
raise StreamingError(f"File {realFile} does not exist.")
try:
ff = await ffprobe(realFile)
except UnicodeDecodeError:
pass
tmpDir: str = tempfile.gettempdir()
tmpFile = os.path.join(tmpDir, f'ytptube_stream.{hashlib.md5(realFile.encode("utf-8")).hexdigest()}')
@ -57,24 +64,27 @@ class Segments:
fargs.append("-map_metadata")
fargs.append("-1")
fargs.append("-pix_fmt")
fargs.append("yuv420p")
fargs.append("-g")
fargs.append("52")
# video section.
if ff.has_video():
fargs.append("-pix_fmt")
fargs.append("yuv420p")
fargs.append("-g")
fargs.append("52")
fargs.append("-map")
fargs.append("0:v:0")
fargs.append("-strict")
fargs.append("-2")
fargs.append("-map")
fargs.append("0:v:0")
fargs.append("-strict")
fargs.append("-2")
fargs.append("-codec:v")
fargs.append(self.vcodec if self.vconvert else "copy")
fargs.append("-codec:v")
fargs.append(self.vcodec if self.vconvert else "copy")
# audio section.
fargs.append("-map")
fargs.append("0:a:0")
fargs.append("-codec:a")
fargs.append(self.acodec if self.aconvert else "copy")
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")

View file

@ -7,6 +7,31 @@ import functools
import json
import operator
import os
from functools import lru_cache
from typing import TypedDict
# parameter-less decorator
def async_lru_cache_decorator(async_function):
@functools.lru_cache
def cached_async_function(*args, **kwargs):
coroutine = async_function(*args, **kwargs)
return asyncio.ensure_future(coroutine)
return cached_async_function
# decorator with options
def async_lru_cache(*lru_cache_args, **lru_cache_kwargs):
def async_lru_cache_decorator(async_function):
@functools.lru_cache(*lru_cache_args, **lru_cache_kwargs)
def cached_async_function(*args, **kwargs):
coroutine = async_function(*args, **kwargs)
return asyncio.ensure_future(coroutine)
return cached_async_function
return async_lru_cache_decorator
class FFProbeError(Exception):
@ -163,78 +188,86 @@ class FFStream:
raise FFProbeError("None integer bit_rate")
class FFProbe:
"""
FFProbe wraps the ffprobe command and pulls the data into an object form::
metadata=FFProbe('multimedia-file.mov')
"""
class FFProbeResult:
def __init__(self):
self.metadata: dict = {}
self.video: list[FFStream] = []
self.audio: list[FFStream] = []
self.subtitle: list[FFStream] = []
self.attachment: list[FFStream] = []
audio: list[FFStream] = []
attachment: list[FFStream] = []
streams: list[FFStream] = []
subtitle: list[FFStream] = []
video: list[FFStream] = []
metadata: dict = {}
path_to_video: str = ""
def get(self, key: str, default=None):
return getattr(self, key) if hasattr(self, key) else default
def __init__(self, path_to_video):
self.path_to_video = path_to_video
def streams(self) -> list[FFStream]:
"List of all streams."
return self.video + self.audio + self.subtitle + self.attachment
async def run(self):
try:
with open(os.devnull, "w") as tempf:
await asyncio.create_subprocess_exec("ffprobe", "-h", stdout=tempf, stderr=tempf)
except FileNotFoundError:
raise IOError("ffprobe not found.")
def has_video(self):
"Is there a video stream?"
return len(self.video) > 0
if not os.path.isfile(self.path_to_video):
raise IOError(f"No such media file '{self.path_to_video}'.")
def has_audio(self):
"Is there an audio stream?"
return len(self.audio) > 0
args = [
"-v",
"quiet",
"-of",
"json",
"-show_streams",
"-show_format",
self.path_to_video,
]
p = await asyncio.create_subprocess_exec(
"ffprobe",
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
exitCode = await p.wait()
data, err = await p.communicate()
if 0 == exitCode:
parsed: dict = json.loads(data.decode("utf-8"))
else:
raise FFProbeError(f"FFProbe return with non-0 exit code. '{err.decode('utf-8')}'")
stream = False
self.streams = []
self.video = []
self.audio = []
self.subtitle = []
self.attachment = []
for stream in parsed.get("streams", []):
self.streams.append(FFStream(stream))
self.metadata = parsed.get("format", {})
for stream in self.streams:
if stream.is_audio():
self.audio.append(stream)
elif stream.is_video():
self.video.append(stream)
elif stream.is_subtitle():
self.subtitle.append(stream)
elif stream.is_attachment():
self.attachment.append(stream)
def has_subtitle(self):
"Is there a subtitle stream?"
return len(self.subtitle) > 0
def __repr__(self):
return "<FFprobe: {metadata}, {video}, {audio}, {subtitle}, {attachment}>".format(**vars(self))
@async_lru_cache(maxsize=512)
async def ffprobe(file: str) -> FFProbeResult:
"""
Run ffprobe on a file and return the parsed data as a dictionary.
Args:
file (str): The path to the media file.
Returns:
dict: A dictionary containing the parsed data.
"""
try:
with open(os.devnull, "w") as tempf:
await asyncio.create_subprocess_exec("ffprobe", "-h", stdout=tempf, stderr=tempf)
except FileNotFoundError:
raise IOError("ffprobe not found.")
if not os.path.isfile(file):
raise IOError(f"No such media file '{file}'.")
args = ["-v", "quiet", "-of", "json", "-show_streams", "-show_format", file]
p = await asyncio.create_subprocess_exec(
"ffprobe",
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
exitCode = await p.wait()
data, err = await p.communicate()
if 0 == exitCode:
parsed: dict = json.loads(data.decode("utf-8"))
else:
raise FFProbeError(f"FFProbe return with non-0 exit code. '{err.decode('utf-8')}'")
result = FFProbeResult()
result.metadata = parsed.get("format", {})
for stream in parsed.get("streams", []):
stream = FFStream(stream)
if stream.is_audio():
result.audio.append(stream)
elif stream.is_video():
result.video.append(stream)
elif stream.is_subtitle():
result.subtitle.append(stream)
elif stream.is_attachment():
result.attachment.append(stream)
return result