Skip to content

Vanilla JavaScript Guide

This guide demonstrates how to use MediaFox with vanilla JavaScript/TypeScript without any framework dependencies.

Basic Setup

MediaFox works perfectly with vanilla JavaScript. Here's a simple setup:

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MediaFox Vanilla Player</title>
    <style>
        .player-container {
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }

        canvas {
            width: 100%;
            height: auto;
            background: black;
            border-radius: 8px;
        }

        .controls {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-top: 10px;
            padding: 15px;
            background: #f5f5f5;
            border-radius: 8px;
        }

        .seek-bar {
            flex: 1;
            height: 6px;
        }

        button {
            padding: 8px 16px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        button:hover {
            background: #0056b3;
        }

        button:disabled {
            background: #ccc;
            cursor: not-allowed;
        }
    </style>
</head>
<body>
    <div class="player-container">
        <h1>MediaFox Media Player</h1>
        <canvas id="video-canvas"></canvas>

        <div class="controls">
            <button id="play-btn">Play</button>
            <input type="range" id="seek-bar" class="seek-bar" min="0" max="100" value="0">
            <span id="time-display">0:00 / 0:00</span>
            <input type="range" id="volume-bar" min="0" max="1" step="0.1" value="1">
            <button id="mute-btn">Mute</button>
            <button id="fullscreen-btn">Fullscreen</button>
        </div>

        <div class="file-input">
            <input type="file" id="file-input" accept="video/*">
            <button id="load-url-btn">Load URL</button>
            <input type="url" id="url-input" placeholder="Enter video URL">
        </div>
    </div>

    <script type="module" src="./player.js"></script>
</body>
</html>

JavaScript Implementation

javascript
// player.js
import { MediaFox } from '@mediafox/core';

class VanillaVideoPlayer {
    constructor() {
        this.player = null;
        this.initializeElements();
        this.setupEventListeners();
    }

    initializeElements() {
        this.canvas = document.getElementById('video-canvas');
        this.playBtn = document.getElementById('play-btn');
        this.seekBar = document.getElementById('seek-bar');
        this.timeDisplay = document.getElementById('time-display');
        this.volumeBar = document.getElementById('volume-bar');
        this.muteBtn = document.getElementById('mute-btn');
        this.fullscreenBtn = document.getElementById('fullscreen-btn');
        this.fileInput = document.getElementById('file-input');
        this.loadUrlBtn = document.getElementById('load-url-btn');
        this.urlInput = document.getElementById('url-input');
    }

    async initializePlayer() {
        if (this.player) {
            this.player.destroy();
        }

        this.player = new MediaFox({
            canvas: this.canvas
        });

        // Subscribe to state changes
        this.player.store.subscribe(this.handleStateChange.bind(this));

        // Setup player event listeners
        this.setupPlayerEvents();
    }

    setupEventListeners() {
        this.playBtn.addEventListener('click', this.togglePlay.bind(this));
        this.seekBar.addEventListener('input', this.handleSeek.bind(this));
        this.volumeBar.addEventListener('input', this.handleVolumeChange.bind(this));
        this.muteBtn.addEventListener('click', this.toggleMute.bind(this));
        this.fullscreenBtn.addEventListener('click', this.toggleFullscreen.bind(this));
        this.fileInput.addEventListener('change', this.handleFileSelect.bind(this));
        this.loadUrlBtn.addEventListener('click', this.loadUrl.bind(this));

        // Keyboard shortcuts
        document.addEventListener('keydown', this.handleKeyboard.bind(this));

        // Initialize player when DOM is ready
        this.initializePlayer();
    }

    setupPlayerEvents() {
        if (!this.player) return;

        this.player.on('play', () => {
            console.log('Playback started');
        });

        this.player.on('pause', () => {
            console.log('Playback paused');
        });

        this.player.on('ended', () => {
            console.log('Playback ended');
            this.playBtn.textContent = 'Replay';
        });

        this.player.on('error', (error) => {
            console.error('Player error:', error);
            alert(`Error: ${error.message}`);
        });

        this.player.on('loadstart', () => {
            console.log('Loading started');
            this.playBtn.disabled = true;
        });

        this.player.on('loadeddata', () => {
            console.log('Data loaded');
            this.playBtn.disabled = false;
        });

        this.player.on('seeking', () => {
            console.log('Seeking...');
        });

        this.player.on('seeked', () => {
            console.log('Seek completed');
        });
    }

    handleStateChange(state) {
        if (!state) return;

        // Update play button
        this.playBtn.textContent = state.playing ? 'Pause' : 'Play';

        // Update seek bar
        if (state.duration > 0) {
            const progress = (state.currentTime / state.duration) * 100;
            this.seekBar.value = progress;
        }

        // Update time display
        this.timeDisplay.textContent = this.formatTime(state.currentTime, state.duration);

        // Update volume controls
        this.volumeBar.value = state.volume;
        this.muteBtn.textContent = state.muted ? 'Unmute' : 'Mute';
    }

    formatTime(current, total) {
        const currentMins = Math.floor(current / 60);
        const currentSecs = Math.floor(current % 60);
        const totalMins = Math.floor(total / 60);
        const totalSecs = Math.floor(total % 60);

        const currentStr = `${currentMins}:${currentSecs.toString().padStart(2, '0')}`;
        const totalStr = `${totalMins}:${totalSecs.toString().padStart(2, '0')}`;

        return `${currentStr} / ${totalStr}`;
    }

    async togglePlay() {
        if (!this.player) return;

        try {
            if (this.player.playing) {
                await this.player.pause();
            } else {
                await this.player.play();
            }
        } catch (error) {
            console.error('Error toggling play:', error);
        }
    }

    handleSeek(event) {
        if (!this.player || this.player.duration === 0) return;

        const percentage = parseFloat(event.target.value);
        const time = (percentage / 100) * this.player.duration;
        this.player.seek(time);
    }

    handleVolumeChange(event) {
        if (!this.player) return;

        const volume = parseFloat(event.target.value);
        this.player.volume = volume;
    }

    toggleMute() {
        if (!this.player) return;

        this.player.muted = !this.player.muted;
    }

    async toggleFullscreen() {
        if (!document.fullscreenElement) {
            try {
                await this.canvas.requestFullscreen();
                this.fullscreenBtn.textContent = 'Exit Fullscreen';
            } catch (error) {
                console.error('Error entering fullscreen:', error);
            }
        } else {
            try {
                await document.exitFullscreen();
                this.fullscreenBtn.textContent = 'Fullscreen';
            } catch (error) {
                console.error('Error exiting fullscreen:', error);
            }
        }
    }

    async handleFileSelect(event) {
        const file = event.target.files[0];
        if (!file) return;

        try {
            await this.player.load(file);
        } catch (error) {
            console.error('Error loading file:', error);
            alert(`Error loading file: ${error.message}`);
        }
    }

    async loadUrl() {
        const url = this.urlInput.value.trim();
        if (!url) return;

        try {
            await this.player.load(url);
        } catch (error) {
            console.error('Error loading URL:', error);
            alert(`Error loading URL: ${error.message}`);
        }
    }

    handleKeyboard(event) {
        if (!this.player) return;

        // Ignore if user is typing in an input
        if (event.target.tagName === 'INPUT') return;

        switch (event.code) {
            case 'Space':
                event.preventDefault();
                this.togglePlay();
                break;
            case 'ArrowLeft':
                event.preventDefault();
                this.player.seek(Math.max(0, this.player.currentTime - 10));
                break;
            case 'ArrowRight':
                event.preventDefault();
                this.player.seek(Math.min(this.player.duration, this.player.currentTime + 10));
                break;
            case 'ArrowUp':
                event.preventDefault();
                this.player.volume = Math.min(1, this.player.volume + 0.1);
                break;
            case 'ArrowDown':
                event.preventDefault();
                this.player.volume = Math.max(0, this.player.volume - 0.1);
                break;
            case 'KeyM':
                event.preventDefault();
                this.toggleMute();
                break;
            case 'KeyF':
                event.preventDefault();
                this.toggleFullscreen();
                break;
        }
    }

    destroy() {
        if (this.player) {
            this.player.destroy();
            this.player = null;
        }
    }
}

// Initialize the player
const player = new VanillaVideoPlayer();

// Cleanup on page unload
window.addEventListener('beforeunload', () => {
    player.destroy();
});

TypeScript Implementation

For TypeScript projects, here's a more type-safe version:

typescript
// player.ts
import { MediaFox, type PlayerState, type PlayerEvents } from '@mediafox/core';

interface PlayerElements {
    canvas: HTMLCanvasElement;
    playBtn: HTMLButtonElement;
    seekBar: HTMLInputElement;
    timeDisplay: HTMLSpanElement;
    volumeBar: HTMLInputElement;
    muteBtn: HTMLButtonElement;
    fullscreenBtn: HTMLButtonElement;
    fileInput: HTMLInputElement;
    loadUrlBtn: HTMLButtonElement;
    urlInput: HTMLInputElement;
}

class TypedVideoPlayer {
    private player: MediaFox | null = null;
    private elements: PlayerElements;

    constructor() {
        this.elements = this.initializeElements();
        this.setupEventListeners();
        this.initializePlayer();
    }

    private initializeElements(): PlayerElements {
        const getElement = <T extends HTMLElement>(id: string): T => {
            const element = document.getElementById(id) as T;
            if (!element) {
                throw new Error(`Element with id "${id}" not found`);
            }
            return element;
        };

        return {
            canvas: getElement<HTMLCanvasElement>('video-canvas'),
            playBtn: getElement<HTMLButtonElement>('play-btn'),
            seekBar: getElement<HTMLInputElement>('seek-bar'),
            timeDisplay: getElement<HTMLSpanElement>('time-display'),
            volumeBar: getElement<HTMLInputElement>('volume-bar'),
            muteBtn: getElement<HTMLButtonElement>('mute-btn'),
            fullscreenBtn: getElement<HTMLButtonElement>('fullscreen-btn'),
            fileInput: getElement<HTMLInputElement>('file-input'),
            loadUrlBtn: getElement<HTMLButtonElement>('load-url-btn'),
            urlInput: getElement<HTMLInputElement>('url-input')
        };
    }

    private async initializePlayer(): Promise<void> {
        if (this.player) {
            this.player.destroy();
        }

        this.player = new MediaFox({
            canvas: this.elements.canvas
        });

        this.player.store.subscribe(this.handleStateChange.bind(this));
        this.setupPlayerEvents();
    }

    private setupEventListeners(): void {
        this.elements.playBtn.addEventListener('click', this.togglePlay.bind(this));
        this.elements.seekBar.addEventListener('input', this.handleSeek.bind(this));
        this.elements.volumeBar.addEventListener('input', this.handleVolumeChange.bind(this));
        this.elements.muteBtn.addEventListener('click', this.toggleMute.bind(this));
        this.elements.fullscreenBtn.addEventListener('click', this.toggleFullscreen.bind(this));
        this.elements.fileInput.addEventListener('change', this.handleFileSelect.bind(this));
        this.elements.loadUrlBtn.addEventListener('click', this.loadUrl.bind(this));

        document.addEventListener('keydown', this.handleKeyboard.bind(this));
    }

    private setupPlayerEvents(): void {
        if (!this.player) return;

        const eventHandlers: Partial<Record<keyof PlayerEvents, Function>> = {
            play: () => console.log('Playback started'),
            pause: () => console.log('Playback paused'),
            ended: () => {
                console.log('Playback ended');
                this.elements.playBtn.textContent = 'Replay';
            },
            error: (error: Error) => {
                console.error('Player error:', error);
                this.showError(`Error: ${error.message}`);
            },
            loadstart: () => {
                console.log('Loading started');
                this.elements.playBtn.disabled = true;
            },
            loadeddata: () => {
                console.log('Data loaded');
                this.elements.playBtn.disabled = false;
            }
        };

        Object.entries(eventHandlers).forEach(([event, handler]) => {
            this.player!.on(event as keyof PlayerEvents, handler);
        });
    }

    private handleStateChange(state: PlayerState): void {
        this.elements.playBtn.textContent = state.playing ? 'Pause' : 'Play';

        if (state.duration > 0) {
            const progress = (state.currentTime / state.duration) * 100;
            this.elements.seekBar.value = progress.toString();
        }

        this.elements.timeDisplay.textContent = this.formatTime(state.currentTime, state.duration);
        this.elements.volumeBar.value = state.volume.toString();
        this.elements.muteBtn.textContent = state.muted ? 'Unmute' : 'Mute';
    }

    private formatTime(current: number, total: number): string {
        const format = (time: number): string => {
            const mins = Math.floor(time / 60);
            const secs = Math.floor(time % 60);
            return `${mins}:${secs.toString().padStart(2, '0')}`;
        };

        return `${format(current)} / ${format(total)}`;
    }

    private async togglePlay(): Promise<void> {
        if (!this.player) return;

        try {
            if (this.player.playing) {
                await this.player.pause();
            } else {
                await this.player.play();
            }
        } catch (error) {
            this.handleError('Error toggling play', error);
        }
    }

    private handleSeek(event: Event): void {
        if (!this.player || this.player.duration === 0) return;

        const target = event.target as HTMLInputElement;
        const percentage = parseFloat(target.value);
        const time = (percentage / 100) * this.player.duration;
        this.player.seek(time);
    }

    private handleVolumeChange(event: Event): void {
        if (!this.player) return;

        const target = event.target as HTMLInputElement;
        const volume = parseFloat(target.value);
        this.player.volume = volume;
    }

    private toggleMute(): void {
        if (!this.player) return;
        this.player.muted = !this.player.muted;
    }

    private async toggleFullscreen(): Promise<void> {
        try {
            if (!document.fullscreenElement) {
                await this.elements.canvas.requestFullscreen();
                this.elements.fullscreenBtn.textContent = 'Exit Fullscreen';
            } else {
                await document.exitFullscreen();
                this.elements.fullscreenBtn.textContent = 'Fullscreen';
            }
        } catch (error) {
            this.handleError('Fullscreen error', error);
        }
    }

    private async handleFileSelect(event: Event): Promise<void> {
        const target = event.target as HTMLInputElement;
        const file = target.files?.[0];
        if (!file) return;

        try {
            await this.player?.load(file);
        } catch (error) {
            this.handleError('Error loading file', error);
        }
    }

    private async loadUrl(): Promise<void> {
        const url = this.elements.urlInput.value.trim();
        if (!url) return;

        try {
            await this.player?.load(url);
        } catch (error) {
            this.handleError('Error loading URL', error);
        }
    }

    private handleKeyboard(event: KeyboardEvent): void {
        if (!this.player || event.target instanceof HTMLInputElement) return;

        const keyActions: Record<string, () => void> = {
            Space: () => {
                event.preventDefault();
                this.togglePlay();
            },
            ArrowLeft: () => {
                event.preventDefault();
                this.player!.seek(Math.max(0, this.player!.currentTime - 10));
            },
            ArrowRight: () => {
                event.preventDefault();
                this.player!.seek(Math.min(this.player!.duration, this.player!.currentTime + 10));
            },
            ArrowUp: () => {
                event.preventDefault();
                this.player!.volume = Math.min(1, this.player!.volume + 0.1);
            },
            ArrowDown: () => {
                event.preventDefault();
                this.player!.volume = Math.max(0, this.player!.volume - 0.1);
            },
            KeyM: () => {
                event.preventDefault();
                this.toggleMute();
            },
            KeyF: () => {
                event.preventDefault();
                this.toggleFullscreen();
            }
        };

        const action = keyActions[event.code];
        if (action) {
            action();
        }
    }

    private handleError(message: string, error: unknown): void {
        console.error(message, error);
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.showError(`${message}: ${errorMessage}`);
    }

    private showError(message: string): void {
        alert(message); // In production, use a better error display method
    }

    public destroy(): void {
        if (this.player) {
            this.player.destroy();
            this.player = null;
        }
    }
}

// Initialize the player
const player = new TypedVideoPlayer();

// Cleanup on page unload
window.addEventListener('beforeunload', () => {
    player.destroy();
});

Advanced Features

Custom Controls

javascript
class AdvancedControls {
    constructor(player) {
        this.player = player;
        this.createCustomControls();
    }

    createCustomControls() {
        const container = document.createElement('div');
        container.className = 'advanced-controls';

        // Playback speed control
        const speedControl = this.createSpeedControl();
        container.appendChild(speedControl);

        // Quality selector
        const qualityControl = this.createQualityControl();
        container.appendChild(qualityControl);

        // Track selector
        const trackControl = this.createTrackControl();
        container.appendChild(trackControl);

        document.body.appendChild(container);
    }

    createSpeedControl() {
        const container = document.createElement('div');
        container.innerHTML = `
            <label>Speed:</label>
            <select id="speed-select">
                <option value="0.5">0.5x</option>
                <option value="1" selected>1x</option>
                <option value="1.25">1.25x</option>
                <option value="1.5">1.5x</option>
                <option value="2">2x</option>
            </select>
        `;

        const select = container.querySelector('#speed-select');
        select.addEventListener('change', (e) => {
            this.player.playbackRate = parseFloat(e.target.value);
        });

        return container;
    }

    createQualityControl() {
        const container = document.createElement('div');
        container.innerHTML = `
            <label>Quality:</label>
            <select id="quality-select">
                <option value="auto">Auto</option>
            </select>
        `;

        // Populate with available qualities when tracks change
        this.player.on('tracksChanged', (tracks) => {
            const select = container.querySelector('#quality-select');
            select.innerHTML = '<option value="auto">Auto</option>';

            tracks.video.forEach((track, index) => {
                const option = document.createElement('option');
                option.value = index;
                option.textContent = `${track.width}x${track.height}`;
                select.appendChild(option);
            });
        });

        return container;
    }

    createTrackControl() {
        const container = document.createElement('div');
        container.innerHTML = `
            <label>Audio Track:</label>
            <select id="audio-track-select">
                <option value="0">Default</option>
            </select>
        `;

        this.player.on('tracksChanged', (tracks) => {
            const select = container.querySelector('#audio-track-select');
            select.innerHTML = '';

            tracks.audio.forEach((track, index) => {
                const option = document.createElement('option');
                option.value = index;
                option.textContent = track.language || `Track ${index + 1}`;
                select.appendChild(option);
            });
        });

        return container;
    }
}

Progress Tracking

javascript
class ProgressTracker {
    constructor(player) {
        this.player = player;
        this.watchedSegments = [];
        this.setupTracking();
    }

    setupTracking() {
        this.player.store.subscribe((state) => {
            this.trackProgress(state.currentTime, state.duration);
        });
    }

    trackProgress(currentTime, duration) {
        if (duration === 0) return;

        const percentage = (currentTime / duration) * 100;
        const segment = Math.floor(percentage / 5) * 5; // Track in 5% segments

        if (!this.watchedSegments.includes(segment)) {
            this.watchedSegments.push(segment);
            this.updateProgressDisplay();
        }
    }

    updateProgressDisplay() {
        const progress = (this.watchedSegments.length / 20) * 100; // 20 segments = 100%
        console.log(`Video ${progress.toFixed(1)}% watched`);

        // Update visual progress indicator
        const indicator = document.getElementById('progress-indicator');
        if (indicator) {
            indicator.style.width = `${progress}%`;
        }
    }

    getWatchedPercentage() {
        return (this.watchedSegments.length / 20) * 100;
    }
}

This guide provides a solid foundation for using MediaFox in vanilla JavaScript environments, with examples ranging from basic setup to advanced features and TypeScript integration.

MIT License