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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,31 @@ import functools
import json import json
import operator import operator
import os 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): class FFProbeError(Exception):
@ -163,78 +188,86 @@ class FFStream:
raise FFProbeError("None integer bit_rate") raise FFProbeError("None integer bit_rate")
class FFProbe: class FFProbeResult:
""" def __init__(self):
FFProbe wraps the ffprobe command and pulls the data into an object form:: self.metadata: dict = {}
metadata=FFProbe('multimedia-file.mov') self.video: list[FFStream] = []
""" self.audio: list[FFStream] = []
self.subtitle: list[FFStream] = []
self.attachment: list[FFStream] = []
audio: list[FFStream] = [] def get(self, key: str, default=None):
attachment: list[FFStream] = [] return getattr(self, key) if hasattr(self, key) else default
streams: list[FFStream] = []
subtitle: list[FFStream] = []
video: list[FFStream] = []
metadata: dict = {}
path_to_video: str = ""
def __init__(self, path_to_video): def streams(self) -> list[FFStream]:
self.path_to_video = path_to_video "List of all streams."
return self.video + self.audio + self.subtitle + self.attachment
async def run(self): def has_video(self):
try: "Is there a video stream?"
with open(os.devnull, "w") as tempf: return len(self.video) > 0
await asyncio.create_subprocess_exec("ffprobe", "-h", stdout=tempf, stderr=tempf)
except FileNotFoundError:
raise IOError("ffprobe not found.")
if not os.path.isfile(self.path_to_video): def has_audio(self):
raise IOError(f"No such media file '{self.path_to_video}'.") "Is there an audio stream?"
return len(self.audio) > 0
args = [ def has_subtitle(self):
"-v", "Is there a subtitle stream?"
"quiet", return len(self.subtitle) > 0
"-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 __repr__(self): def __repr__(self):
return "<FFprobe: {metadata}, {video}, {audio}, {subtitle}, {attachment}>".format(**vars(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