这是一个纯原生的H5播放器,尽管网上有很多第三方库,但是基础打的牢固,一定会帮你走的更远。
大厂也非常重视基础,再说了那些第三方库也是基础一点点搭起来的,所以有兴趣学习的同学可以下载来学习。
代码中的细节我都写了注释了。
也非常欢迎大家进行拓展,有兴趣拓展功能,可以另起一个分支,最好写个文档,说明一下添加了哪些功能。
这是用H5的video标签做的视频播放器
只是学习用的,所以已有的功能可能存在问题,当然功能肯定是不完备的。
但作为学习H5的video,应该是可以的。
index.html
是我一开始写播放器的代码
core
是我把代码分开的后文件夹。
写好的功能有
- 播放和暂停
- 进度条动画,进度条拖拽
- 播放时间
- 音量调控
- 倍数播放
- 全屏
当然并不是这样就OK了,大家可以在已有的基础上进行添加,一方面可以锻炼自己的思维,另一方面可以磨练自己的代码阅读能力。
我想到的功能
- 播放状态下,当鼠标悬停太久或移出播放器一段时间,控制器应该消失。
- 网页全屏
- 清晰度调节
- 点击进度条跳转
- 弹幕
- 键盘事件,用键盘调整进度和音量
gitee
效果
如果不想下载文件的话,可以看核心代码
核心代码
// 传入视频资源路径,返回一个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) { // 初始化外层DOMconst 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:20const 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 normaldocument.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.pageYclientY = 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 / totalTimelet totalWidth = 0;let totalTime = 0;video.addEventListener("canplaythrough", function () {// 视频资源加载完成totalTime = this.duration;let clicked = false;let clientX, clickX;circle.addEventListener("mousedown", function (e) {// 如果获取元素属性为最开始,那么全屏缩放会出现BUGtotalWidth = 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;
}