Skip to content

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 ​

Compositor Performance (Timeline Preview) ​

If you're building an editor-style timeline with the Compositor (e.g. a preview that renders at 30/60fps), the most important performance rule is:

  • Do not do per-frame random access decoding.

The Problem: Per-Frame Random Access ​

During Compositor preview, the render loop will request frames repeatedly:

ts
// The Compositor render loop will effectively do this every preview tick:
await layer.source.getFrameAt(currentTimeSeconds);

If getFrameAt(time) is implemented using a random access API like CanvasSink.getCanvas(time) for every preview tick, playback turns into a "seek + decode" workload per frame. On high-resolution or long-GOP sources, that causes massive stalls (or full UI freezes), even though scrubbing might appear to work.

Why caching often doesn't save you:

  • Timeline time is usually continuously increasing, so every request is a new timestamp.
  • Keyframe distance (GOP size) can make random access expensive even for small time changes.
  • LRU frame caches help repeated access to the same key; they don't prevent repeated seeks.

The Fix: Sequential Decode for Preview Playback ​

The correct approach for smooth preview is to decode sequentially while time increases:

  • Keep a long-lived CanvasSink.canvases(startTime) iterator open.
  • Advance it forward as preview time increases.
  • Maintain a 1-frame lookahead so you can stop once you pass the requested time.
  • When the user scrubs backwards or jumps a large distance, restart the iterator from the new time.
  • Guard iterator access with a small async mutex to avoid concurrent next() calls corrupting iterator state.

This is implemented in:

  • packages/mediafox/src/compositor/source-pool.ts (VideoSource.getFrameAt)

Key behavior:

  • Preview playback is smooth because decoding stays sequential.
  • Scrubbing stays responsive because large jumps trigger an iterator restart instead of iterating through huge gaps.
  • Memory stays bounded by keeping only currentFrame and nextFrame in memory (instead of a large frame cache).

Practical Guidance ​

  • Prefer sequential iteration for preview playback (timeline time increasing).
  • Use random access (getCanvas(time)) only as a fallback or for sparse, non-realtime requests.
  • If you add your own layer source types, mirror this pattern: sequential for playback, restart on seeks.

Worker Mode ​

Whether you run the Compositor in the main thread or in a worker, the same rule applies: avoid per-frame random access decoding. Worker mode can keep the UI thread responsive, but it cannot remove the underlying decode work if every preview tick is forcing a seek.

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 requestAnimationFrame for 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 requestIdleCallback for 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.

MIT License