Skip to content

Instantly share code, notes, and snippets.

@ShizukuIchi
Last active November 11, 2019 03:38
Show Gist options
  • Save ShizukuIchi/874b9c5aa631b5e9d0ba53f3c42a276f to your computer and use it in GitHub Desktop.
Save ShizukuIchi/874b9c5aa631b5e9d0ba53f3c42a276f to your computer and use it in GitHub Desktop.
// 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