1. HTML5 视频播放
通常,我们在页面上进行播放的视频是通过video
标签,在src 属性里加入一个我们需要展示的视频地址,浏览器会根据其设置的属性,例如宽度、高度、是否自动播放、循环播放等属性,通过浏览器默认的视频控件播放视频。
<video controls width="250">
<source src="/media/cc0-videos/flower.webm" type="video/webm">
<source src="/media/cc0-videos/flower.mp4" type="video/mp4">
Sorry, your browser doesn't support embedded videos.
</video>
复制代码
2. 常见事件和属性
常见实现:
属性 | 描述 |
---|---|
autoplay | 指定该属性后,不会等待数据加载完成,直接自动播放 |
buffered | 可以读取到哪段时间范围内的媒体被缓存了 |
controls | 允许用户控制视频的播放,例如音量、暂停、恢复等 |
controlslist | 当指定了controls,controlslist可以帮助浏览器选择媒体元素上的显示控件(接收参数:nodownload,nofullscreen …. ) |
currentTime | 设置改值后,video会将其设置为当前播放的开始时间 |
volume | 在0.0到1.0之间设置音频音量的相对值,或者查询当前音量相对值 |
muted | 是否静音,为文件设置静音或消除静音 |
startTime | 一般为0,如果为流媒体或者不从0开始的资源,则不为0 |
duration | only-read 媒体文件的总长度。有些媒体(例如未知的实时流、网络广播、来自WebRTC的媒体)没有该值,返回时NAN |
paused | only-read 是否被暂停 |
ended | only-read 音频/视频的播放是否已结束 |
height / width | 视频的高/宽度,单位是css像素 |
loop | 指定后,当视频播放到末尾会自动返回视频开始的地方 |
poster | 视频封面,没有播放时显示的图片 |
preload | none: 不提前缓存视频, metadata: 合理抓取源数据。 auto:需要优先加载这个视频 |
src | 嵌入视频的URL |
常见事件:
事件 | 触发时机 |
---|---|
loadstart | 开始加载 |
durationchange | duration 属性值修改时触发 |
ratechange | 播放速率改变时触发 |
seeking | seeking 寻找中 点击一个为(缓存)下载的区域 |
seeked | seeked 寻找完成时触发 |
play | 开始播放时触发 |
waiting | 播放由于下一帧数据未获取到导致播放停止,但是播放器没有主动预期其停止,仍然在努力的获取数据,简单的说就是在等待下一帧视频数据,暂时还无法播放。 |
playing | 我们能看到视频时触发,也就是真正处于播放状态 |
canplay | 浏览器可以播放媒体文件,但是没有足够的数据支撑到播放结束,需要不停缓存更多内容 |
pause | 暂停播放时触发 |
ended | 视频停止,media已经播放到终点时触发, loop 的情况下不会触发 |
volumechange | 音量改变时触发 |
loadedmetadata | 获取视频meta信息完毕,这个时候播放器已经获取到了视频时长和视频资源的文件大小。 |
loadeddata | media中的首帧已经加载时触发, 视频播放器第一次完成了当前播放位置的视频渲染。 |
abort | 客户端主动终止下载(不是因为错误引起) |
error | video.error.code: 1.用户终止 2.网络错误 3.解码错误 4.URL无效 |
canplaythrough | 浏览器可以播放文件,不需要停止缓存更多内容 |
progress | 客户端请求数据 |
timeupdate | 当video.currentTime发生改变时触发该事件 |
stalled | 网速失速 |
suspend | 延迟下载 |
方法:
方法 | 描述 |
---|---|
play() | 播放视频 |
pause() | 暂停视频 |
canPlayType() | 测试video元素是否支持给定MIME类型的文件 |
requestFullscreen() / mozRequestFullScreen() / webkitRequestFullScreen() | 全屏 |
3. 自定义视频播放器
首先需要去掉video
身上的属性controls
属性,将所有播放的动作交由我们自己控制。
3.1 自定义播放或暂停
const playBtn = document.getElementById('playBtnId');
playBtn.addEventListener('click', function() {
if (video.paused) {
video.play();
playBtn.textContent = '||'; // 切换样式
} else {
video.pause();
playBtn.textContent = '>'; // 切换样式
}
});
复制代码
3.2 音量控制
// 音量增加
const volIncBtn = document.getElementById('volIncId');
volIncBtn.addEventListener('click', function() {
video.volume > 0.9 ? (video.volume = 1) : (video.volume += 0.1);
});
// 音量减小
const volDecBtn = document.getElementById('volDecId');
volDecBtn.addEventListener('click', function() {
video.volume < 0.1 ? (video.volume = 0) : (video.volume -= 0.1);
});
复制代码
3.3 静音
const mutedBtn = document.getElementById('mutedId');
mutedBtn.addEventListener('click', function() {
video.muted = !video.muted;
mutedBtn.textContent = video.muted ? '恢复' : '静音';
});
复制代码
3.4 播放快进/快退
- 快进
const speedUpBtn = document.getElementById(speedUpId);
let _speed = 1;
speedUpBtn.addEventListener('click', function() {
_speed = _speed * 2;
if (_speed > 4) {
_speed = 1;
}
video.playbackRate = _speed;
speedUpBtn.textContent = _speed === 1 ? '快进' : '快进x' + _speed;
});
复制代码
- 快退
const backBtn = document.getElementById(backBtnId);
let back_speed = 1;
let _t;
backBtn.addEventListener('click', function() {
back_speed = back_speed * 2;
if (back_speed > 4) {
video.playbackRate = 1;
back_speed = 1;
clearInterval(_t);
} else {
video.playbackRate = 0;
clearInterval(_t);
_t = setInterval(function() {
video.currentTime -= back_speed * 0.1;
}, 100);
}
backBtn.textContent = back_speed === 1 ? '快退' : '快退x' + back_speed;
});
复制代码
3.5 全屏
const fullScreenBtn = document.getElementById(fullScreenId);
const fullScreen = function() {
fullScreenBtn.addEventListener('click', function() {
if (video.requestFullscreen) {
video.requestFullscreen();
} else if (video.mozRequestFullScreen) {
video.mozRequestFullScreen();
} else if (video.webkitRequestFullScreen) {
video.webkitRequestFullScreen();
}
});
};
复制代码
3.6 进度条和时间显示
const getTime = function() {
// 当前播放时间
nowTime.textContent = 0;
// 总时长
duration.textContent = 0;
video.addEventListener('timeupdate', function() {
// 当前播放时间, parseTime: 格式化时间
nowTime.textContent = parseTime(video.currentTime);
// 计算进度条
const percent = video.currentTime / video.duration;
playProgress.style.width = percent * progressWrap.offsetWidth + 'px';
});
video.addEventListener('loadedmetadata', function() {
// 更新视频总时长
duration.textContent = parseTime(video.duration);
});
};
复制代码
3.7 手动点击进度条快进(视频跳跃)
progressWrap.addEventListener('click', function(e) {
if (video.paused || video.ended) {
video.play();
}
const length = e.pageX - progressWrap.offsetLeft;
const percent = length / progressWrap.offsetWidth;
playProgress.style.width = percent * progressWrap.offsetWidth + 'px';
video.currentTime = percent * video.duration;
});
复制代码
4. 视频分段加载
上面进行了HTML5 视频播放器的相关信息,那么接下来,我们需要从服务器端获取video到客户端。
当视频文件很大的时候,建议使用流式传输视频,它支持任何大小。通过利用fs.createReadStream()
,服务器可以读取流中的文件,而不是一次性读取整个文件到内存中。然后通过range的方式请求,将视频发送到客户端。而客户端也不用等页面从服务端下载整个视频,可以在视频播放开始的前几秒钟请求服务器,就可以达到边请求边播放视频了。
- fs.statSync(): 该方法用于获取文件的统计信息,我们可以拿到当前加载的chunk到达文件末端时候的文件大小。
fileSize = fs.statSync(filePath).size
- fs.createReadStream(): 给指定文件创建流fs.createReadStream(filePath, { start, end })
- 返回整个数据块的大小: endChunk – startChunk。
- HTTP 206: 用于不间断地向前端提供数据块。下面的信息再请求时是必须的:
- ‘Content-Range’: ‘bytes chunkStart-chunkEnd/chunkSize’
- ‘Accept-Ranges’: ‘bytes’
- ‘Content-Length’: chunkSize
- ‘Content-Type’: ‘video/webm’
这里,我使用egg的框架,实现了range请求video的功能。
async getVideo() {
const { ctx } = this;
const req = ctx.request;
try {
const homedir = `${process.env.HOME || process.env.USERPROFILE}/`;
const filePath = path.resolve(`${process.env.NODE_ENV === 'development' ? '' : homedir}${req.query.filePath}`);
const range = req.headers.range;
const fileSize = fs.statSync(filePath).size;
if (range) {
const positions = range.replace(/bytes=/, '').split('-');
const start = parseInt(positions[0], 10);
const end = positions[1] ? parseInt(positions[1], 10) : fileSize - 1;
const chunksize = end - start + 1;
if (start >= fileSize) {
ctx.status = 416;
ctx.body =
'Requested range not satisfiable\n' + start + ' >= ' + fileSize;
return;
}
ctx.status = 206;
const header = {
'Accept-Ranges': 'bytes',
'Content-Type': 'video/webm',
'Content-Length': chunksize,
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'cache-control': 'public,max-age=31536000',
};
ctx.set(header);
ctx.body = fs
.createReadStream(filePath, {
start,
end,
autoClose: true,
})
.on('err', err => {
console.log(`[Video Play]: ${req.url}, 'pip stream error`);
ctx.body = err;
ctx.status = 500;
});
} else {
this.ctx.set('Content-Length', fileSize);
this.ctx.set('Content-Type', 'video/webm');
this.ctx.status = 200;
this.ctx.body = fs.createReadStream(filePath);
}
} catch (err) {
console.log(err);
ctx.body = err;
ctx.status = 500;
}
}
复制代码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END