Angular Integration
MediaFox integrates seamlessly with Angular using services and components. This guide covers Angular 16+ with standalone components.
Angular Service
Create a service to manage the player instance:
typescript
// mediafox.service.ts
import { Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { MediaFox, PlayerStateData, PlayerOptions } from '@mediafox/core';
@Injectable({
providedIn: 'root'
})
export class MediaFoxService implements OnDestroy {
private player: MediaFox | null = null;
private stateSubject = new BehaviorSubject<PlayerStateData | null>(null);
private errorSubject = new BehaviorSubject<Error | null>(null);
state$: Observable<PlayerStateData | null> = this.stateSubject.asObservable();
error$: Observable<Error | null> = this.errorSubject.asObservable();
initialize(options: PlayerOptions): void {
if (this.player) {
this.player.dispose();
}
this.player = new MediaFox(options);
// Subscribe to state changes
this.player.subscribe(state => {
this.stateSubject.next(state);
});
// Handle errors
this.player.on('error', error => {
this.errorSubject.next(error);
});
}
async load(source: any): Promise<void> {
if (!this.player) {
throw new Error('Player not initialized');
}
try {
this.errorSubject.next(null);
await this.player.load(source);
} catch (error) {
this.errorSubject.next(error as Error);
throw error;
}
}
async play(): Promise<void> {
await this.player?.play();
}
pause(): void {
this.player?.pause();
}
async seek(time: number): Promise<void> {
await this.player?.seek(time);
}
setVolume(volume: number): void {
if (this.player) {
this.player.volume = volume;
}
}
setMuted(muted: boolean): void {
if (this.player) {
this.player.muted = muted;
}
}
ngOnDestroy(): void {
this.player?.dispose();
this.stateSubject.complete();
this.errorSubject.complete();
}
}Basic Media Player Component
typescript
// video-player.component.ts
import { Component, ElementRef, Input, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MediaFoxService } from './mediafox.service';
import { formatTime } from '@mediafox/core';
@Component({
selector: 'app-video-player',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="video-player">
<canvas #videoCanvas class="video-canvas"></canvas>
<div class="controls" *ngIf="state$ | async as state">
<button (click)="togglePlay()">
{{ state.playing ? '⏸' : '▶️' }}
</button>
<div class="progress" (click)="handleSeek($event)">
<div
class="progress-bar"
[style.width.%]="(state.currentTime / state.duration) * 100"
></div>
</div>
<span class="time">
{{ formatTime(state.currentTime) }} / {{ formatTime(state.duration) }}
</span>
<input
type="range"
min="0"
max="1"
step="0.01"
[value]="state.volume"
(input)="updateVolume($event)"
/>
</div>
<div class="error" *ngIf="error$ | async as error">
Error: {{ error.message }}
</div>
</div>
`,
styles: [`
.video-player {
background: #000;
border-radius: 8px;
overflow: hidden;
}
.video-canvas {
width: 100%;
height: auto;
display: block;
}
.controls {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
background: #333;
}
.progress {
flex: 1;
height: 4px;
background: #666;
cursor: pointer;
position: relative;
}
.progress-bar {
height: 100%;
background: #0066cc;
}
.time {
color: white;
font-size: 14px;
}
.error {
padding: 10px;
background: #f44336;
color: white;
}
`]
})
export class VideoPlayerComponent implements OnInit, OnDestroy {
@ViewChild('videoCanvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>;
@Input() src?: string | File | Blob;
@Input() autoplay = false;
state$ = this.playerService.state$;
error$ = this.playerService.error$;
formatTime = formatTimeUtil;
constructor(private playerService: MediaFoxService) {}
ngOnInit(): void {
// Initialize player with canvas
this.playerService.initialize({
renderTarget: this.canvasRef.nativeElement,
autoplay: this.autoplay
});
// Load source if provided
if (this.src) {
this.playerService.load(this.src);
}
}
ngOnDestroy(): void {
// Service handles cleanup
}
async togglePlay(): Promise<void> {
const state = await this.state$.pipe(take(1)).toPromise();
if (state?.playing) {
this.playerService.pause();
} else {
await this.playerService.play();
}
}
handleSeek(event: MouseEvent): void {
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
const percent = (event.clientX - rect.left) / rect.width;
const state = this.playerService.state$.value;
if (state) {
const time = percent * state.duration;
this.playerService.seek(time);
}
}
updateVolume(event: Event): void {
const value = parseFloat((event.target as HTMLInputElement).value);
this.playerService.setVolume(value);
}
}Advanced Player with Full Controls
typescript
// advanced-player.component.ts
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MediaFox, VideoTrackInfo, AudioTrackInfo, formatTime } from '@mediafox/core';
@Component({
selector: 'app-advanced-player',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="advanced-player" (mousemove)="showControls()">
<canvas #videoCanvas class="video-canvas"></canvas>
<!-- Loading spinner -->
<div class="loading" *ngIf="loading">
<div class="spinner"></div>
</div>
<!-- Controls overlay -->
<div class="controls-overlay" [class.hidden]="!controlsVisible && playing">
<!-- Center play button -->
<button
class="play-center"
*ngIf="!playing && loaded"
(click)="play()"
>
▶️
</button>
<!-- Bottom controls -->
<div class="controls-bar">
<!-- Progress bar -->
<div class="progress-container" (click)="seek($event)">
<div class="buffered"
*ngFor="let range of bufferedRanges"
[style.left.%]="(range.start / duration) * 100"
[style.width.%]="((range.end - range.start) / duration) * 100"
></div>
<div class="progress" [style.width.%]="progressPercent"></div>
<div class="scrubber" [style.left.%]="progressPercent"></div>
</div>
<!-- Control buttons -->
<div class="controls-row">
<div class="controls-left">
<button (click)="togglePlay()">{{ playing ? '⏸' : '▶️' }}</button>
<button (click)="stop()">⏹</button>
<button (click)="skipBackward()">⏮ -10s</button>
<button (click)="skipForward()">⏭ +10s</button>
<span class="time">
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
</span>
</div>
<div class="controls-right">
<button (click)="toggleMute()">
{{ muted ? 'Mute' : 'Unmute' }}
</button>
<input
type="range"
class="volume-slider"
min="0"
max="1"
step="0.01"
[(ngModel)]="volume"
(input)="updateVolume()"
/>
<!-- Playback speed -->
<select [(ngModel)]="playbackRate" (change)="updatePlaybackRate()">
<option [value]="0.25">0.25x</option>
<option [value]="0.5">0.5x</option>
<option [value]="0.75">0.75x</option>
<option [value]="1">1x</option>
<option [value]="1.25">1.25x</option>
<option [value]="1.5">1.5x</option>
<option [value]="2">2x</option>
</select>
<!-- Quality selector -->
<select *ngIf="videoTracks.length > 1" (change)="selectVideoTrack($event)">
<option *ngFor="let track of videoTracks" [value]="track.id">
{{ track.width }}x{{ track.height }}
</option>
</select>
<button (click)="toggleFullscreen()">Fullscreen</button>
<button (click)="screenshot()">Screenshot</button>
</div>
</div>
</div>
</div>
</div>
`,
styles: [`
.advanced-player {
position: relative;
background: #000;
width: 100%;
user-select: none;
}
.video-canvas {
width: 100%;
height: auto;
display: block;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.spinner {
width: 50px;
height: 50px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.controls-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
to bottom,
rgba(0,0,0,0.7) 0%,
transparent 20%,
transparent 80%,
rgba(0,0,0,0.7) 100%
);
transition: opacity 0.3s;
}
.controls-overlay.hidden {
opacity: 0;
pointer-events: none;
}
.play-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80px;
height: 80px;
border-radius: 50%;
background: rgba(0,0,0,0.7);
border: 2px solid white;
color: white;
font-size: 32px;
cursor: pointer;
}
.controls-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 10px;
pointer-events: all;
}
.progress-container {
height: 5px;
background: rgba(255,255,255,0.3);
margin-bottom: 10px;
cursor: pointer;
position: relative;
}
.buffered {
position: absolute;
height: 100%;
background: rgba(255,255,255,0.5);
}
.progress {
height: 100%;
background: #0066cc;
}
.scrubber {
position: absolute;
top: -5px;
width: 15px;
height: 15px;
background: white;
border-radius: 50%;
transform: translateX(-50%);
}
.controls-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.controls-left, .controls-right {
display: flex;
align-items: center;
gap: 10px;
}
button {
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.3);
color: white;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: rgba(255,255,255,0.2);
}
.time {
color: white;
font-size: 14px;
}
.volume-slider {
width: 100px;
}
select {
background: rgba(0,0,0,0.5);
color: white;
border: 1px solid rgba(255,255,255,0.3);
padding: 5px;
border-radius: 4px;
}
`]
})
export class AdvancedPlayerComponent implements OnInit {
@ViewChild('videoCanvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>;
@Input() src!: string | File | Blob;
player!: MediaFox;
loading = false;
loaded = false;
playing = false;
currentTime = 0;
duration = 0;
volume = 1;
muted = false;
playbackRate = 1;
progressPercent = 0;
bufferedRanges: Array<{start: number, end: number}> = [];
videoTracks: VideoTrackInfo[] = [];
audioTracks: AudioTrackInfo[] = [];
controlsVisible = true;
private controlsTimeout?: number;
formatTime = formatTime;
async ngOnInit(): Promise<void> {
// Initialize player
this.player = new MediaFox({
renderTarget: this.canvasRef.nativeElement
});
// Subscribe to state changes
this.player.subscribe(state => {
this.playing = state.playing;
this.currentTime = state.currentTime;
this.duration = state.duration;
this.volume = state.volume;
this.muted = state.muted;
this.bufferedRanges = state.buffered;
if (this.duration > 0) {
this.progressPercent = (this.currentTime / this.duration) * 100;
}
});
// Load media
await this.loadMedia();
// Keyboard shortcuts
this.setupKeyboardControls();
}
async loadMedia(): Promise<void> {
this.loading = true;
try {
await this.player.load(this.src);
this.loaded = true;
// Get available tracks
this.videoTracks = this.player.getVideoTracks();
this.audioTracks = this.player.getAudioTracks();
} catch (error) {
console.error('Failed to load media:', error);
} finally {
this.loading = false;
}
}
async play(): Promise<void> {
await this.player.play();
}
pause(): void {
this.player.pause();
}
stop(): void {
this.player.stop();
}
async togglePlay(): Promise<void> {
if (this.playing) {
this.pause();
} else {
await this.play();
}
}
seek(event: MouseEvent): void {
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
const percent = (event.clientX - rect.left) / rect.width;
this.player.currentTime = percent * this.duration;
}
skipBackward(): void {
this.player.currentTime = Math.max(0, this.currentTime - 10);
}
skipForward(): void {
this.player.currentTime = Math.min(this.duration, this.currentTime + 10);
}
toggleMute(): void {
this.player.muted = !this.muted;
}
updateVolume(): void {
this.player.volume = this.volume;
}
updatePlaybackRate(): void {
this.player.playbackRate = this.playbackRate;
}
async selectVideoTrack(event: Event): Promise<void> {
const trackId = (event.target as HTMLSelectElement).value;
await this.player.selectVideoTrack(trackId);
}
async screenshot(): Promise<void> {
const blob = await this.player.screenshot({ format: 'png' });
if (blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'screenshot.png';
a.click();
URL.revokeObjectURL(url);
}
}
toggleFullscreen(): void {
const element = this.canvasRef.nativeElement;
if (!document.fullscreenElement) {
element.requestFullscreen();
} else {
document.exitFullscreen();
}
}
showControls(): void {
this.controlsVisible = true;
clearTimeout(this.controlsTimeout);
this.controlsTimeout = window.setTimeout(() => {
if (this.playing) {
this.controlsVisible = false;
}
}, 3000);
}
private setupKeyboardControls(): void {
document.addEventListener('keydown', (e) => {
switch(e.key) {
case ' ':
e.preventDefault();
this.togglePlay();
break;
case 'ArrowLeft':
this.skipBackward();
break;
case 'ArrowRight':
this.skipForward();
break;
case 'ArrowUp':
this.player.volume = Math.min(1, this.volume + 0.1);
break;
case 'ArrowDown':
this.player.volume = Math.max(0, this.volume - 0.1);
break;
case 'm':
this.toggleMute();
break;
case 'f':
this.toggleFullscreen();
break;
}
});
}
ngOnDestroy(): void {
clearTimeout(this.controlsTimeout);
this.player.dispose();
}
}Using with Angular Forms
typescript
// player-form.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-player-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<form [formGroup]="playerForm">
<div class="form-group">
<label>Volume</label>
<input
type="range"
formControlName="volume"
min="0"
max="1"
step="0.01"
/>
<span>{{ playerForm.get('volume')?.value | percent }}</span>
</div>
<div class="form-group">
<label>
<input type="checkbox" formControlName="muted" />
Muted
</label>
</div>
<div class="form-group">
<label>Playback Speed</label>
<select formControlName="playbackRate">
<option [value]="0.5">0.5x</option>
<option [value]="1">1x</option>
<option [value]="1.5">1.5x</option>
<option [value]="2">2x</option>
</select>
</div>
</form>
`
})
export class PlayerFormComponent {
playerForm: FormGroup;
constructor(
private fb: FormBuilder,
private playerService: MediaFoxService
) {
this.playerForm = this.fb.group({
volume: [1],
muted: [false],
playbackRate: [1]
});
// React to form changes
this.playerForm.valueChanges.subscribe(values => {
this.playerService.setVolume(values.volume);
this.playerService.setMuted(values.muted);
// Set playback rate...
});
// Update form from player state
this.playerService.state$.subscribe(state => {
if (state) {
this.playerForm.patchValue({
volume: state.volume,
muted: state.muted
}, { emitEvent: false });
}
});
}
}Directive for Easy Integration
typescript
// mediafox.directive.ts
import { Directive, ElementRef, Input, OnInit, OnDestroy } from '@angular/core';
import { MediaFox } from '@mediafox/core';
@Directive({
selector: '[xiaoMeiPlayer]',
standalone: true
})
export class MediaFoxDirective implements OnInit, OnDestroy {
@Input() xiaoMeiPlayer?: string | File | Blob;
@Input() xiaoMeiOptions: any = {};
private player?: MediaFox;
constructor(private el: ElementRef<HTMLCanvasElement>) {}
ngOnInit(): void {
if (this.el.nativeElement.tagName !== 'CANVAS') {
console.error('xiaoMeiPlayer directive must be used on a canvas element');
return;
}
this.player = new MediaFox({
renderTarget: this.el.nativeElement,
...this.xiaoMeiOptions
});
if (this.xiaoMeiPlayer) {
this.player.load(this.xiaoMeiPlayer);
}
}
ngOnDestroy(): void {
this.player?.dispose();
}
}
// Usage:
// <canvas [xiaoMeiPlayer]="videoUrl" [xiaoMeiOptions]="{autoplay: true}"></canvas>Testing
typescript
// video-player.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VideoPlayerComponent } from './video-player.component';
import { MediaFoxService } from './mediafox.service';
describe('VideoPlayerComponent', () => {
let component: VideoPlayerComponent;
let fixture: ComponentFixture<VideoPlayerComponent>;
let mockService: jasmine.SpyObj<MediaFoxService>;
beforeEach(async () => {
mockService = jasmine.createSpyObj('MediaFoxService', [
'initialize',
'load',
'play',
'pause'
]);
await TestBed.configureTestingModule({
imports: [VideoPlayerComponent],
providers: [
{ provide: MediaFoxService, useValue: mockService }
]
}).compileComponents();
fixture = TestBed.createComponent(VideoPlayerComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should initialize player on init', () => {
fixture.detectChanges();
expect(mockService.initialize).toHaveBeenCalled();
});
it('should load source when provided', async () => {
component.src = 'test.mp4';
fixture.detectChanges();
expect(mockService.load).toHaveBeenCalledWith('test.mp4');
});
});Best Practices
1. Use OnPush Change Detection
typescript
@Component({
selector: 'app-video-player',
changeDetection: ChangeDetectionStrategy.OnPush,
// ...
})2. Unsubscribe Properly
typescript
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
export class PlayerComponent implements OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.playerService.state$
.pipe(takeUntil(this.destroy$))
.subscribe(state => {
// Handle state
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}3. Handle Zone.js
typescript
import { NgZone } from '@angular/core';
constructor(private zone: NgZone) {}
// Run outside Angular zone for performance
this.zone.runOutsideAngular(() => {
this.player.on('timeupdate', ({ currentTime }) => {
// Update progress without triggering change detection
this.progressBar.style.width = `${(currentTime / duration) * 100}%`;
});
});Next Steps
- Svelte Integration - Using MediaFox with Svelte
- State Management - Advanced state handling
- API Reference - Complete API documentation
