直播流视频播放组件总结

最近做了一个播放实时视频的功能,主要是对视频直播流的播放。
在这里 写个博客来做个总结和以后借鉴参考。

在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();
   });

以上只做粗浅记录,并非教程或说明,请酌情参考。

@快乐是一切

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值