← 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');
}
});