Files
iptvnator/apps/web/src/app/services/player.service.ts
T
4grayandClaude Opus 5.5 5dbad2383f perf(web): keep channel lists, EPG views and the Stalker layer off the initial path (#1712)
The root shell imported WindowControlsComponent and DialogService through the @iptvnator/ui/components barrel, and esbuild keeps every Angular component module a barrel re-exports, so channel lists, EPG views, @angular/forms, date-fns and the whole Stalker data layer sat in main.js. The shell now uses file-level entries, the Stalker connection editor is a lazy proxy, and the release-notes and external-player info dialogs load on demand with a handled failure path.

renderer.initialBytes 2,714,336 -> 1,626,019 bytes (-40%); the baseline is lowered to the ubuntu ratchet measurement and the production/PWA initial budgets drop to 1.8/2 MB. J1: did-finish-load about -16 ms, first card within noise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:48:01 +02:00

130 lines
4.1 KiB
TypeScript

import { Injectable, inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { DataService } from '@iptvnator/services';
import {
ExternalPlayerSession,
OPEN_MPV_PLAYER,
OPEN_VLC_PLAYER,
PlayerContentInfo,
ResolvedPortalPlayback,
VideoPlayer,
} from '@iptvnator/shared/interfaces';
import type { ExternalPlayerName } from '@iptvnator/shared/interfaces';
import { SettingsStore } from './settings-store.service';
@Injectable({
providedIn: 'root',
})
export class PlayerService {
private dialog = inject(MatDialog);
private dataService = inject(DataService);
private settingsStore = inject(SettingsStore);
isEmbeddedPlayer(
player = this.settingsStore.player() ?? VideoPlayer.VideoJs
): boolean {
return (
player === VideoPlayer.VideoJs ||
player === VideoPlayer.Html5Player ||
player === VideoPlayer.ArtPlayer ||
player === VideoPlayer.EmbeddedMpv
);
}
openPlayer(
streamUrl: string,
title: string,
thumbnail?: string,
hideExternalInfoDialog = true,
isLiveContent = false,
userAgent?: string,
referer?: string,
origin?: string,
contentInfo?: PlayerContentInfo,
startTime?: number,
headers?: Record<string, string>
): Promise<ExternalPlayerSession | void> {
return this.openResolvedPlayback(
{
streamUrl,
title,
thumbnail,
isLive: isLiveContent,
startTime,
contentInfo,
headers,
userAgent,
referer,
origin,
},
hideExternalInfoDialog
);
}
async openResolvedPlayback(
playback: ResolvedPortalPlayback,
hideExternalInfoDialog = true
): Promise<ExternalPlayerSession | void> {
const player = this.settingsStore.player() ?? VideoPlayer.VideoJs;
if (player === VideoPlayer.MPV) {
if (!hideExternalInfoDialog) {
this.showExternalPlayerInfoDialog();
}
return await this.openExternalPlayback(playback, 'mpv');
} else if (player === VideoPlayer.VLC) {
if (!hideExternalInfoDialog) {
this.showExternalPlayerInfoDialog();
}
return await this.openExternalPlayback(playback, 'vlc');
}
return;
}
/**
* Loaded on demand: the dialog's Material checkbox brings @angular/forms,
* and this service is created with the app, so a static import would put
* both on the initial path. Playback does not wait for the dialog.
*/
private showExternalPlayerInfoDialog(): void {
this.loadExternalPlayerInfoDialog()
.then(({ ExternalPlayerInfoDialogComponent }) =>
this.dialog.open(ExternalPlayerInfoDialogComponent)
)
.catch((error: unknown) => {
// Informational only: playback is already starting.
console.error(
'Could not load the external player info dialog:',
error
);
});
}
/** The dynamic import; a field so specs can substitute it. */
loadExternalPlayerInfoDialog = () =>
import('./external-player-info-dialog.lazy');
async openExternalPlayback(
playback: ResolvedPortalPlayback,
player: ExternalPlayerName
): Promise<ExternalPlayerSession | void> {
const ipcEvent = player === 'mpv' ? OPEN_MPV_PLAYER : OPEN_VLC_PLAYER;
return await this.dataService.sendIpcEvent<ExternalPlayerSession>(
ipcEvent,
{
url: playback.streamUrl,
title: playback.title,
thumbnail: playback.thumbnail,
'user-agent': playback.userAgent,
referer: playback.referer,
origin: playback.origin,
headers: playback.headers,
contentInfo: playback.contentInfo,
startTime: playback.startTime,
}
);
}
}