👉 个人博客主页 👈
📝 一个努力学习的程序猿
专栏:
HTML和CSS
JavaScript
jQuery
Vue
Vue3
React
TypeScript
uni-app
Linux
个人经历+面经+学习路线【内含免费下载初级前端面试题】
更多前端面试分享
前端学习+方案分享(VitePress、html2canvas+jspdf、vuedraggable、videojs)
前端踩坑日记(ElementUI)
在最近的开发中遇到一个需求:需要在页面上看到直播流(摄像头监控 / 直播推流)。本文将实现该功能,并不断解决遇到的问题 + 进行优化。
寻找测试直播流的朋友看这里:
rtsp、rtmp、m3u8、flv、mkv、3gp、mp4直播流测试地址
可进行测试的 m3u8 链接
除此以外,最简单的方法是使用 obs studio 自己推流,即可看到播放效果。
前言(video标签使用 + videojs介绍)
关于播放视频,第一时间就想到了 video 标签。示例代码:
<template>
<div class="video-container">
<div v-if="video" class="video-btn">
<button @click="playPause">
{{ isVideoPlay ? '暂停' : '播放' }}
</button>
<button @click="skipAhead(1)">
快进1s
</button>
<button @click="changeVolume(-5)">
减小音量
</button>
<button @click="changeVolume(5)">
放大音量
</button>
</div>
<video
id="basicVideo"
controls
autoplay
loop
muted
preload="auto"
width="800"
height="450"
>
<source src="http://www.w3school.com.cn/i/movie.mp4" type="video/mp4">
<source src="http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8" type="application/x-mpegURL">
对不起,您的浏览器不支持 HTML5 视频。
</video>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from '@vue/composition-api'
export default defineComponent({
name: 'VideoPage',
setup() {
const video = ref(null) as any
const isVideoPlay = ref(true)
onMounted(() => {
video.value = document.getElementById('basicVideo')
if (video.value) {
// 监听视频播放事件
video.value.addEventListener('play', function() {
console.log('视频开始播放')
})
// 监听视频暂停事件
video.value.addEventListener('pause', function() {
console.log('视频已暂停')
})
// 监听视频结束事件
video.value.addEventListener('ended', function() {
console.log('视频播放结束')
})
// 监听视频时间更新事件
video.value.addEventListener('timeupdate', function() {
console.log('当前播放时间:', video.value.currentTime)
})
}
})
function playPause() {
if (video.value.paused) {
video.value.play()
isVideoPlay.value = true
} else {
video.value.pause()
isVideoPlay.value = false
}
}
function skipAhead(seconds) {
video.value.currentTime += seconds
}
function changeVolume(delta) {
video.value.volume = Math.max(0, Math.min(1, video.value.volume + delta))
}
return {
video,
isVideoPlay,
playPause,
skipAhead,
changeVolume,
}
},
})
</script>
<style scoped lang="scss">
.video-container {
max-width: 800px;
margin: 0 auto;
.video-btn {
display: flex;
align-items: center;
button {
margin-right: 10px;
}
}
video {
width: 100%;
height: auto;
}
}
</style>
其实如果只是简单场景,播放个视频,video 标签已经很适用,它能作用于几乎全主流的浏览器,而且用起来也很简单。但放弃它原因很简单:
1、video 标签支持的主要是基于 HTTP 的流媒体协议,如 HLS。(不过这也不算是缺陷,当前 RTMP 已经无法使用了,主流全部都是 HTTP。在下文中有说明 RTMP 的问题)
2、video 标签播放功能相对简单。换句话说,如果我想要为其增加功能做扩展,可能需要处理不同设备的兼容性问题,考虑的事情很多,不如直接开箱即用的组件库,即 videojs。
videojs 是一个开源 HTML5 播放器框架,在 videojs 官网 中可以看到它很多特点:
1、videojs 不仅可以播放传统的文件格式,比如:MP4、WebM、Ogg,也能够支持自适应流格式,如 Hls、rtmp;
2、播放器开箱即用,而且可以很轻松的设置额外的 CSS 样式;
3、使用 videojs 可以支持现代的所有浏览器,包括桌面和移动浏览器。
因为官网无中文汉化,且内容较多,不太好找到关键配置,所以本文也参考了很多成功的文章案例,感谢各位大佬们的付出。本文希望能给各位提供帮助。
videojs 官网:https://videojs.com/
视频播放插件Video.js:https://www.jq22.com/plugin/404
vue项目中video.js的使用总结:https://blog.csdn.net/weixin_39135926/article/details/118225035
videojs的一些监听事件汇总:https://blog.csdn.net/Q147351/article/details/106663908/
1、Vue 中使用 videojs 做直播流
由于网上真的有很多繁杂的内容,而且大多是直接在 html 中使用,而不是针对 Vue,所以我在寻找解决方案的过程中,也是费了一些时间。在这里直接给出代码:
npm install video.js --save
npm install videojs-contrib-hls --save
在main.js引入样式文件
import 'video.js/dist/video-js.css'
示例:
<template>
<div class="video-container">
<video
id="my-video"
class="video-js vjs-default-skin"
width="800"
height="450"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from '@vue/composition-api'
import videojs from 'video.js'
import 'videojs-contrib-hls'
export default defineComponent({
name: 'VideoTest',
setup() {
const video = ref(null) as any
onMounted(() => {
video.value = videojs('my-video', {
autoplay: true, // 是否自动播放
controls: true, // 是否显示控件
fluid: true, // 是否使用流式布局(自适应父容器)
responsive: true, // 是否响应式
playbackRates: [0.5, 1, 1.5, 2], // 可选的播放速率
muted: false, // 是否静音
loop: false, // 是否循环播放
preload: 'auto', // 预加载方式:'auto', 'metadata', 'none'
// poster: '', // 视频封面图片
// width: 500, // 播放器宽度
// height: 264, // 播放器高度
aspectRatio: '16:9', // 视频宽高比
controlBar: { // 控制栏配置
volumePanel: { inline: false }, // 音量控制是否内联
fullscreenToggle: true // 是否显示全屏按钮
},
language: 'zh-CN', // 播放器语言
// 视频源
// sources: [{
// type: 'video/mp4',
// src: 'http://www.w3school.com.cn/i/movie.mp4'
// }],
// techOrder: ['html5', 'flash'], // 技术优先级
}, onPlayerReady)
// 因为通常需要等待接口返回地址,所以可以用这样的方式,重新绑定直播流
video.value.src({
src: 'http://www.w3school.com.cn/i/movie.mp4',
type: 'video/mp4',
})
/** 可选type值
video/mp4: MP4 格式
video/webm: WebM 格式
video/ogg: Ogg 格式
application/x-mpegURL 或 application/vnd.apple.mpegurl: HLS 流媒体格式
rtmp/mp4: RTMP 流媒体(MP4)
rtmp/flv: RTMP 流媒体(FLV)
video/x-flv: FLV 格式
video/3gpp: 3GPP 格式(常用于移动设备)
video/quicktime: QuickTime 格式
video/x-matroska: MKV 格式
video/x-ms-wmv: Windows Media Video 格式
application/dash+xml: MPEG-DASH 流媒体格式
* */
})
// 事件监听
const onPlayerReady = () => {
if (video.value) {
video.value.on('loadstart', function() {
console.log('开始请求数据')
})
video.value.on('progress', function() {
console.log('正在请求数据')
})
video.value.on('loadedmetadata', function() {
console.log('获取资源长度完成')
})
video.value.on('canplaythrough', function() {
console.log('视频源数据加载完成')
})
video.value.on('waiting', function() {
console.log('等待数据')
})
video.value.on('play', function() {
console.log('视频开始播放')
})
video.value.on('playing', function() {
console.log('视频播放中')
})
video.value.on('pause', function() {
console.log('视频已暂停')
})
video.value.on('ended', function() {
console.log('视频播放结束')
})
video.value.on('error', function() {
console.log('加载错误')
})
video.value.on('seeking', function() {
console.log('视频跳转中')
})
video.value.on('seeked', function() {
console.log('视频跳转结束')
})
video.value.on('ratechange', function() {
console.log('播放速率改变')
})
video.value.on('timeupdate', function() {
console.log('播放时长改变')
})
video.value.on('volumechange', function() {
console.log('音量改变')
})
video.value.on('stalled', function() {
console.log('网速异常')
})
}
}
return {}
},
})
</script>
<style scoped lang="scss">
.video-container {
max-width: 800px;
margin: 0 auto;
video {
width: 100%;
height: auto;
}
}
</style>
在测试的时候,如果用测试直播流可能会碰到跨域的问题,烦请自行解决。
2、videojs 遇到的问题总结
接下来是本文的主要内容,记录了我自己在做直播流过程中,遇到的问题和相关解决方案。
问题一:The element or ID supplied is not valid
在最开始使用的时候,我遇到了下面的报错:
针对这个报错,在网上有一些相关的参考方案:
video.js多个视频初次加载报错The element or ID supplied is not valid
而我在尝试了几种方法后,发现并没有解决。所以在回到原点后,针对报错信息思考了一下,其实就是在使用 videojs 获取 ID 时,遇到了问题。
经过自己的排查发现,问题的原因其实很简单。我在使用 getElementByID 时,ID 写错了,其中包含了特殊字符。也就是说需要注意,在写 ID 名时,确保不含有 # 特殊字符(其他特殊字符或许也会有问题,尽量避免)。除此以外,请务必注意执行顺序(比如在 created 生命周期调用 getElementById 肯定拿不到这个元素,因为这时候元素还未生成。需要把这样的操作,放到 mounted 中)。
问题二:video 销毁问题
实现销毁看起来是很简单的,就是在对应的时间点上去触发 dispose 事件:
beforeDestroy() {
if (video.value !== null) {
video.value.dispose() // dispose()会直接删除Dom元素
video.value = null
}
},
但是需要注意的是,当我们对 videojs 对象使用了 dispose 后,它会直接删除这个 DOM 元素。那么这会有什么后果,又为什么要进行销毁?
1、首先,我们必须在不想播放该视频流的时候进行销毁。
因为是直播,如果我们不对其进行销毁,那么离开当前页面后,这部分切片内容(即视频)仍然会不断的进行获取。那显而易见的,如果用户又点击查看了不同的视频流,最后获取到的内容越来越多。当多到一定限额时,后果可想而知。
如果你想着 F5 就完事大吉了,那也不对。因为生成直播流,一定是前端告知后端去生成(无论你们技术选型如何)。如果你不告知后端,前端已经关闭,后端可能依然会生成切片内容到主机(除非停止推流)。这也是需要考虑的前后端交互问题。
2、现在我们已经知道了需要对它进行销毁,那么直接使用 dispose 会有什么后果?
因为这样操作会直接删除 DOM 元素,所以这就会导致使用 dispose 后,如果还想重启 videojs,你就没办法再像 mounted 里那样:video.value = videojs('my-video', ...)
了,因为 id 为 'my-video'
的 DOM 元素已经被删除了。
所以现在针对问题,要做的事情是:
(1)在离开当前页面或必要情景时,需要将之前的 videojs 进行销毁(如有必要,需要告知后端,前端已经停止获取)。
(2)在 videojs 被销毁后,想重新播放视频流时,要保证 video 标签的存在。
3、解决方案:
(1)如果你想直接操作 DOM,那你可以这样做
// 假设你有一个重新初始化的函数
function reinitializePlayer() {
// 1. 确保旧实例被销毁
if (video.value) {
video.value.dispose();
video.value = null;
}
// 2. 重新创建 video 元素
const videoContainer = document.getElementById('video-container');
const videoElement = document.createElement('video');
videoElement.id = 'my-video';
videoElement.className = 'video-js';
videoContainer.appendChild(videoElement);
// 3. 重新初始化 video.js
video.value = videojs('my-video', {
// ... 配置项
}, onPlayerReady);
// 4. 设置新的视频源(如果需要)
video.value.src({
src: 'https://example.com/new-video-source.mp4',
type: 'video/mp4'
});
}
(2)更巧妙的方式是利用 Vue 中 v-if 的机制:它在 true、false 转化时,对组件是重新销毁和创建的。你可以把上述 videojs 的功能封装一个组件,当要播放时 v-if 为 true,此时就会正常进入 mounted 播放,DOM 也都存在。当想要实现销毁时,就 v-if 为 false,从而触发该组件的 beforeDestroy 事件。
(3)最后的最后,如果需要告知后端停止生成,就可以放在 beforeDestroy 事件中。通信可以考虑使用 websocket ,或者在页面销毁时直接调用http接口通知后端,或者其他技术选型。
问题三:防抖问题
如果你是通过按钮来开启 videojs 视频流的话,防抖必不可少。
<button @click="startTransFlow">开始生成</button>
防抖的代码:
if (timer.value === null) {
/* 防抖 */
timer.value = setTimeout(() => {
timer.value = null
}, 25000)
/* 调用接口,video处理 */
}
防抖就是来避免用户可能会误操作点击多次按钮的,在这里如果触发多次 video 事件,显然可能会出现页面问题。当然,就算不是点击按钮触发,平常也需要注意页面中是否可能会多次触发这些事件。如果多次触发,带来的后果将是不可预估的。
问题四:未获取到对应视频流的重新加载问题
为了更好的解释问题,代码简单展示如下:(代码不完整,仅用于展示问题)
<template>
<div>
<el-button
type="primary"
@click="startTransFlow"
>测试</el-button>
<video
v-if="showVideo"
id="cameraIndexCode"
class="video-js vjs-default-skin"
width="500"
height="264"
></video>
</div>
</template>
<script>
export default {
// ...
methods: {
startTransFlow() {
if (this.timer === null) {
/* 防抖 */
this.timer = setTimeout(() => {
this.timer = null
}, 35000)
// ...
// 调用接口,触发生成 m3u8 文件的事件。最后拿到接口返回的url
// ...
const _this = this;
this.$message.info('摄像头加载中');
this.showVideo = true;
setTimeout(() => {
// 选中的要播放的video标签
this.videoPlayer = this.$video(document.getElementById('cameraIndexCode'), {
autoplay: true, // 是否自动播放
controls: true // 是否显示控件
}, function onPlayerReady() {
let num = 1;
this.on('canplaythrough', () => {
_this.$message.success('摄像头加载成功');
});
// 加载失败会在有限次数内,不断重试
this.on('error', () => {
if (num > 3) {
_this.$message.error('摄像头加载失败,请重新尝试');
num = 1;
} else {
_this.$message.info('摄像头第' + num + '次尝试加载....');
num++;
setTimeout(() => {
this.src(url); // 放入接口返回的url
this.play();
}, 10000)
}
});
this.src(url); // 放入接口返回的url
this.play();
})
}, 5000)
}
}
}
}
</script>
在这里使用到了 function onPlayerReady()
去反复加载,并进行相应的提示。先阐述一下,为什么会需要反复加载的功能。
在项目中,播放视频流需要通过按钮去触发,从而调用接口,告知后台去对视频流进行处理。此时,后台处理视频流必然需要一些时间,才能给前端返回一个对应的存放 m3u8 (或者其他格式视频)位置的路径。
所以就出现了在获取时 可能获取不到,从而报错的问题(也就是大家看直播经常需要加载和反复刷新的问题)。
那问题就很明确了,无论什么原因,此时直播视频流文件还没生成出来,所以前端也应该给用户一些友好提示,并作出短暂时间的等待。而为了解决这个问题,就如同上面代码一样,用到了 error 事件。只要遇到了报错,就会进入到该事件中。在代码中将会在有限的次数和时间下,反复去尝试对应的视频源是否加载完毕,并在加载完成后去进行播放。
而反复去尝试的原理,就是使用:
this.src(url);
this.play();
只要 src 变化,并尝试播放,videojs 就会被调用。它的流程就是这样的,比较简单:
http-flv 低延时优化说明
说到这里,笔者还想补充说明一些内容。
在上文和之前自己的开发过程中,使用的是 videojs 做直播流,因为这种方式相对简单。但是在后续项目的迭代过程中发现,使用 hls 直播流是会有一些问题的。只要您真的实现了功能,一拿直播流进行测试,查看北京时间就会发现,hls 的延时和其他方案相比很高。这样一来,就无法满足低延时的需求。如果您对低延时没有要求,那就不用考虑太多问题了。
以下为自己测试多个视频流的测试结果:
hls 延时会达到 15 ~ 30 秒。
rtmp 延时只有 1 ~ 5 秒。
http-flv 延时只有 4 ~ 10 秒。
那很不幸的是,在我的开发需求中,视频流播放的延时一定要低。那显然的,hls 直播流就不满足需求了。所以我就尝试使用了 http-flv 和 rtmp 两种格式。
但实际上最终的解决方案只能是用 http-flv。因为在使用 rtmp 流时,会发生这样的报错:
起初,没觉得是什么大问题,最多也就是跨域问题。但通过在网上参考了很多篇文章,发现这个问题完全解决不掉。最后,一篇文章吸引了我的注意力:Chrome将在2020年底不再支持Flash,那如何播放rtmp格式的监控或直播视频?
那也就是说,出现这个问题的最根本原因,就是 chrome 及其他浏览器要逐步淘汰 flash(而经过测试,至少 2022年 chrome 最新版本已经不支持了)。
当然,我们肯定不能要求使用者只使用特定版本的浏览器。所以才有了下文对 http-flv 的使用说明。
那么言归正传,http-flv 该如何使用呢?由于笔者在参考了以下几篇文章后发现完全可行,且没有遇到其他问题,所以尊重原创,在这里就不大篇幅引用了。 关于更详细的使用内容,您需要前往以下文章查阅:
flv.js API (API 全英)
Debian使用Nginx和Nginx-http-flv-module来实现简单的直播服务
至此前端在 Vue 做直播流的内容就完成了。希望这些参考文章能够为您提供帮助!