class VideoControls{
constructor(controlsContainer,videoElement){
if(typeof VideoControls.rate === 'undefined' || VideoControls.rate === 0){
VideoControls.rate = 1.0;
}
if(typeof VideoControls.rateRange === 'undefined'){
VideoControls.rateRange = VideoRateRange();
}
this.container = controlsContainer;
this.video = videoElement;
this.lastVolume = 1.0;
this.keydownHandler =(e)=> this.keyDown(e);
this.mousemoveHandler = null;
this.mouseupHandler = null;
this.createControls();
this.attachEventListeners();
document.addEventListener('keydown',this.keydownHandler);
this.setRate(VideoControls.rate);
}
cleanup(){
document.removeEventListener('keydown',this.keydownHandler);
if(this.mousemoveHandler)document.removeEventListener('mousemove',this.mousemoveHandler);
if(this.mouseupHandler)document.removeEventListener('mouseup',this.mouseupHandler);
this.video = null;
}
createControls(){
let rateOptions = '';
for(let rate = VideoControls.rateRange.max;rate >= VideoControls.rateRange.min;rate *= 0.5){
rateOptions += '<div class="video-rate-option" data-rate="'+rate+'">'+rate+'x</div>';
}
this.container.innerHTML = `
						<button class="video-controls-button play-pause">
								<svg class="video-controls-icon play-icon" viewBox="0 0 24 24">
										<path d="M8 5v14l11-7z"/>
								</svg>
								<svg class="video-controls-icon pause-icon" viewBox="0 0 24 24" style="display: none;">
										<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
								</svg>
						</button>
						<button class="video-controls-button skip-backward">
								<svg class="video-controls-icon" viewBox="0 0 24 24">
										<path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>
								</svg>
						</button>
						<button class="video-controls-button skip-forward">
								<svg class="video-controls-icon" viewBox="0 0 24 24">
										<path d="M12 5V1l5 5-5 5V7c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6h2c0 4.42-3.58 8-8 8s-8-3.58-8-8 3.58-8 8-8z"/>
								</svg>
						</button>
						<div class="video-progress-container">
								<div class="video-progress-bar"></div>
						</div>
						<span class="video-time-display">0:00 / 0:00</span>
						<div class="video-volume-container">
								<button class="video-controls-button volume-toggle">
										<svg class="video-controls-icon volume-icon" viewBox="0 0 24 24">
												<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
										</svg>
										<svg class="video-controls-icon muted-icon" viewBox="0 0 24 24" style="display: none;">
												<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
										</svg>
								</button>
								<div class="video-volume-slider-container">
										<input type="range" class="video-volume-slider" min="0" max="1" step="0.01" value="1">
								</div>
						</div>
						<div class="video-rate-controls">
								<button class="video-controls-button video-rate-toggle">1x</button>
								<div class="video-rate-options">
										${rateOptions}
								</div>
						</div>`;
}
attachEventListeners(){
const playPauseBtn = this.container.querySelector('.play-pause');
const skipBackward = this.container.querySelector('.skip-backward');
const skipForward = this.container.querySelector('.skip-forward');
const progress = this.container.querySelector('.video-progress-container');
const progressBar = this.container.querySelector('.video-progress-bar');
const volumeToggle = this.container.querySelector('.volume-toggle');
const volumeSlider = this.container.querySelector('.video-volume-slider');
const timeDisplay = this.container.querySelector('.video-time-display');
const rateOptions = this.container.querySelectorAll('.video-rate-option');
const rateToggle = this.container.querySelector('.video-rate-toggle');
const togglePlay =()=>{
if(!this.video)return;
this.setRate(this.video.paused ? VideoControls.rate : 0);
};
playPauseBtn.addEventListener('click',togglePlay);
this.video.addEventListener('click',togglePlay);
skipBackward.addEventListener('click',()=>{if(this.video)this.video.currentTime -= 10;});
skipForward.addEventListener('click',()=>{if(this.video)this.video.currentTime += 10;});
const updateProgress =()=>{
if(!this.video)return;
const percent =(this.video.currentTime / this.video.duration)* 100;
progressBar.style.width = percent + '%';
timeDisplay.textContent = isNaN(this.video.duration)? 'Loading…' : `${this.formatTime(this.video.currentTime)} / ${this.formatTime(this.video.duration)}`;
this.updatePlayPauseUI();
};
this.video.addEventListener('timeupdate',updateProgress);
this.video.addEventListener('progress',updateProgress);
this.video.addEventListener('seeking',updateProgress);
this.video.addEventListener('seeked',updateProgress);
this.video.addEventListener('loadedmetadata',updateProgress);
progress.addEventListener('click',(e)=>{
if(!this.video)return;
const rect = progress.getBoundingClientRect();
const pos =(e.clientX - rect.left)/ rect.width;
this.video.currentTime = pos * this.video.duration;
});
let isDragging = false;
let wasPlaying = false;
progress.addEventListener('mousedown',()=>{
isDragging = true;
wasPlaying = this.video && !this.video.paused;
if(wasPlaying)this.video.pause();
});
this.mousemoveHandler =(e)=>{
if(isDragging){
const rect = progress.getBoundingClientRect();
const pos = Math.max(0,Math.min(1,(e.clientX - rect.left)/ rect.width));
if(this.video)this.video.currentTime = pos * this.video.duration;
}
};
this.mouseupHandler =()=>{
if(isDragging && wasPlaying)this.setRate(VideoControls.rate);
isDragging = false;
};
document.addEventListener('mousemove',this.mousemoveHandler);
document.addEventListener('mouseup',this.mouseupHandler);
const updateVolumeUI =()=>{
if(!this.video)return;
const volumeIcon = this.container.querySelector('.volume-icon');
const mutedIcon = this.container.querySelector('.muted-icon');
if(this.video.muted || this.video.volume === 0){
volumeIcon.style.display = 'none';
mutedIcon.style.display = 'block';
}else{
volumeIcon.style.display = 'block';
mutedIcon.style.display = 'none';
}
volumeSlider.value = this.video.volume;
};
volumeSlider.addEventListener('input',()=>{
if(!this.video)return;
this.video.volume = volumeSlider.value;
this.video.muted = volumeSlider.value ? false : true;
this.lastVolume = this.video.volume;
updateVolumeUI();
});
volumeToggle.addEventListener('click',()=>{
if(!this.video)return;
if(this.video.volume > 0){
this.lastVolume = this.video.volume;
this.video.volume = 0;
this.video.muted = true;
}else{
this.video.volume = this.lastVolume || 1;
this.video.muted = false;
}
updateVolumeUI();
});
rateOptions.forEach(option =>{
option.addEventListener('click',()=>{
this.setRate(parseFloat(option.dataset.rate));
});
});
this.video.volume = 0;
updateVolumeUI();
}
formatTime(totalSeconds){
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600)/ 60);
const seconds = Math.floor(totalSeconds % 60);
const minutesSeconds = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
return hours > 0 ? `${hours}:${minutesSeconds}` : minutesSeconds;
}
keyDown(e){
if(!this.video)return;
let oldRate = this.video.paused ? 0 : VideoControls.rate;
let rate = oldRate;
let handled = true;
switch(e.code){
case 'KeyJ':
if(rate > 1)rate *= 0.5;
else if(rate == 1)rate = 0;
break;
case 'KeyK':
case 'Space':
rate = this.video.paused ? VideoControls.rate : 0;
break;
case 'KeyL':
if(this.video.paused)rate = VideoControls.rate;
else if(rate < VideoControls.rateRange.max)rate *= 2;
break;
case 'ArrowLeft':
this.video.currentTime -= e.shiftKey ? 10 : 1;
break;
case 'ArrowRight':
this.video.currentTime += e.shiftKey ? 10 : 1;
break;
default:
handled = false;
}
if(rate != oldRate)this.setRate(rate);
if(handled)e.preventDefault();
}
setRate(rate){
if(!this.video)return;
if(rate){
if(rate < VideoControls.rateRange.min)rate = VideoControls.rateRange.min;
if(rate > VideoControls.rateRange.max)rate = VideoControls.rateRange.max;
try{
this.video.playbackRate = rate;
}catch(error){}
VideoControls.rate = rate;
this.video.play().catch(e =>{});
this.container.querySelector('.video-rate-toggle').textContent = this.video.playbackRate + 'x';
}
else{
this.video.pause();
}
this.updatePlayPauseUI();
}
updatePlayPauseUI(){
if(!this.video || this.video.paused){
this.container.querySelector('.play-icon').style.display = 'block';
this.container.querySelector('.pause-icon').style.display = 'none';
}else{
this.container.querySelector('.play-icon').style.display = 'none';
this.container.querySelector('.pause-icon').style.display = 'block';
}
}
}
function VideoRateRange(){
let tempVideo = document.createElement('video');
let min = 0.5,max = 2;
let isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
for(let i = min;i >= 0.25;i *= 0.5){
try{
tempVideo.playbackRate = i;
if(tempVideo.playbackRate === i)min = i;
else break;
}catch(error){}
}
if(!isSafari){
for(let i = max;i <= 16;i *= 2){
try{
tempVideo.playbackRate = i;
if(tempVideo.playbackRate === i)max = i;
else break;
}catch(error){}
}
}
return{min: min,max: max};
}
