mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
feat(remote-control): add channel selection by number and volume control features
- Implemented `getChannelItemByNumber` utility function to retrieve channels by their number. - Enhanced `StalkerLiveStreamLayoutComponent` and `LiveStreamLayoutComponent` to handle remote control commands for channel selection and volume adjustments. - Updated typings to include new remote control commands and status updates. - Refactored `RemoteControlComponent` to manage numeric input for channel selection and volume control actions. - Improved UI for remote control with new volume and channel navigation features. - Added service methods for volume up, down, and mute toggle in `RemoteControlService`.
This commit is contained in:
1 parent
620ecb7518
commit
7c9db4bc7d
14 files changed
+1100
-400
No files matched your search
@@ -10,6 +10,24 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
ipcRenderer.on('CHANNEL_CHANGE', handler);
|
||||
return () => ipcRenderer.off('CHANNEL_CHANGE', handler);
|
||||
},
|
||||
onRemoteControlCommand: (
|
||||
callback: (data: {
|
||||
type:
|
||||
| 'channel-select-number'
|
||||
| 'volume-up'
|
||||
| 'volume-down'
|
||||
| 'volume-toggle-mute';
|
||||
number?: number;
|
||||
}) => void
|
||||
) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, data: any) =>
|
||||
callback(data);
|
||||
ipcRenderer.on('REMOTE_CONTROL_COMMAND', handler);
|
||||
return () => ipcRenderer.off('REMOTE_CONTROL_COMMAND', handler);
|
||||
},
|
||||
updateRemoteControlStatus: (status: any) => {
|
||||
ipcRenderer.send('REMOTE_CONTROL_STATUS_UPDATE', status);
|
||||
},
|
||||
// Player error listener
|
||||
onPlayerError: (
|
||||
callback: (data: {
|
||||
|
||||
@@ -3,7 +3,37 @@ import * as http from 'http';
|
||||
import { httpServer } from '../server/http-server';
|
||||
import { store } from '../services/store.service';
|
||||
|
||||
interface RemoteControlCommand {
|
||||
type:
|
||||
| 'channel-select-number'
|
||||
| 'volume-up'
|
||||
| 'volume-down'
|
||||
| 'volume-toggle-mute';
|
||||
number?: number;
|
||||
}
|
||||
|
||||
interface RemoteControlStatus {
|
||||
portal: 'm3u' | 'xtream' | 'stalker' | 'unknown';
|
||||
isLiveView: boolean;
|
||||
channelName?: string;
|
||||
channelNumber?: number;
|
||||
epgTitle?: string;
|
||||
epgStart?: string;
|
||||
epgEnd?: string;
|
||||
supportsVolume?: boolean;
|
||||
volume?: number;
|
||||
muted?: boolean;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
class RemoteControlEvents {
|
||||
private remoteControlStatus: RemoteControlStatus = {
|
||||
portal: 'unknown',
|
||||
isLiveView: false,
|
||||
supportsVolume: false,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Bootstrap remote control events
|
||||
*/
|
||||
@@ -18,6 +48,26 @@ class RemoteControlEvents {
|
||||
'/api/remote-control/channel/down',
|
||||
this.handleChannelDown.bind(this)
|
||||
);
|
||||
httpServer.registerRemoteControlHandler(
|
||||
'/api/remote-control/channel/select-number',
|
||||
this.handleChannelSelectNumber.bind(this)
|
||||
);
|
||||
httpServer.registerRemoteControlHandler(
|
||||
'/api/remote-control/volume/up',
|
||||
this.handleVolumeUp.bind(this)
|
||||
);
|
||||
httpServer.registerRemoteControlHandler(
|
||||
'/api/remote-control/volume/down',
|
||||
this.handleVolumeDown.bind(this)
|
||||
);
|
||||
httpServer.registerRemoteControlHandler(
|
||||
'/api/remote-control/volume/toggle-mute',
|
||||
this.handleToggleMute.bind(this)
|
||||
);
|
||||
httpServer.registerRemoteControlHandler(
|
||||
'/api/remote-control/status',
|
||||
this.handleGetStatus.bind(this)
|
||||
);
|
||||
|
||||
// Start HTTP server if remote control is enabled in settings
|
||||
const remoteControlEnabled = store.get('remoteControl', false);
|
||||
@@ -34,6 +84,17 @@ class RemoteControlEvents {
|
||||
ipcMain.handle('REMOTE_CONTROL_CHANNEL_DOWN', () => {
|
||||
return this.changeChannelDown();
|
||||
});
|
||||
|
||||
ipcMain.on(
|
||||
'REMOTE_CONTROL_STATUS_UPDATE',
|
||||
(_event, status: Partial<RemoteControlStatus>) => {
|
||||
this.remoteControlStatus = {
|
||||
...this.remoteControlStatus,
|
||||
...status,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,6 +135,96 @@ class RemoteControlEvents {
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP handler for selecting channel by number
|
||||
*/
|
||||
private handleChannelSelectNumber(
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse
|
||||
): void {
|
||||
if (req.method !== 'POST') {
|
||||
res.writeHead(405, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
||||
return;
|
||||
}
|
||||
|
||||
this.readJsonBody(req, res, (body) => {
|
||||
const value = Number(body?.number);
|
||||
if (!Number.isFinite(value) || value < 1) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Invalid channel number' }));
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendRemoteCommandToRenderer({
|
||||
type: 'channel-select-number',
|
||||
number: Math.floor(value),
|
||||
});
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
});
|
||||
}
|
||||
|
||||
private handleVolumeUp(
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse
|
||||
): void {
|
||||
if (req.method !== 'POST') {
|
||||
res.writeHead(405, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendRemoteCommandToRenderer({ type: 'volume-up' });
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
}
|
||||
|
||||
private handleVolumeDown(
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse
|
||||
): void {
|
||||
if (req.method !== 'POST') {
|
||||
res.writeHead(405, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendRemoteCommandToRenderer({ type: 'volume-down' });
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
}
|
||||
|
||||
private handleToggleMute(
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse
|
||||
): void {
|
||||
if (req.method !== 'POST') {
|
||||
res.writeHead(405, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendRemoteCommandToRenderer({ type: 'volume-toggle-mute' });
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
}
|
||||
|
||||
private handleGetStatus(
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse
|
||||
): void {
|
||||
if (req.method !== 'GET') {
|
||||
res.writeHead(405, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(this.remoteControlStatus));
|
||||
}
|
||||
|
||||
/**
|
||||
* Change to the next channel
|
||||
*/
|
||||
@@ -102,6 +253,42 @@ class RemoteControlEvents {
|
||||
console.warn('No browser windows found to send channel change');
|
||||
}
|
||||
}
|
||||
|
||||
private sendRemoteCommandToRenderer(command: RemoteControlCommand): void {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
if (windows.length > 0) {
|
||||
windows[0].webContents.send('REMOTE_CONTROL_COMMAND', command);
|
||||
console.log(
|
||||
`Sent REMOTE_CONTROL_COMMAND ${command.type} to renderer`
|
||||
);
|
||||
} else {
|
||||
console.warn('No browser windows found to send remote command');
|
||||
}
|
||||
}
|
||||
|
||||
private readJsonBody(
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
callback: (body: any) => void
|
||||
): void {
|
||||
let data = '';
|
||||
req.on('data', (chunk: Buffer) => {
|
||||
data += chunk.toString();
|
||||
});
|
||||
req.on('end', () => {
|
||||
if (!data) {
|
||||
callback({});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
callback(JSON.parse(data));
|
||||
} catch {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Invalid JSON payload' }));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new RemoteControlEvents();
|
||||
Reference in new issue
Block a user