mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
Merge pull request #245 from 4gray/feature/mpv-integration
Feature/mpv integration
This commit is contained in:
15 files changed
+132
-26
No files matched your search
+37
-11
@@ -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)
|
||||
);
|
||||
|
||||
|
||||
+1
-13
@@ -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
|
||||
|
||||
@@ -72,6 +72,7 @@ export class AppMenu {
|
||||
{
|
||||
label: 'Exit',
|
||||
click: () => app.quit(),
|
||||
accelerator: 'CmdOrCtrl+Q',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Generated
+13
-2
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
@@ -25,6 +25,7 @@
|
||||
[class.hide-captions]="!playerSettings.showCaptions"
|
||||
></app-vjs-player>
|
||||
</ng-container>
|
||||
|
||||
<!-- default html player component -->
|
||||
<app-html-video-player
|
||||
*ngIf="playerSettings.player === 'html5'"
|
||||
@@ -33,6 +34,13 @@
|
||||
>
|
||||
</app-html-video-player>
|
||||
|
||||
<ng-container *ngIf="(activeChannel$ | async) && playerSettings.player === 'mpv'">
|
||||
<div class="mpv-player">
|
||||
<img src="./assets/images/custom-player.png" />
|
||||
<p>The channel is played in a separate window in the MPV player. (<a [routerLink] style="cursor: pointer;" (click)="openUrl('https://github.com/4gray/iptvnator/wiki/What-is-mpv-video-player-and-how-to-install-it-on-different-operating-systems%3F')">Installation instructions</a>)</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- channel overlay -->
|
||||
<app-info-overlay
|
||||
[channel]="activeChannel$ | async"
|
||||
|
||||
@@ -22,3 +22,17 @@
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.mpv-player {
|
||||
display: flex;
|
||||
margin-top: 50px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
filter: invert(100%);
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
} from 'rxjs';
|
||||
import { Channel } from '../../../../../shared/channel.interface';
|
||||
import {
|
||||
ERROR,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
} from '../../../../../shared/ipc-commands';
|
||||
@@ -73,6 +75,14 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
|
||||
/** IPC Renderer commands list with callbacks */
|
||||
commandsList = [
|
||||
{
|
||||
id: ERROR,
|
||||
execute: (response: { message: string }): void => {
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -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 <a target="_blank" href="https://github.com/4gray/iptvnator/wiki/What-is-mpv-video-player-and-how-to-install-it-on-different-operating-systems%3F">wiki page</a>.',
|
||||
button: {
|
||||
text: 'YAY',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** Options for the "what is new" modal dialogs */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Theme } from './theme.enum';
|
||||
export enum VideoPlayer {
|
||||
VideoJs = 'videojs',
|
||||
Html5Player = 'html5',
|
||||
MPV = 'mpv',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
) {}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 281 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
Reference in new issue
Block a user