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.
// 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.
// 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.
// 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.
// 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);
}
}
// 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.
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();
},
},
});
What to read next
- Configuration: every config option and default
- API Methods: full method reference
- Events: all events and payload shapes
- Plugin Development: writing your own plugins
- Framework: Vue: Vue 3 composable pattern
- Framework: React: React useRef pattern