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
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
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:
// 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
currentFrameandnextFramein 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:
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
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
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
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
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
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
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
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
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.
