mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-05 18:19:56 +02:00
fix: recover mpv playback after the OS resumes from sleep (#2172)
mpv/ffmpeg had no network-level timeout or reconnect options, so a network stream left open across a system sleep would block forever on the now-dead TCP connection instead of failing or reconnecting. Since Node-MPV's IPC commands only resolve when mpv replies, a wedged mpv process also made quit()/restart hang indefinitely, so the only way out was to kill the whole app. - Add --network-timeout and ffmpeg reconnect options to mpv's default parameters so a stalled stream fails fast instead of hanging. - Make the quit() helper resilient to an unresponsive mpv process by racing it against a timeout and force-killing as a fallback. - Listen for Electron's powerMonitor 'resume' event and tell the renderer to reload mpv, so playback recovers automatically instead of requiring a manual app restart.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import console from 'console';
|
import console from 'console';
|
||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain, powerMonitor } from 'electron';
|
||||||
import { access, rm } from 'fs/promises';
|
import { access, rm } from 'fs/promises';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
import MpvAPI from 'node-mpv';
|
import MpvAPI from 'node-mpv';
|
||||||
@@ -85,6 +85,19 @@ const DEFAULT_MPV_PARAMETERS = (extraParameters?: string[]) => {
|
|||||||
parameters.push('--prefetch-playlist=yes');
|
parameters.push('--prefetch-playlist=yes');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Without these, mpv/ffmpeg will block indefinitely on a dead TCP connection
|
||||||
|
// instead of failing or reconnecting. This commonly happens when the OS network
|
||||||
|
// adapter resets after the system wakes from sleep while a stream is open.
|
||||||
|
if (!extraParameters?.some((param) => param.startsWith('--network-timeout'))) {
|
||||||
|
parameters.push('--network-timeout=10');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extraParameters?.some((param) => param.startsWith('--stream-lavf-o'))) {
|
||||||
|
parameters.push(
|
||||||
|
'--stream-lavf-o=reconnect=1,reconnect_streamed=1,reconnect_at_eof=1,reconnect_delay_max=5',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return parameters;
|
return parameters;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -191,13 +204,9 @@ export const getMpvInstance = () => {
|
|||||||
return mpvInstance;
|
return mpvInstance;
|
||||||
};
|
};
|
||||||
|
|
||||||
const quit = async (instance?: MpvAPI | null) => {
|
const QUIT_TIMEOUT_MS = 3000;
|
||||||
const mpv = instance || getMpvInstance();
|
|
||||||
if (mpv) {
|
const killMpvProcess = (mpv: MpvAPI) => {
|
||||||
try {
|
|
||||||
await mpv.quit();
|
|
||||||
} catch {
|
|
||||||
// If quit() fails, try to kill the process directly
|
|
||||||
const mpvProcess = (mpv as any).process || (mpv as any).mpvProcess;
|
const mpvProcess = (mpv as any).process || (mpv as any).mpvProcess;
|
||||||
if (mpvProcess && typeof mpvProcess.kill === 'function') {
|
if (mpvProcess && typeof mpvProcess.kill === 'function') {
|
||||||
try {
|
try {
|
||||||
@@ -206,6 +215,33 @@ const quit = async (instance?: MpvAPI | null) => {
|
|||||||
mpvLog({ action: 'Failed to kill mpv process' }, killErr as NodeMpvError);
|
mpvLog({ action: 'Failed to kill mpv process' }, killErr as NodeMpvError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const quit = async (instance?: MpvAPI | null) => {
|
||||||
|
const mpv = instance || getMpvInstance();
|
||||||
|
if (mpv) {
|
||||||
|
try {
|
||||||
|
// mpv.quit() resolves only when mpv replies over IPC. If mpv's command queue
|
||||||
|
// is wedged (e.g. blocked on a dead network stream after the system resumes
|
||||||
|
// from sleep), that reply never arrives, so this must not be allowed to hang
|
||||||
|
// forever - fall back to killing the process directly.
|
||||||
|
let timedOut = false;
|
||||||
|
await Promise.race([
|
||||||
|
mpv.quit(),
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
timedOut = true;
|
||||||
|
resolve(undefined);
|
||||||
|
}, QUIT_TIMEOUT_MS);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (timedOut) {
|
||||||
|
killMpvProcess(mpv);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If quit() fails, try to kill the process directly
|
||||||
|
killMpvProcess(mpv);
|
||||||
}
|
}
|
||||||
if (!isWindows()) {
|
if (!isWindows()) {
|
||||||
try {
|
try {
|
||||||
@@ -666,6 +702,17 @@ const cleanupMpv = async (force = false) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// When the OS resumes from sleep, any network stream mpv had open is likely dead
|
||||||
|
// (the connection silently dropped while the network adapter was suspended). Tell
|
||||||
|
// the renderer to reload mpv so it reconnects with a fresh stream instead of staying
|
||||||
|
// stuck on the old, now-dead connection until the app is manually restarted.
|
||||||
|
powerMonitor.on('resume', () => {
|
||||||
|
if (getMpvInstance()) {
|
||||||
|
mpvLog({ action: 'System resumed from sleep, reloading mpv' });
|
||||||
|
getMainWindow()?.webContents.send('renderer-mpv-reconnect');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.on('before-quit', async (event) => {
|
app.on('before-quit', async (event) => {
|
||||||
switch (mpvState) {
|
switch (mpvState) {
|
||||||
case MpvState.DONE:
|
case MpvState.DONE:
|
||||||
|
|||||||
@@ -174,6 +174,10 @@ const rendererPlayerFallback = (cb: (data: boolean) => void) => {
|
|||||||
ipcRenderer.on('renderer-player-fallback', (_, data) => cb(data));
|
ipcRenderer.on('renderer-player-fallback', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rendererMpvReconnect = (cb: () => void) => {
|
||||||
|
ipcRenderer.on('renderer-mpv-reconnect', () => cb());
|
||||||
|
};
|
||||||
|
|
||||||
export const mpvPlayer = {
|
export const mpvPlayer = {
|
||||||
autoNext,
|
autoNext,
|
||||||
cleanup,
|
cleanup,
|
||||||
@@ -205,6 +209,7 @@ export const mpvPlayerListener = {
|
|||||||
rendererAutoNext,
|
rendererAutoNext,
|
||||||
rendererCurrentTime,
|
rendererCurrentTime,
|
||||||
rendererError,
|
rendererError,
|
||||||
|
rendererMpvReconnect,
|
||||||
rendererNext,
|
rendererNext,
|
||||||
rendererPause,
|
rendererPause,
|
||||||
rendererPlay,
|
rendererPlay,
|
||||||
|
|||||||
@@ -68,9 +68,13 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
eventEmitter.on('MPV_RELOAD', handleMpvReload);
|
eventEmitter.on('MPV_RELOAD', handleMpvReload);
|
||||||
|
// The main process notifies us after the OS resumes from sleep, since the
|
||||||
|
// stream mpv had open is likely on a now-dead connection.
|
||||||
|
mpvPlayerListener?.rendererMpvReconnect(handleMpvReload);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
eventEmitter.off('MPV_RELOAD', handleMpvReload);
|
eventEmitter.off('MPV_RELOAD', handleMpvReload);
|
||||||
|
ipc?.removeAllListeners('renderer-mpv-reconnect');
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user