Fixed audio only streaming
This commit is contained in:
parent
62c457df65
commit
d8f73a618a
5 changed files with 135 additions and 94 deletions
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -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",
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue