一、需求

一开始用的海康服务插件做的视频监控,但是后来需要付费了,于是不得不寻找新的解决方案。
需要实现的几个功能:
1、初始按顺序加载所有监控(默认)
2、窗口可选择,可手动控制监控点和监控窗口,同一窗口可切换监控画面
3、监控视频(摄像头)可控制方向、缩放
4、各监控窗口左上角显示当前摄像头的名称及位置信息
5、切换窗口规格(1x1,2x2,3x3等)
6、切换规格时,已有的监控画面不更新,直接缩小到对应尺寸(未实现)
7、全屏
8、浏览器长连接(浏览器同域名并发限制-我的chrome浏览器限制个数是6;意思就是,如果一个页面是3x3的9个监控,但是只能加载6个,剩下的3个都一直处于等待状态中)2021.12.02 更新:浏览器对ws没有并发限制
9、监控延时(未实现)
10、监控画面出现前展示加载动画,黑屏用户体验不友好(未实现)
11、画面铺满整个播放器

二、效果

ipcMain监控窗体大小变化 监控屏幕如何调大小_加载

三、实现

法1:
首先index.html中引入插件,

<script src="./video/EasyWasmPlayer.js"></script>

这里有个坑,create react app中new WasmPlayer时会报’WasmPlayer is not defined‘的错,这里提醒一下大家,如果有这个错,改成 new window.WasmPlayer就好了,或者自己修改webpack配置。

法2
npm 安装

npm install @easydarwin/easywasmplayer --save

引入

import WasmPlayer from '@easydarwin/easywasmplayer' //导入WasmPlayer.js

这样直接new WasmPlayer就好了

1、首先把动态的窗口排一下

1、render里面

<img onClick={this.quanping} src={imgURL } className="quanping"/>
<div className='VideoMonitorNewMix' ref={fullScreenRef}>//在你想要设置成全屏的div里面写ref
    {this.createGrid()} //父元素用display: flex;flex-wrap: wrap;布局                   
</div>

2、js里面

const fullScreenRef = React.createRef();
quanping = () => {
   fullScreenRef.current.requestFullscreen();//全屏
}
componentDidMount() {
	for(let i=0;i<this.state.Size;i++){
    	window['flvPlayer'+i] = new WasmPlayer(
    		null, 
    		'video'+i, 
    		this.callbackfun,{
		        Height:true
		    })
	}
}
// 动态生成Grid
createGrid() {
    var res = [];
    for (let i = 0; i < this.state.Size; i++) {
        res.push(
            <div className={'video_panel' + Math.sqrt(this.state.Size)}>
                <div id={'video'+i} onClick={(e)=>{this.getCameraIndex(i+'',e)}}></div>//getCameraIndex控制窗口的选中状态
                <div id={"info"+i} className="info"></div>
            </div>);
    }
    return res;
}

3、scss

.VideoMonitorNewMix{
    width: 100%;
    height: calc(100% - 40px);
    display: flex;
    flex-wrap: wrap;
	//@mixin video_panel($count) {
	//width:100% / $count;
	//height:100% / $count;
	//border:1px solid #061b31;
	//}
	//1x1
	.video_panel1 {
	    // @include video_panel(1);//想用scss写的,报错了,尴尬!!
	    width:100%;
	    height:100%;
	    border:1px solid #061b31;
	    position: relative;
	    .info{
	        color: yellow;
	        position: absolute;
	        left: 10px;
	        top: 10px;
	    }
	}
	// 2*2
	 .video_panel2 {
	     // @include video_panel(2);
	     width:50%;
	     height:50%;
	     border:1px solid #061b31;
	     position: relative;
	     .info{
	         color: yellow;
	         position: absolute;
	         left: 10px;
	         top: 10px;
	     }
	 }
 }
2、初始按顺序加载所有监控(默认)

在componentDidMount中,确保已经获取所有监控数据this.state.MonitorData过后,开始加载,这里注意一下窗口数量和监控数量的取舍,保证资源的可利用性

