refactor(player,epg): add MPV reuse, IPC commands, and EPG handlers

Introduce MPV process reuse and IPC control, and add basic EPG IPC
handlers.

- Add GET_CHANNEL_PROGRAMS and FETCH_EPG ipc handlers to epg.events.ts
  to expose fetching EPG data by channel or URLs.
- Rework player.events.ts:
  - Export proper bootstrap and add module-level mpvProcess and
    mpvSocketPath to track a reusable MPV instance.
  - Add sendMpvCommand helper to communicate with MPV via its IPC socket.
  - Implement OPEN_MPV_PLAYER handler that:
    - Reads MPV path and MPV_REUSE_INSTANCE setting from store.
    - Attempts to reuse an existing MPV process by sending loadfile
      command over the socket when reuse is enabled.
    - Falls back to spawning a new mpv process, creating a unique IPC
      socket path, and storing the process/socket when reuse is enabled.
    - Adds error and exit handlers to clear stored references.
  - Add necessary imports (ChildProcess, existsSync, createConnection).
  - Remove/comment unused import block for shared-interfaces and adapt
    existing code to the new flow.

Why:
- Enable reusing a single MPV instance to improve resource usage and
  provide faster playback switching.
- Add IPC endpoints for EPG to support frontend requests for program
  data.
This commit is contained in:
4gray committed 2025-10-18 11:53:57 +02:00
1 parent b61530f279
commit d9cbb1ce87
7 files changed
+291 -56

No files matched your search

@@ -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),
});
@@ -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
});
@@ -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<void> {
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';
}
@@ -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);
});
+2
View File
@@ -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()) {
Vendored
+8 -3
View File
@@ -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<Playlist[]>;
fetchEpg: (urls: string[]) => Promise<any>;
getChannelPrograms: (channelId: string) => Promise<any>;
updateSettings: (settings: any) => Promise<void>;
setMpvPlayerPath: (mpvPlayerPath: string) => Promise<void>;
setVlcPlayerPath: (vlcPlayerPath: string) => Promise<void>;
};
process: NodeJS.Process;
require: NodeRequire;
@@ -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';