Skip to content

Video Player: Angular Integration

AfterViewInit / OnDestroy pattern

The player requires a real DOM element before it can mount, so initialization belongs in ngAfterViewInit, not in the constructor or ngOnInit. Clean up with player.dispose() in ngOnDestroy to release the HLS instance and all event listeners.

Do not store the player in an Angular signal or a BehaviorSubject directly. The player is a class instance with a rich internal state tree, and reactive wrappers will try to observe its properties, which causes interference. Keep the reference as a plain class field and push only primitive state values into observables.

TypeScript
// nomercy-player.component.ts
import {
AfterViewInit,
Component,
Input,
OnDestroy,
} from '@angular/core';
import nmplayer from '@nomercy-entertainment/nomercy-video-player';
import { DesktopUiPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';
import type {
NMVideoPlayer,
VideoPlayerConfig,
} from '@nomercy-entertainment/nomercy-video-player';

@Component({
selector: 'app-nomercy-player',
standalone: true,
template: `
<div>
<div [id]="containerId" style="width: 100%; aspect-ratio: 16/9;"></div>
<div class="controls">
<button (click)="togglePlayback()">Play / Pause</button>
<span>{{ currentTime | number:'1.0-0' }}s / {{ duration | number:'1.0-0' }}s</span>
</div>
</div>
`,
})
export class NMPlayerComponent implements AfterViewInit, OnDestroy {
@Input() containerId = 'nomercy-player';
@Input() config!: VideoPlayerConfig;

player: NMVideoPlayer | null = null;
currentTime = 0;
duration = 0;

ngAfterViewInit(): void {
this.player = nmplayer(this.containerId)
.addPlugin(DesktopUiPlugin)
.setup(this.config);

this.player.on('ready', () => {
this.player!.item(0, { autoplay: true });
});

this.player.on('time', ({ time }) => {
this.currentTime = time;
});

this.player.on('duration', ({ duration }) => {
this.duration = duration;
});
}

ngOnDestroy(): void {
this.player?.dispose();
this.player = null;
}

togglePlayback(): void {
void this.player?.togglePlayback();
}
}

Using the component

Pass a VideoPlayerConfig from the parent component. Use baseUrl together with relative paths on each playlist item so the same config works across environments without string concatenation.

TypeScript
// app.component.ts
import { Component } from '@angular/core';
import { NMPlayerComponent } from './nomercy-player.component';
import type {
VideoPlayerConfig,
VideoPlaylistItem,
} from '@nomercy-entertainment/nomercy-video-player';

const playlist: VideoPlaylistItem[] = [
{
id: 'sintel',
title: 'Sintel',
url: '/Sintel.(2010)/Sintel.(2010).NoMercy.m3u8',
image: '/w780/q2bVM5z90tCGbmXYtq2J38T5hSX.jpg',
duration: 888,
subtitles: [
{
id: 'eng',
kind: 'subtitles',
language: 'eng',
label: 'English',
url: '/Sintel.(2010)/subtitles/Sintel.(2010).NoMercy.eng.full.vtt',
},
],
},
];

@Component({
selector: 'app-root',
standalone: true,
imports: [NMPlayerComponent],
template: `
<app-nomercy-player
containerId="nomercy-player"
[config]="playerConfig"
/>
`,
})
export class AppComponent {
playerConfig: VideoPlayerConfig = {
baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Films',
baseImageUrl: 'https://image.tmdb.org/t/p',
playlist,
};
}

Service pattern: shared player state across components

When several components need to read or control the same player, an Angular service is the right place to own the instance. Expose primitive state as BehaviorSubject observables so components can subscribe with the async pipe or takeUntilDestroyed.

TypeScript
// nomercy-player.service.ts
import { Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import nmplayer from '@nomercy-entertainment/nomercy-video-player';
import { DesktopUiPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';
import type {
NMVideoPlayer,
VideoPlayerConfig,
} from '@nomercy-entertainment/nomercy-video-player';

@Injectable({ providedIn: 'root' })
export class NMPlayerService implements OnDestroy {
private player: NMVideoPlayer | null = null;

readonly isPlaying$ = new BehaviorSubject<boolean>(false);
readonly currentTime$ = new BehaviorSubject<number>(0);
readonly duration$ = new BehaviorSubject<number>(0);

init(containerId: string, config: VideoPlayerConfig): void {
this.player = nmplayer(containerId)
.addPlugin(DesktopUiPlugin)
.setup(config);

this.player.on('ready', () => {
this.player!.item(0, { autoplay: true });
});

this.player.on('play', () => this.isPlaying$.next(true));
this.player.on('pause', () => this.isPlaying$.next(false));

this.player.on('time', ({ time }) => {
this.currentTime$.next(time);
});

this.player.on('duration', ({ duration }) => {
this.duration$.next(duration);
});
}

togglePlayback(): void {
void this.player?.togglePlayback();
}

ngOnDestroy(): void {
this.player?.dispose();
this.player = null;
}
}

Consuming the service in a component

The player container component initializes the service. Other components, such as a transport bar, inject the same service and subscribe to the observables.

TypeScript
// player-container.component.ts
import { AfterViewInit, Component, inject } from '@angular/core';
import { NMPlayerService } from './nomercy-player.service';
import type { VideoPlayerConfig } from '@nomercy-entertainment/nomercy-video-player';

@Component({
selector: 'app-player-container',
standalone: true,
template: `<div id="nomercy-player" style="width: 100%; aspect-ratio: 16/9;"></div>`,
})
export class PlayerContainerComponent implements AfterViewInit {
private readonly playerService = inject(NMPlayerService);

private readonly config: VideoPlayerConfig = {
baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Films',
playlist: [
{
id: 'bbb',
title: 'Big Buck Bunny',
url: '/Big.Buck.Bunny.(2008)/Big.Buck.Bunny.(2008).NoMercy.m3u8',
duration: 596,
},
],
};

ngAfterViewInit(): void {
this.playerService.init('nomercy-player', this.config);
}
}
TypeScript
// transport-bar.component.ts
import { Component, inject } from '@angular/core';
import { AsyncPipe, DecimalPipe } from '@angular/common';
import { NMPlayerService } from './nomercy-player.service';

@Component({
selector: 'app-transport-bar',
standalone: true,
imports: [AsyncPipe, DecimalPipe],
template: `
<div class="transport">
<button (click)="playerService.togglePlayback()">
{{ (playerService.isPlaying$ | async) ? 'Pause' : 'Play' }}
</button>
<span>
{{ (playerService.currentTime$ | async) | number:'1.0-0' }}s
/
{{ (playerService.duration$ | async) | number:'1.0-0' }}s
</span>
</div>
`,
})
export class TransportBarComponent {
readonly playerService = inject(NMPlayerService);
}

Auth-protected streams

Pass the auth object in the config when your HLS manifests and segments require a bearer token. On 401, refreshOnUnauthenticated fires once and the request is retried automatically. 403 propagates and is not retried.

TypeScript
this.playerService.init('nomercy-player', {
playlist: [
{ id: '1', url: 'https://protected.cdn.your-domain.com/stream.m3u8' },
],
auth: {
bearerToken: () => this.authService.getAccessToken(),
refreshOnUnauthenticated: async () => {
await this.authService.refresh();
},
},
});