这是一个纯原生的H5播放器,尽管网上有很多第三方库,但是基础打的牢固,一定会帮你走的更远。
大厂也非常重视基础,再说了那些第三方库也是基础一点点搭起来的,所以有兴趣学习的同学可以下载来学习。
代码中的细节我都写了注释了。
也非常欢迎大家进行拓展,有兴趣拓展功能,可以另起一个分支,最好写个文档,说明一下添加了哪些功能。
这是用H5的video标签做的视频播放器
只是学习用的,所以已有的功能可能存在问题,当然功能肯定是不完备的。
但作为学习H5的video,应该是可以的。
index.html
是我一开始写播放器的代码
core
是我把代码分开的后文件夹。
写好的功能有
- 播放和暂停
- 进度条动画,进度条拖拽
- 播放时间
- 音量调控
- 倍数播放
- 全屏
当然并不是这样就OK了,大家可以在已有的基础上进行添加,一方面可以锻炼自己的思维,另一方面可以磨练自己的代码阅读能力。
我想到的功能
- 播放状态下,当鼠标悬停太久或移出播放器一段时间,控制器应该消失。
- 网页全屏
- 清晰度调节
- 点击进度条跳转
- 弹幕
- 键盘事件,用键盘调整进度和音量
效果
如果不想下载文件的话,可以看核心代码
核心代码
// 传入视频资源路径,返回一个videoWrapperDOM
function createVideo({
videoUrl,
width,
height,
dragSvg = ""
}) {
const videoWrapperDOM = initWrapperDOM(width, height);
const video = document.createElement('video');
video.classList.add("own-video")
video.src = videoUrl;
videoWrapperDOM.appendChild(video);
function initWrapperDOM(width, height) { // 初始化外层DOM
const videoWrapperDOM = document.createElement("div");
videoWrapperDOM.classList.add("own-video-wrapper")
videoWrapperDOM.style.width = width + "px";
videoWrapperDOM.style.height = height + "px";
return videoWrapperDOM;
}
let timeFrameId = null; // 播放时间展示动画
const videoControl = {
paused: true,//那个video自带的那个还不是好用
videoDOM: null,
play(playBtn) {
this.videoDOM.play();
updateTime();
playBtn.classList.remove("pause");
playBtn.classList.add("play");
this.paused = false;
},
pause(playBtn) {
this.videoDOM.pause();
cancelAnimationFrame(timeFrameId);
playBtn.classList.remove("play");
playBtn.classList.add("pause");
this.paused = true;
},
setVolume(v) {
v = v < 0 ? 0 : v;
this.videoDOM.volume = v;
},
w: 960, // 最开始播放器的大小
h: 540,
setVideoDOM(video, w, h) {
// 看似很奇怪
// 这个应该分一个模块,然后私有化,对外暴露方法,但作为练习我就没那么做了
this.videoDOM = video;
this.w = w;
this.h = h;
this.setVolume(.5);
},
setFullScreen(isFullScreen) {
if (isFullScreen) {
this.videoDOM.parentNode.classList.add("video-full-screen");
} else {
this.videoDOM.parentNode.classList.remove("video-full-screen");
}
}
}
videoControl.setVideoDOM(video, width, height);
let playBtn = document.createElement("div");
function createPlayBtn(video) {
// 播放按钮
playBtn.classList.add("btn", "play-btn", "pause");
playBtn.addEventListener("click", function () {
if (videoControl.paused) {
// 视频状态暂停播放
console.log("paused");
videoControl.play(this);
} else {
console.log("play");
videoControl.pause(this);
}
})
return playBtn;
}
const current = document.createElement("span"); // 当前播放时间
const total = document.createElement("span"); // 总时间
function updateTime() {
const totalTime = parseTime(video.duration);
const currentTime = parseTime(video.currentTime);
current.innerText = currentTime;
total.innerText = totalTime;
timeFrameId = requestAnimationFrame(updateTime);
}
function createTimeDisplay(video) {
// 01:00 / 5:20
const timeDisplayWrapper = document.createElement("div");
timeDisplayWrapper.classList.add("time-wrapper")
timeDisplayWrapper.appendChild(current);
timeDisplayWrapper.appendChild(total);
video.addEventListener("canplaythrough", function () {
// 视频加载完成更新最新的播放时间
const totalTime = parseTime(video.duration);
const currentTime = parseTime(video.currentTime);
current.innerText = currentTime;
total.innerText = totalTime;
})
return timeDisplayWrapper
}
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
function createFullScreen() {
const fullScreenWrapper = document.createElement("div");
fullScreenWrapper.classList.add("full-screen-wrapper");
const arr = ["全屏", "小屏"];
fullScreenWrapper.innerText = arr[0];
let status = "normal"; // 浏览器的一个状态 full normal
document.addEventListener("fullscreenchange", function () {
console.log(document.fullscreenElement);
if (document.fullscreenElement !== null) {
// 进入全屏模式
status = "full";
} else {
status = 'normal';
exitFullscreen();
videoControl.setFullScreen(false);
fullScreenWrapper.innerText = arr[0];
}
});
fullScreenWrapper.addEventListener("click", function () {
if (status === 'normal') {
document.documentElement.requestFullscreen();
videoControl.setFullScreen(true);
this.innerText = arr[1];
}
if (status === 'full') {
exitFullscreen();
videoControl.setFullScreen(false);
this.innerText = arr[0];
}
})
return fullScreenWrapper;
}
function createVolumeWrapper(video) {
const volumeWrapper = document.createElement('div');
const volumeIco = document.createElement("div");
const volumeBarWrapper = document.createElement('div');
volumeWrapper.classList.add("volume-wrapper");
volumeIco.classList.add("volume-ico");
volumeBarWrapper.classList.add("volume-bar-wrapper");
const bgBar = document.createElement("div");
bgBar.classList.add("volume-bg-bar");
const innerBar = document.createElement("div");
innerBar.classList.add("volume-inner-bar");
const circle = document.createElement("span");
circle.classList.add("volume-circle");
volumeBarWrapper.appendChild(bgBar);
bgBar.appendChild(innerBar);
bgBar.appendChild(circle);
volumeWrapper.appendChild(volumeIco);
volumeWrapper.appendChild(volumeBarWrapper);
let clicked = false;
let clientY, clickY;
let barHeight = 0;
circle.addEventListener("mousedown", function (e) {
barHeight = bgBar.clientHeight; // 最大拖拽范围
// 在父元素display:none,尺寸不准确
clicked = true; // 代表可以进行拖拽了
clickY = e.pageY
clientY = innerBar.clientHeight;
})
volumeWrapper.addEventListener("mousemove", function (e) {
if (!clicked) {
return; // 点击之后才能进行拖拽
}
const moveY = clickY - e.pageY; // 这条公式要注意一下
// 因为这个相比进度条拖拽它就是一个单向动画,只需要计算出当前的音量大小就好了
let h = clientY + moveY;
h = h < 0 ? 0 : h; // 边界处理
h = h > barHeight ? barHeight : h;
innerBar.style.height = h + "px";
circle.style.bottom = h + "px";
const v = h / barHeight; // 音量
// 为了可阅读性,我尽量没有都挤在一行写,这是一个人的职业素养
videoControl.setVolume(v);
})
const stopDragVolumeBar = function () {
if (clicked) {
// 正在拖拽
clicked = false; // 取消拖拽状态
}
}
document.addEventListener("mouseup", stopDragVolumeBar)
volumeWrapper.addEventListener("mouseleave", stopDragVolumeBar);
return volumeWrapper;
}
function createDragBar(video, dragSvg) {
// 播放进度条
const barWrapper = document.createElement("div");
barWrapper.classList.add("bar-wrapper");
const innerBar = document.createElement("div");
innerBar.classList.add("inner-bar");
barWrapper.appendChild(innerBar);
const circle = document.createElement("span");
circle.classList.add("circle");
if (dragSvg) {
// 自定义svg拖拽图标
circle.innerHTML = dragSvg;
circle.classList.add("svg-circle")
} else {
circle.classList.add("normal");
}
window.circle = circle;
barWrapper.appendChild(circle);
let frameId = null; // 拖拽的时候需要取消动画
// 拖拽功能
// 我需要知道总的进度条的长度
// 还需要知道拖动的距离
// 拖动的距离 / 总的进度条长度 = currentTime / totalTime
let totalWidth = 0;
let totalTime = 0;
video.addEventListener("canplaythrough", function () {
// 视频资源加载完成
totalTime = this.duration;
let clicked = false;
let clientX, clickX;
circle.addEventListener("mousedown", function (e) {
// 如果获取元素属性为最开始,那么全屏缩放会出现BUG
totalWidth = barWrapper.offsetWidth; // 为了适应元素的最新大小
// 暂停播放
// 点击时记住点击瞬间的位置
cancelAnimationFrame(frameId); // 取消播放动画 优化性能减少不必要的麻烦
// 不然你操控不了元素
videoControl.pause(playBtn);
clientX = this.offsetLeft;
clickX = e.pageX;
clicked = true; // 进入拖拽状态
})
document.addEventListener("mousemove", function (e) {
if (!clicked) {
return; // 非拖拽状态
}
cancelAnimationFrame(frameId);
let moveX = e.pageX - clickX;
let per = (clientX + moveX) / totalWidth;
per = per < 0 ? 0 : per; // 做边界处理
per = per > 1 ? 1 : per;
per = per * 100 + "%";
circle.style.left = per;
innerBar.style.width = per;
})
document.addEventListener("mouseup", function (e) {
if (clicked) {
clicked = false;
let moveX = e.pageX - clickX;
let per = (clientX + moveX) / totalWidth;
video.currentTime = getCurrentTime(video, per);
per = per > 1 ? 1 : per;
per = per < 0 ? 0 : per;
per = per * 100 + "%";
circle.style.left = per;
innerBar.style.width = per;
videoControl.play(playBtn);
barAnimate();
}
})
})
function barAnimate() {
const per = getPercentage(video) * 100 + "%";
circle.style.left = per;
innerBar.style.width = per;
frameId = requestAnimationFrame(barAnimate);
}
barAnimate();
return barWrapper;
}
function createPlayRate(video) {
// 倍速播放
const rateWrapper = document.createElement("div");
rateWrapper.classList.add("rate-wrapper");
const currentRateWrapper = document.createElement('div');
currentRateWrapper.innerText = "1X";
const ul = document.createElement("ul");
ul.classList.add("rateSelectorWrapper");
ul.innerHTML = `
<li>2X</li>
<li>1.5X</li>
<li>1X</li>
<li>.5X</li>
`
ul.addEventListener("click", function (e) {
if (e.target.tagName === "LI") {
currentRateWrapper.innerText = e.target.innerText;
const rate = parseFloat(e.target.innerText);
video.playbackRate = rate;
}
})
rateWrapper.appendChild(currentRateWrapper);
rateWrapper.appendChild(ul);
return rateWrapper;
}
function initBar() {
const bar = document.createElement("div");
bar.classList.add("own-video-control-bar");
return bar;
}
const bar = initBar();
const timeDisplayWrapper = createTimeDisplay(video);
const dragBar = createDragBar(video, dragSvg);
const rateWrapper = createPlayRate(video);
// 为了将右边的按钮聚起来而已,我觉得那样会更好看
const container = document.createElement("div");
container.classList.add("video-btn-container");
container.appendChild(rateWrapper);
container.appendChild(createVolumeWrapper(video));
container.appendChild(createFullScreen());
bar.appendChild(createPlayBtn(video));
bar.appendChild(timeDisplayWrapper);
bar.appendChild(dragBar);
bar.appendChild(container);
videoWrapperDOM.appendChild(bar);
return videoWrapperDOM;
}