web自定义视频控件:提升用户体验
需求:由于默认的控件无法满足我们的使用需求,我们希望对自定义视频控件进行美化和简化按钮操作。
目的:提升用户体验,使视频控件更具吸引力和易用性,满足产品的需要。
解析video标签
属性解析
属性 | 说明 |
---|---|
src | 指定要播放的视频文件的 URL |
autoplay | 指定视频在加载完成后自动播放。可以设置为 autoplay |
controls | 显示视频的控制条,包括播放/暂停、音量控制、进度条等。可以设置为 controls |
loop | 设置视频循环播放。可以设置为 loop |
muted | 设置视频静音播放。可以设置为 muted |
poster | 指定视频封面图像的 URL,用于在视频加载之前显示 |
preload | 指定视频的预加载行为。可选值包括 auto 、metadata 和 none ,分别表示自动预加载、仅预加载元数据和不预加载 |
width | 设置视频播放区域的宽度 |
height | 设置视频播放区域的高度 |
事件解析:
html/React | vue | 说明 |
---|---|---|
onloadstart | @loadstart | 视频开始加载时触发 |
onloadedmetadata | @loadedmetadata | 视频的元数据加载完成时触发 |
onloadeddata | @loadeddata | 视频的第一帧数据加载完成时触发 |
oncanplay | @canplay | 视频可以开始播放时触发 |
oncanplaythrough | @canplaythrough | 视频可以完整播放时触发 |
onplay | @play | 视频开始播放时触发 |
onpause | @pause | 视频暂停播放时触发 |
onended | @ended | 视频播放结束时触发 |
onseeking | @seeking | 视频正在寻找指定的播放位置时触发 |
onseeked | @seeked | 视频寻找指定的播放位置完成时触发 |
onprogress | @progress | 视频正在下载中,触发下载进度事件 |
onerror | @error | 视频加载或播放错误时触发 |
这些事件可以通过给 <video>
标签添加对应的事件属性来监听。
例如:事件监听:
<video onplay="handlePlay()" onpause = "handlePause()">
</video>
需要注意的是,不同浏览器对于视频事件的支持和触发时机可能会有一些差异,因此在开发过程中应该进行兼容性测试,并根据需要做出相应的兼容处理。
开始设计
取消默认controls
首先,您可以使用:controls="false"
或者不在 <video>
标签中写入 controls
属性来隐藏默认的控件
另外,您也可以使用以下 CSS 代码隐藏控件:
.custom-video::-webkit-media-controls {
display: none !important;
}
接下来,您可以设计一个全新的控件,并将其放置在与 <video>
标签并列的位置。
<div class="player">
<!-- 上方滚动的标题 -->
<div class="noticeBar"></div>
<video class="custom-video" preload="auto" src="./1.mp4">
您的浏览器不支持video标签。
</video>
<div class="ui-wrapper">
<!-- 遮罩层 -->
<div class="ui-mask"></div>
<!-- 控件 -->
<div class="ui-control">
<!-- 控件的具体内容 -->
</div>
<!-- 加载逻辑 -->
<div class="ui-loading"></div>
</div>
</div>
设计控件空间
按照需求去设计
下面是我按照市场常用的样式,我分了 上下两块、 上部分:进度条 下部分:按键操作
<div class="ui-control">
<!-- 进度条 -->
<div class="progress">
<input type="range" min="0" max="100" id="myRange">
</div>
<!-- 控制详情 -->
<div class="control-type">
<!-- ...... -->
</div>
</div>
控件可以分为左边和右边两部分。
左边部分包括常规的暂停、播放等按钮。
右边部分包括全屏、页面内全屏、画中画、倍速、声音等按钮。
<!-- 控制详情 -->
<div class="control-type">
<!-- 左边部分 -->
<div class="left">
<!-- 暂停/播放按钮 -->
<div class="play_pause">
<img id="play_pause_img" class="control-img" src="./image/pause.png" alt />
</div>
<!-- 时间显示 -->
<div class="timer">00:00 / 00:00</div>
</div>
<!-- 右边部分 -->
<div class="right">
<!-- 页面内全屏按钮 -->
<img class="control-img" src="./image/pageFullScreen.png" alt/>
<!-- 画中画按钮 -->
<img class="control-img" src="./image/pictureInPictureScreen.png" alt />
<!-- 全屏按钮 -->
<img class="control-img" src="./image/windowFullScreen.png" alt />
</div>
</div>
CSS处理
接下来是对 CSS 的处理:
视频完整展示
为了确保视频能够完整显示,可以使用以下 CSS 代码:
.player {
position: relative;
width: 766px; /* 视频容器的宽度 */
height: 431px; /* 视频容器的高度 */
background-color: #000;
overflow: hidden;
}
.custom-video {
width: 100%;
height: 100%;
}
对视频控件布局
对控件进行封装:
.ui-wrapper {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
/* mask 肯定要将视频完全掩盖住 */
.ui-wrapper .ui-mask {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1;
}
/* control 肯定要比mask层级 + 1 */
.ui-wrapper .ui-control {
position: absolute;
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
bottom: 0px;
border-radius: 5px;
padding: 12px 0;
background-image: linear-gradient(180deg, rgba(0, 0, 0, 0.1) 0%, #000000 100%);
z-index: 2;
transition: all 0.25s cubic-bezier(0.39, -0.04, 0.09, 0.75);
}
处理进度条,使其占满整个宽度:
.progress {
width: 100%;
}
input[type="range"] {
width: 100%;
}
根据喜好设计其他按钮的样式:
.control-type {
display: flex;
justify-content: space-between;
width: 100%;
}
.control-type .left {
display: flex;
align-self: flex-start;
gap: 15px;
}
.control-type .right {
display: flex;
gap: 15px;
}
.......
通过以上优化和说明,您可以根据需要进行进一步的调整和样式设计,使您的自定义视频控件满足您的需求。
事件处理
视频标签有很多事件
<video
class="custom-video"
preload="auto"
src="./1.mp4"
ondurationchange="handleDurationChange(event)"
ontimeupdate="handleUpdateVideo(event)"
onwaiting="handleVideoLoading()"
onended="handleVideoEnded()"
onloadedmetadata="handleVideoCache(event)"
>
您的浏览器不支持video标签。
</video>
参数
提供处理事件思路:
// 参数
let speed = 1; // 视频的倍速
let duration = 0; // 视频的时间
let videoPlayState = 0; /// 0-暂停 1-播放 2-加载 3-视频播放结束
let ispageFullScreen = false;// 页面全屏的状态
let iswindowFullScreen = false;// 浏览器全屏的状态
let prePlayerStyle = { // 保留全屏前的页面视频参数
width: "766px",
height: "431px"
};
// 获取dom
// 获取player为了处理全屏问题
let playerNode = document.querySelector(".player");
// 获取videoDom为了处理,视频的暂停与播放,缓冲等
let videoNode = playerNode.querySelector(".custom-video");
// 在html中写的,需要动态的变化数值时间
let timerNode = document.querySelector(".timer");
// 获取progress进度条,修改进度value
let myRangeNode = document.querySelector("#myRange");
// 在html中写的,需要动态的变化暂停与播放图标
let playPauseImgNode = document.querySelector("#play_pause_img");
视频的操作
视频的暂停与播放
// 视频的暂停与播放
function videoPlayOrPause () {
videoNode.paused ? videoNode.play() : videoNode.pause();
}
// 监听视频是否播放,修改图标与状态
videoNode.addEventListener('play', videoPlayedListener)
function videoPlayedListener (e) {
if (e.returnValue) {
videoPlayState = 1;
playPauseImgNode.src = "./image/play.png";
}
}
videoNode.addEventListener('pause', videoPausedListener);
function videoPausedListener (e) {
if (e.returnValue) {
videoPlayState = 0;
playPauseImgNode.src = "./image/pause.png";
}
}
获取视频的时间
// 获取视频的时长
function handleDurationChange (event) {
// 获取duration的数据
const videoElement = event.target;
duration = videoElement.duration
// 未播放视频的时候,格式化时间
formatTimeString(videoElement);
}
// 格式化视频进度时间
function formatTimeString (node) {
let currentTime = node.currentTime;
myRangeNode.value = currentTime / duration \* 100;
const formatTime = (t) => {
// 时间格式化
let h = parseInt(t / 3600);
let m = parseInt((t % 3600) / 60);
m = h \* 60 + m;
m = m < 10 ? "0" + m : m;
let s = parseInt(t % 60);
s = s < 10 ? "0" + s : s;
return m + ":" + s;
}
timerNode.innerText = `${formatTime(currentTime)} / ${formatTime(duration)}`
}
注意:在格式化时间的时候,修改了myRangeNode.value
的值,也就是进度条的进度。
视频时间进度
利用上面的时间格式化
function handleUpdateVideo (event) {
const videoElement = event.target;
formatTimeString(videoElement);
}
视频进度条
拖动进度条,时间就会发生变化
// 设计进度条
function onInputProgress (event) {
// 点击判断修改事件
videoNode.currentTime = duration * event.target.value / 100;
}
同样可以运用到音量上面。
视频倍速
通过点击某一个按钮,传递来number数据:比如:0.5
, 1
, 1.5
, 2
, 3
// 比如
onChageSpeed(count) {
// 视频获取速率
let videoSpeed = videoNode.playbackRate;
// 视频倍速和上一次一样就无需修改
if (videoSpeed !== speed) {
// 存储要修改的倍速
speed = count;
videoNode.playbackRate = speed;
}
}
浏览器全屏
视频全屏问题:
videoDom的视频全屏时,视频控件会自动出现,
解决办法:使用视频容器全屏,就不会出现这个问题。
页面全屏时,仅仅是video容器全屏了,不然不能实现video控件的自定义
// const element = document.querySelector(".player");
// windows的窗口全屏
function handlewindowFullScreen (currentIsFull, isChangeWindow = true) {
const pageFullScreen = () => {
if (playerNode.requestFullscreen) {
playerNode.requestFullscreen();
} else if (playerNode.mozRequestFullScreen) {
playerNode.mozRequestFullScreen();
} else if (playerNode.webkitRequestFullscreen) {
playerNode.webkitRequestFullscreen();
} else if (playerNode.msRequestFullscreen) {
playerNode.msRequestFullscreen();
}
return true;
}
const exitpageFullScreen = () => {
if (document.exitFullScreen) {
document.exitFullScreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
return false;
}
return !currentIsFull ? pageFullScreen() : exitpageFullScreen();
}
解释:
当前的全屏状态为true,表示状态为全屏中,你点击了按钮,就表示,需要exit退出全屏,使用 true ---》exitpageFullScreen()
页面内全屏
制造页面下的全屏
// 全屏的使用StyleModify修改
function handleFullScreenStyle (currentIsFull) {
if (currentIsFull) {
playerNode.style.position = 'fixed';
playerNode.style.height = "100vh";
playerNode.style.width = "100vw";
} else {
playerNode.style.position = 'relative';
playerNode.style.height = prePlayerStyle.height;
playerNode.style.width = prePlayerStyle.width;
}
}
// 页面的全屏
function handlePageFullScreen (isFull) {
handleFullScreenStyle(!isFull);
// 点击了,就切换一下
return !isFull;
}
画中画
画中画(Picture-in-Picture)是一种在网页中播放视频时,将视频窗口缩小并浮动在其他内容上方的功能。它可以提供更好的多任务处理和用户体验
// 画中画的视角
function handlePictureInPicture () {
const enterPictureInPicture = ()=> {
videoNode.requestPictureInPicture()
.then(() => {
// 进入画中画模式成功
console.error('进入画中画模式成功');
})
.catch(error => {
console.error('进入画中画模式失败:', error);
});
}
if (document.pictureInPictureEnabled) {
// 浏览器支持画中画功能
enterPictureInPicture();
} else {
// 浏览器不支持画中画功能
}
}
缓冲
看看视频加载了多少
function handleVideoCache (event) {
// 视频的缓冲进度
const buffered = event.target.buffered;
let bufferedTime = 0;
if (this.bufferedLength !== buffered.length) {
for (let i = 0; i < buffered.length; i++) {
bufferedTime += buffered.end(i) - buffered.start(i);
}
console.log(`已缓冲 ${bufferedTime} 秒`);
this.bufferedTime = bufferedTime;
this.bufferedLength = buffered.length;
}
}
视频加载中
一般是:视频开始加载中,就去展示加载的动画。
handleVideoWaiting() {
// 加载动画, videoPlayState加载状态
videoPlayState = 2;
},
视频播放结束
一般是,读秒开始下一个视频或者去重新循环播放视频
handleVideoEnded() {
// 视频播放完成,videoPlayState的完成状态
videoPlayState = 3;
},
优化操作
点击遮罩层
function handleMaskClick () {
// 处理视频的播放暂停
videoPlayOrPause();
// 控制栏隐藏
controlShow(2);
}
自动隐藏控制栏
let controlNode = playerNode.querySelector(".ui-control");
let controlTimer = null;
// 默认2.5s后隐藏
function controlShow (closeTime = 2.5) {
// 点击了,就取消对control的隐藏,重新倒数
controlNode.style.transform = `translateY(0)`;
clearTimeout(controlTimer);
controlTimer = setTimeout(() => {
controlNode.style.transform = `translateY(100%)`;
clearTimeout(controlTimer);
}, closeTime * 1000);
}
上述代码中,通过使用 HTML 和 CSS 创建了一个包含自定义视频控件的容器,并为播放/暂停按钮和进度条添加了相应的样式。然后,通过 JavaScript 获取相关的 DOM 元素,并为播放/暂停按钮添加点击事件和视频的时间更新事件,以实现播放控制和进度条更新的功能
下面我给出Demo,是部分的实现。
Demo
视频的icon
视频的链接
我使用的本地
完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
<style>
.container {
height: 100vh;
max-width: 100vw;
/* min-width: 1080px; */
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
box-sizing: content-box;
}
.container .player {
position: relative;
width: 766px;
height: 431px;
background-color: #000;
overflow: hidden;
}
.custom-video {
width: 100%;
height: 100%;
}
.content {
width: 304px;
background-color: rgb(47, 218, 218);
}
.ui-wrapper {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.ui-wrapper .ui-mask {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1;
}
.ui-wrapper .ui-control {
position: absolute;
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
bottom: 0px;
border-radius: 5px;
padding: 12px 0;
background-image: linear-gradient(180deg, rgba(0, 0, 0, 0.1) 0%, #000000 100%);
z-index: 2;
transition: all 0.25s cubic-bezier(0.39, -0.04, 0.09, 0.75);
}
.progress {
width: 100%;
}
input[type="range"] {
width: 100%;
}
.control-type {
display: flex;
justify-content: space-between;
width: 100%;
}
.control-type .left {
display: flex;
align-self: flex-start;
gap: 15px;
}
.timer {
color: #FFF;
}
.control-type .right {
display: flex;
gap: 15px;
}
img[class="control-img"] {
width: 24px;
height: 24px;
display: block;
}
</style>
</head>
<body>
<div class="container">
<div class="player">
<div class="noticeBar"></div>
<video class="custom-video" preload="auto" src="./1.mp4" ondurationchange="handleDurationChange(event)"
ontimeupdate="handleUpdateVideo(event)" onwaiting="handleVideoLoading()" onended="handleVideoEnded()"
onloadedmetadata="handleVideoCache(event)">
您的浏览器不支持video标签。
</video>
<div class="ui-wrapper">
<div class="ui-mask" onclick="handleMaskClick()"></div>
<div class="ui-control" onclick="controlShow(2)">
<div class="progress">
<input type="range" min="0" max="100" id="myRange" oninput="onInputProgress(event)">
</div>
<!-- 控制详情 -->
<div class="control-type">
<div class="left">
<div class="play_pause" onclick="videoPlayOrPause()">
<img id="play_pause_img" class="control-img" src="./image/pause.png" alt />
<!-- <img class="control-img" src="./image/pause.png" alt /> -->
</div>
<div class="timer">00:00 / 00:00</div>
</div>
<div class="right">
<!-- <DoubleSpeed ref="doubleSpeedRef" class="ctr-double-speed" @onChageSpeed="onChageSpeed" /> -->
<img class="control-img" src="./image/pageFullScreen.png" alt onclick="handleChangeScreen('page')" />
<img class="control-img" src="./image/pictureInPictureScreen.png" alt
onclick="handleChangeScreen('pictureInPicture')" />
<img class="control-img" src="./image/windowFullScreen.png" alt onclick="handleChangeScreen('window')" />
</div>
</div>
</div>
<div class="ui-loading">
</div>
</div>
</div>
<div class="content">1</div>
</div>
<script>
// 参数
let duration = 0;
let videoPlayState = false; // 0-暂停 1-播放 2-加载
let playerNode = document.querySelector(".player");
let videoNode = playerNode.querySelector(".custom-video");
let timerNode = document.querySelector(".timer");
let myRangeNode = document.querySelector("#myRange");
let playPauseImgNode = document.querySelector("#play_pause_img");
function handleMaskClick () {
// 处理视频的播放
videoPlayOrPause();
controlShow(2);
}
let controlNode = playerNode.querySelector(".ui-control");
let controlTimer = null;
function controlShow (closeTime = 2.5) {
// 点击了,就取消对control的隐藏,重新倒数
controlNode.style.transform = `translateY(0)`;
clearTimeout(controlTimer);
controlTimer = setTimeout(() => {
// controlNode.style.transform = `translateY(100%)`;
clearTimeout(controlTimer);
}, closeTime * 1000);
}
// 设计进度条
function onInputProgress (event) {
// 点击判断修改事件
videoNode.currentTime = duration * event.target.value / 100;
}
// 操作
function videoPlayOrPause () {
videoNode.paused ? videoNode.play() : videoNode.pause();
}
// 监听视频是否播放
videoNode.addEventListener('play', videoPlayedListener)
function videoPlayedListener (e) {
if (e.returnValue) {
videoPlayState = 1;
playPauseImgNode.src = "./image/play.png";
}
}
videoNode.addEventListener('pause', videoPausedListener);
function videoPausedListener (e) {
if (e.returnValue) {
videoPlayState = 0;
playPauseImgNode.src = "./image/pause.png";
}
}
//. video标签的一些事件
function handleDurationChange (event) {
// 获取duration的数据
const videoElement = event.target;
duration = videoElement.duration
// 未播放视频的时候,格式化时间
formatTimeString(videoElement);
}
function handleUpdateVideo (event) {
const videoElement = event.target;
formatTimeString(videoElement);
}
function handleVideoCache (event) {
// 视频的缓冲进度
const buffered = event.target.buffered;
let bufferedTime = 0;
if (this.bufferedLength !== buffered.length) {
for (let i = 0; i < buffered.length; i++) {
bufferedTime += buffered.end(i) - buffered.start(i);
}
console.log(`已缓冲 ${bufferedTime} 秒`);
this.bufferedTime = bufferedTime;
this.bufferedLength = buffered.length;
}
}
function handleVideoEnded () {
console.log("视频播放结束:");
}
function handleVideoLoading () {
// 视频正在加载中动画
console.log("视频正在加载");
}
let ispageFullScreen = false;
let iswindowFullScreen = false;
let prePlayerStyle = {
width: "766px",
height: "431px"
};
// 全屏
function handleChangeScreen (type) {
switch (type) {
case "page":
ispageFullScreen = handlePageFullScreen(ispageFullScreen);
break;
case "window":
iswindowFullScreen = handlewindowFullScreen(iswindowFullScreen);
break;
case "pictureInPicture":
handlePictureInPicture();
break;
default:
break;
}
}
function handleFullScreenStyle (currentIsFull) {
if (currentIsFull) {
playerNode.style.position = 'fixed';
playerNode.style.height = "100vh";
playerNode.style.width = "100vw";
} else {
playerNode.style.position = 'relative';
playerNode.style.height = prePlayerStyle.height;
playerNode.style.width = prePlayerStyle.width;
}
}
// 页面的全屏
function handlePageFullScreen (isFull) {
handleFullScreenStyle(!isFull);
// 点击了,就切换一下
return !isFull;
}
// 页面全屏时,仅仅时element全屏了,不然不能实现video控件的自定义
// 获取整个页面元素
function handlewindowFullScreen (isFull, isChangeWindow = true) {
const pageFullScreen = () => {
if (playerNode.requestFullscreen) {
playerNode.requestFullscreen();
} else if (playerNode.mozRequestFullScreen) {
playerNode.mozRequestFullScreen();
} else if (playerNode.webkitRequestFullscreen) {
playerNode.webkitRequestFullscreen();
} else if (playerNode.msRequestFullscreen) {
playerNode.msRequestFullscreen();
}
return true;
}
const exitpageFullScreen = () => {
if (document.exitFullScreen) {
document.exitFullScreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
return false;
}
return !isFull ? pageFullScreen() : exitpageFullScreen();
}
document.addEventListener('fullscreenchange', function (e) {
// 监听document页面是否全屏
if (document.fullscreenElement === playerNode) {
// 视频进入全屏状态
console.log("视频进入全屏状态");
// 将页面变成false;
ispageFullScreen = false;
} else {
// 视频退出全屏状态
// ispageFullScreen = false;
// iswindowFullScreen = false;
}
});
// 画中画的视角
function handlePictureInPicture (isFull) {
if (document.pictureInPictureEnabled) {
// 浏览器支持画中画功能
enterPictureInPicture();
} else {
// 浏览器不支持画中画功能
}
function enterPictureInPicture () {
videoNode.requestPictureInPicture()
.then(() => {
// 进入画中画模式成功
console.error('进入画中画模式成功');
})
.catch(error => {
console.error('进入画中画模式失败:', error);
});
}
}
// 格式化时间
function formatTimeString (node) {
let currentTime = node.currentTime;
myRangeNode.value = currentTime / duration * 100;
const formatTime = (t) => {
// 时间格式化
let h = parseInt(t / 3600);
let m = parseInt((t % 3600) / 60);
m = h * 60 + m;
m = m < 10 ? "0" + m : m;
let s = parseInt(t % 60);
s = s < 10 ? "0" + s : s;
return m + ":" + s;
}
timerNode.innerText = `${formatTime(currentTime)} / ${formatTime(duration)}`
}
</script>
</body>
</html>
大概就这些了,😄。
转载自:https://juejin.cn/post/7241101553712365627