← back to Jill Website

public/js/video.js

280 lines

/**
 * Responsive Video Gallery Component
 * Features: Adaptive loading, connection speed detection, custom controls, mobile optimization
 */

class VideoGallery {
    constructor(galleryElement) {
        this.gallery = galleryElement;
        this.videos = Array.from(this.gallery.querySelectorAll('.video-item'));
        this.connectionSpeed = 'unknown';
        this.currentPlayingVideo = null;

        this.init();
    }

    async init() {
        await this.detectConnectionSpeed();
        this.setupVideoPlayers();
        this.setupIntersectionObserver();
    }

    /**
     * Detect user's connection speed using Network Information API
     * Falls back to test download if not available
     */
    async detectConnectionSpeed() {
        // Try Network Information API first (supported in Chrome, Edge, Opera)
        if ('connection' in navigator) {
            const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;

            if (connection && connection.effectiveType) {
                const effectiveType = connection.effectiveType;

                // Map effective types to our speed categories
                switch(effectiveType) {
                    case 'slow-2g':
                    case '2g':
                        this.connectionSpeed = 'slow';
                        break;
                    case '3g':
                        this.connectionSpeed = 'medium';
                        break;
                    case '4g':
                    case '5g':
                        this.connectionSpeed = 'fast';
                        break;
                    default:
                        this.connectionSpeed = 'medium';
                }

                console.log(`📡 Connection Speed Detected: ${this.connectionSpeed} (${effectiveType})`);
                return;
            }
        }

        // Fallback: Test download speed with small image
        try {
            const testImageSize = 50000; // 50KB test image
            const testImageUrl = 'data:image/png;base64,' + 'A'.repeat(testImageSize);

            const startTime = performance.now();
            await fetch(testImageUrl);
            const endTime = performance.now();

            const durationMs = endTime - startTime;
            const bitsLoaded = testImageSize * 8;
            const speedBps = bitsLoaded / (durationMs / 1000);
            const speedMbps = speedBps / (1024 * 1024);

            // Categorize based on speed
            if (speedMbps < 1) {
                this.connectionSpeed = 'slow';
            } else if (speedMbps < 5) {
                this.connectionSpeed = 'medium';
            } else {
                this.connectionSpeed = 'fast';
            }

            console.log(`📡 Connection Speed Test: ${speedMbps.toFixed(2)} Mbps (${this.connectionSpeed})`);
        } catch (error) {
            console.warn('Connection speed test failed, defaulting to medium:', error);
            this.connectionSpeed = 'medium';
        }
    }

    /**
     * Setup video players with custom controls
     */
    setupVideoPlayers() {
        this.videos.forEach((videoItem, index) => {
            const videoElement = videoItem.querySelector('.video-element');
            const playButton = videoItem.querySelector('.video-play-button');
            const loadingIndicator = videoItem.querySelector('.video-loading-indicator');

            // Setup play button
            playButton.addEventListener('click', (e) => {
                e.stopPropagation();
                this.togglePlay(videoElement, videoItem);
            });

            // Setup video click to play/pause
            videoElement.addEventListener('click', () => {
                this.togglePlay(videoElement, videoItem);
            });

            // Keyboard accessibility
            videoItem.addEventListener('keypress', (e) => {
                if (e.key === 'Enter' || e.key === ' ') {
                    e.preventDefault();
                    this.togglePlay(videoElement, videoItem);
                }
            });

            // Video event listeners
            videoElement.addEventListener('play', () => {
                playButton.innerHTML = '<i class="fas fa-pause"></i>';
                videoItem.classList.add('playing');

                // Pause other videos
                if (this.currentPlayingVideo && this.currentPlayingVideo !== videoElement) {
                    this.currentPlayingVideo.pause();
                }
                this.currentPlayingVideo = videoElement;
            });

            videoElement.addEventListener('pause', () => {
                playButton.innerHTML = '<i class="fas fa-play"></i>';
                videoItem.classList.remove('playing');
            });

            videoElement.addEventListener('ended', () => {
                playButton.innerHTML = '<i class="fas fa-play"></i>';
                videoItem.classList.remove('playing');
                this.currentPlayingVideo = null;
            });

            videoElement.addEventListener('loadstart', () => {
                loadingIndicator.style.display = 'flex';
            });

            videoElement.addEventListener('loadeddata', () => {
                loadingIndicator.style.display = 'none';
                videoItem.classList.add('loaded');
            });

            videoElement.addEventListener('error', () => {
                loadingIndicator.style.display = 'none';
                videoItem.classList.add('error');
                console.error('Failed to load video:', videoElement.src);
            });

            // Add native controls for accessibility (hidden by CSS)
            videoElement.setAttribute('controls', 'true');
        });
    }

