mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
feat: mpv player integration
This commit is contained in:
1 parent
c7a2b3f6aa
commit
21b9c8f804
8 files changed
+102
-11
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));
|
||||
|
||||
/** @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)
|
||||
);
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
|
||||
Reference in new issue
Block a user