diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index 609821456..7fdfec469 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -1,5 +1,4 @@ import { contextBridge, ipcRenderer } from 'electron'; -import { AUTO_UPDATE_PLAYLISTS, Playlist } from 'shared-interfaces'; contextBridge.exposeInMainWorld('electron', { getAppVersion: () => ipcRenderer.invoke('get-app-version'), @@ -10,7 +9,7 @@ contextBridge.exposeInMainWorld('electron', { ipcRenderer.invoke('update-playlist-from-file-path', filePath, title), openPlaylistFromFile: () => ipcRenderer.invoke('open-playlist-from-file'), setUserAgent: (userAgent: string, referer?: string) => - ipcRenderer.send('set-user-agent', userAgent, referer), + ipcRenderer.invoke('set-user-agent', userAgent, referer), openInMpv: ( url: string, path: string, @@ -19,8 +18,8 @@ contextBridge.exposeInMainWorld('electron', { referer?: string, origin?: string ) => - ipcRenderer.send( - 'open-in-mpv', + ipcRenderer.invoke( + 'OPEN_MPV_PLAYER', url, path, title, @@ -36,8 +35,8 @@ contextBridge.exposeInMainWorld('electron', { referer?: string, origin?: string ) => - ipcRenderer.send( - 'open-in-vlc', + ipcRenderer.invoke( + 'OPEN_VLC_PLAYER', url, path, title, @@ -45,6 +44,16 @@ contextBridge.exposeInMainWorld('electron', { referer, origin ), - autoUpdatePlaylists: (playlists: Playlist[]) => - ipcRenderer.send(AUTO_UPDATE_PLAYLISTS, playlists), + autoUpdatePlaylists: (playlists) => + ipcRenderer.invoke('AUTO_UPDATE_PLAYLISTS', playlists), + fetchEpg: (urls: string[]) => + ipcRenderer.invoke('FETCH_EPG', { url: urls }), + getChannelPrograms: (channelId: string) => + ipcRenderer.invoke('GET_CHANNEL_PROGRAMS', { channelId }), + setMpvPlayerPath: (mpvPlayerPath: string) => + ipcRenderer.invoke('SET_MPV_PLAYER_PATH', mpvPlayerPath), + setVlcPlayerPath: (vlcPlayerPath: string) => + ipcRenderer.invoke('SET_VLC_PLAYER_PATH', vlcPlayerPath), + updateSettings: (settings: any) => + ipcRenderer.invoke('SETTINGS_UPDATE', settings), }); diff --git a/apps/electron-backend/src/app/events/epg.events.ts b/apps/electron-backend/src/app/events/epg.events.ts index 2679226ee..d54676598 100644 --- a/apps/electron-backend/src/app/events/epg.events.ts +++ b/apps/electron-backend/src/app/events/epg.events.ts @@ -5,3 +5,11 @@ export default class EpgEvents { return ipcMain; } } + +ipcMain.handle('GET_CHANNEL_PROGRAMS', async (event, channelId) => { + // fetch EPG data for the given channelId +}); + +ipcMain.handle('FETCH_EPG', async (event, urls: string[]) => { + // fetch EPG data from the given URLs +}); diff --git a/apps/electron-backend/src/app/events/player.events.ts b/apps/electron-backend/src/app/events/player.events.ts index 7a94f6492..22be20597 100644 --- a/apps/electron-backend/src/app/events/player.events.ts +++ b/apps/electron-backend/src/app/events/player.events.ts @@ -1,17 +1,20 @@ import { ipcMain } from 'electron'; -import { +/* import { OPEN_MPV_PLAYER, OPEN_VLC_PLAYER, SET_MPV_PLAYER_PATH, SET_VLC_PLAYER_PATH, -} from 'shared-interfaces'; +} from 'shared-interfaces'; */ import { MPV_PLAYER_PATH, + MPV_REUSE_INSTANCE, store, VLC_PLAYER_PATH, } from '../services/store.service'; -import { spawn } from 'child_process'; +import { ChildProcess, spawn } from 'child_process'; +import { existsSync } from 'fs'; +import { createConnection } from 'net'; import path from 'path'; export default class PlayerEvents { @@ -20,55 +23,173 @@ export default class PlayerEvents { } } -ipcMain.handle(OPEN_MPV_PLAYER, async (event, { url }) => { - /* try { - if (this.mpv === null) { - this.mpv = this.createMpvInstance(); +// Keep track of the running MPV process for reuse +let mpvProcess: ChildProcess | null = null; +let mpvSocketPath: string | null = null; + +// Helper function to send command to MPV via IPC +function sendMpvCommand(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + if (!mpvSocketPath) { + reject(new Error('No MPV socket path available')); + return; } - if (this.mpv.isRunning()) { - await this.mpv.load(url); + + const client = createConnection(mpvSocketPath); + // MPV expects commands in the format: { "command": ["cmd", "arg1", "arg2"] } + const request = JSON.stringify({ command: [command, ...args] }) + '\n'; + + client.on('connect', () => { + console.log('Connected to MPV socket, sending command:', request); + client.write(request); + client.end(); + resolve(); + }); + + client.on('error', (err) => { + console.error('MPV socket error:', err); + reject(err); + }); + }); +} + +ipcMain.handle('OPEN_MPV_PLAYER', async (event, url) => { + try { + const mpvPath = getMpvPath(); + const reuseInstance = store.get(MPV_REUSE_INSTANCE, false); + + console.log('Opening MPV player with path:', mpvPath); + console.log('Reuse instance:', reuseInstance); + console.log('URL:', url); + + // If reuse is enabled and there's an existing process, try to use it + if ( + reuseInstance && + mpvProcess && + !mpvProcess.killed && + mpvSocketPath + ) { + console.log('Reusing existing MPV instance'); + try { + await sendMpvCommand('loadfile', [url, 'replace']); + console.log( + 'Successfully loaded new URL in existing MPV instance' + ); + return; + } catch (err) { + console.error('Failed to send command to existing MPV:', err); + // If it fails, clear the reference and create a new one + mpvProcess = null; + mpvSocketPath = null; + } + } + + // Create new MPV process + console.log('Creating new MPV instance'); + + // Generate unique socket path + const socketPath = + process.platform === 'win32' + ? `\\\\.\\pipe\\mpv-${Date.now()}` + : `/tmp/mpvsocket-${Date.now()}`; + + const args = [`--input-ipc-server=${socketPath}`, '--idle=yes', url]; + + const proc = spawn(mpvPath, args, { + shell: false, + detached: !reuseInstance, + stdio: 'ignore', + }); + + proc.on('error', (err) => { + console.error('Failed to start MPV player:', err); + mpvProcess = null; + mpvSocketPath = null; + /* event.sender.send(ERROR, { + message: `Error: Failed to start MPV player. Make sure that mpv player is installed on your system and the path is correct.`, + }); */ + }); + + proc.on('exit', (code) => { + console.log(`MPV exited with code ${code}`); + mpvProcess = null; + mpvSocketPath = null; + }); + + // Store the process reference if reuse is enabled + if (reuseInstance) { + mpvProcess = proc; + mpvSocketPath = socketPath; + console.log( + 'Stored MPV process for reuse with socket:', + socketPath + ); } else { - await this.mpv.start(); - await this.mpv.load(url); + // Detach the process so it can continue running independently + proc.unref(); } } catch (error) { console.log(error); - event.sender.send(ERROR, { - message: `Error: ${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - error?.verbose ?? - 'Something went wrong. Make sure that mpv player is installed on your system.' - } `, - }); - } */ + mpvProcess = null; + mpvSocketPath = null; + } }); -ipcMain.handle(SET_MPV_PLAYER_PATH, (_event, mpvPlayerPath) => { +ipcMain.handle('SET_MPV_PLAYER_PATH', (_event, mpvPlayerPath) => { console.log('... setting mpv player path', mpvPlayerPath); store.set(MPV_PLAYER_PATH, mpvPlayerPath); - - /* - // recreate mpv player instance with new binary path if it was changed - if (store.get(MPV_PLAYER_PATH, mpvPlayerPath) !== mpvPlayerPath) - mpv = createMpvInstance(); */ }); -ipcMain.handle(OPEN_VLC_PLAYER, (event, { url }) => { - const proc = spawn(getVlcPath(), [`"${url as string}"`], { - shell: true, +ipcMain.handle('SET_MPV_REUSE_INSTANCE', (_event, reuseInstance: boolean) => { + console.log('... setting mpv reuse instance', reuseInstance); + store.set(MPV_REUSE_INSTANCE, reuseInstance); + + // If disabling reuse, kill the existing process + if (!reuseInstance && mpvProcess && !mpvProcess.killed) { + console.log('Disabling reuse, cleaning up existing MPV process'); + mpvProcess.kill(); + mpvProcess = null; + mpvSocketPath = null; + } +}); + +ipcMain.handle('OPEN_VLC_PLAYER', (event, url) => { + const vlcPath = getVlcPath(); + console.log('Opening VLC player with path:', vlcPath); + console.log('URL:', url); + + const proc = spawn(vlcPath, [url as string], { + shell: false, + detached: true, + stdio: 'ignore', + }); + + proc.on('error', (err) => { + console.error('Failed to start VLC player:', err); }); proc.on('exit', (code) => { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions console.log(`VLC exited with code ${code}`); }); + + // Detach the process so it can continue running independently + proc.unref(); }); -ipcMain.handle(SET_VLC_PLAYER_PATH, (_event, vlcPlayerPath) => { +ipcMain.handle('SET_VLC_PLAYER_PATH', (_event, vlcPlayerPath) => { console.log('... setting vlc player path', vlcPlayerPath); store.set(VLC_PLAYER_PATH, vlcPlayerPath); }); +function getMpvPath() { + const customMpvPath = store.get(MPV_PLAYER_PATH); + if (customMpvPath) { + return customMpvPath; + } else { + return getDefaultMpvPath(); + } +} + function getVlcPath() { const customVlcPath = store.get(VLC_PLAYER_PATH); if (customVlcPath) { @@ -78,18 +199,103 @@ function getVlcPath() { } } +function getDefaultMpvPath() { + if (process.platform === 'win32') { + // Check multiple common Windows paths + const windowsPaths = [ + path.join('C:', 'Program Files', 'mpv', 'mpv.exe'), + path.join('C:', 'Program Files (x86)', 'mpv', 'mpv.exe'), + ]; + + // Check if any of the paths exist + for (const mpvPath of windowsPaths) { + if (existsSync(mpvPath)) { + return mpvPath; + } + } + // Default to just 'mpv' if it's in PATH + return 'mpv'; + } else if (process.platform === 'linux') { + // Check multiple common Linux paths + const linuxPaths = [ + '/usr/bin/mpv', + '/usr/local/bin/mpv', + '/snap/bin/mpv', + ]; + + for (const mpvPath of linuxPaths) { + if (existsSync(mpvPath)) { + return mpvPath; + } + } + return 'mpv'; + } else if (process.platform === 'darwin') { + // Check multiple common macOS paths + const macosPaths = [ + '/Applications/mpv.app/Contents/MacOS/mpv', + '/opt/homebrew/bin/mpv', + '/usr/local/bin/mpv', + ]; + + for (const mpvPath of macosPaths) { + if (existsSync(mpvPath)) { + return mpvPath; + } + } + return 'mpv'; + } + + // Fallback to just 'mpv' hoping it's in PATH + return 'mpv'; +} + function getDefaultVlcPath() { if (process.platform === 'win32') { - return path.join( - 'C:', - 'Program Files (x86)', - 'VideoLAN', - 'VLC', - 'vlc.exe' - ); // TODO: define more default paths like in tauri + // Check multiple common Windows paths (64-bit and 32-bit) + const windowsPaths = [ + path.join('C:', 'Program Files', 'VideoLAN', 'VLC', 'vlc.exe'), + path.join( + 'C:', + 'Program Files (x86)', + 'VideoLAN', + 'VLC', + 'vlc.exe' + ), + ]; + + for (const vlcPath of windowsPaths) { + if (existsSync(vlcPath)) { + return vlcPath; + } + } + return 'vlc'; } else if (process.platform === 'linux') { - return '/usr/bin/vlc'; + // Check multiple common Linux paths + const linuxPaths = [ + '/usr/bin/vlc', + '/usr/local/bin/vlc', + '/snap/bin/vlc', + ]; + + for (const vlcPath of linuxPaths) { + if (existsSync(vlcPath)) { + return vlcPath; + } + } + return 'vlc'; } else if (process.platform === 'darwin') { - return '/Applications/VLC.app/Contents/MacOS/VLC'; + // Check multiple common macOS paths + const macosPaths = [ + '/Applications/VLC.app/Contents/MacOS/VLC', + '/opt/homebrew/Caskroom/vlc/*/VLC.app/Contents/MacOS/VLC', + ]; + + for (const vlcPath of macosPaths) { + if (existsSync(vlcPath)) { + return vlcPath; + } + } + return 'vlc'; } + return 'vlc'; } diff --git a/apps/electron-backend/src/app/events/setttings.events.ts b/apps/electron-backend/src/app/events/setttings.events.ts index d86e90be1..3c512c1fd 100644 --- a/apps/electron-backend/src/app/events/setttings.events.ts +++ b/apps/electron-backend/src/app/events/setttings.events.ts @@ -1,5 +1,9 @@ import { ipcMain } from 'electron'; -import { SETTINGS_UPDATE } from 'shared-interfaces'; +import { + MPV_PLAYER_PATH, + MPV_REUSE_INSTANCE, + store, +} from '../services/store.service'; export default class SettingsEvents { static bootstrapSettingsEvents(): Electron.IpcMain { @@ -7,8 +11,7 @@ export default class SettingsEvents { } } -ipcMain.handle(SETTINGS_UPDATE, (_event, arg) => { - /* TODO: Implement settings update logic */ - /* this.settings = arg; - this.server.updateSettings(); */ +ipcMain.handle('SETTINGS_UPDATE', (_event, arg) => { + console.log('Received SETTINGS_UPDATE with data:', arg); + store.set(MPV_REUSE_INSTANCE, arg.mpvReuseInstance); }); diff --git a/apps/electron-backend/src/main.ts b/apps/electron-backend/src/main.ts index 3c0f0bcf3..24f2932c5 100644 --- a/apps/electron-backend/src/main.ts +++ b/apps/electron-backend/src/main.ts @@ -4,6 +4,7 @@ import App from './app/app'; import ElectronEvents from './app/events/electron.events'; import PlayerEvents from './app/events/player.events'; import PlaylistEvents from './app/events/playlist.events'; +import SettingsEvents from './app/events/setttings.events'; import SharedEvents from './app/events/shared.events'; import SquirrelEvents from './app/events/squirrel.events'; @@ -24,6 +25,7 @@ export default class Main { PlaylistEvents.bootstrapPlaylistEvents(); SharedEvents.bootstrapSharedEvents(); PlayerEvents.bootstrapPlayerEvents(); + SettingsEvents.bootstrapSettingsEvents(); // initialize auto updater service if (!App.isDevelopmentMode()) { diff --git a/global.d.ts b/global.d.ts index 9c97149f3..ca7473234 100644 --- a/global.d.ts +++ b/global.d.ts @@ -29,7 +29,7 @@ declare global { userAgent: string, referer?: string, origin?: string - ) => any; + ) => void; openInVlc: ( url: string, path: string, @@ -37,8 +37,13 @@ declare global { userAgent: string, referer?: string, origin?: string - ) => any; - autoUpdatePlaylists: (playlists: Playlist[]) => any; + ) => void; + autoUpdatePlaylists: (playlists: Playlist[]) => Promise; + fetchEpg: (urls: string[]) => Promise; + getChannelPrograms: (channelId: string) => Promise; + updateSettings: (settings: any) => Promise; + setMpvPlayerPath: (mpvPlayerPath: string) => Promise; + setVlcPlayerPath: (vlcPlayerPath: string) => Promise; }; process: NodeJS.Process; require: NodeRequire; diff --git a/libs/shared/interfaces/src/lib/ipc-commands.ts b/libs/shared/interfaces/src/lib/ipc-commands.ts index a5be23f48..84783f203 100644 --- a/libs/shared/interfaces/src/lib/ipc-commands.ts +++ b/libs/shared/interfaces/src/lib/ipc-commands.ts @@ -7,7 +7,8 @@ export const EPG_GET_PROGRAM_DONE = 'EPG:GET_PROGRAM_DONE'; export const EPG_GET_CHANNELS = 'EPG:GET_CHANNELS'; export const EPG_GET_CHANNELS_DONE = 'EPG:GET_CHANNELS_DONE'; export const EPG_GET_CHANNELS_BY_RANGE = 'EPG:EPG_GET_CHANNELS_BY_RANGE'; -export const EPG_GET_CHANNELS_BY_RANGE_RESPONSE = 'EPG:EPG_GET_CHANNELS_BY_RANGE_RESPONSE'; +export const EPG_GET_CHANNELS_BY_RANGE_RESPONSE = + 'EPG:EPG_GET_CHANNELS_BY_RANGE_RESPONSE'; export const EPG_FORCE_FETCH = 'EPG:EPG_FORCE_FETCH'; // Playlist related commands @@ -34,6 +35,7 @@ export const AUTO_UPDATE_PLAYLISTS_RESPONSE = 'AUTO_UPDATE_RESPONSE'; // Experimental export const OPEN_MPV_PLAYER = 'OPEN_MPV_PLAYER'; export const SET_MPV_PLAYER_PATH = 'SET_MPV_PLAYER_PATH'; +export const SET_MPV_REUSE_INSTANCE = 'SET_MPV_REUSE_INSTANCE'; export const OPEN_VLC_PLAYER = 'OPEN_VLC_PLAYER'; export const SET_VLC_PLAYER_PATH = 'SET_VLC_PLAYER_PATH';