for(let i=0;i<(this.state.Size<=this.state.MonitorData.length?this.state.Size:this.state.MonitorData.length);i++){
   this.flv_reload(this.state.MonitorData[i].Url,i)//初始按顺序加载所有监控
}
flv_reload(rtspUrl,index=0){
        window['flvPlayer'+index]&&window['flvPlayer'+index].destroy()//释放播放器
        let deleteIndex,camera
        for(let i=0;i<this.state.cameraInfo.length;i++){
            if(this.state.cameraInfo[i].index==index){
                camera=this.state.cameraInfo[i]
                deleteIndex=i
            }
        }

        if(camera){//如果正在播放,先停止
            this.state.cameraInfo.splice(deleteIndex,1)//从正在播放的监控中删除
            //为了多客户端,不发停止指令
            // getFlvOpt({
            //     opt: 'stop',
            //     rtspaddr:camera.rtspUrl,
            //     FlvAddr:camera.playurl
            // }).then(res => {
            //     this.state.cameraInfo.splice(deleteIndex,1)//从正在播放的监控中删除
            //     console.log(this.state.cameraInfo)
            //     //法1:WasmPlayer.js播放器
            //     // 实例化播放器
            //     // window['flvPlayer'+index] = new WasmPlayer(null, 'video'+key+index, this.callbackfun)
            //     // 调用播放
            //     window['flvPlayer'+index].stop(camera.playurl, 1)
            // })
        }
        getFlvOpt({
            opt: 'start',
            rtspaddr:rtspUrl
        }).then(res => {
            let playurl =res.FlvAddr
            this.state.cameraInfo.push({index:index+'',rtspUrl:rtspUrl,playurl:playurl})//把当前窗口播放的监控url保存
            console.log(this.state.cameraInfo)
            //法1:WasmPlayer.js播放器
            // 实例化播放器
            window['flvPlayer'+index] = new WasmPlayer(null, 'video'+index, this.callbackfun,{
                Height:true
            })
            // 调用播放--浏览器长连接-6个解决方案
            if(this.state.cameraInfo.length>6){
                let videoUrl=playurl.split('/')
                let port=videoUrl[2].split(':')
                playurl=videoUrl[0]+'//'+'videoService:'+port[1]+'/'+videoUrl[3]+'/'+videoUrl[4]
            }
            window['flvPlayer'+index].play(playurl, 1)

            //添加摄像头名称、位置信息
            let moniInfo=this.state.MonitorData.filter(item=>{
                return item.Url==rtspUrl
            })
            if(moniInfo&&moniInfo.length>0){
                //1、获取div
                let div = document.getElementById("info"+index);
                div.innerHTML = "";
                div.innerHTML="<p>名称:"+moniInfo[0].Name+"</p>"+
                            "<p>位置:"+moniInfo[0].Location+"</p>"
            }
        })
    }

2021.12.02 更新
由于浏览器对http请求的并发限制,导致多窗口视频监控时总需要配置hosts更改域名。但是对ws并没有这个限制,于是上段代码示例中 getFlvOpt优化如下:

getFlvOptNew({
            opt:"getvideobyrtsp",
            rtspaddr:rtspUrl,
            type:'flv'
        }).then(res => {
            if(res.code==200){                
                //ws播放,浏览器没有长连接限制--2021.12.02优化
                let ws_addrArr=res.data.ws_url.split('/')
                let ws_playFlvAddr=ws_addrArr[0]+"//"+window.location.host+"/"+ws_addrArr[3]+"/"+ws_addrArr[4]
                res.data.ws_url=ws_playFlvAddr
                let ws_playurl =res.data.ws_url
                //ws方案-end
                this.state.cameraInfo.push({index:index+'',rtspUrl:rtspUrl,playurl:playurl,DevId:DevId})//把当前窗口播放的监控url保存
                console.log(this.state.cameraInfo)
                //法1:WasmPlayer.js播放器
                // 实例化播放器
                window['flvPlayer'+index] = new WasmPlayer(null, 'video'+index, this.callbackfun,{
                    Height:true
                })
                window['flvPlayer'+index].play(ws_playurl, 1)

                //添加摄像头名称、位置信息
                let moniInfo=this.state.MonitorData.filter(item=>{
                    return item.Url==rtspUrl
                })
                if(moniInfo&&moniInfo.length>0){
                    //1、获取div
                    let div = document.getElementById("info"+index);
                    div.innerHTML = "";
                    div.innerHTML="<p>名称:"+moniInfo[0].Name+"</p>"+
                                "<p>位置:"+moniInfo[0].Location+"</p>"
                }
            }else{
                Feedback.toast.prompt(res.message);
            }
        })

