Add Qt mac backend support, Safari/Chrome playback fixes, and AV1 reel updates
This commit is contained in:
+5
-5
@@ -44,19 +44,19 @@ export function getVideoPreviewUrl(url: string): string {
|
|||||||
|
|
||||||
export function getVideoSourceAttributes(path: string | null | undefined): VideoSourceAttributes {
|
export function getVideoSourceAttributes(path: string | null | undefined): VideoSourceAttributes {
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return { type: 'video/mp4; codecs="hvc1"', codecs: 'hvc1' };
|
return { type: 'video/mp4', codecs: 'hvc1' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\.webm$/i.test(path)) {
|
if (/\.webm$/i.test(path)) {
|
||||||
return { type: 'video/webm; codecs="av01"', codecs: 'av01' };
|
return { type: 'video/webm', codecs: 'av1' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\.mp4$/i.test(path) || /\.m4v$/i.test(path)) {
|
if (/\.mp4$/i.test(path) || /\.m4v$/i.test(path)) {
|
||||||
return { type: 'video/mp4; codecs="hvc1"', codecs: 'hvc1' };
|
return { type: 'video/mp4', codecs: 'hvc1' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\.mov$/i.test(path)) {
|
if (/\.mov$/i.test(path)) {
|
||||||
return { type: 'video/quicktime; codecs="hvc1"', codecs: 'hvc1' };
|
return { type: 'video/quicktime', codecs: 'hvc1' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\.avi$/i.test(path)) {
|
if (/\.avi$/i.test(path)) {
|
||||||
@@ -67,7 +67,7 @@ export function getVideoSourceAttributes(path: string | null | undefined): Video
|
|||||||
return { type: 'video/x-matroska', codecs: '' };
|
return { type: 'video/x-matroska', codecs: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: 'video/mp4; codecs="hvc1"', codecs: 'hvc1' };
|
return { type: 'video/mp4', codecs: 'hvc1' };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ async def _subprocess_exec(*args, **kwargs):
|
|||||||
# Showreel timestamp positions in seconds (5, 10, 15, 20, 25 minutes)
|
# Showreel timestamp positions in seconds (5, 10, 15, 20, 25 minutes)
|
||||||
SHOWREEL_TIMESTAMPS = [5 * 60, 10 * 60, 15 * 60, 20 * 60, 25 * 60]
|
SHOWREEL_TIMESTAMPS = [5 * 60, 10 * 60, 15 * 60, 20 * 60, 25 * 60]
|
||||||
REEL_SOURCE_EXTENSIONS = [".webm", ".mp4"]
|
REEL_SOURCE_EXTENSIONS = [".webm", ".mp4"]
|
||||||
|
# Temporary rollout switch: keep platform-native MP4/H.265 path available but disabled.
|
||||||
|
ENABLE_PLATFORM_NATIVE_REELS = False
|
||||||
|
|
||||||
|
|
||||||
def get_reel_source_extensions() -> list[str]:
|
def get_reel_source_extensions() -> list[str]:
|
||||||
@@ -52,11 +54,15 @@ def _to_media_path(path: Path, media_root: Optional[Path] = None) -> str:
|
|||||||
|
|
||||||
def get_reel_extension() -> str:
|
def get_reel_extension() -> str:
|
||||||
"""Return the platform-native reel file extension."""
|
"""Return the platform-native reel file extension."""
|
||||||
|
if not ENABLE_PLATFORM_NATIVE_REELS:
|
||||||
|
return ".webm"
|
||||||
return ".mp4" if sys.platform == "darwin" else ".webm"
|
return ".mp4" if sys.platform == "darwin" else ".webm"
|
||||||
|
|
||||||
|
|
||||||
async def get_reel_video_encoder() -> str:
|
async def get_reel_video_encoder() -> str:
|
||||||
"""Return the platform-native reel video encoder."""
|
"""Return the platform-native reel video encoder."""
|
||||||
|
if not ENABLE_PLATFORM_NATIVE_REELS:
|
||||||
|
return await get_av1_encoder()
|
||||||
if sys.platform == "darwin":
|
if sys.platform == "darwin":
|
||||||
return "libx265"
|
return "libx265"
|
||||||
return await get_av1_encoder()
|
return await get_av1_encoder()
|
||||||
@@ -73,6 +79,8 @@ def get_reel_video_options(encoder: str) -> list[str]:
|
|||||||
|
|
||||||
def get_reel_audio_options() -> list[str]:
|
def get_reel_audio_options() -> list[str]:
|
||||||
"""Return ffmpeg audio and container options for the current platform."""
|
"""Return ffmpeg audio and container options for the current platform."""
|
||||||
|
if not ENABLE_PLATFORM_NATIVE_REELS:
|
||||||
|
return ["-c:a", "libopus", "-ac", "2", "-b:a", "128k"]
|
||||||
if sys.platform == "darwin":
|
if sys.platform == "darwin":
|
||||||
return ["-c:a", "aac", "-ac", "2", "-b:a", "128k", "-movflags", "+faststart"]
|
return ["-c:a", "aac", "-ac", "2", "-b:a", "128k", "-movflags", "+faststart"]
|
||||||
return ["-c:a", "libopus", "-ac", "2", "-b:a", "128k"]
|
return ["-c:a", "libopus", "-ac", "2", "-b:a", "128k"]
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Shared data/protocol models for MediaHive."""
|
||||||
+25
-2
@@ -28,6 +28,8 @@ import webview
|
|||||||
from mediahive.__main__ import resolve_media_root
|
from mediahive.__main__ import resolve_media_root
|
||||||
from mediahive.config import load_config, save_config
|
from mediahive.config import load_config, save_config
|
||||||
|
|
||||||
|
logger = logging.getLogger("mediahive.winmain")
|
||||||
|
|
||||||
BACKEND_HOST = "127.0.0.1"
|
BACKEND_HOST = "127.0.0.1"
|
||||||
BACKEND_PORT = 8420
|
BACKEND_PORT = 8420
|
||||||
HEALTH_TIMEOUT = 2 # seconds
|
HEALTH_TIMEOUT = 2 # seconds
|
||||||
@@ -669,6 +671,19 @@ def _icon_path() -> str | None:
|
|||||||
return str(ico) if ico.exists() else None
|
return str(ico) if ico.exists() else None
|
||||||
|
|
||||||
|
|
||||||
|
def _webview_start_kwargs() -> dict[str, str]:
|
||||||
|
"""Return platform-specific pywebview startup kwargs."""
|
||||||
|
# On macOS, force Qt backend so pywebview uses Chromium/WebEngine instead of WKWebView.
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
return {"gui": "qt"}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _selected_webview_backend() -> str:
|
||||||
|
"""Return the configured pywebview GUI backend name for logging."""
|
||||||
|
return _webview_start_kwargs().get("gui", "default")
|
||||||
|
|
||||||
|
|
||||||
def _run_initial_setup() -> str | None:
|
def _run_initial_setup() -> str | None:
|
||||||
"""Show a setup window, prompt for a folder, then close and return the path.
|
"""Show a setup window, prompt for a folder, then close and return the path.
|
||||||
|
|
||||||
@@ -691,7 +706,7 @@ def _run_initial_setup() -> str | None:
|
|||||||
chosen.append(result[0])
|
chosen.append(result[0])
|
||||||
window.destroy()
|
window.destroy()
|
||||||
|
|
||||||
webview.start(func=on_shown, icon=_icon_path())
|
webview.start(func=on_shown, icon=_icon_path(), **_webview_start_kwargs())
|
||||||
return chosen[0] if chosen else None
|
return chosen[0] if chosen else None
|
||||||
|
|
||||||
|
|
||||||
@@ -766,6 +781,7 @@ def winmain() -> None:
|
|||||||
raise RuntimeError(f"Backend did not become ready within {HEALTH_TIMEOUT}s")
|
raise RuntimeError(f"Backend did not become ready within {HEALTH_TIMEOUT}s")
|
||||||
|
|
||||||
api = JsApi()
|
api = JsApi()
|
||||||
|
logger.info("Configured pywebview backend: %s", _selected_webview_backend())
|
||||||
window = webview.create_window(
|
window = webview.create_window(
|
||||||
title="MediaHive",
|
title="MediaHive",
|
||||||
url=backend_url,
|
url=backend_url,
|
||||||
@@ -778,11 +794,18 @@ def winmain() -> None:
|
|||||||
|
|
||||||
def on_shown() -> None:
|
def on_shown() -> None:
|
||||||
api._window = window
|
api._window = window
|
||||||
|
try:
|
||||||
|
user_agent = window.evaluate_js("navigator.userAgent")
|
||||||
|
if isinstance(user_agent, str):
|
||||||
|
logger.info("Embedded webview user agent: %s", user_agent)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Could not read embedded user agent: %s", exc)
|
||||||
|
|
||||||
nonlocal poll_thread
|
nonlocal poll_thread
|
||||||
if poll_thread is None and _supports_gamepad_remote():
|
if poll_thread is None and _supports_gamepad_remote():
|
||||||
poll_thread = _start_gamepad_remote(poll_stop, mediaroot)
|
poll_thread = _start_gamepad_remote(poll_stop, mediaroot)
|
||||||
|
|
||||||
webview.start(func=on_shown, icon=_icon_path())
|
webview.start(func=on_shown, icon=_icon_path(), **_webview_start_kwargs())
|
||||||
|
|
||||||
poll_stop.set()
|
poll_stop.set()
|
||||||
if poll_thread is not None:
|
if poll_thread is not None:
|
||||||
|
|||||||
+4
-1
@@ -46,7 +46,10 @@ parse-torrent-title = { git = "https://github.com/platelminto/parse-torrent-titl
|
|||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
gui = [
|
gui = [
|
||||||
"pywebview>=6.2.1",
|
"pywebview>=6.2.1; platform_system != 'Darwin'",
|
||||||
|
"pywebview[qt5]>=6.2.1; platform_system == 'Darwin'",
|
||||||
|
"qtpy>=2.4.1; platform_system == 'Darwin'",
|
||||||
|
"PyQt5>=5.15.11; platform_system == 'Darwin'",
|
||||||
"pythonnet>=3.1.0rc0; platform_system == 'Windows' and python_version >= '3.14'",
|
"pythonnet>=3.1.0rc0; platform_system == 'Windows' and python_version >= '3.14'",
|
||||||
"pyinstaller>=6.0",
|
"pyinstaller>=6.0",
|
||||||
]
|
]
|
||||||
|
|||||||
+46
-30
@@ -36,41 +36,57 @@ if _icon_win.exists():
|
|||||||
if _icon_mac.exists():
|
if _icon_mac.exists():
|
||||||
_datas.append((str(_icon_mac), "mediahive/assets"))
|
_datas.append((str(_icon_mac), "mediahive/assets"))
|
||||||
|
|
||||||
|
_hiddenimports = [
|
||||||
|
# uvicorn dynamic imports
|
||||||
|
"uvicorn.logging",
|
||||||
|
"uvicorn.loops",
|
||||||
|
"uvicorn.loops.auto",
|
||||||
|
"uvicorn.loops.asyncio",
|
||||||
|
"uvicorn.protocols",
|
||||||
|
"uvicorn.protocols.http",
|
||||||
|
"uvicorn.protocols.http.auto",
|
||||||
|
"uvicorn.protocols.http.h11_impl",
|
||||||
|
"uvicorn.protocols.websockets",
|
||||||
|
"uvicorn.protocols.websockets.auto",
|
||||||
|
"uvicorn.protocols.websockets.websockets_impl",
|
||||||
|
"uvicorn.lifespan",
|
||||||
|
"uvicorn.lifespan.on",
|
||||||
|
# mediahive & hivescan modules imported at runtime
|
||||||
|
"mediahive.server",
|
||||||
|
"mediahive.hivescan.scanner",
|
||||||
|
"mediahive.hivescan.indexer",
|
||||||
|
"mediahive.hivescan.scanning",
|
||||||
|
"mediahive.hivescan.images",
|
||||||
|
"mediahive.hivescan.showreel",
|
||||||
|
"mediahive.hivescan.tmdb_client",
|
||||||
|
# async / ASGI internals
|
||||||
|
"anyio",
|
||||||
|
"anyio._backends._asyncio",
|
||||||
|
"starlette.routing",
|
||||||
|
# msgspec TOML write backend
|
||||||
|
"tomli_w",
|
||||||
|
]
|
||||||
|
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
_hiddenimports.extend(
|
||||||
|
[
|
||||||
|
# pywebview Qt backend selected dynamically via webview.start(gui="qt")
|
||||||
|
"webview.platforms.qt",
|
||||||
|
"qtpy",
|
||||||
|
"PyQt5",
|
||||||
|
"PyQt5.QtCore",
|
||||||
|
"PyQt5.QtGui",
|
||||||
|
"PyQt5.QtWidgets",
|
||||||
|
"PyQt5.QtWebEngineWidgets",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
[mediahive.winmain.__file__],
|
[mediahive.winmain.__file__],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=_binaries,
|
binaries=_binaries,
|
||||||
datas=_datas,
|
datas=_datas,
|
||||||
hiddenimports=[
|
hiddenimports=_hiddenimports,
|
||||||
# uvicorn dynamic imports
|
|
||||||
"uvicorn.logging",
|
|
||||||
"uvicorn.loops",
|
|
||||||
"uvicorn.loops.auto",
|
|
||||||
"uvicorn.loops.asyncio",
|
|
||||||
"uvicorn.protocols",
|
|
||||||
"uvicorn.protocols.http",
|
|
||||||
"uvicorn.protocols.http.auto",
|
|
||||||
"uvicorn.protocols.http.h11_impl",
|
|
||||||
"uvicorn.protocols.websockets",
|
|
||||||
"uvicorn.protocols.websockets.auto",
|
|
||||||
"uvicorn.protocols.websockets.websockets_impl",
|
|
||||||
"uvicorn.lifespan",
|
|
||||||
"uvicorn.lifespan.on",
|
|
||||||
# mediahive & hivescan modules imported at runtime
|
|
||||||
"mediahive.server",
|
|
||||||
"mediahive.hivescan.scanner",
|
|
||||||
"mediahive.hivescan.indexer",
|
|
||||||
"mediahive.hivescan.scanning",
|
|
||||||
"mediahive.hivescan.images",
|
|
||||||
"mediahive.hivescan.showreel",
|
|
||||||
"mediahive.hivescan.tmdb_client",
|
|
||||||
# async / ASGI internals
|
|
||||||
"anyio",
|
|
||||||
"anyio._backends._asyncio",
|
|
||||||
"starlette.routing",
|
|
||||||
# msgspec TOML write backend
|
|
||||||
"tomli_w",
|
|
||||||
],
|
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
excludes=[],
|
excludes=[],
|
||||||
|
|||||||
Reference in New Issue
Block a user