最近做了一个播放实时视频的功能,主要是对视频直播流的播放。
在这里 写个博客来做个总结和以后借鉴参考。
在vue中播放视频直播流
视频直播流的常见格式是 .w3u8
这里也主要说的是此格式的视频地址的播放。
总体来说,播放视频获取直播流并没有太大的区别,主要的页面元素就是 video 标签和 source 标签。
同时在js中获取到此标签,通过定义好的方法控制视频的播放,关闭,画中画这些功能。
页面代码
这里主要是定义播放视频的组件,视频播放主要的元素是 一个 video
标签,里面包含一个soruce
标签。跟video标签相关的说明:
1、video 标签需要设置一个 唯一id,在js中通过此id来找到视频播放的元素。
2、在video标签上可以设置很多跟视频相关的属性,不过我不直接在标签上设置,而是在js中初始化视频播放的时候设置。
3、video标签中的文字是当浏览器不支持ci标签时展示的文字。
4、source 标签上主要是 播放的地址 nowVideoUrl
和视频类型 type
。
5、在video标签后跟有一个包含一张图片的div标签,这部分主要是当视频不能播放时展示的默认图片。
<template>
<div
ref="vc"
class="video-box"
style="width: 100%;
>
<video
:id="playId"
class="video-js vjs-default-skin"
>
您的浏览器不支持播放此视频
<source :src="nowVideoUrl" type="application/x-mpegURL" />
</video>
<div class="videoNull" v-show="isNull">
<img :src="imgPath" @click="showVideo">
</div>
</div>
</template>
js代码
以下是在实际在项目中使用的视频播放组件,含有大部分富余代码,酌情参考。
// 导入相关依赖
import 'video.js/dist/video-js.css'
import videojs from "video.js";
import "videojs-contrib-hls";
export default {
name: "VideoBox",
destroyed() {
if (this.player) {
this.player = null;
}
},
data(){
return {
player: null,
playId : '',
baseId: '',
isNull: false,
isPlay: true,
playBtnImg: require('../assets/map/playVideo.png'),
imgPath: require('../assets/map/errVideo.png'),
nowVideoUrl: '',
loading: true,
startInterval: null,
stopInterval: null,
refrTimeout: null,
storage: {name: 'lh_oneMap_video',request: null, db: null, get: this.storage_get, set: this.storage_set, put: this.storage_put}
}
},
components: {},
props: { // 继承上层控制属性
videoUrl: {
type: String,
default: ''
},
height: {
type: String,
default: '100%'
},
width: {
type: String,
default: '100%'
},
vStyle: {
type: String,
default: 'width: 100%;height:100%;'
},
autoplay: {
type: Boolean,
default: true
},
controls: {
type: Boolean,
default: true
},
preload: {
type: String,
default: 'auto'
},
stopTime: {
type: Number,
default: 0
},
startTime: {
type: Number,
default: 2 * 60 * 1000
},
playTim: {
type: Number,
default: 10 * 60 * 1000
},
cover: {
type: String,
default: ''
}
},
created(){
// 同一个每次调用此组件的时候,使用不同的id
this.playId = this.randomId()
this.baseId = this.randomId()
// 这一部分是打开浏览器数据库,将视频的一些信息保存起来。
this.storage.name = null
if (this.storage.name){
this.storage.request = window.indexedDB.open(this.storage.name)
let _this = this
this.storage.request.onerror = function(err){
_this.storage.request = null
}
this.storage.request.onsuccess = function(event){
_this.storage.db = this.result
}
this.storage.request.onupgradeneeded = function(event){
_this.storage.db = event.target.result
if (!_this.storage.db.objectStoreNames.contains(_this.storage.name)){
let objectStore = _this.storage.db.createObjectStore(_this.storage.name, { keyPath: 'url'})
objectStore.createIndex('url', 'url', { unique: false });
objectStore.createIndex('img', 'img', { unique: false });
}
}
}
// 设置中文
videojs.addLanguage('zh-CN', {
"Play": "播放",
"Pause": "暂停",
"LIVE": "直播",
"Loaded": "加载完毕",
"Fullscreen": "全屏",
"Non-Fullscreen": "退出全屏",
"Mute": "静音",
"Unmute": "取消静音",
"Picture-in-Picture": "画中画",
"A network error caused the media download to fail part-way.": "网络错误导致视频下载中途失败。",
"The media could not be loaded, either because the server or network failed or because the format is not supported.": "视频因格式不支持或者服务器或网络的问题无法加载。",
"The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "由于视频文件损坏或是该视频使用了你的浏览器不支持的功能,播放终止。",
"No compatible source was found for this media.": "无法找到此视频兼容的源。",
})
},
mounted() {
},
methods: {
randomId(cLen = 6){ // 生成随机id函数
let chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz'
let max = chars.length
let rId = ''
for (var i=0;i<cLen;i++){
let r = parseInt(Math.random() * max)
rId += chars[r]
}
return rId
},
// 加载视频源
showVideo(){
this.isNull = false
this.isPlay = true
if (!document.getElementById(this.playId)){
let video = document.createElement('video')
video.id = this.playId
video.className = 'video-js vjs-default-skin'
if (document.getElementById(this.baseId)){
document.getElementById(this.baseId).append(video)
}else{
console.log(this.vc)
}
}
if (this.player){
clearInterval(this.stopInterval)
clearInterval(this.startInterval)
this.player.pause()
this.player.src([{
type: "application/x-mpegURL",
src: this.nowVideoUrl
}])
this.player.play()
}else{
try{
let options = {
controls: this.controls,
language: 'zh-CN',
sources: [{
type: "application/x-mpegURL",
src: this.nowVideoUrl
}],
muted: true,
preload:this.preload,
fluid:true,
width:this.width,
height:this.height,
autoplay: this.autoplay,
liveui: true,
notSupportedMessage: "此视频暂时无法播放,请检查视频源。",
hls: {
withCredentials: true
}
}
if (this.nowVideoUrl && this.playId){
this.player = videojs(this.playId, options)
}else{
this.playClose()
return
}
}catch(err){
console.log(err)
return
}
}
let _this = this
this.player.on('loadedmetadata', function(){
this.play();
})
this.player.on('playing', function(){
setTimeout(() => {
_this.storage.get(_this.nowVideoUrl, (data) => {
if (!data){
_this.getImg()
}
})
}, 1 * 1000)
setTimeout(() => {
_this.getImg()
_this.showImg()
}, _this.playTim)
})
this.player.on('error', function(){
_this.isPlay = false
_this.playChange()
_this.storage.set({'url': _this.nowVideoUrl, 'img': 'error'})
})
this.onInterval()
this.$once('hook:beforeDestroy', function () {
this.playClose()
})
},
// 加载定时刷新
onInterval(){
if (this.stopTime > 0){
this.stopInterval = setInterval(() => {
this.player.pause()
}, this.stopTime)
}
if (this.startTime < -1){
this.startInterval = setInterval(() => {
this.showVideo()
}, this.startTime)
}
},
// 获取视频截图并展示
getImg(){
if (this.player){
let video = this.player.children_[0]
let canvas = document.createElement("canvas")
let scale = 0.8;
canvas.width = video.videoWidth * scale;
canvas.height = video.videoHeight * scale;
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
let img = canvas.toDataURL("image/png");
this.imgPath = img
if (this.storage.db){
this.storage.set({'url': this.nowVideoUrl, 'img': this.imgPath})
}
}
},
showImg(){
this.playClose()
},
playClose(){
if (this.player){
try{
this.player.dispose()
this.player = null
}catch{}
}else{
if(document.getElementById(this.playId)){
this.player = videojs(this.playId)
this.player.dispose()
this.player = null
}
}
this.isNull = true
if (this.stopInterval) clearInterval(this.stopInterval)
if (this.startInterval) clearInterval(this.startInterval)
if (this.refrTimeout) clearTimeout(this.refrTimeout)
},
playChange(){
this.loading = false
if (this.isPlay){
if (this.player) this.player.play()
}else{
if (this.startInterval) clearInterval(this.startInterval)
if (this.$parent.changeVideo){
this.$parent.changeVideo(this.nowVideoUrl)
setTimeout(() => {this.isPlay = true}, 2000)
}else{
this.playClose()
}
}
},
storage_get(url, call=function(data){}){
if (this.storage && this.storage.db){
let _this = this
let objectStore = _this.storage.db.transaction([this.storage.name]).objectStore(this.storage.name)
let res = objectStore.get(url)
res.onsuccess = function(event){
let data = this.result
call(data)
}
}
},
storage_set(data, call=function(data){}){
if (!data || !data.url || !data.img){
return
}
if (this.storage && this.storage.db){
let _this = this
let objectStore = _this.storage.db.transaction([_this.storage.name], 'readwrite').objectStore(this.storage.name)
let res = objectStore.add(data)
res.onerror = function(event){
// 更新视频图片
setTimeout((data) => {
_this.storage.put(data)
}, 1000)
}
res.onsuccess = function(event){
call(data)
}
}
},
storage_put(data, call=function(data){}){
if (!data || !data.url || !data.img){
return
}
if (this.storage && this.storage.db){
let _this = this
let objectStore = _this.storage.db.transaction([this.storage.name]).objectStore(this.storage.name)
let res = objectStore.put(data)
res.onerror = function(err){
}
res.onsuccess = function(event){
call(data)
}
}
}
},
watch: {
videoUrl(val){
if (this.refrTimeout) clearTimeout(this.refrTimeout)
// 判断url格式是否可以播放
if (val == '' || val == null || typeof(val) != typeof('a')) {
this.isPlay = false
this.playChange()
return
}else{
if (val.indexOf('m3u8') == -1 || val.length < 15){
this.isPlay = false
this.playChange()
return
}
}
// 判断是否已被标记不可播放
// 判断是否已有图片可以直接展示或不可播放
if (this.storage.name && this.storage.db){
this.storage.get(val, (data) => {
if (data){
if(data.img == 'error'){
this.isPlay = false
}else{
this.imgPath = data.img
}
this.playClose()
}
})
}
if (val != this.nowVideoUrl){
this.nowVideoUrl = val
// 判断播放器当前状态决定播放时机
if (this.loading){
this.loading = false
this.showVideo()
return
}
if (this.imgPath.length > 10000){
this.refrTimeout = setTimeout(() => {
this.showVideo()
}, 0 * 1000)
return
}
this.refrTimeout = setTimeout(() => {
this.showVideo()
}, 0* 3.5 * 1000)
}
},
isPlay(){
this.playChange()
},
isNull(){
if (this.isNull && this.imgPath.length < 10){
this.imgPath = require('../assets/map/errVideo.png')
if (!this.nowVideoUrl){
this.showVideo()
}
}
},
}
}
在h5页面中播放视频流
在 h5 页面中播放视频流,主要是用了 ezuikit 这个框架来实现。
页面
<script src="../ezuikit.js"></script>
<script src="../js/jquery.min.js"></script>
<button id="start">开始播放</button>
<button id="pause">暂停播放</button>
<button id="stop">停止播放</button>
<video
id="myPlayer"
poster=""
controls
playsInline
webkit-playsinline
>
<source src="http://122.{{路由隐藏}}.35:83/openUrl/Q8Bmgla/live.m3u8" type="application/x-mpegURL" />
</video>
h5 的js部分
var player = new window.EZUIKit.EZUIPlayer('myPlayer');
// 日志
player.on('log', log);
function log(str){
var div = document.createElement('DIV');
div.innerHTML = (new Date()).Format('yyyy-MM-dd hh:mm:ss.S') + JSON.stringify(str);
document.body.appendChild(div);
}
$("#start").click(function(){
console.log("开始播放");
player.play();
});
$("#pause").click(function(){
console.log("暂停播放");
player.pause();
});
$("#stop").click(function(){
console.log("结束播放",player);
player.stop();
});
注
以上只做粗浅记录,并非教程或说明,请酌情参考。