2021.12.02 更新end

3、窗口可选择,可手动控制监控点和监控窗口,同一窗口可切换监控画面
//选择窗口-为了选择监控点
    getCameraIndex=(index,e)=>{
        for(let i=0;i<this.state.Size;i++){
            document.getElementById('video'+i).className = '';
        }
        document.getElementById('video'+index).className = 'active';
        this.setState({
            index:index+'',
            video:''
        })
    }
.active{
   border:1px solid yellow !important;
}
4、监控视频(摄像头)可控制方向、缩放
<div className="VideoMixControl">
   <div className='controlBtn'>
        <Icon type="arrow-up" size="small" className="up" onMouseDown={(e)=>this.controlCamera('up',e)} onMouseUp={(e)=>this.stopControlCamera('up',e)}/>
        <div style={{display:"flex"}}>
            <Icon type="arrow-left" size="small" className="left" onMouseDown={(e)=>this.controlCamera('left',e)} onMouseUp={(e)=>this.stopControlCamera('left',e)}/>
            <div className="control"></div>
            <Icon type="arrow-right" size="small" className="right" onMouseDown={(e)=>this.controlCamera('right',e)} onMouseUp={(e)=>this.stopControlCamera('right',e)}/>
        </div>
        <Icon type="arrow-down" size="small" className="down" onMouseDown={(e)=>this.controlCamera('down',e)} onMouseUp={(e)=>this.stopControlCamera('down',e)}/>
    </div>
    <div className="controlZoomBtn">
        <Icon type="add" size="large" className="add" onMouseDown={(e)=>this.controlCamera('add',e)} />
        <Icon type="minus" size="large" className="minus" onMouseDown={(e)=>this.controlCamera('minus',e)}/>
    </div>
    <img onClick={::this.quanping} src={imgURL } className="quanping"/>
</div>
//控制摄像头方向
    controlCamera=(direction,e)=>{
        if(this.state.index==""){
            Feedback.toast.prompt("请选择窗口");
            return
        }
        let camera=this.state.cameraInfo.filter(item=>{
            return item.index==this.state.index
        })
        if(camera.length==0){
            Feedback.toast.prompt("当前窗口没有监控");
            return
        }
        let CameraisCtrl=this.state.MonitorData.filter(item=>{
            return item.Url==camera[0].rtspUrl
        })
        console.log(CameraisCtrl)
        let opt=''
        switch(direction){
            case 'up':
                opt='UpStart';break;
            case 'right':
                opt='RightStart';break;
            case 'down':
                opt='DownStart';break;
            case 'left':
                opt='LeftStart';break;
            case 'add':
                opt='ZoomIn';break;
            case 'minus':
                opt='ZoomOut';break;
        }
        controlCameraOpt({
            opt:opt,
            ipaddr:CameraisCtrl[0].IPAddr,
        }).then(res => {

        })
    }
    //停止摄像头旋转
    stopControlCamera=(direction,e)=>{
        if(this.state.index==""){
            Feedback.toast.prompt("请选择窗口");
            return
        }
        let camera=this.state.cameraInfo.filter(item=>{
            return item.index==this.state.index
        })
        if(camera.length==0){
            Feedback.toast.prompt("当前窗口没有监控");
            return
        }
        let CameraisCtrl=this.state.MonitorData.filter(item=>{
            return item.Url==camera[0].rtspUrl
        })
        console.log(CameraisCtrl)
        let opt=''
        switch(direction){
            case 'up':
                opt='UpStop';break;
            case 'right':
                opt='RightStop';break;
            case 'down':
                opt='DownStop';break;
            case 'left':
                opt='LeftStop';break;
        }
        controlCameraOpt({
            opt:opt,
            ipaddr:CameraisCtrl[0].IPAddr,
        }).then(res => {

        })
    }

四、总结

主要技术点:
1、动态排列成栅格布局–参考这个 2、浏览器长连接–了解详细请点击这里 3、画面铺满整个播放器–了解详情点击这里

五、参考

easywasmplayer文档easyWasmPlayer demo开源流媒体服务器-