最近在重啃《JavaScript 高级程序设计》,看到 “HTML5 脚本编程” 章节时,突然想到现在很多网页都已经不用 flash 而改用 H5 播放器了,各种功能都很完善,于是决定动手实现一个简易的播放器。
功能梳理
以 B 站的播放页面为例,分析 H5 播放器常具备的功能:
image-20200615205412915.png
- 播放/暂停:点击按钮时切换 “播放” / “暂停” 两款按钮样式,并且媒体响应对应的操作;
- 播放时长: 加载完成时,更新显示媒体的总时长;播放过程中,实时更新显示已播放时长;
- 倍速播放: 悬浮按钮时提供倍速列表;点击可以切换媒体的播放速度;
- 音量控制: 增加/减少媒体的播放音量;
- 全屏/宽屏/网页全屏: 改变媒体的宽度、高度,以适应网页或屏幕;
- 进度条: 播放过程中,改变进度条的长度;暂停播放时,进度条长度停止变动;点击进度条可以定位到对应的位置播放。
功能涉及到的 API 和属性
- 播放/暂停
- video 元素,具有
play()
和pause()
方法,可以实现媒体的播放和暂停;
- 播放时长
- 媒体加载完成时,将触发 video 的
canpalythrough
事件; - video 元素,
duration
属性可以获取媒体的总时长(秒); - video 元素,
currentTime
属性可以获取当前已播放的时长(秒)
- 倍速播放
- video 元素,
playbackRate
属性可以获取/设置当前的播放速度;
- 音量控制
- video 元素,
volume
属性可以获取/设置当前音量,值为 0.0 ~ 1.0;
- 全屏
- video 元素,
requestFullscreen()
方法,可以实现该元素全屏;
- 进度条
- 可以根据
duration
属性和currentTime
属性动态更改进度条的宽度。
HTML 布局及样式实现
该播放器容器内部包含两部分,分别是媒体播放区域和控件区域,其中控件浮动在容器的底部。
控件区域又由上方的进度条和下方的控件组构成。进度条可以包括两层 DIV,一层作为底部背景,一层位于背景之上,作为实际的进度显示。控件组的布局左边浮动 “播放/暂停” 按钮,“时长” 信息,右边浮动 “音量、“倍速”、“全屏” 按钮。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#videoBox {
width: 800px;
padding: 20px 0 55px 0;
background-color: #000;
position: relative;
}
.videoContent {
width: 100%;
}
.controlWarp {
width: 100%;
height: 35px;
position: absolute;
bottom: 20px;
color: #fff;
}
.videoProgress {
width: 100%;
height: 5px;
position: relative;
}
.progressContent {
width: 100%;
height: 100%;
background-color: #aaa;
cursor: pointer;
}
.progressTrack {
width: 0px;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-color: cornflowerblue;
}
.controlBar {
width: 100%;
height: 30px;
line-height: 30px;
position: relative;
}
.controlBar div {
float: left;
}
.controlLeft {
position: absolute;
left: 0;
top: 0;
}
.controlRight {
position: absolute;
right: 0;
top: 0;
}
.controlItem {
margin-right: 10px;
}
.speedList {
width: 40px;
list-style: none;
padding: 0;
margin: 0;
text-align: center;
background-color: rgb(0, 0, 0, .6);
position: absolute;
top: -120px;
display: none;
font-size: 12px;
}
.speedList li {
cursor: pointer;
}
</style>
</head>
<body>
<div id="videoBox">
<video
class="videoContent"
src="../images/video.mp4"
></video>
<div class="controlWarp">
<!-- 进度条 -->
<div class="videoProgress">
<div class="progressContent"></div>
<div class="progressTrack"></div>
</div>
<!-- 媒体控制按钮 -->
<div class="controlBar">
<div class="controlLeft">
<!-- 播放/暂停按钮 -->
<div class="btnPlay controlItem">
<button class="btn play">播放</button>
</div>
<!-- 播放时长文本信息 -->
<div class="videoTime controlItem">
<span class="currentTime">00:00</span>/<span class="totalTime">00:00</span>
</div>
</div>
<div class="controlRight">
<!-- 音量控制按钮,此处为了示例,仅做增减按钮 -->
<div class="controlItem">
<button class="btn addVolume">音量+</button>
<button class="btn subVolume">音量-</button>
</div>
<!-- 倍速控制按钮 -->
<div class="controlItem speedWarp">
<!-- 倍速列表 -->
<ul class="speedList">
<li data-val='2'>2.0x</li>
<li data-val='1.5'>1.5x</li>
<li data-val='1'>1.0x</li>
<li data-val='0.5'>0.5x</li>
</ul>
<button class="btn speed">倍速</button>
</div>
<!-- 全屏按钮 -->
<button class="btn fullScreen controlItem">全屏</button>
</div>
</div>
</div>
</div>
</body>
功能行为
接下来就需要为各个控件按钮添加对应的事件行为
// 预先定义一个格式化播放时长的函数
function formatTime(seconds) {
let h = 0, m = 0, s = 0;
const formatNumber = n => n > 9 ? n : `0${n}`;
if(seconds > 3600) {
h = parseInt(seconds / 3600);
seconds = seconds - h * 3600;
h = formatNumber(h);
}
if(seconds > 60) {
m = parseInt(seconds / 60);
seconds = seconds - m * 60;
m = formatNumber(m);
}
s = formatNumber(parseInt(seconds));
return `${h ? `${h}:` : ''}${m ? m : '00'}:${s ? s : '00'}`
}
let videoBox = document.getElementById('videoBox');
let video = videoBox.getElementsByClassName('videoContent')[0];
let totalSeconds = 0;
// 进度条交互
let progressContent = videoBox.getElementsByClassName('videoProgress')[0];
let progressTrack = videoBox.getElementsByClassName('progressTrack')[0];
// 点击时需要实现两个功能:1. 进度条位置调整到点击处;2. 视频播放到对应的位置
progressContent.addEventListener('click', function (ev) {
let target = ev.target;
let track = progressTrack;
let total = target.clientWidth;
let offset = ev.offsetX;
track.style.width = `${offset}px`;
let rate = offset / total;
video.currentTime = rate * totalSeconds;
currentTime.innerHTML = formatTime(rate * totalSeconds);
})
// 设置播放总时长
let totalTime = videoBox.getElementsByClassName('totalTime')[0];
let currentTime = videoBox.getElementsByClassName('currentTime')[0];
video.addEventListener('canplaythrough', function (ev) {
totalSeconds = ev.target.duration;
totalTime.innerHTML = formatTime(totalSeconds);
})
// 播放过程中更改进度条和已播放时间
let timer = null;
video.addEventListener('playing', function (ev) {
if(timer) {
clearInterval(timer);
}
timer = setInterval(function () {
let currentSeconds = ev.target.currentTime;
currentTime.innerHTML = formatTime(currentSeconds);
let progressWidth = progressContent.clientWidth * (currentSeconds/totalSeconds)
progressTrack.style.width = `${progressWidth}px`;
}, 250)
})
video.addEventListener('pause', function (ev) {
if(ev.target.ended) {
progressTrack.style.width = `${progressContent.clientWidth}px`;
playBtn.innerHTML = '播放';
}
clearInterval(timer);
})
// 播放暂停
let playBtn = videoBox.getElementsByClassName('play')[0];
playBtn.addEventListener('click', function (ev) {
if(video.paused) {
video.play();
ev.target.innerHTML = '暂停';
} else {
video.pause();
ev.target.innerHTML = '播放';
}
})
// 音量调整:这里简单点,就直接设置两个按钮,控制音量的加减
let addVolumeBtn = videoBox.getElementsByClassName('addVolume')[0];
let subVolumeBtn = videoBox.getElementsByClassName('subVolume')[0];
let step = 0.1;
addVolumeBtn.addEventListener('click', function () {
if(video.volume < 1) {
video.volume += step;
} else {
video.volume = 1;
}
console.log('当前音量:', 100*video.volume)
})
subVolumeBtn.addEventListener('click', function () {
if(video.volume > 0) {
video.volume -= step;
} else {
video.volume = 0;
}
console.log('当前音量:', 100*video.volume)
})
// 倍速
let speedWarp = videoBox.getElementsByClassName('speedWarp')[0];
let speedList = videoBox.getElementsByClassName('speedList')[0];
let speedBtn = videoBox.getElementsByClassName('speed')[0];
speedWarp.addEventListener('mouseover', function () {
speedList.style.display = 'block';
})
speedWarp.addEventListener('mouseout', function () {
speedList.style.display = 'none';
})
speedList.addEventListener('click', function (ev) {
let target = ev.target;
if(target.tagName.toLowerCase() === 'li') {
let speed = parseFloat(target.dataset.val);
video.playbackRate = speed;
if(speed != 1) {
speedBtn.innerHTML = target.innerHTML;
} else {
speedBtn.innerHTML = '倍速';
}
}
})
// 全屏
let fullScreenBtn = videoBox.getElementsByClassName('fullScreen')[0];
fullScreenBtn.addEventListener('click', function () {
video.requestFullscreen();
})
实现效果
image-20200615222920576.png