morethanwords
5 years ago
21 changed files with 996 additions and 370 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,375 @@
@@ -0,0 +1,375 @@
|
||||
export class MediaProgressLine { |
||||
public container: HTMLDivElement; |
||||
private filled: HTMLDivElement; |
||||
private seek: HTMLInputElement; |
||||
|
||||
private duration = 0; |
||||
|
||||
constructor(private media: HTMLAudioElement | HTMLVideoElement) { |
||||
this.container = document.createElement('div'); |
||||
this.container.classList.add('media-progress'); |
||||
|
||||
this.filled = document.createElement('div'); |
||||
this.filled.classList.add('media-progress__filled'); |
||||
|
||||
let seek = this.seek = document.createElement('input'); |
||||
seek.classList.add('media-progress__seek'); |
||||
seek.value = '0'; |
||||
seek.setAttribute('min', '0'); |
||||
seek.setAttribute('max', '0'); |
||||
seek.type = 'range'; |
||||
seek.step = '0.1'; |
||||
|
||||
this.setSeekMax(); |
||||
this.setListeners(); |
||||
|
||||
this.container.append(this.filled, seek); |
||||
} |
||||
|
||||
private setSeekMax() { |
||||
let seek = this.seek; |
||||
this.duration = this.media.duration; |
||||
if(this.duration > 0) { |
||||
seek.setAttribute('max', '' + this.duration * 1000); |
||||
} else { |
||||
this.media.addEventListener('loadeddata', () => { |
||||
this.duration = this.media.duration; |
||||
seek.setAttribute('max', '' + this.duration * 1000); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
private setProgress() { |
||||
let currentTime = this.media.currentTime; |
||||
|
||||
let scaleX = (currentTime / this.duration); |
||||
this.filled.style.transform = 'scaleX(' + scaleX + ')'; |
||||
this.seek.value = '' + currentTime * 1000; |
||||
} |
||||
|
||||
private setListeners() { |
||||
let mousedown = false; |
||||
let stopAndScrubTimeout = 0; |
||||
|
||||
this.media.addEventListener('ended', () => { |
||||
this.setProgress(); |
||||
}); |
||||
|
||||
this.media.addEventListener('play', () => { |
||||
let r = () => { |
||||
this.setProgress(); |
||||
!this.media.paused && window.requestAnimationFrame(r); |
||||
}; |
||||
|
||||
window.requestAnimationFrame(r); |
||||
}); |
||||
|
||||
this.container.addEventListener('mousemove', (e) => { |
||||
mousedown && this.scrub(e); |
||||
}); |
||||
|
||||
this.container.addEventListener('mousedown', (e) => { |
||||
this.scrub(e); |
||||
//Таймер для того, чтобы стопать видео, если зажал мышку и не отпустил клик
|
||||
stopAndScrubTimeout = setTimeout(() => { |
||||
!this.media.paused && this.media.pause(); |
||||
stopAndScrubTimeout = 0; |
||||
}, 150); |
||||
|
||||
mousedown = true; |
||||
}); |
||||
|
||||
this.container.addEventListener('mouseup', () => { |
||||
if(stopAndScrubTimeout) { |
||||
clearTimeout(stopAndScrubTimeout); |
||||
} |
||||
|
||||
this.media.paused && this.media.play(); |
||||
mousedown = false; |
||||
}); |
||||
} |
||||
|
||||
private scrub(e: MouseEvent) { |
||||
let scrubTime = e.offsetX / this.container.offsetWidth * this.duration; |
||||
this.media.currentTime = scrubTime; |
||||
let scaleX = scrubTime / this.duration; |
||||
|
||||
if(scaleX > 1) scaleX = 1; |
||||
if(scaleX < 0) scaleX = 0; |
||||
|
||||
this.filled.style.transform = 'scaleX(' + scaleX + ')'; |
||||
} |
||||
} |
||||
|
||||
export default class VideoPlayer { |
||||
public wrapper: HTMLDivElement; |
||||
private skin: string; |
||||
private progress: MediaProgressLine; |
||||
|
||||
constructor(public video: HTMLVideoElement, play = false) { |
||||
this.wrapper = document.createElement('div'); |
||||
this.wrapper.classList.add('ckin__player'); |
||||
|
||||
video.parentNode.insertBefore(this.wrapper, video); |
||||
this.wrapper.appendChild(video); |
||||
|
||||
this.skin = video.dataset.ckin ?? 'default'; |
||||
|
||||
this.stylePlayer(); |
||||
|
||||
if(this.skin == 'default') { |
||||
let controls = this.wrapper.querySelector('.default__controls.ckin__controls') as HTMLDivElement; |
||||
this.progress = new MediaProgressLine(video); |
||||
controls.prepend(this.progress.container); |
||||
} |
||||
|
||||
if(play) { |
||||
(this.wrapper.querySelector('.toggle') as HTMLButtonElement).click(); |
||||
} |
||||
} |
||||
|
||||
private stylePlayer() { |
||||
let player = this.wrapper; |
||||
let video = this.video; |
||||
|
||||
let skin = this.skin; |
||||
player.classList.add(skin); |
||||
|
||||
let html = this.buildControls(); |
||||
player.insertAdjacentHTML('beforeend', html); |
||||
let updateInterval = 0; |
||||
let elapsed = 0; |
||||
let prevTime = 0; |
||||
|
||||
if(skin === 'default') { |
||||
var toggle = player.querySelectorAll('.toggle') as NodeListOf<HTMLElement>; |
||||
var fullScreenButton = player.querySelector('.fullscreen') as HTMLElement; |
||||
var timeElapsed = player.querySelector('#time-elapsed'); |
||||
var timeDuration = player.querySelector('#time-duration') as HTMLElement; |
||||
timeDuration.innerHTML = String(video.duration | 0).toHHMMSS(); |
||||
|
||||
Array.from(toggle).forEach((button) => { |
||||
return button.addEventListener('click', () => { |
||||
this.togglePlay(); |
||||
}); |
||||
}); |
||||
|
||||
video.addEventListener('click', () => { |
||||
this.togglePlay(); |
||||
}); |
||||
|
||||
video.addEventListener('play', () => { |
||||
this.updateButton(toggle); |
||||
}); |
||||
|
||||
video.addEventListener('pause', () => { |
||||
this.updateButton(toggle); |
||||
clearInterval(updateInterval); |
||||
}); |
||||
|
||||
video.addEventListener('dblclick', () => { |
||||
return this.toggleFullScreen(fullScreenButton); |
||||
}) |
||||
|
||||
fullScreenButton.addEventListener('click', (e) => { |
||||
return this.toggleFullScreen(fullScreenButton); |
||||
}); |
||||
|
||||
let b = () => this.onFullScreen(); |
||||
'webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange'.split(' ').forEach(eventName => { |
||||
player.addEventListener(eventName, b, false); |
||||
}); |
||||
} |
||||
|
||||
if(skin === 'circle') { |
||||
let wrapper = document.createElement('div'); |
||||
wrapper.classList.add('circle-time-left'); |
||||
video.parentNode.insertBefore(wrapper, video); |
||||
wrapper.innerHTML = '<div class="circle-time"></div><div class="iconVolume tgico-nosound"></div>'; |
||||
|
||||
var circle = player.querySelector('.progress-ring__circle') as SVGCircleElement; |
||||
var radius = circle.r.baseVal.value; |
||||
var circumference = 2 * Math.PI * radius; |
||||
var timeDuration = player.querySelector('.circle-time') as HTMLElement; |
||||
var iconVolume = player.querySelector('.iconVolume') as HTMLDivElement; |
||||
circle.style.strokeDasharray = circumference + ' ' + circumference; |
||||
circle.style.strokeDashoffset = '' + circumference; |
||||
circle.addEventListener('click', () => { |
||||
this.togglePlay(); |
||||
}); |
||||
|
||||
video.addEventListener('play', () => { |
||||
iconVolume.style.display = 'none'; |
||||
updateInterval = setInterval(() => { |
||||
//elapsed += 0.02; // Increase with timer interval
|
||||
if(video.currentTime != prevTime) { |
||||
elapsed = video.currentTime; // Update if getCurrentTime was changed
|
||||
prevTime = video.currentTime; |
||||
} |
||||
|
||||
let offset = circumference - elapsed / video.duration * circumference; |
||||
circle.style.strokeDashoffset = '' + offset; |
||||
if(video.paused) clearInterval(updateInterval); |
||||
}, 20); |
||||
}); |
||||
|
||||
video.addEventListener('pause', () => { |
||||
iconVolume.style.display = ''; |
||||
}); |
||||
} |
||||
|
||||
if(video.duration > 0) { |
||||
timeDuration.innerHTML = String(Math.round(video.duration)).toHHMMSS(); |
||||
} else { |
||||
video.addEventListener('loadeddata', () => { |
||||
timeDuration.innerHTML = String(Math.round(video.duration)).toHHMMSS(); |
||||
}); |
||||
} |
||||
|
||||
video.addEventListener('timeupdate', () => { |
||||
if(skin == 'default') { |
||||
timeElapsed.innerHTML = String(video.currentTime | 0).toHHMMSS(); |
||||
} |
||||
|
||||
updateInterval = this.handleProgress(timeDuration, circumference, circle, updateInterval); |
||||
}); |
||||
} |
||||
|
||||
public togglePlay(stop?: boolean) { |
||||
if(stop) { |
||||
this.video.pause(); |
||||
this.wrapper.classList.remove('is-playing'); |
||||
return; |
||||
} else if(stop === false) { |
||||
this.video.play(); |
||||
this.wrapper.classList.add('is-playing'); |
||||
return; |
||||
} |
||||
|
||||
this.video[this.video.paused ? 'play' : 'pause'](); |
||||
this.video.paused ? this.wrapper.classList.remove('is-playing') : this.wrapper.classList.add('is-playing'); |
||||
} |
||||
|
||||
private handleProgress(timeDuration: HTMLElement, circumference: number, circle: SVGCircleElement, updateInterval: number) { |
||||
let video = this.video; |
||||
let skin = this.skin; |
||||
|
||||
clearInterval(updateInterval); |
||||
let elapsed = 0; |
||||
let prevTime = 0; |
||||
|
||||
if(skin === 'circle') { |
||||
updateInterval = setInterval(() => { |
||||
if(video.currentTime != prevTime) { |
||||
elapsed = video.currentTime; // Update if getCurrentTime was changed
|
||||
prevTime = video.currentTime; |
||||
} |
||||
let offset = circumference - elapsed / video.duration * circumference; |
||||
circle.style.strokeDashoffset = '' + offset; |
||||
if(video.paused) clearInterval(updateInterval); |
||||
}, 20); |
||||
|
||||
let timeLeft = String((video.duration - video.currentTime) | 0).toHHMMSS(); |
||||
if(timeLeft != '0') timeDuration.innerHTML = timeLeft; |
||||
|
||||
return updateInterval; |
||||
} |
||||
} |
||||
|
||||
private buildControls() { |
||||
let skin = this.skin; |
||||
let html = []; |
||||
if(skin === 'default') { |
||||
html.push('<button class="' + skin + '__button--big toggle tgico-largeplay" title="Toggle Play"></button>'); |
||||
html.push('<div class="' + skin + '__gradient-bottom ckin__controls"></div>'); |
||||
html.push('<div class="' + skin + '__controls ckin__controls">'); |
||||
html.push('<div class="bottom-controls">', |
||||
'<div class="left-controls"><button class="' + skin + '__button toggle tgico-play" title="Toggle Video"></button>', |
||||
'<div class="time">', |
||||
'<time id="time-elapsed">0:00</time>', |
||||
'<span> / </span>', |
||||
'<time id="time-duration">0:00</time>', |
||||
'</div>', |
||||
'</div>', |
||||
'<div class="right-controls"><button class="' + skin + '__button fullscreen tgico-fullscreen" title="Full Screen"></button></div></div>'); |
||||
html.push('</div>'); |
||||
} else if(skin === 'circle') { |
||||
html.push('<svg class="progress-ring" width="200px" height="200px">', |
||||
'<circle class="progress-ring__circle" stroke="white" stroke-opacity="0.3" stroke-width="3.5" cx="100" cy="100" r="93" fill="transparent" transform="rotate(-90, 100, 100)"/>', |
||||
'</svg>'); |
||||
} |
||||
|
||||
return html.join(''); |
||||
} |
||||
|
||||
public updateButton(toggle: NodeListOf<HTMLElement>) { |
||||
let icon = this.video.paused ? 'tgico-play' : 'tgico-pause'; |
||||
Array.from(toggle).forEach((button) => { |
||||
button.classList.remove('tgico-play', 'tgico-pause'); |
||||
button.classList.add(icon); |
||||
}); |
||||
} |
||||
|
||||
public toggleFullScreen(fullScreenButton: HTMLElement) { |
||||
// alternative standard method
|
||||
let player = this.wrapper; |
||||
|
||||
// @ts-ignore
|
||||
if(!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) { |
||||
player.classList.add('ckin__fullscreen'); |
||||
|
||||
if(player.requestFullscreen) { |
||||
player.requestFullscreen(); |
||||
// @ts-ignore
|
||||
} else if(player.mozRequestFullScreen) { |
||||
// @ts-ignore
|
||||
player.mozRequestFullScreen(); // Firefox
|
||||
// @ts-ignore
|
||||
} else if(player.webkitRequestFullscreen) { |
||||
// @ts-ignore
|
||||
player.webkitRequestFullscreen(); // Chrome and Safari
|
||||
// @ts-ignore
|
||||
} else if(player.msRequestFullscreen) { |
||||
// @ts-ignore
|
||||
player.msRequestFullscreen(); |
||||
} |
||||
|
||||
fullScreenButton.classList.remove('tgico-fullscreen'); |
||||
fullScreenButton.classList.add('tgico-smallscreen'); |
||||
fullScreenButton.setAttribute('title', 'Exit Full Screen'); |
||||
} else { |
||||
player.classList.remove('ckin__fullscreen'); |
||||
|
||||
// @ts-ignore
|
||||
if(document.cancelFullScreen) { |
||||
// @ts-ignore
|
||||
document.cancelFullScreen(); |
||||
// @ts-ignore
|
||||
} else if(document.mozCancelFullScreen) { |
||||
// @ts-ignore
|
||||
document.mozCancelFullScreen(); |
||||
// @ts-ignore
|
||||
} else if(document.webkitCancelFullScreen) { |
||||
// @ts-ignore
|
||||
document.webkitCancelFullScreen(); |
||||
// @ts-ignore
|
||||
} else if(document.msExitFullscreen) { |
||||
// @ts-ignore
|
||||
document.msExitFullscreen(); |
||||
} |
||||
|
||||
fullScreenButton.classList.remove('tgico-smallscreen'); |
||||
fullScreenButton.classList.add('tgico-fullscreen'); |
||||
fullScreenButton.setAttribute('title', 'Full Screen'); |
||||
} |
||||
} |
||||
|
||||
public onFullScreen() { |
||||
// @ts-ignore
|
||||
let isFullscreenNow = document.webkitFullscreenElement !== null; |
||||
if(!isFullscreenNow) { |
||||
this.wrapper.classList.remove('ckin__fullscreen'); |
||||
} else { |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue