Last active
November 11, 2019 03:38
-
-
Save ShizukuIchi/874b9c5aa631b5e9d0ba53f3c42a276f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// specify a root element to insert video node | |
// such as a div | |
// videos in playlist will turn into a single video | |
// const playlist = [ | |
// { src: 'bunny.mp4', start: 5, end: 20 }, | |
// { src: 'bunny.mp4', start: 15, end: 18 }, | |
// { src: 'bunny.mp4', start: 22, end: 30 }, | |
// ]; | |
class ListPlayer { | |
constructor({ container, playlist }) { | |
this.playlist = playlist.map(v => ({ ...v })); | |
this.container = null; | |
this.width = '320px'; | |
this.height = '240px'; | |
this._duration = 0; | |
this._currentTime = 0; | |
this.playingIndex = 0; | |
this.nodes = { | |
videos: [], | |
progress: { | |
container: null, | |
text: null, | |
bar: null, | |
}, | |
}; | |
this.target = null; | |
this.paused = true; | |
this.init(container); | |
} | |
init(container) { | |
if (typeof container === 'string') | |
this.container = document.querySelector(container); | |
else this.container = container; | |
if (!this.container instanceof HTMLElement) | |
throw new Error('Invalid container'); | |
const { position, width, height } = getComputedStyle(this.container); | |
if ( | |
position !== 'relative' || | |
position !== 'fixed' || | |
position !== 'absolute' | |
) | |
this.container.style.position = 'relative'; | |
this.width = width; | |
this.height = height; | |
if (Number(width.slice(0, -2)) === 0) { | |
this.width = '320px'; | |
this.container.style.width = this.width; | |
} | |
if (Number(height.slice(0, -2)) === 0) { | |
this.height = '240px'; | |
this.container.style.height = this.height; | |
} | |
this.initVideoNodes(); | |
this.initProgress(); | |
this.updateDuration(); | |
this.setCurrentTimeUpdater(); | |
this.container.onclick = e => { | |
if (this.nodes.videos.findIndex(v => v === e.target) === -1) return; | |
if (this.paused) { | |
this.play(); | |
} else { | |
this.pause(); | |
} | |
}; | |
console.log(`Player is ready. ${this.playlist.length} videos to play.`); | |
} | |
initVideoNodes() { | |
const video1 = document.createElement('video'); | |
video1.style.width = this.width; | |
video1.style.height = this.height; | |
video1.style.objectFit = 'contain'; | |
video1.style.position = 'absolute'; | |
video1.style.display = 'initial'; | |
this.target = video1; | |
this.setVideoSrc(); | |
this.container.appendChild(video1); | |
this.nodes.videos.push(video1); | |
if (this.playlist.length < 2) return; | |
const video2 = video1.cloneNode(); | |
this.nodes.videos.push(video2); | |
video2.style.display = 'none'; | |
this.container.appendChild(video2); | |
this.setNextVideoSrc(); | |
video1.onpause = () => { | |
if (this.paused || this.playingIndex + 1 === this.playlist.length) { | |
return (this.paused = true); | |
} | |
video2.play().then(() => { | |
this.target = video2; | |
this.playingIndex += 1; | |
video2.style.display = 'initial'; | |
video1.style.display = 'none'; | |
this.setNextVideoSrc(); | |
}); | |
}; | |
video2.onpause = () => { | |
if (this.paused || this.playingIndex + 1 === this.playlist.length) { | |
return (this.paused = true); | |
} | |
video1.play().then(() => { | |
this.target = video1; | |
this.playingIndex += 1; | |
video1.style.display = 'initial'; | |
video2.style.display = 'none'; | |
this.setNextVideoSrc(); | |
}); | |
}; | |
} | |
initProgress() { | |
const progress = document.createElement('div'); | |
const progressValueBar = document.createElement('div'); | |
const progressValueText = document.createElement('span'); | |
progress.style.cssText = `position: absolute; bottom: 10%; left: 5%; width: 90%; background-color: rgba(100,100,100,0.7); border-radius: 2px; height: 4px; box-shadow: 1px 1px 3px rgba(0,0,0,0.2); cursor: pointer;`; | |
progressValueBar.style.cssText = `position: absolute; top: 0; left: 0; width: 0; background-color: #fff; border-radius: 2px; height: 4px; max-width: 100%;`; | |
progressValueText.style.cssText = `position: absolute; bottom: 12px; left: 6px; color: white; text-shadow: 1px 1px 3px rgba(0,0,0,0.2); font-size: 14px;`; | |
progressValueText.textContent = '0:00 / 0:00'; | |
progress.appendChild(progressValueBar); | |
progress.appendChild(progressValueText); | |
this.container.appendChild(progress); | |
progress.onclick = e => this.onProgressClick(e); | |
this.nodes.progress.container = progress; | |
this.nodes.progress.bar = progressValueBar; | |
this.nodes.progress.text = progressValueText; | |
} | |
updateDuration() { | |
this._duration = 0; | |
for (let i = this.playlist.length; i--; ) { | |
const video = this.playlist[i]; | |
const duration = video.end - video.start; | |
if (duration < 0) throw new Error('invalid time range'); | |
video.duration = duration; | |
this._duration += duration; | |
} | |
} | |
setCurrentTimeUpdater() { | |
this.nodes.videos.forEach(videoNode => { | |
videoNode.ontimeupdate = () => { | |
if (this.target !== videoNode) return; | |
const start = this.currentVideo.start; | |
const currentVideoTime = videoNode.currentTime - start; | |
const playedTime = this.playlist | |
.slice(0, this.playingIndex) | |
.reduce((acc, cur) => acc + cur.duration, 0); | |
this._currentTime = playedTime + currentVideoTime; | |
this.drawBar(); | |
}; | |
}); | |
} | |
pause() { | |
this.paused = true; | |
this.target.pause(); | |
} | |
play() { | |
this.paused = false; | |
if (this._currentTime >= this._duration) { | |
this._currentTime = 0; | |
return this.seek(0); | |
} | |
this.target.play(); | |
} | |
setVideoSrc() { | |
const { src, start, end } = this.currentVideo; | |
this.target.src = `${src}#t=${start},${end}`; | |
} | |
setNextVideoSrc() { | |
const nextIndex = (this.playingIndex + 1) % this.playlist.length; | |
const { src, start, end } = this.playlist[nextIndex]; | |
const target = this.nodes.videos.find(t => t !== this.target); | |
target.src = `${src}#t=${start},${end}`; | |
} | |
seek(time) { | |
this.pause(); | |
if (time >= this._duration) { | |
this.playingIndex = this.playlist.length - 1; | |
this.setVideoSrc(); | |
this.target.currentTime = this.currentVideo.end; | |
this.setNextVideoSrc(); | |
return; | |
} | |
if (time < 0) time = 0; | |
let index = -1; | |
while (time >= 0) { | |
const duration = this.playlist[++index].duration; | |
time -= duration; | |
} | |
this.playingIndex = index; | |
this.setVideoSrc(); | |
const { start, duration } = this.currentVideo; | |
this.target.currentTime = start + duration + time; | |
this.setNextVideoSrc(); | |
// invoke timeupdate event handler first to update this._currentTime | |
setTimeout(() => { | |
this.play(); | |
}); | |
} | |
onProgressClick(e) { | |
const width = e.currentTarget.offsetWidth; | |
const offset = e.offsetX; | |
this.seek(this._duration * (offset / width)); | |
} | |
get videoNode() { | |
return this.target; | |
} | |
get currentVideo() { | |
return this.playlist[this.playingIndex]; | |
} | |
get duration() { | |
return this._duration; | |
} | |
get currentTime() { | |
return this._currentTime > this._duration | |
? this._duration | |
: this._currentTime; | |
} | |
set currentTime(t) { | |
this.seek(t); | |
} | |
drawBar() { | |
const percent = (this._currentTime / this._duration) * 100; | |
this.nodes.progress.bar.style.width = `${percent}%`; | |
let current = Math.floor(this._currentTime); | |
let currentM = 0; | |
while (current >= 60) { | |
currentM++; | |
current -= 60; | |
} | |
if (current < 10) current = '0' + current; | |
let duration = Math.floor(this._duration); | |
let durationM = 0; | |
while (duration >= 60) { | |
durationM++; | |
duration -= 60; | |
} | |
if (duration < 10) duration = '0' + duration; | |
this.nodes.progress.text.textContent = `${currentM}:${current} / ${durationM}:${duration}`; | |
} | |
} | |
// const listPlayer = new ListPlayer({ | |
// container: document.querySelector('div'), | |
// playlist, | |
// }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment