Plugins
MediaFox provides a powerful plugin system that allows you to extend the player with custom functionality. Plugins can hook into lifecycle events, render overlays, modify state, intercept events, and customize the audio graph.
Quick Start
import { MediaFox, type MediaFoxPlugin } from '@mediafox/core';
// Create a simple watermark plugin
const watermarkPlugin: MediaFoxPlugin = {
name: 'watermark',
hooks: {
render: {
onOverlay: {
zIndex: 10,
render(ctx, time, { width, height }) {
ctx.font = '14px sans-serif';
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fillText('© MediaFox', width - 100, height - 20);
}
}
}
},
install(context) {
context.log('Watermark plugin installed');
}
};
// Use the plugin
const player = new MediaFox({ renderTarget: canvas });
await player.use(watermarkPlugin);Plugin Interface
Every plugin must implement the MediaFoxPlugin interface:
interface MediaFoxPlugin {
// Required: unique plugin name
name: string;
// Optional: version for debugging
version?: string;
// Optional: plugins that must be installed first
dependencies?: string[];
// Called when plugin is installed
install(context: PluginContext): void | Promise<void>;
// Optional: called when plugin is uninstalled
uninstall?(): void | Promise<void>;
// Optional: define hooks
hooks?: PluginHooks;
}Plugin Context
When a plugin is installed, it receives a PluginContext object with controlled access to player internals:
interface PluginContext {
// Reference to the player
player: MediaFox;
// State access
getState(): Readonly<PlayerStateData>;
subscribe(listener: (state: PlayerStateData) => void): () => void;
// Plugin-specific state (isolated)
getPluginState<T>(): T | undefined;
setPluginState<T>(state: T): void;
// Events (auto-cleanup on uninstall)
on<K extends keyof PlayerEventMap>(event: K, handler: Function): void;
off<K extends keyof PlayerEventMap>(event: K, handler: Function): void;
// Canvas access
getCanvas(): HTMLCanvasElement | OffscreenCanvas | null;
// Inter-plugin communication
getPlugin<T extends MediaFoxPlugin>(name: string): T | undefined;
hasPlugin(name: string): boolean;
// Logging (prefixed with plugin name)
log(...args: unknown[]): void;
warn(...args: unknown[]): void;
error(...args: unknown[]): void;
}Hook Categories
Lifecycle Hooks
Lifecycle hooks are called at key points during player operations. They can be async and may cancel or modify operations.
interface LifecycleHooks {
// Before/after loading media
beforeLoad?(source: MediaSource): MaybePromise<HookResult<MediaSource> | void>;
afterLoad?(mediaInfo: MediaInfo): MaybePromise<void>;
// Before/after play
beforePlay?(): MaybePromise<HookResult | void>;
afterPlay?(): void;
// Before/after pause
beforePause?(): MaybePromise<HookResult | void>;
afterPause?(): void;
// Before/after seek
beforeSeek?(time: number): MaybePromise<HookResult<number> | void>;
afterSeek?(time: number): void;
// Before/after stop
beforeStop?(): MaybePromise<HookResult | void>;
afterStop?(): void;
// Error handling
onError?(error: Error): { handled?: boolean } | void;
onEnded?(): void;
}Example: Analytics Plugin
const analyticsPlugin: MediaFoxPlugin = {
name: 'analytics',
install(ctx) {
ctx.setPluginState({ plays: 0, seeks: 0, totalWatchTime: 0 });
},
hooks: {
lifecycle: {
afterPlay() {
const state = this.context.getPluginState<{ plays: number }>();
this.context.setPluginState({ ...state, plays: state.plays + 1 });
sendAnalytics('play');
},
afterSeek(time) {
const state = this.context.getPluginState<{ seeks: number }>();
this.context.setPluginState({ ...state, seeks: state.seeks + 1 });
sendAnalytics('seek', { time });
},
onError(error) {
sendAnalytics('error', { message: error.message });
return { handled: false }; // Don't suppress the error
}
}
}
};Render Hooks
Render hooks are called during the video rendering pipeline. These must be synchronous for performance.
interface RenderHooks {
// Skip or allow frame rendering
beforeRender?(frame: WrappedCanvas, time: number): { skip?: boolean } | void;
// Transform frame before rendering
transformFrame?(frame: WrappedCanvas): WrappedCanvas;
// After frame is rendered
afterRender?(canvas: HTMLCanvasElement | OffscreenCanvas): void;
// Draw overlays on top of video
onOverlay?: {
zIndex?: number; // Higher = on top (default: 0)
render(
ctx: CanvasRenderingContext2D,
time: number,
dimensions: { width: number; height: number }
): void;
};
}Example: Subtitles Plugin
interface Subtitle {
start: number;
end: number;
text: string;
}
const subtitlesPlugin: MediaFoxPlugin = {
name: 'subtitles',
install(ctx) {
ctx.setPluginState<{ subtitles: Subtitle[]; current: Subtitle | null }>({
subtitles: [],
current: null
});
},
hooks: {
render: {
onOverlay: {
zIndex: 100, // Render on top of other overlays
render(ctx, time, { width, height }) {
const state = this.context.getPluginState<{ subtitles: Subtitle[] }>();
const subtitle = state.subtitles.find(
s => time >= s.start && time <= s.end
);
if (subtitle) {
ctx.font = 'bold 24px sans-serif';
ctx.fillStyle = 'white';
ctx.strokeStyle = 'black';
ctx.lineWidth = 3;
ctx.textAlign = 'center';
const y = height - 60;
ctx.strokeText(subtitle.text, width / 2, y);
ctx.fillText(subtitle.text, width / 2, y);
}
}
}
}
}
};
// Usage
await player.use(subtitlesPlugin);
// Load subtitles later
const plugin = player.getPlugin<typeof subtitlesPlugin>('subtitles');
plugin?.context.setPluginState({
subtitles: [
{ start: 0, end: 5, text: 'Hello, world!' },
{ start: 5, end: 10, text: 'Welcome to MediaFox' }
]
});State Hooks
State hooks allow you to intercept and modify player state changes.
interface StateHooks {
// Intercept state updates (return null to cancel)
beforeStateUpdate?(
update: Partial<PlayerStateData>
): Partial<PlayerStateData> | null | void;
// React to state changes
onStateChange?(state: PlayerStateData, prevState: PlayerStateData): void;
}Example: Volume Limiter Plugin
const volumeLimiterPlugin: MediaFoxPlugin = {
name: 'volume-limiter',
hooks: {
state: {
beforeStateUpdate(update) {
// Limit volume to 80%
if (update.volume !== undefined && update.volume > 0.8) {
return { ...update, volume: 0.8 };
}
return update;
}
}
},
install(ctx) {
ctx.log('Volume limited to 80%');
}
};Event Hooks
Event hooks allow intercepting and modifying player events before they're emitted.
interface EventHooks {
// Intercept events (can cancel or modify)
beforeEvent?<K extends keyof PlayerEventMap>(
event: K,
data: PlayerEventMap[K]
): HookResult<PlayerEventMap[K]> | void;
// Observe events after emission
afterEvent?<K extends keyof PlayerEventMap>(
event: K,
data: PlayerEventMap[K]
): void;
}Example: Event Logger Plugin
const eventLoggerPlugin: MediaFoxPlugin = {
name: 'event-logger',
hooks: {
event: {
afterEvent(event, data) {
console.log(`[MediaFox Event] ${event}:`, data);
}
}
},
install(ctx) {
ctx.log('Event logging enabled');
}
};Audio Hooks
Audio hooks allow you to modify the Web Audio API graph.
interface AudioHooks {
// Modify audio graph (return modified node)
onAudioNode?(
audioContext: AudioContext,
sourceNode: GainNode
): AudioNode | void;
}Example: Audio Visualizer Plugin
const audioVisualizerPlugin: MediaFoxPlugin = {
name: 'audio-visualizer',
hooks: {
audio: {
onAudioNode(audioContext, gainNode) {
// Create analyzer
const analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
// Insert into chain
gainNode.connect(analyser);
// Store for visualization
this.context.setPluginState({ analyser });
return analyser; // Return the new output node
}
},
render: {
onOverlay: {
zIndex: 5,
render(ctx, time, { width, height }) {
const state = this.context.getPluginState<{ analyser: AnalyserNode }>();
if (!state?.analyser) return;
const dataArray = new Uint8Array(state.analyser.frequencyBinCount);
state.analyser.getByteFrequencyData(dataArray);
// Draw visualization
const barWidth = width / dataArray.length;
ctx.fillStyle = 'rgba(100, 200, 255, 0.5)';
for (let i = 0; i < dataArray.length; i++) {
const barHeight = (dataArray[i] / 255) * 50;
ctx.fillRect(
i * barWidth,
height - barHeight,
barWidth - 1,
barHeight
);
}
}
}
}
},
install(ctx) {
ctx.log('Audio visualizer enabled');
}
};Intercepting Operations
Plugins can cancel or modify player operations using the HookResult return type:
interface HookResult<T = void> {
cancel?: boolean; // Cancel the operation
data?: T; // Modified value
}Example: Seek Blocker Plugin
const seekBlockerPlugin: MediaFoxPlugin = {
name: 'seek-blocker',
install(ctx) {
ctx.setPluginState({ maxTime: 60 }); // Allow seeking up to 60s
},
hooks: {
lifecycle: {
beforeSeek(time) {
const state = this.context.getPluginState<{ maxTime: number }>();
if (time > state.maxTime) {
// Block seek past allowed time
this.context.warn(`Seek blocked: ${time}s exceeds limit`);
return { cancel: true };
}
// Or clamp the value
// return { data: Math.min(time, state.maxTime) };
}
}
}
};Overlay zIndex
When multiple plugins render overlays, zIndex controls the order:
- Higher zIndex renders on top
- Same zIndex renders in plugin registration order
- Default zIndex is 0
// Background overlay (behind others)
const backgroundPlugin: MediaFoxPlugin = {
name: 'background',
hooks: {
render: {
onOverlay: {
zIndex: -10,
render(ctx, time, { width, height }) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.fillRect(0, 0, width, height);
}
}
}
},
install() {}
};
// Foreground overlay (on top)
const watermarkPlugin: MediaFoxPlugin = {
name: 'watermark',
hooks: {
render: {
onOverlay: {
zIndex: 100,
render(ctx, time, { width, height }) {
ctx.fillText('Watermark', 10, 30);
}
}
}
},
install() {}
};Plugin Dependencies
Plugins can declare dependencies on other plugins:
const advancedSubtitlesPlugin: MediaFoxPlugin = {
name: 'advanced-subtitles',
dependencies: ['subtitles'], // Requires base subtitles plugin
install(ctx) {
const basePlugin = ctx.getPlugin('subtitles');
// Extend base functionality...
}
};Installing and Uninstalling
// Install a plugin
await player.use(watermarkPlugin);
// Uninstall by name
await player.unuse('watermark');Error Handling
Plugin errors are isolated - they won't crash the player or affect other plugins:
const buggyPlugin: MediaFoxPlugin = {
name: 'buggy',
hooks: {
lifecycle: {
afterPlay() {
throw new Error('Oops!');
// Error is caught and logged, player continues working
}
}
},
install() {}
};Best Practices
- Keep render hooks fast - They run every frame, avoid heavy computation
- Use plugin state - Don't store state in global variables
- Clean up in uninstall - Release external resources
- Log with context - Use
ctx.log()for prefixed logs - Handle errors gracefully - Don't let errors break the player
- Use TypeScript - Full type support for better DX
TypeScript Types
All plugin types are exported for TypeScript users:
import type {
MediaFoxPlugin,
PluginContext,
PluginHooks,
LifecycleHooks,
RenderHooks,
StateHooks,
EventHooks,
AudioHooks,
HookResult,
MaybePromise,
OverlayDimensions,
} from '@mediafox/core';