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",
|
"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",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue