feat: simple remote control support on http

This commit is contained in:
Serkan committed 2024-10-09 18:21:53 +03:00
1 parent 5a4cdd848f
commit d6a07cc0cd
14 files changed
+146 -19

No files matched your search

+13 -1
View File
@@ -32,6 +32,7 @@ import {
PLAYLIST_UPDATE_RESPONSE,
SET_MPV_PLAYER_PATH,
SET_VLC_PLAYER_PATH,
SETTINGS_UPDATE,
STALKER_REQUEST,
STALKER_RESPONSE,
XTREAM_REQUEST,
@@ -40,6 +41,7 @@ import {
import { Playlist } from '../shared/playlist.interface';
import { createPlaylistObject } from '../shared/playlist.utils';
import { ParsedPlaylist } from '../src/typings.d';
import { Server } from './server';
const fs = require('fs');
const https = require('https');
@@ -88,7 +90,12 @@ export class Api {
mpv;
settings;
server;
constructor(store) {
this.server = new Server(this);
this.store = store;
this.mpv = this.createMpvInstance();
this.mpv
@@ -286,7 +293,11 @@ export class Api {
.on(SET_VLC_PLAYER_PATH, (_event, vlcPlayerPath) => {
console.log('... setting vlc player path', vlcPlayerPath);
store.set(VLC_PLAYER_PATH, vlcPlayerPath);
});
})
.on(SETTINGS_UPDATE, (_event, arg) => {
this.settings = arg;
this.server.updateSettings();
}) ;
// listeners for EPG events
ipcMain
@@ -625,4 +636,5 @@ export class Api {
}
});
}
}
+1 -3
View File
@@ -4,7 +4,6 @@ import * as path from 'path';
import * as url from 'url';
import { Api } from './api';
import { AppMenu } from './menu';
import { Server } from './server';
const fixPath = require('fix-path');
const {
@@ -121,8 +120,7 @@ try {
const menu = new AppMenu(win);
Menu.setApplicationMenu(menu.getMenu());
api.setMainWindow(win);
new Server(3000, api);
// create hidden window for epg worker
createEpgWorkerWindow();
});
+40 -12
View File
@@ -1,4 +1,5 @@
import { IncomingMessage, ServerResponse } from "http";
import { REMOTE_CONTROL_CHANGE_CHANNEL } from "../shared/ipc-commands";
const http = require('http');
const Sqrl = require('squirrelly');
@@ -8,47 +9,74 @@ const path = require('path');
export class Server {
/** Server instance */
server: any;
/** Port number */
port: number;
/** Api instance */
api: any;
/**
* Creates a new server instance
* @param port port number
* @param api application instance
*/
constructor(_port: number, _api: any) {
constructor(_api: any) {
this.api = _api;
this.port = _port;
this.server = http.createServer(this.requestListener.bind(this));
this.server.listen(this.port);
}
updateSettings() {
if (this.api.settings.remoteControl && !this.server.listening) {
this.server.listen(this.api.settings.remoteControlPort);
} else if (this.server.listening && !this.api.settings.remoteControl) {
this.server.close();
}
}
async getTranslation() {
const language = this.api.settings && this.api.settings.language ? this.api.settings.language : 'en';
const languageFileContent = await fs.promises.readFile(path.join(__dirname, `../src/assets/i18n/${language}.json`), 'utf-8');
return JSON.parse(languageFileContent);
}
getTranslationValue(key: string, translation: any, defaultValue: string) {
if (translation && translation.REMOTE_CONTROL) {
const translationValue = translation.REMOTE_CONTROL[key];
if (translationValue) {
return translationValue;
}
}
return defaultValue;
}
async requestListener(request: IncomingMessage, response: ServerResponse) {
const currentPage = request.url?.split('?')[0]?.split('/').splice(-1)[0];
console.log(this.api.store);
const translation = await this.getTranslation();
switch (currentPage) {
case '': {
const indexFile = await fs.promises.readFile(path.join(__dirname, './remote-control/', 'index.sqrl'), 'utf-8');
const renderedHtml = Sqrl.render(indexFile, {
headerText: 'My Dynamic Header',
upChannelText: 'Up Channel',
downChannelText: 'Down Channel',
headerText: this.getTranslationValue('HEADER', translation, 'Remote Control'),
upChannelText: this.getTranslationValue('UP_CHANNEL', translation, 'Up Channel'),
downChannelText: this.getTranslationValue('DOWN_CHANNEL', translation, 'Down Channel'),
upChannelUrl: '/upChannel',
downChannelUrl: '/downChannel',
title: 'IPTVNator'
title: this.getTranslationValue('TITLE', translation, 'IPTVNator'),
footer: this.getTranslationValue('FOOTER', translation, 'IPTVNator')
});
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end(renderedHtml);
break;
}
case 'upChannel': {
// this.api.upChannel();
this.api.mainWindow.webContents.send(REMOTE_CONTROL_CHANGE_CHANNEL, { type: 'up' });
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end('Up Channel');
break;
}
case 'downChannel': {
this.api.mainWindow.webContents.send(REMOTE_CONTROL_CHANGE_CHANNEL, { type: 'down' });
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end('Down Channel');
break;
}
default:
response.writeHead(404, { 'Content-Type': 'text/html' });
response.end('404');
+6
View File
@@ -54,3 +54,9 @@ export const XTREAM_RESPONSE = 'XTREAM_RESPONSE';
// Stalker
export const STALKER_REQUEST = 'STALKER_REQUEST';
export const STALKER_RESPONSE = 'STALKER_RESPONSE';
// Settings
export const SETTINGS_UPDATE = 'SETTINGS_UPDATE';
// Remote Control
export const REMOTE_CONTROL_CHANGE_CHANNEL = 'REMOTE_CONTROL_CHANGE_CHANNEL';
+2
View File
@@ -13,6 +13,7 @@ import {
EPG_FETCH_DONE,
ERROR,
OPEN_FILE,
SETTINGS_UPDATE,
SHOW_WHATS_NEW,
VIEW_ADD_PLAYLIST,
VIEW_SETTINGS,
@@ -148,6 +149,7 @@ export class AppComponent {
.getValueFromLocalStorage(STORE_KEY.Settings)
.subscribe((settings: Settings) => {
if (settings && Object.keys(settings).length > 0) {
this.electronService.sendIpcEvent(SETTINGS_UPDATE, settings);
this.translate.use(settings.language ?? this.DEFAULT_LANG);
if (
settings.epgUrl?.length > 0 &&
@@ -30,6 +30,7 @@
trackBy: trackBy;
templateCacheSize: 0
"
[selected]="item.stream_id === activeLiveStream?.stream_id"
[value]="item"
(click)="itemClicked.emit(item)"
>
@@ -46,6 +46,7 @@ export class LiveStreamLayoutComponent {
@Input({ required: true }) player: VideoPlayer = VideoPlayer.VideoJs;
@Input() epgItems: EpgItem[];
@Input() streamUrl: string;
@Input() activeLiveStream: XtreamItem;
@Output() itemClicked = new EventEmitter<XtreamItem>();
+30
View File
@@ -200,6 +200,36 @@
</div>
</div>
<mat-divider></mat-divider>
<div class="row">
<div class="column">
{{ 'SETTINGS.REMOTE_CONTROL' | translate }}
<p>{{ 'SETTINGS.REMOTE_CONTROL_DESCRIPTION' | translate }}</p>
</div>
<div class="column" style="margin-right: 10px">
<mat-checkbox
class="column"
formControlName="remoteControl"
></mat-checkbox>
</div>
</div>
<mat-divider></mat-divider>
<div class="row" *ngIf="settingsForm.value.remoteControl === true">
<div class="column">
{{ 'SETTINGS.REMOTE_CONTROL_PORT' | translate }}
<p>{{ 'SETTINGS.REMOTE_CONTROL_PORT_DESCRIPTION' | translate }}</p>
</div>
<div class="column" style="margin-right: 10px">
<mat-form-field appearance="outline" class="full-width">
<input
matInput
type="text"
id="remoteControlPort"
formControlName="remoteControlPort"
/>
</mat-form-field>
</div>
</div>
<mat-divider></mat-divider>
<div class="row">
<div class="column">
{{ 'SETTINGS.VERSION' | translate }}
+7
View File
@@ -25,6 +25,7 @@ import {
EPG_FORCE_FETCH,
SET_MPV_PLAYER_PATH,
SET_VLC_PLAYER_PATH,
SETTINGS_UPDATE,
} from '../../../shared/ipc-commands';
import { Playlist } from '../../../shared/playlist.interface';
import { DataService } from '../services/data.service';
@@ -111,6 +112,8 @@ export class SettingsComponent implements OnInit {
theme: Theme.LightTheme,
mpvPlayerPath: '',
vlcPlayerPath: '',
remoteControl: false,
remoteControlPort: 3000
});
/** Form array with epg sources */
@@ -161,6 +164,8 @@ export class SettingsComponent implements OnInit {
theme: settings.theme ?? Theme.LightTheme,
mpvPlayerPath: settings.mpvPlayerPath ?? '',
vlcPlayerPath: settings.vlcPlayerPath ?? '',
remoteControl: settings.remoteControl ?? false,
remoteControlPort: settings.remoteControlPort ?? 3000
});
} catch (error) {
throw new Error(error);
@@ -250,6 +255,8 @@ export class SettingsComponent implements OnInit {
this.applyChangedSettings();
});
this.electronService.sendIpcEvent(SETTINGS_UPDATE, this.settingsForm.value);
this.electronService.sendIpcEvent(
SET_MPV_PLAYER_PATH,
this.settingsForm.value.mpvPlayerPath
+2
View File
@@ -23,4 +23,6 @@ export interface Settings {
theme: Theme;
mpvPlayerPath: string;
vlcPlayerPath: string;
remoteControl: boolean;
remoteControlPort: number;
}
@@ -36,6 +36,7 @@
[player]="player"
[streamUrl]="streamUrl"
[epgItems]="epgItems"
[activeLiveStream]="activeLiveStream"
(itemClicked)="itemClicked($event)"
/>
} @else {
@@ -21,6 +21,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { Store } from '@ngrx/store';
import { IpcCommand } from '../../../shared/ipc-command.class';
import {
REMOTE_CONTROL_CHANGE_CHANNEL,
ERROR,
OPEN_MPV_PLAYER,
OPEN_VLC_PLAYER,
@@ -183,6 +184,7 @@ export class XtreamMainContainerComponent implements OnInit {
streamUrl: string;
epgItems = [];
hideExternalInfoDialog = this.portalStore.hideExternalInfoDialog;
activeLiveStream: XtreamLiveStream;
commandsList = [
new IpcCommand(XTREAM_RESPONSE, (response: XtreamResponse) =>
@@ -191,6 +193,9 @@ export class XtreamMainContainerComponent implements OnInit {
new IpcCommand(ERROR, (response: { message: string; status: number }) =>
this.showErrorAsNotification(response)
),
new IpcCommand(REMOTE_CONTROL_CHANGE_CHANNEL, (response: { type: 'up' | 'down' }) => {
this.remoteControlChangeChannel(response.type);
})
];
constructor() {
@@ -345,6 +350,7 @@ export class XtreamMainContainerComponent implements OnInit {
playLiveStream(item: XtreamLiveStream) {
const { serverUrl, username, password } = this.currentPlaylist();
const streamUrl = `${serverUrl}/${item.stream_type}/${username}/${password}/${item.stream_id}.m3u8`;
this.activeLiveStream = item;
this.openPlayer(streamUrl, item.name);
}
@@ -530,4 +536,22 @@ export class XtreamMainContainerComponent implements OnInit {
this.currentLayout = 'favorites';
this.setInitialBreadcrumb();
}
remoteControlChangeChannel(type: 'up' | 'down') {
if (this.currentLayout === 'category_content' && this.activeLiveStream) {
let nextItem;
const index = this.activeLiveStream.num - 1;
if (type === 'up') {
if (index - 1 >= 0) {
nextItem = this.items[index - 1];
}
} else if (type === 'down') {
const index = this.activeLiveStream.num - 1;
if (index + 1 < this.items.length) {
nextItem = this.items[index + 1];
}
}
this.itemClicked(nextItem);
}
}
}
+5 -1
View File
@@ -138,7 +138,11 @@
"MPV_PLAYER_PATH": "mpv player path",
"VLC_PLAYER_PATH_LABEL": "VLC path",
"VLC_PLAYER_PATH_DESCRIPTION": "Set path to VLC player in your system. It will use the provided path to a VLC binary instead of the default path for our system.",
"VLC_PLAYER_PATH": "VLC player path"
"VLC_PLAYER_PATH": "VLC player path",
"REMOTE_CONTROL": "Remote control",
"REMOTE_CONTROL_DESCRIPTION": "Enable remote control of the application.",
"REMOTE_CONTROL_PORT": "Remote control port",
"REMOTE_CONTROL_PORT_DESCRIPTION": "Port used for remote control of the application. Port 3000 is used by default."
},
"THEMES": {
"DARK_THEME": "Dark theme",
+13 -2
View File
@@ -138,7 +138,11 @@
"MPV_PLAYER_PATH": "mpv oynatıcı yolu",
"VLC_PLAYER_PATH_LABEL": "VLC yolu",
"VLC_PLAYER_PATH_DESCRIPTION": "Sisteminizdeki VLC oynatıcı yolunu ayarlayın. Sistemimiz için varsayılan yolu kullanmak yerine, sağlanan VLC ikili dosyası yolunu kullanacaktır.",
"VLC_PLAYER_PATH": "VLC oynatıcı yolu"
"VLC_PLAYER_PATH": "VLC oynatıcı yolu",
"REMOTE_CONTROL": "Uzaktan kontrol",
"REMOTE_CONTROL_DESCRIPTION": "Uygulamanın uzaktan kontrolünü etkinleştirin.",
"REMOTE_CONTROL_PORT": "Uzaktan kontrol portu",
"REMOTE_CONTROL_PORT_DESCRIPTION": "Uzaktan kontrol için bir port belirleyin. Varsayılan olarak 3080 kullanılır."
},
"THEMES": {
"DARK_THEME": "Koyu Tema",
@@ -283,5 +287,12 @@
"STREAM_URL_COPIED": "Yayın URL'si panoya kopyalandı",
"COPY_STREAM_URL": "Yayın URL'sini Kopyala"
},
"INFORMATION": "Bilgi"
"INFORMATION": "Bilgi",
"REMOTE_CONTROL": {
"TITLE": "Uzaktan Kontrol",
"UP_CHANNEL": "Yukarı Kanal",
"DOWN_CHANNEL": "Aşağı Kanal",
"HEADER": "IPTVNator Uzaktan Kontrol",
"FOOTER": "IPTVNator Uzaktan Kontrol"
}
}