diff --git a/electron/api.ts b/electron/api.ts index 502d0511f..1fac5f391 100644 --- a/electron/api.ts +++ b/electron/api.ts @@ -24,6 +24,7 @@ import { MIGRATE_PLAYLISTS, MIGRATE_PLAYLISTS_RESPONSE, OPEN_FILE, + OPEN_MPV_PLAYER, PLAYLIST_PARSE_BY_URL, PLAYLIST_PARSE_RESPONSE, PLAYLIST_UPDATE, @@ -36,6 +37,11 @@ import { ParsedPlaylist } from '../src/typings.d'; const fs = require('fs'); const https = require('https'); +const mpvAPI = require('node-mpv'); +const createMpvInstance = () => new mpvAPI({}, ['--autofit=70%']); +let mpv = createMpvInstance(); +mpv.on('quit', () => (mpv = null)).on('crash', () => (mpv = null)); + /** @deprecated - used only for migration */ const Nedb = require('nedb-promises'); @@ -143,7 +149,7 @@ export class Api { ) .on( CHANNEL_SET_USER_AGENT, - (event, args: { userAgent: string; referer?: string }) => { + (_event, args: { userAgent: string; referer?: string }) => { if (args.userAgent && args.referer) { this.setUserAgent(args.userAgent, args.referer); } else { @@ -205,44 +211,64 @@ export class Api { }) .on(DELETE_ALL_PLAYLISTS, (event) => { this.removeAllPlaylists(event); + }) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + .on(OPEN_MPV_PLAYER, async (event, { url }) => { + try { + if (mpv === null) { + mpv = createMpvInstance(); + } + if (mpv.isRunning()) { + await mpv.load(url); + } else { + await mpv.start(); + await mpv.load(url); + } + } catch (error) { + console.log(error); + event.sender.send(ERROR, { + message: + 'Error: Something went wrong. Make sure that mpv player is installed on your system.', + }); + } }); // listeners for EPG events ipcMain - .on(EPG_GET_PROGRAM, (event, arg) => + .on(EPG_GET_PROGRAM, (_event, arg) => this.workerWindow.webContents.send(EPG_GET_PROGRAM, arg) ) - .on(EPG_GET_CHANNELS, (event, arg) => + .on(EPG_GET_CHANNELS, (_event, arg) => this.workerWindow.webContents.send(EPG_GET_CHANNELS, arg) ) - .on(EPG_GET_CHANNELS_DONE, (event, arg) => + .on(EPG_GET_CHANNELS_DONE, (_event, arg) => this.mainWindow.webContents.send(EPG_GET_CHANNELS_DONE, arg) ) - .on(EPG_GET_PROGRAM_DONE, (event, arg) => { + .on(EPG_GET_PROGRAM_DONE, (_event, arg) => { this.mainWindow.webContents.send(EPG_GET_PROGRAM_DONE, arg); }) - .on(EPG_FETCH, (event, arg) => + .on(EPG_FETCH, (_event, arg) => this.workerWindow.webContents.send(EPG_FETCH, arg?.url) ) - .on(EPG_FETCH_DONE, (event, arg) => + .on(EPG_FETCH_DONE, (_event, arg) => this.mainWindow.webContents.send(EPG_FETCH_DONE, arg) ) - .on(EPG_ERROR, (event, arg) => + .on(EPG_ERROR, (_event, arg) => this.mainWindow.webContents.send(EPG_ERROR, arg) ) - .on(EPG_GET_CHANNELS_BY_RANGE, (event, arg) => { + .on(EPG_GET_CHANNELS_BY_RANGE, (_event, arg) => { this.workerWindow.webContents.send( EPG_GET_CHANNELS_BY_RANGE, arg ); }) - .on(EPG_GET_CHANNELS_BY_RANGE_RESPONSE, (event, arg) => + .on(EPG_GET_CHANNELS_BY_RANGE_RESPONSE, (_event, arg) => this.mainWindow.webContents.send( EPG_GET_CHANNELS_BY_RANGE_RESPONSE, arg ) ) - .on(EPG_FORCE_FETCH, (event, arg) => + .on(EPG_FORCE_FETCH, (_event, arg) => this.workerWindow.webContents.send(EPG_FORCE_FETCH, arg) ); diff --git a/electron/main.ts b/electron/main.ts index ea8ac697a..8e0d868cb 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,5 +1,5 @@ /* eslint-disable no-useless-catch */ -import { app, BrowserWindow, globalShortcut, Menu } from 'electron'; +import { app, BrowserWindow, Menu } from 'electron'; import * as path from 'path'; import * as url from 'url'; import { Api } from './api'; @@ -78,21 +78,9 @@ function createWindow(): BrowserWindow { } }); - registerGlobalShortcuts(); return win; } -/** - * Registers common keyboard shortcuts - */ -function registerGlobalShortcuts() { - if (process.platform === 'darwin') { - globalShortcut.register('Command+Q', () => { - app.quit(); - }); - } -} - /** * Creates hidden window for EPG worker * Hidden window is used as an additional thread to avoid blocking of the UI by long operations diff --git a/electron/menu.ts b/electron/menu.ts index 1ff76235c..519e88386 100644 --- a/electron/menu.ts +++ b/electron/menu.ts @@ -72,6 +72,7 @@ export class AppMenu { { label: 'Exit', click: () => app.quit(), + accelerator: 'CmdOrCtrl+Q', }, ], }); diff --git a/package-lock.json b/package-lock.json index 16045dc81..b37bcfb36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "iptvnator", - "version": "0.11.1", + "version": "0.12.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "iptvnator", - "version": "0.11.1", + "version": "0.12.0", "hasInstallScript": true, "dependencies": { "@angular/animations": "15.1.1", @@ -36,6 +36,7 @@ "ngx-indexed-db": "9.4.2", "ngx-skeleton-loader": "7.0.0", "ngx-whats-new": "0.4.0", + "node-mpv": "2.0.0-beta.2", "rxjs": "7.8.0", "semver": "7.3.8", "uuid": "9.0.0", @@ -18653,6 +18654,11 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node_modules/node-mpv": { + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/node-mpv/-/node-mpv-2.0.0-beta.2.tgz", + "integrity": "sha512-jf1InAB6tSXYlLs53DSw7ZEGCAhuWibILMF8GU6FmX6jXvkScLfGa9B7nmrIidG8euDpi3hdnUht0PqrHlAbXA==" + }, "node_modules/node-polyfill-webpack-plugin": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-1.1.4.tgz", @@ -41633,6 +41639,11 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node-mpv": { + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/node-mpv/-/node-mpv-2.0.0-beta.2.tgz", + "integrity": "sha512-jf1InAB6tSXYlLs53DSw7ZEGCAhuWibILMF8GU6FmX6jXvkScLfGa9B7nmrIidG8euDpi3hdnUht0PqrHlAbXA==" + }, "node-polyfill-webpack-plugin": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-1.1.4.tgz", diff --git a/package.json b/package.json index 8bfcfbfcc..0fbd0ce20 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "ngx-indexed-db": "9.4.2", "ngx-skeleton-loader": "7.0.0", "ngx-whats-new": "0.4.0", + "node-mpv": "2.0.0-beta.2", "rxjs": "7.8.0", "semver": "7.3.8", "uuid": "9.0.0", diff --git a/shared/ipc-commands.ts b/shared/ipc-commands.ts index b227bc0f5..02a230682 100644 --- a/shared/ipc-commands.ts +++ b/shared/ipc-commands.ts @@ -40,3 +40,6 @@ export const DELETE_ALL_PLAYLISTS = 'DELETE_ALL_PLAYLISTS'; // Auto-update export const AUTO_UPDATE_PLAYLISTS = 'AUTO_UPDATE'; export const AUTO_UPDATE_PLAYLISTS_RESPONSE = 'AUTO_UPDATE_RESPONSE'; + +// Experimental +export const OPEN_MPV_PLAYER = 'OPEN_MPV_PLAYER'; diff --git a/src/app/player/components/video-player/video-player.component.html b/src/app/player/components/video-player/video-player.component.html index 92a789537..ff78276e8 100644 --- a/src/app/player/components/video-player/video-player.component.html +++ b/src/app/player/components/video-player/video-player.component.html @@ -25,6 +25,7 @@ [class.hide-captions]="!playerSettings.showCaptions" > + + +
+ +

The channel is played in a separate window in the MPV player. (Installation instructions)

+
+
+ { + this.snackBar.open(response.message, '', { + duration: 3100, + }); + }, + }, { id: PLAYLIST_PARSE_RESPONSE, execute: (response: { payload: Playlist }): void => { @@ -109,6 +119,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy { private ngZone: NgZone, private overlay: Overlay, private playlistsService: PlaylistsService, + private snackBar: MatSnackBar, private storage: StorageMap, private store: Store ) {} @@ -217,4 +228,8 @@ export class VideoPlayerComponent implements OnInit, OnDestroy { this.overlayRef.addPanelClass('epg-overlay'); this.overlayRef.attach(componentPortal); } + + openUrl(url: string) { + window.open(url, '_blank'); + } } diff --git a/src/app/services/whats-new.service.ts b/src/app/services/whats-new.service.ts index 0137228a2..4ea8e2e2b 100644 --- a/src/app/services/whats-new.service.ts +++ b/src/app/services/whats-new.service.ts @@ -245,6 +245,20 @@ export class WhatsNewService { }, }, ], + '0.13.0': [ + { + imageHeight: 400, + imageBgColor: '#333', + imageSrc: './assets/updates/0130/mpv-player.png', + title: '🚀 MPV player integration', + html: 'Long-awaited mpv player support, which can now be selected from the settings. Before activating the player from the settings page, make sure it is installed on your system. For more details about mvp integration, check the wiki page.', + button: { + text: 'YAY', + textColor: '#ccc', + bgColor: '#111', + }, + }, + ], }; /** Options for the "what is new" modal dialogs */ diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts index 766b5902f..fbbbc521f 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -47,6 +47,10 @@ export class SettingsComponent implements OnInit { id: VideoPlayer.VideoJs, label: 'VideoJs Player', }, + { + id: VideoPlayer.MPV, + label: 'MPV Player', + }, ]; /** Current version of the app */ diff --git a/src/app/settings/settings.interface.ts b/src/app/settings/settings.interface.ts index c55e83e7b..544b45ff3 100644 --- a/src/app/settings/settings.interface.ts +++ b/src/app/settings/settings.interface.ts @@ -8,6 +8,7 @@ import { Theme } from './theme.enum'; export enum VideoPlayer { VideoJs = 'videojs', Html5Player = 'html5', + MPV = 'mpv', } /** diff --git a/src/app/state/effects.ts b/src/app/state/effects.ts index fcc0845a2..b1ae3b1ac 100644 --- a/src/app/state/effects.ts +++ b/src/app/state/effects.ts @@ -4,14 +4,19 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { Router } from '@angular/router'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; +import { StorageMap } from '@ngx-pwa/local-storage'; import { TranslateService } from '@ngx-translate/core'; +import { firstValueFrom } from 'rxjs'; import { combineLatestWith, map, switchMap, tap } from 'rxjs/operators'; import { CHANNEL_SET_USER_AGENT, EPG_GET_PROGRAM, + OPEN_MPV_PLAYER, } from '../../../shared/ipc-commands'; import { DataService } from '../services/data.service'; import { PlaylistsService } from '../services/playlists.service'; +import { Settings, VideoPlayer } from '../settings/settings.interface'; +import { STORE_KEY } from '../shared/enums/store-keys.enum'; import * as PlaylistActions from './actions'; import { selectActivePlaylistId, @@ -72,6 +77,20 @@ export class PlaylistEffects { userAgent: channel.http['user-agent'], }); } + + firstValueFrom(this.storage.get(STORE_KEY.Settings)).then( + (settings: Settings) => { + if ( + settings && + Object.keys(settings).length > 0 && + settings.player === VideoPlayer.MPV + ) + this.dataService.sendIpcEvent(OPEN_MPV_PLAYER, { + url: channel.url, + }); + } + ); + return PlaylistActions.setActiveChannelSuccess({ channel: action.channel, }); @@ -230,6 +249,7 @@ export class PlaylistEffects { private dataService: DataService, private router: Router, private snackBar: MatSnackBar, + private storage: StorageMap, private store: Store, private translate: TranslateService ) {} diff --git a/src/assets/images/custom-player.png b/src/assets/images/custom-player.png new file mode 100644 index 000000000..42c19314b Binary files /dev/null and b/src/assets/images/custom-player.png differ diff --git a/src/assets/updates/0130/mpv-player.png b/src/assets/updates/0130/mpv-player.png new file mode 100644 index 000000000..d6fc1932d Binary files /dev/null and b/src/assets/updates/0130/mpv-player.png differ