    /**
     * Setup Intersection Observer to load videos when in viewport
     */
    setupIntersectionObserver() {
        if (!('IntersectionObserver' in window)) {
            // Fallback: load all videos immediately
            this.videos.forEach(item => this.loadVideo(item));
            return;
        }

        const videoObserver = new IntersectionObserver((entries, observer) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    this.loadVideo(entry.target);
                    observer.unobserve(entry.target);
                }
            });
        }, {
            rootMargin: '100px 0px', // Start loading 100px before entering viewport
            threshold: 0.01
        });

        this.videos.forEach(item => {
            videoObserver.observe(item);
        });
    }

    /**
     * Load video sources based on connection speed
     */
    loadVideo(videoItem) {
        const videoElement = videoItem.querySelector('.video-element');
        const mp4Src = videoElement.getAttribute('data-mp4');
        const webmSrc = videoElement.getAttribute('data-webm');

        if (!mp4Src) return;

        videoItem.classList.add('loading');

        // Add video sources based on browser support and connection speed
        // Prefer WebM for better compression on fast connections, MP4 for compatibility
        if (this.connectionSpeed === 'fast' && webmSrc && this.supportsWebM()) {
            this.addSource(videoElement, webmSrc, 'video/webm');
            this.addSource(videoElement, mp4Src, 'video/mp4'); // Fallback
        } else {
            this.addSource(videoElement, mp4Src, 'video/mp4');
            if (webmSrc && this.supportsWebM()) {
                this.addSource(videoElement, webmSrc, 'video/webm'); // Fallback
            }
        }

        // Load the video
        videoElement.load();
    }

    /**
     * Add source element to video
     */
    addSource(videoElement, src, type) {
        const source = document.createElement('source');
        source.src = src;
        source.type = type;
        videoElement.appendChild(source);
    }

    /**
     * Check if browser supports WebM
     */
    supportsWebM() {
        const video = document.createElement('video');
        return video.canPlayType('video/webm; codecs="vp8, vorbis"') !== '';
    }

    /**
     * Toggle play/pause for video
     */
    togglePlay(videoElement, videoItem) {
        if (videoElement.paused) {
            // Ensure video sources are loaded
            if (!videoItem.classList.contains('loaded') && !videoItem.classList.contains('loading')) {
                this.loadVideo(videoItem);
            }

            videoElement.play().catch(error => {
                console.error('Error playing video:', error);
                videoItem.classList.add('error');
            });
        } else {
            videoElement.pause();
        }
    }

    /**
     * Get quality recommendation based on connection speed
     */
    getQualityRecommendation() {
        switch(this.connectionSpeed) {
            case 'slow':
                return '360p or lower';
            case 'medium':
                return '480p-720p';
            case 'fast':
                return '720p-1080p';
            default:
                return '480p-720p';
        }
    }
}

/**
 * Initialize all video galleries on page load
 */
document.addEventListener('DOMContentLoaded', () => {
    const galleries = document.querySelectorAll('.video-gallery');

    galleries.forEach(gallery => {
        new VideoGallery(gallery);
    });

    if (galleries.length > 0) {
        console.log('🎬 Video Gallery Component Initialized');
    }
});