mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
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:
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);
|
||||
});
|
||||
@@ -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
@@ -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';
|
||||
|
||||
|
||||
Reference in new issue
Block a user