Merge pull request #698 from 4gray/refactor/mpv-vlc-error-handling

chore: improve player error handling
This commit is contained in:
4gray authored and GitHub committed 2025-12-25 23:54:18 +01:00
commit 65b21aa0f2
4 files changed
+244 -15

No files matched your search

@@ -2,9 +2,21 @@ import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electron', {
// Remote control channel change listener
onChannelChange: (callback: (data: { direction: 'up' | 'down' }) => void) => {
onChannelChange: (
callback: (data: { direction: 'up' | 'down' }) => void
) => {
ipcRenderer.on('CHANNEL_CHANGE', (_event, data) => callback(data));
},
// Player error listener
onPlayerError: (
callback: (data: {
player: string;
error: string;
originalError: string;
}) => void
) => {
ipcRenderer.on('player-error', (_event, data) => callback(data));
},
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
platform: process.platform,
fetchPlaylistByUrl: (url: string, title?: string) =>
@@ -5,6 +5,7 @@ import { ipcMain } from 'electron';
SET_MPV_PLAYER_PATH,
SET_VLC_PLAYER_PATH,
} from 'shared-interfaces'; */
import App from '../app';
import {
MPV_PLAYER_PATH,
MPV_REUSE_INSTANCE,
@@ -27,6 +28,45 @@ export default class PlayerEvents {
let mpvProcess: ChildProcess | null = null;
let mpvSocketPath: string | null = null;
// Helper function to send error notifications to the renderer
function sendPlayerErrorNotification(player: 'MPV' | 'VLC', error: string) {
if (App.mainWindow && !App.mainWindow.isDestroyed()) {
// Make error message more user-friendly
let userMessage = error;
if (error.includes('Failed to open')) {
userMessage =
'Failed to open stream. The URL may be invalid or the server is not responding.';
} else if (
error.includes('Protocol not found') ||
error.includes('Unsupported protocol')
) {
userMessage =
'Unsupported stream protocol. Please check the stream URL.';
} else if (
error.includes('Connection refused') ||
error.includes('Could not connect')
) {
userMessage =
'Cannot connect to the stream server. Please check your internet connection.';
} else if (error.includes('403') || error.includes('Forbidden')) {
userMessage =
'Access denied. The stream may require valid credentials or headers.';
} else if (error.includes('404') || error.includes('Not Found')) {
userMessage =
'Stream not found. The URL may be incorrect or expired.';
} else if (error.includes('Timed out') || error.includes('timeout')) {
userMessage = 'Connection timed out. The server is not responding.';
}
App.mainWindow.webContents.send('player-error', {
player,
error: userMessage,
originalError: error,
});
}
}
// Helper function to send command to MPV via IPC
function sendMpvCommand(command: string, args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
@@ -140,9 +180,64 @@ ipcMain.handle(
const proc = spawn(mpvPath, args, {
shell: false,
detached: !reuseInstance,
stdio: 'ignore',
// Only pipe stdio when reusing instance; use 'ignore' for detached to allow clean shutdown
stdio: reuseInstance ? ['ignore', 'pipe', 'pipe'] : 'ignore',
});
// Capture stdout
if (proc.stdout) {
proc.stdout.on('data', (data) => {
const output = data.toString().trim();
if (output) {
console.log('[MPV stdout]:', output);
// MPV sometimes outputs errors to stdout instead of stderr
if (
output.includes('Failed to open') ||
output.includes('Error opening') ||
output.includes('Protocol not found') ||
output.includes('Connection refused') ||
output.includes('error') ||
output.includes('403') ||
output.includes('404')
) {
console.error(
'[MPV ERROR from stdout]:',
output
);
sendPlayerErrorNotification('MPV', output);
}
}
});
}
// Capture stderr (MPV outputs most messages here)
if (proc.stderr) {
proc.stderr.on('data', (data) => {
const output = data.toString().trim();
if (output) {
console.error('[MPV stderr]:', output);
// Check for common error patterns and send notifications
if (
output.includes('Failed to open') ||
output.includes(
'Exiting... (Errors when loading file)'
) ||
output.includes('Error opening') ||
output.includes('Protocol not found') ||
output.includes('Connection refused') ||
output.includes('error') ||
output.includes('403') ||
output.includes('404')
) {
console.error('[MPV ERROR]:', output);
sendPlayerErrorNotification('MPV', output);
}
}
});
}
proc.on('error', (err) => {
console.error('Failed to start MPV player:', err);
mpvProcess = null;
@@ -158,6 +253,17 @@ ipcMain.handle(
console.log(`MPV exited with code ${code}`);
mpvProcess = null;
mpvSocketPath = null;
// Log non-zero exit codes as errors and notify user
if (code !== 0 && code !== null) {
console.error(
`[MPV ERROR] MPV exited with error code ${code}`
);
sendPlayerErrorNotification(
'MPV',
`MPV player closed unexpectedly (exit code: ${code})`
);
}
});
// Store the process reference if reuse is enabled
@@ -254,9 +360,62 @@ ipcMain.handle(
const proc = spawn(vlcPath, args, {
shell: false,
detached: true,
stdio: 'ignore',
stdio: 'ignore', // Use 'ignore' for detached to allow clean shutdown
});
// Capture stdout
if (proc.stdout) {
proc.stdout.on('data', (data) => {
const output = data.toString().trim();
if (output) {
console.log('[VLC stdout]:', output);
// VLC sometimes outputs errors to stdout instead of stderr
if (
output.includes('error') ||
output.includes('Error') ||
output.includes('failed') ||
output.includes('Failed') ||
output.includes('cannot open') ||
output.includes('Connection refused') ||
output.includes('403') ||
output.includes('404')
) {
console.error(
'[VLC ERROR from stdout]:',
output
);
sendPlayerErrorNotification('VLC', output);
}
}
});
}
// Capture stderr
if (proc.stderr) {
proc.stderr.on('data', (data) => {
const output = data.toString().trim();
if (output) {
console.error('[VLC stderr]:', output);
// Check for common error patterns and send notifications
if (
output.includes('error') ||
output.includes('Error') ||
output.includes('failed') ||
output.includes('Failed') ||
output.includes('cannot open') ||
output.includes('Connection refused') ||
output.includes('403') ||
output.includes('404')
) {
console.error('[VLC ERROR]:', output);
sendPlayerErrorNotification('VLC', output);
}
}
});
}
proc.on('error', (err) => {
console.error('Failed to start VLC player:', err);
reject(
@@ -268,6 +427,17 @@ ipcMain.handle(
proc.on('exit', (code) => {
console.log(`VLC exited with code ${code}`);
// Log non-zero exit codes as errors and notify user
if (code !== 0 && code !== null) {
console.error(
`[VLC ERROR] VLC exited with error code ${code}`
);
sendPlayerErrorNotification(
'VLC',
`VLC player closed unexpectedly (exit code: ${code})`
);
}
});
// Detach the process so it can continue running independently
+42 -7
View File
@@ -27,6 +27,30 @@ export class ElectronService extends DataService {
constructor() {
super();
console.log('Electron service initialized...');
this.setupPlayerErrorListener();
}
private setupPlayerErrorListener() {
// Listen for player errors from the backend
if (window.electron?.onPlayerError) {
window.electron.onPlayerError(
(data: {
player: string;
error: string;
originalError: string;
}) => {
console.error(`${data.player} Error:`, data.originalError);
this.snackBar.open(
`${data.player} Error: ${data.error}`,
'Close',
{
duration: 7000,
panelClass: ['error-snackbar'],
}
);
}
);
}
}
getAppVersion(): string {
@@ -70,9 +94,13 @@ export class ElectronService extends DataService {
/* thumbnail: data.thumbnail ?? '', */
} catch (error: any) {
const errorMessage = error?.message || String(error);
this.snackBar.open(`Error launching MPV: ${errorMessage}`, 'Close', {
duration: 5000,
});
this.snackBar.open(
`Error launching MPV: ${errorMessage}`,
'Close',
{
duration: 5000,
}
);
console.error('MPV launch error:', error);
throw error;
}
@@ -88,9 +116,13 @@ export class ElectronService extends DataService {
);
} catch (error: any) {
const errorMessage = error?.message || String(error);
this.snackBar.open(`Error launching VLC: ${errorMessage}`, 'Close', {
duration: 5000,
});
this.snackBar.open(
`Error launching VLC: ${errorMessage}`,
'Close',
{
duration: 5000,
}
);
console.error('VLC launch error:', error);
throw error;
}
@@ -230,7 +262,10 @@ export class ElectronService extends DataService {
// Log error to console
if (isStatusCheck) {
console.log('Portal status check failed - portal may be unavailable:', error.message || error);
console.log(
'Portal status check failed - portal may be unavailable:',
error.message || error
);
} else {
console.error('Xtream request error:', error.message);
}
Vendored
+17 -5
View File
@@ -79,17 +79,21 @@ declare global {
dbDeletePlaylist: (
playlistId: string
) => Promise<{ success: boolean }>;
dbDeleteXtreamContent: (
playlistId: string
) => Promise<{
dbDeleteXtreamContent: (playlistId: string) => Promise<{
success: boolean;
favoritedXtreamIds: number[];
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[];
recentlyViewedXtreamIds: {
xtreamId: number;
viewedAt: string;
}[];
}>;
dbRestoreXtreamUserData: (
playlistId: string,
favoritedXtreamIds: number[],
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[]
recentlyViewedXtreamIds: {
xtreamId: number;
viewedAt: string;
}[]
) => Promise<{ success: boolean }>;
dbHasCategories: (
playlistId: string,
@@ -160,6 +164,14 @@ declare global {
onChannelChange?: (
callback: (data: { direction: 'up' | 'down' }) => void
) => void;
// Player error notifications
onPlayerError?: (
callback: (data: {
player: string;
error: string;
originalError: string;
}) => void
) => void;
getLocalIpAddresses: () => Promise<string[]>;
};
process: NodeJS.Process;