Add Qt mac backend support, Safari/Chrome playback fixes, and AV1 reel updates

This commit is contained in:
2026-05-22 01:39:28 +00:00
parent eeed9f3ef7
commit 7eb81877fd
6 changed files with 89 additions and 38 deletions
+5 -5
View File
@@ -44,19 +44,19 @@ export function getVideoPreviewUrl(url: string): string {
export function getVideoSourceAttributes(path: string | null | undefined): VideoSourceAttributes {
if (!path) {
return { type: 'video/mp4; codecs="hvc1"', codecs: 'hvc1' };
return { type: 'video/mp4', codecs: 'hvc1' };
}
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)) {
return { type: 'video/mp4; codecs="hvc1"', codecs: 'hvc1' };
return { type: 'video/mp4', codecs: 'hvc1' };
}
if (/\.mov$/i.test(path)) {
return { type: 'video/quicktime; codecs="hvc1"', codecs: 'hvc1' };
return { type: 'video/quicktime', codecs: 'hvc1' };
}
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/mp4; codecs="hvc1"', codecs: 'hvc1' };
return { type: 'video/mp4', codecs: 'hvc1' };
}
/**
+8
View File
@@ -33,6 +33,8 @@ async def _subprocess_exec(*args, **kwargs):
# Showreel timestamp positions in seconds (5, 10, 15, 20, 25 minutes)
SHOWREEL_TIMESTAMPS = [5 * 60, 10 * 60, 15 * 60, 20 * 60, 25 * 60]
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]:
@@ -52,11 +54,15 @@ def _to_media_path(path: Path, media_root: Optional[Path] = None) -> str:
def get_reel_extension() -> str:
"""Return the platform-native reel file extension."""
if not ENABLE_PLATFORM_NATIVE_REELS:
return ".webm"
return ".mp4" if sys.platform == "darwin" else ".webm"
async def get_reel_video_encoder() -> str:
"""Return the platform-native reel video encoder."""
if not ENABLE_PLATFORM_NATIVE_REELS:
return await get_av1_encoder()
if sys.platform == "darwin":
return "libx265"
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]:
"""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":
return ["-c:a", "aac", "-ac", "2", "-b:a", "128k", "-movflags", "+faststart"]
return ["-c:a", "libopus", "-ac", "2", "-b:a", "128k"]
+1
View File
@@ -0,0 +1 @@
"""Shared data/protocol models for MediaHive."""
+25 -2
View File
@@ -28,6 +28,8 @@ import webview
from mediahive.__main__ import resolve_media_root
from mediahive.config import load_config, save_config
logger = logging.getLogger("mediahive.winmain")
BACKEND_HOST = "127.0.0.1"
BACKEND_PORT = 8420
HEALTH_TIMEOUT = 2 # seconds
@@ -669,6 +671,19 @@ def _icon_path() -> str | 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:
"""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])
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
@@ -766,6 +781,7 @@ def winmain() -> None:
raise RuntimeError(f"Backend did not become ready within {HEALTH_TIMEOUT}s")
api = JsApi()
logger.info("Configured pywebview backend: %s", _selected_webview_backend())
window = webview.create_window(
title="MediaHive",
url=backend_url,
@@ -778,11 +794,18 @@ def winmain() -> None:
def on_shown() -> None:
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
if poll_thread is None and _supports_gamepad_remote():
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()
if poll_thread is not None:
+4 -1
View File
@@ -46,7 +46,10 @@ parse-torrent-title = { git = "https://github.com/platelminto/parse-torrent-titl
[project.optional-dependencies]
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'",
"pyinstaller>=6.0",
]
+46 -30
View File
@@ -36,41 +36,57 @@ if _icon_win.exists():
if _icon_mac.exists():
_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(
[mediahive.winmain.__file__],
pathex=[],
binaries=_binaries,
datas=_datas,
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",
],
hiddenimports=_hiddenimports,
hookspath=[],
runtime_hooks=[],
excludes=[],