Performance Optimization Guide
This guide covers best practices and techniques for optimizing MediaFox performance in various scenarios and use cases.
Core Performance Concepts
MediaFox is built with performance in mind, but understanding its internals helps you optimize your implementation:
- Frame Buffering: MediaFox buffers video frames for smooth playback
- Audio Scheduling: Audio is scheduled ahead of time using Web Audio API
- Canvas Rendering: Efficient canvas operations with frame pooling
- State Management: Batched state updates to minimize re-renders
- Memory Management: Automatic cleanup of video frames and audio buffers
Memory Optimization
Frame Management
typescript
import { MediaFox } from '@mediafox/core';
// Configure frame buffer size based on use case
const player = new MediaFox({
canvas: canvasElement,
frameBufferSize: 30, // Number of frames to buffer (default: 10)
maxMemoryUsage: 100 * 1024 * 1024 // 100MB memory limit
});
// Monitor memory usage
player.on('memoryUsage', (usage) => {
console.log(`Memory usage: ${(usage / 1024 / 1024).toFixed(2)} MB`);
// Adjust buffer size based on memory pressure
if (usage > 50 * 1024 * 1024) { // 50MB
player.setFrameBufferSize(5); // Reduce buffer
}
});
// Manual memory cleanup when needed
function forceCleanup() {
player.flush(); // Clear all buffers
if (window.gc) window.gc(); // Force garbage collection (dev only)
}Canvas Pool Management
typescript
class CanvasPoolManager {
private pool: HTMLCanvasElement[] = [];
private maxPoolSize = 5;
getCanvas(width: number, height: number): HTMLCanvasElement {
let canvas = this.pool.pop();
if (!canvas) {
canvas = document.createElement('canvas');
}
canvas.width = width;
canvas.height = height;
return canvas;
}
returnCanvas(canvas: HTMLCanvasElement) {
if (this.pool.length < this.maxPoolSize) {
// Clear canvas before returning to pool
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
this.pool.push(canvas);
}
}
clear() {
this.pool = [];
}
}
// Use with MediaFox
const canvasPool = new CanvasPoolManager();
// Configure player to use canvas pool
const player = new MediaFox({
canvas: canvasElement,
canvasPool: canvasPool
});Rendering Optimization
Multi-Renderer System
MediaFox now includes an automatic multi-renderer system that optimizes performance by selecting the best available rendering backend:
typescript
import { MediaFox } from '@mediafox/core';
// Check available renderers in your environment
const supported = MediaFox.getSupportedRenderers();
console.log('Supported renderers:', supported);
// Output: ['webgpu', 'webgl', 'canvas2d'] (in order of preference)
// Let MediaFox auto-select the best renderer
const player = new MediaFox({
renderTarget: canvas
// renderer is auto-detected
});
// Or manually specify a renderer
const webglPlayer = new MediaFox({
renderTarget: canvas,
renderer: 'webgl' // Force WebGL renderer
});
// Get current renderer type
console.log(`Using ${player.getRendererType()} renderer`);
// Switch renderers dynamically
async function optimizeRenderer() {
const currentRenderer = player.getRendererType();
// Switch based on content or performance metrics
if (hasHighFrameRate && currentRenderer !== 'webgpu') {
try {
await player.switchRenderer('webgpu');
console.log('Switched to WebGPU for better performance');
} catch (error) {
console.log('WebGPU unavailable, using fallback');
}
}
}
// Listen to renderer changes
player.on('rendererchange', (type) => {
console.log(`Renderer changed to: ${type}`);
updateUIIndicator(type);
});
player.on('rendererfallback', ({ from, to }) => {
console.warn(`Renderer fallback: ${from} -> ${to}`);
// Notify user or adjust quality settings
});Renderer Performance Characteristics
WebGPU Renderer
- Performance: Highest - Direct GPU command submission
- Memory: Efficient texture handling
- CPU Usage: Minimal
- Best For: High-resolution content, 4K/8K video, HDR
- Availability: Modern browsers with WebGPU support
WebGL Renderer
- Performance: High - Hardware-accelerated
- Memory: Good with proper texture management
- CPU Usage: Low
- Best For: Most use cases, wide browser support
- Availability: All modern browsers
Canvas2D Renderer
- Performance: Moderate - Software rendering
- Memory: Higher due to pixel manipulation
- CPU Usage: Higher
- Best For: Universal fallback, simple playback
- Availability: All browsers
Dynamic Renderer Selection
typescript
class AdaptiveRenderer {
private player: MediaFox;
private performanceMonitor: PerformanceMonitor;
constructor(player: MediaFox) {
this.player = player;
this.performanceMonitor = new PerformanceMonitor();
this.setupAdaptiveRendering();
}
private setupAdaptiveRendering() {
// Monitor performance metrics
setInterval(() => {
this.checkAndAdaptRenderer();
}, 10000); // Check every 10 seconds
}
private async checkAndAdaptRenderer() {
const metrics = this.performanceMonitor.getMetrics();
const currentRenderer = this.player.getRendererType();
// If experiencing frame drops with Canvas2D, try to upgrade
if (currentRenderer === 'canvas2d' && metrics.frameDropRate > 0.1) {
const supported = MediaFox.getSupportedRenderers();
for (const renderer of supported) {
if (renderer !== 'canvas2d') {
try {
await this.player.switchRenderer(renderer);
console.log(`Upgraded to ${renderer} for better performance`);
break;
} catch (error) {
continue; // Try next renderer
}
}
}
}
// If GPU memory is constrained, consider downgrading
if (currentRenderer === 'webgpu' && metrics.gpuMemoryPressure) {
try {
await this.player.switchRenderer('webgl');
console.log('Switched to WebGL due to memory pressure');
} catch (error) {
// Fall back to canvas2d if needed
await this.player.switchRenderer('canvas2d');
}
}
}
}Efficient Canvas Operations
typescript
class OptimizedRenderer {
private lastFrameTime = 0;
private rafId: number | null = null;
private needsRedraw = false;
constructor(private player: MediaFox) {
this.setupEfficientRendering();
}
private setupEfficientRendering() {
// Only render when frame actually changes
this.player.on('frameChanged', () => {
this.needsRedraw = true;
this.scheduleRender();
});
// Stop rendering when paused
this.player.on('pause', () => {
if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
});
this.player.on('play', () => {
this.scheduleRender();
});
}
private scheduleRender() {
if (this.rafId) return;
this.rafId = requestAnimationFrame((timestamp) => {
this.rafId = null;
// Throttle to ~60fps max
if (timestamp - this.lastFrameTime < 16.67) {
this.scheduleRender();
return;
}
if (this.needsRedraw) {
this.render();
this.needsRedraw = false;
this.lastFrameTime = timestamp;
}
// Continue rendering if playing
if (this.player.playing) {
this.scheduleRender();
}
});
}
private render() {
// Efficient rendering logic
const frame = this.player.getCurrentFrame();
if (frame) {
this.drawFrame(frame);
}
}
private drawFrame(frame: VideoFrame) {
const canvas = this.player.canvas;
const ctx = canvas.getContext('2d')!;
// Use ImageBitmap for better performance if available
if ('createImageBitmap' in window) {
createImageBitmap(frame).then(bitmap => {
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
bitmap.close(); // Important: close to free memory
});
} else {
ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
}
// Close frame to free memory
frame.close();
}
}WebGL Acceleration
typescript
class WebGLRenderer {
private gl: WebGLRenderingContext;
private program: WebGLProgram;
private texture: WebGLTexture;
constructor(canvas: HTMLCanvasElement) {
const gl = canvas.getContext('webgl');
if (!gl) {
throw new Error('WebGL not supported');
}
this.gl = gl;
this.setupShaders();
this.setupTexture();
}
private setupShaders() {
const vertexShader = this.createShader(this.gl.VERTEX_SHADER, `
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}
`);
const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, `
precision mediump float;
uniform sampler2D u_texture;
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_texture, v_texCoord);
}
`);
this.program = this.createProgram(vertexShader, fragmentShader);
}
private createShader(type: number, source: string): WebGLShader {
const shader = this.gl.createShader(type)!;
this.gl.shaderSource(shader, source);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
throw new Error('Shader compilation error: ' + this.gl.getShaderInfoLog(shader));
}
return shader;
}
private createProgram(vertexShader: WebGLShader, fragmentShader: WebGLShader): WebGLProgram {
const program = this.gl.createProgram()!;
this.gl.attachShader(program, vertexShader);
this.gl.attachShader(program, fragmentShader);
this.gl.linkProgram(program);
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
throw new Error('Program linking error: ' + this.gl.getProgramInfoLog(program));
}
return program;
}
private setupTexture() {
this.texture = this.gl.createTexture()!;
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
}
renderFrame(frame: VideoFrame) {
this.gl.texImage2D(
this.gl.TEXTURE_2D,
0,
this.gl.RGBA,
this.gl.RGBA,
this.gl.UNSIGNED_BYTE,
frame
);
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
}
}Network Optimization
Efficient Loading
typescript
class OptimizedLoader {
private loadQueue: string[] = [];
private currentRequests = 0;
private maxConcurrentRequests = 3;
constructor(private player: MediaFox) {
this.setupProgressiveLoading();
}
private setupProgressiveLoading() {
// Preload next segments for smooth playback
this.player.on('progress', (buffered) => {
const currentTime = this.player.currentTime;
const duration = this.player.duration;
// If we're running low on buffer, preload more
const bufferedEnd = this.getBufferedEnd(buffered, currentTime);
if (bufferedEnd - currentTime < 30) { // Less than 30s buffered
this.preloadNextSegment();
}
});
}
private getBufferedEnd(buffered: TimeRanges, currentTime: number): number {
for (let i = 0; i < buffered.length; i++) {
if (buffered.start(i) <= currentTime && currentTime <= buffered.end(i)) {
return buffered.end(i);
}
}
return currentTime;
}
private async preloadNextSegment() {
if (this.currentRequests >= this.maxConcurrentRequests) {
return;
}
const nextSegmentUrl = this.getNextSegmentUrl();
if (nextSegmentUrl && !this.loadQueue.includes(nextSegmentUrl)) {
this.loadQueue.push(nextSegmentUrl);
this.processLoadQueue();
}
}
private async processLoadQueue() {
while (this.loadQueue.length > 0 && this.currentRequests < this.maxConcurrentRequests) {
const url = this.loadQueue.shift()!;
this.currentRequests++;
try {
await this.loadSegment(url);
} catch (error) {
console.warn('Failed to preload segment:', error);
} finally {
this.currentRequests--;
}
}
}
private async loadSegment(url: string): Promise<void> {
const response = await fetch(url, {
headers: {
'Range': 'bytes=0-1048576' // Load first 1MB for quick start
}
});
if (response.ok) {
const buffer = await response.arrayBuffer();
// Cache the segment for later use
this.cacheSegment(url, buffer);
}
}
private cacheSegment(url: string, buffer: ArrayBuffer) {
// Implementation depends on your caching strategy
if ('caches' in window) {
caches.open('mediafox-segments').then(cache => {
cache.put(url, new Response(buffer));
});
}
}
private getNextSegmentUrl(): string | null {
// Implementation depends on your video format and segmentation
return null;
}
}Adaptive Bitrate
typescript
class AdaptiveBitrateController {
private bandwidthHistory: number[] = [];
private lastSwitchTime = 0;
private minSwitchInterval = 10000; // 10 seconds
constructor(private player: MediaFox) {
this.setupBandwidthMonitoring();
this.setupQualityAdaptation();
}
private setupBandwidthMonitoring() {
// Monitor download speed
this.player.on('progress', () => {
this.measureBandwidth();
});
// Use Network Information API if available
if ('connection' in navigator) {
const connection = (navigator as any).connection;
connection.addEventListener('change', () => {
this.adaptToNetworkCondition(connection.downlink);
});
}
}
private measureBandwidth() {
// Simple bandwidth estimation based on download progress
const now = performance.now();
const buffered = this.player.buffered;
if (buffered && buffered.length > 0) {
const bufferedAmount = buffered.end(buffered.length - 1);
const downloadTime = now - this.lastMeasureTime;
if (downloadTime > 1000) { // Measure every second
const bandwidth = (bufferedAmount / downloadTime) * 8000; // Convert to kbps
this.bandwidthHistory.push(bandwidth);
// Keep only recent measurements
if (this.bandwidthHistory.length > 10) {
this.bandwidthHistory.shift();
}
this.lastMeasureTime = now;
this.adaptQuality();
}
}
}
private lastMeasureTime = performance.now();
private adaptQuality() {
const now = Date.now();
if (now - this.lastSwitchTime < this.minSwitchInterval) {
return; // Too soon to switch again
}
const avgBandwidth = this.getAverageBandwidth();
const currentTrack = this.player.currentVideoTrack;
const tracks = this.player.tracks.video;
if (tracks.length <= 1) return;
let targetTrack = currentTrack;
// Find appropriate quality based on bandwidth
for (let i = tracks.length - 1; i >= 0; i--) {
const track = tracks[i];
if (track.bitrate && avgBandwidth > track.bitrate * 1.5) {
targetTrack = i;
break;
}
}
// Switch if needed
if (targetTrack !== currentTrack) {
this.player.selectVideoTrack(targetTrack);
this.lastSwitchTime = now;
console.log(`Switched to quality ${targetTrack} (${avgBandwidth.toFixed(0)} kbps)`);
}
}
private getAverageBandwidth(): number {
if (this.bandwidthHistory.length === 0) return 0;
const sum = this.bandwidthHistory.reduce((a, b) => a + b, 0);
return sum / this.bandwidthHistory.length;
}
private adaptToNetworkCondition(downlink: number) {
// Immediately adapt to major network changes
const bandwidth = downlink * 1000; // Convert to kbps
this.bandwidthHistory = [bandwidth]; // Reset history
this.adaptQuality();
}
}CPU and Battery Optimization
Efficient Event Handling
typescript
class PerformantEventManager {
private eventQueue: Array<{type: string, data: any}> = [];
private processingScheduled = false;
constructor(private player: MediaFox) {
this.setupBatchedEvents();
}
private setupBatchedEvents() {
// Batch frequent events to reduce CPU usage
const frequentEvents = ['timeupdate', 'progress'];
frequentEvents.forEach(eventType => {
this.player.on(eventType as any, (data) => {
this.queueEvent(eventType, data);
});
});
}
private queueEvent(type: string, data: any) {
this.eventQueue.push({ type, data });
if (!this.processingScheduled) {
this.processingScheduled = true;
requestIdleCallback(() => {
this.processEventQueue();
this.processingScheduled = false;
});
}
}
private processEventQueue() {
// Group events by type and only process the latest
const latestEvents = new Map<string, any>();
this.eventQueue.forEach(({ type, data }) => {
latestEvents.set(type, data);
});
// Process latest events
latestEvents.forEach((data, type) => {
this.processEvent(type, data);
});
this.eventQueue = [];
}
private processEvent(type: string, data: any) {
switch (type) {
case 'timeupdate':
this.updateTimeDisplay(data);
break;
case 'progress':
this.updateProgressBar(data);
break;
}
}
private updateTimeDisplay(time: number) {
// Throttled time display update
const display = document.getElementById('time-display');
if (display) {
display.textContent = this.formatTime(time);
}
}
private updateProgressBar(buffered: TimeRanges) {
// Throttled progress bar update
const progressBar = document.getElementById('progress-bar');
if (progressBar && buffered.length > 0) {
const percent = (buffered.end(0) / this.player.duration) * 100;
progressBar.style.width = `${percent}%`;
}
}
private formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
}Background Tab Optimization
typescript
class VisibilityOptimizer {
private isVisible = true;
private reducedQuality = false;
constructor(private player: MediaFox) {
this.setupVisibilityHandling();
}
private setupVisibilityHandling() {
document.addEventListener('visibilitychange', () => {
this.isVisible = !document.hidden;
this.optimizeForVisibility();
});
// Also handle window focus/blur
window.addEventListener('blur', () => {
this.isVisible = false;
this.optimizeForVisibility();
});
window.addEventListener('focus', () => {
this.isVisible = true;
this.optimizeForVisibility();
});
}
private optimizeForVisibility() {
if (!this.isVisible) {
// Reduce quality when not visible
this.reduceQuality();
// Reduce frame rate
this.player.setFrameRate(15); // Lower frame rate
// Reduce buffer size
this.player.setFrameBufferSize(5);
} else {
// Restore quality when visible again
this.restoreQuality();
// Restore frame rate
this.player.setFrameRate(30);
// Restore buffer size
this.player.setFrameBufferSize(10);
}
}
private reduceQuality() {
if (this.reducedQuality) return;
const tracks = this.player.tracks.video;
if (tracks.length > 1) {
// Switch to lowest quality
const lowestQuality = this.findLowestQualityTrack(tracks);
this.player.selectVideoTrack(lowestQuality);
this.reducedQuality = true;
}
}
private restoreQuality() {
if (!this.reducedQuality) return;
// Let adaptive bitrate controller handle quality selection
this.reducedQuality = false;
}
private findLowestQualityTrack(tracks: any[]): number {
let minPixels = Infinity;
let lowestIndex = 0;
tracks.forEach((track, index) => {
const pixels = track.width * track.height;
if (pixels < minPixels) {
minPixels = pixels;
lowestIndex = index;
}
});
return lowestIndex;
}
}Monitoring and Debugging
Performance Monitor
typescript
class PerformanceMonitor {
private metrics = {
frameDrops: 0,
avgFrameTime: 0,
memoryUsage: 0,
bufferHealth: 0,
networkBandwidth: 0
};
private frameTimeHistory: number[] = [];
private lastFrameTime = 0;
constructor(private player: MediaFox) {
this.setupMonitoring();
}
private setupMonitoring() {
// Monitor frame rendering performance
this.player.on('frameRendered', (timestamp) => {
this.trackFramePerformance(timestamp);
});
// Monitor memory usage
setInterval(() => {
this.checkMemoryUsage();
}, 5000);
// Monitor buffer health
this.player.on('progress', () => {
this.checkBufferHealth();
});
}
private trackFramePerformance(timestamp: number) {
if (this.lastFrameTime > 0) {
const frameTime = timestamp - this.lastFrameTime;
this.frameTimeHistory.push(frameTime);
// Keep only recent frame times
if (this.frameTimeHistory.length > 60) {
this.frameTimeHistory.shift();
}
// Calculate average
const sum = this.frameTimeHistory.reduce((a, b) => a + b, 0);
this.metrics.avgFrameTime = sum / this.frameTimeHistory.length;
// Detect frame drops (frame time > 33ms for 30fps)
if (frameTime > 33) {
this.metrics.frameDrops++;
}
}
this.lastFrameTime = timestamp;
}
private checkMemoryUsage() {
if ('memory' in performance) {
const memory = (performance as any).memory;
this.metrics.memoryUsage = memory.usedJSHeapSize;
// Warn if memory usage is high
if (memory.usedJSHeapSize > memory.jsHeapSizeLimit * 0.8) {
console.warn('High memory usage detected:', {
used: Math.round(memory.usedJSHeapSize / 1024 / 1024),
limit: Math.round(memory.jsHeapSizeLimit / 1024 / 1024)
});
}
}
}
private checkBufferHealth() {
const currentTime = this.player.currentTime;
const buffered = this.player.buffered;
if (buffered && buffered.length > 0) {
// Find buffer level at current time
for (let i = 0; i < buffered.length; i++) {
if (buffered.start(i) <= currentTime && currentTime <= buffered.end(i)) {
this.metrics.bufferHealth = buffered.end(i) - currentTime;
break;
}
}
}
// Warn if buffer is low
if (this.metrics.bufferHealth < 5) {
console.warn('Low buffer detected:', this.metrics.bufferHealth);
}
}
getMetrics() {
return { ...this.metrics };
}
logPerformanceReport() {
const report = {
...this.metrics,
avgFrameTime: Math.round(this.metrics.avgFrameTime * 100) / 100,
memoryUsageMB: Math.round(this.metrics.memoryUsage / 1024 / 1024),
bufferHealthSeconds: Math.round(this.metrics.bufferHealth * 100) / 100
};
console.table(report);
}
}
// Usage
const monitor = new PerformanceMonitor(player);
// Log performance report every 30 seconds
setInterval(() => {
monitor.logPerformanceReport();
}, 30000);Best Practices Summary
Memory Management
- Set appropriate frame buffer sizes
- Use canvas pooling for multiple players
- Implement proper cleanup on component unmount
- Monitor memory usage and adapt accordingly
Rendering Performance
- Use
requestAnimationFramefor smooth rendering - Only render when frames actually change
- Consider WebGL for complex video processing
- Implement efficient canvas operations
Network Optimization
- Implement adaptive bitrate streaming
- Use progressive loading for large files
- Cache frequently accessed segments
- Monitor bandwidth and adapt quality
CPU and Battery
- Batch frequent events to reduce processing
- Reduce quality when tab is not visible
- Use
requestIdleCallbackfor non-critical operations - Implement efficient event handling
Monitoring
- Track frame drop rates and rendering performance
- Monitor memory usage and buffer health
- Log performance metrics for debugging
- Implement automated quality adaptation
Following these optimization techniques will help you build high-performance Media Players that work well across different devices and network conditions.
