mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
Merge pull request #698 from 4gray/refactor/mpv-vlc-error-handling
chore: improve player error handling
This commit is contained in:
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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user