Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ app.commandLine.appendSwitch(
);
app.commandLine.appendSwitch(
"disable-features",
"HardwareMediaKeyHandling,MediaSessionService,UseSandboxedXdgPortal",
"UseSandboxedXdgPortal",
);
// Run the network stack in the browser process → one less utility process
app.commandLine.appendSwitch("enable-features", "NetworkServiceInProcess2");
// Also enable hardware media key handling so Bluetooth earbuds, keyboard
// media keys and the Windows media flyout (FluentFlyout) can control playback.
app.commandLine.appendSwitch(
"enable-features",
"HardwareMediaKeyHandling,MediaSessionService,NetworkServiceInProcess2",
);
// NOTE: enable-low-end-device-mode removed, it cuts the GPU texture tile budget
// and causes visible seams/stripes/dots on large images.

Expand Down Expand Up @@ -326,6 +331,23 @@ blockStats.init(getMainWindow);
// get-block-stats lives with its data
ipcMain.handle("get-block-stats", () => blockStats.getBlockStats());

// -- Media Session: inject JS into ALL webview frames -------------------------
// The renderer's executeJavaScript() only reaches the top-level frame.
// VidSrc (and similar) load their video player in a cross-origin iframe, so
// we need the main process to walk the WebFrameMain tree and inject into every
// frame — including cross-origin ones that the renderer cannot touch.
ipcMain.handle("inject-all-frames", async (_, { webContentsId, script }) => {
try {
const wc = webContents.fromId(webContentsId);
if (!wc || wc.isDestroyed()) return;
const injectFrame = async (frame) => {
try { await frame.executeJavaScript(script); } catch {}
for (const child of frame.frames ?? []) await injectFrame(child);
};
await injectFrame(wc.mainFrame);
} catch {}
});

// -- Player memory cleanup ---------------------------------------------
// Called by MoviePage / TVPage on component unmount.
// Destroys the player webview WebContents by tracked ID, then flushes caches and GCs.
Expand Down
39 changes: 0 additions & 39 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,8 @@ contextBridge.exposeInMainWorld("electron", {
},
offScheduledBackupRequested: (h) =>
ipcRenderer.removeListener("scheduled-backup-requested", h),

// Webframe main injection for frames
injectAllFrames: (webContentsId, script) =>
ipcRenderer.invoke("inject-all-frames", { webContentsId, script }),
});
82 changes: 82 additions & 0 deletions src/pages/MoviePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ import {
getAgeLimitSetting,
getRatingCountry,
} from "../utils/ageRating";
import {
setMediaSessionMetadata,
buildWebviewMetadataScript,
registerMediaSessionHandlers,
clearMediaSession,
syncPlaybackState,
} from "../utils/mediaSession";

export default function MoviePage({
item,
Expand Down Expand Up @@ -478,6 +485,9 @@ export default function MoviePage({
if (result && result.duration > 0) {
const ct = result.currentTime;

// Sync OS playback state on every progress tick
syncPlaybackState("playing");

// ── Resolution-change reset detection ──────────────────────────
// Videasy resets to 0 on quality change. We only seek back if:
// - ct is near zero (≤5s)
Expand Down Expand Up @@ -544,6 +554,78 @@ export default function MoviePage({
onHistory({ ...d, media_type: "movie" });
}, [d, onHistory]);

// ── Media Session API integration ─────────────────────────────────────────
// The OS reads media metadata from the renderer that OWNS the playing
// <video> — which is the webview, not this React renderer. So we must
// inject MediaMetadata overrides into the webview via executeJavaScript.
// We also keep main-renderer calls as a secondary layer for compatibility.
useEffect(() => {
if (!playing) {
clearMediaSession();
return;
}

const metadata = {
title: title || "Movie",
artist: "StreamBert",
album: year || "",
posterPath: d.poster_path ?? null,
};

// ─ Secondary: set in main renderer (picked up by some Electron builds) ─
setMediaSessionMetadata(metadata);
syncPlaybackState("playing");

// ─ Register main-renderer handlers (play/pause delegated to webview) ─
registerMediaSessionHandlers(
async () => {
try {
const wv = webviewRef.current;
if (wv) await wv.executeJavaScript(`(() => { const v = document.querySelector('video'); if (v) v.play(); })()`);
syncPlaybackState("playing");
} catch {}
},
async () => {
try {
const wv = webviewRef.current;
if (wv) await wv.executeJavaScript(`(() => { const v = document.querySelector('video'); if (v) v.pause(); })()`);
syncPlaybackState("paused");
} catch {}
},
);

// ─ Primary: inject directly into the webview renderer ───────────────
// The script re-applies itself at +0 ms, +600 ms, and +2 s to reliably
// override any async metadata the embed page sets after initial load.
const script = buildWebviewMetadataScript(metadata);
const injectIntoWebview = () => {
const wv = webviewRef.current;
if (wv && window.electron?.injectAllFrames) {
window.electron.injectAllFrames(wv.getWebContentsId(), script).catch(() => {});
} else if (wv) {
wv.executeJavaScript(script).catch(() => {});
}
};

const wv = webviewRef.current;
if (wv) {
wv.addEventListener("dom-ready", injectIntoWebview);
wv.addEventListener("did-finish-load", injectIntoWebview);
// Try immediately — succeeds if the webview is already loaded,
// fails silently if dom-ready hasn't fired yet (dom-ready handles it).
try { injectIntoWebview(); } catch {}
}

return () => {
clearMediaSession();
const wv = webviewRef.current;
if (wv) {
wv.removeEventListener("dom-ready", injectIntoWebview);
wv.removeEventListener("did-finish-load", injectIntoWebview);
}
};
}, [playing, title, year, d.poster_path]);

// Intercept fullscreen requests from embedded players (vidsrc / 2embed use
// the native Fullscreen API which would otherwise fullscreen the entire app).
// Videasy and AllManga handle fullscreen internally via CSS, skip those.
Expand Down
Loading
Loading