目录

一、测试入口

二、.Binary messages not supported

三、发送start指令

1.使用KurentoUtils(不行)

2.使用awrtc的StartSignaling(可行)

四、处理接收指令

五、暂停等指令接口

六、Unity播放

七、Unity帧率问题(视频分辨率)

八、播放多个摄像头视频

九、js核心原型代码


现在有两种方式:

1).awrtc.js连接java服务端,实现kurento-player-js的功能。

2).kurento-player-js加入awrtc.js里面的获取图片的功能,并对接unity。

先以用awrtc.js连接kurento播放视频为目标,因为awrtc.js和unity的对接已经是做好的了,只要能够播放视频,后续的unity内的代码不用做调整。

awrtc.js是AssetStore里面的插件中的js部分改造后的。

插件:WebRTC Video Chat

awrtc相关博客:WebGL实时视频(5) awrtc.js理解并修改,WebGL实时视频(6) Unity里面显示视频

awrtc本身就是一个完整的webrtc的客户端js库,可以连接自己的服务端,改造后可以连接H5Stream,这次则是要连接kurento的java服务端,这些服务端从概念上讲都是信令服务器。

一、测试入口

在复制原来的func_CAPI_H5Stream_GetVideo的基础上修改出一个func_CAPI_Kurento_GetVideo。

                function func_CAPI_Kurento_GetVideo(rtsp,serverUrl) {//[kurento]console.log("func_CAPI_Kurento_GetVideo",rtsp,serverUrl);BrowserMediaStream.DEBUG_SHOW_ELEMENTS=true;//在网页中显示视频var netConf=new NetworkConfig();netConf.SignalingUrl=serverUrl;//...}

二、.Binary messages not supported

serverUrl传入ws://192.168.1.150:8444/player,其他不变,尝试连接,结果:

awrtc.js:3171 Websocket closed with code: 1003 Binary messages not supported 

原因是出在服务端的PlayerHandler的父类TextWebSocketHandler里面有

而客户端这边,则是在SendVersion那里用InternalSend发送了个Uint8Array。

因为客户端还有其他地方(心跳包)会发送Array,修改服务端,支持BinaryMessage就好了,但是也不用做什么处理,就是可以接收就行。在PlayerHandler中添加handleBinaryMessage

  @Overrideprotected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {log.info(">> PlayerHandler.handleBinaryMessage");//...不做处理}

三、发送start指令

SendVersion等二进制消息不影响后,服务端能够收到消息了。

原来的H5Stream是否通过call发送一个open指令过去的

var address="{\"type\": \"open\"}";//json字符串,unity传递过来的是字符串。不能用单引号。
//必须是这个格式,不然h5stream不会发送后续消息,onmessage也就进不去。
browserCall.Call(address);//=>Connect

而从kurento的客户端(参考kurento-player-js)和服务端代码来看,是先发送一个{id:"start",videourl:...,sdpOffer:...},然后开始整个过程的。

1.使用KurentoUtils(不行)

而在kurento-player-js的index.js里面sdpOffer是通过

 webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options,function(error) {if (error)return console.error(error);webRtcPeer.generateOffer(onOffer);});

这里的webRtcPeer.generateOffer的回调函数onOffer获取的,试着把这部分相关代码拿过来,并把获取的offerSdp组织到start指令中发送过去。

                    var mode="video and audio";var video = document.getElementById('video1');intWebRtcPeer(mode,video,function(sdp){var msg={id:"start",videourl:rtsp,sdpOffer:sdp};browserCall.Call(msg);});
                function intWebRtcPeer(mode,video,callback) {console.log('Creating WebRtcPeer in ' + mode + ' mode and generating local sdp offer ...');// Video and audio by defaultvar userMediaConstraints = {audio : true,video : true}if (mode == 'video-only') {userMediaConstraints.audio = false;} else if (mode == 'audio-only') {userMediaConstraints.video = false;}var options = {remoteVideo : video,mediaConstraints : userMediaConstraints,onicecandidate : onIceCandidate}console.info('User media constraints' + userMediaConstraints);var webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options,function(error) {if (error)return console.error(error);webRtcPeer.generateOffer(function(error, offerSdp){if(!error){if(callback!=null){callback(offerSdp);}}});});}

服务端收到后能够进入start处理,但是后续接不上。

进行下去会出现错误:

awrtc.js:3171 DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: Called in wrong state: kStable

原因出在,这里进行下去的处理过程是用原来的awrtc.js的代码,但是kurento的index.js里面是用了webRtcPeer的processAnswer部分

function startResponse(message) {setState(I_CAN_STOP);console.log('SDP answer received from server. Processing ...');webRtcPeer.processAnswer(message.sdpAnswer, function(error) {if (error)return console.error(error);});
}

processAnswer内部其实就是setRemoteDescription。

generateOffer和processAnswer是对应的,generateOffer里面其实就是

pc.createOffer(createOfferOnSuccess, callback, constraints);

而awrtc.js里面也有相关的createOffer,

                        InnerAWebRtcPeer.prototype.CreateOffer = function() {var rtcPeer = this;console.log("InnerAWebRtcPeer.prototype.CreateOffer",this.mOfferOptions);var offerPro = this.mPeer.createOffer(this.mOfferOptions);offerPro.then(function(answer) {var json = JSON.stringify(answer),promise = rtcPeer.mPeer.setLocalDescription(answer);promise.then(function() {rtcPeer.RtcSetSignalingStarted();rtcPeer.EnqueueOutgoing(json);}),promise.catch(function(error) {Debug.LogError(error),rtcPeer.RtcSetSignalingFailed()})}),offerPro.catch(function(t) {Debug.LogError(t),rtcPeer.RtcSetSignalingFailed()})},

也就是说应该想办法从这里开始的。

2.使用awrtc的StartSignaling(可行)

CreateOffer是StartSignaling调用的,StartSignaling是UpdateSignalingNetwork里面判断的NewConnection分支中调用的

                                else if (netEvent.Type == NetEventType.NewConnection) {console.error("InnerWebRtcNetwork.prototype.UpdateSignalingNetwork",netEvent,this.mInSignaling);var t=this.mInSignaling[netEvent.ConnectionId.id];if(t){t.StartSignaling();}else{this.AddIncomingConnection(netEvent.ConnectionId);}}
UpdateSignalingNetwork其实是处理OnWebsocketOnMessage中的消息的。

以前连接H5Stream是发送一条{type:open}指令给服务器,然后服务器返回信息,然后开始的。

按照这个思路,经过模式,修改后过程如下。

客户端发送一个{id:connect}指令

                    var address={id:"connect"};//对象browserCall.Call(address);//=>Connect

服务端handleTextMessage里面处理connect指令,返回一个结果:

    try {switch (id) {case "connect"://[awrtc]sendMessage(session,"{\"type\":6,\"id\":1,\"data\":\"1\"}");break;

这里的type:6就是上面的NetEventType.NewConnection。

然后客户端在OnWebsocketOnMessage中增加一条路线处理。

                        InnerWebsocketNetwork.prototype.OnWebsocketOnMessage = function(e) {console.log(">>>>>>> OnWebsocketOnMessage",e);if (this.mStatus != WebsocketConnectionStatus.Disconnecting && this.mStatus != WebsocketConnectionStatus.NotConnected) {if(e.data instanceof ArrayBuffer){ //原来的路线var t = new Uint8Array(e.data);this.ParseMessage(t)}else{ //新增的路线,处理h5stream或者kurento的webrtc的信息var dataObj = JSON.parse(e.data);console.log(">>>>>>> OnWebsocketOnMessage [H5Stream]",dataObj);if(dataObj.type && dataObj.id){ //[kurento] 服务端是基于kurento-java修改的console.log(">>>>>>> OnWebsocketOnMessage [NEW NetworkEvent]",dataObj);var evnt=new NetworkEvent(dataObj.type,{id:dataObj.id},dataObj.data);this.HandleIncomingEvent(evnt);}else{ //[H5Stream]//将offer和remoteice作为NetEventType.ReliableMessageReceived信息处理.var evnt=new NetworkEvent(NetEventType.ReliableMessageReceived,this.mLastConnectionId,e.data);//在InnerWebsocketNetwork.prototype.Connect时记录下this.mLastConnectionId.this.HandleIncomingEvent(evnt);}}}},

这里的 var evnt=new NetworkEvent(dataObj.type,{id:dataObj.id},dataObj.data); 处理好后,就会进入StartSignaling,然后CreateOffer。

不过这里的把sdp发送给服务端部分(SendNetworkEvent),需要修改一下,添加id属性,不然无法和服务端处理对接起来。

                    InnerWebsocketNetwork.prototype.SendNetworkEvent = function(networkEvent) {/*InnerWebsocketNetwork.SendNetworkEvent @  awrtc.js:4300InnerWebsocketNetwork.HandleOutgoingEvents @  awrtc.js:4280InnerWebsocketNetwork.Flush    @  awrtc.js:4343InnerWebRtcNetwork.Flush   @  awrtc.js:3846InnerBrowserMediaNetwork.Flush @  awrtc.js:5982InnerAWebRtcCall.Update*/console.log(">>>>>>> SendNetworkEvent",networkEvent);//原来的代码// var t = NetworkEvent.toByteArray(networkEvent);// this.InternalSend(t);//新的代码[H5Stream]if(networkEvent.data instanceof Array){var t = NetworkEvent.toByteArray(networkEvent);this.InternalSend(t);//原来的分支1:发送NetworkEvent}else if(typeof networkEvent.data == 'string'){try {var obj=JSON.parse(networkEvent.data);//不是json格式的会抛出异常if(obj==null){var t = NetworkEvent.toByteArray(networkEvent);this.InternalSend(t);//原来的分支2:发送NetworkEvent}else{//新的分支1:发送data里面的内容的json字符串。对应于"{"type":"open"}"的发送。h5stream必须先发送open指令才能收到onmessage消息this.SendObject(obj);}}catch (ex) {console.error(ex);var t = NetworkEvent.toByteArray(networkEvent);this.InternalSend(t);//原来的分支2:发送NetworkEvent}}else//[H5Stream]或者[kurento]{this.SendObject(networkEvent.data);//新的分支2:发送data里面的内容的json字符串。对应于answer的发送}},
                    InnerWebsocketNetwork.prototype.SendObject = function(data) {if(data==null){console.error("InnerWebsocketNetwork.prototype.SendObject data==null");return;}if(!data.id){//[kurento]添加上idif(data.candidate){ console.log("onIceCandidate",data);data.id="onIceCandidate";var temp=JSON.stringify(data);data.candidate=JSON.parse(temp);//不能直接用 data.candidate=data,会导致JSON序列化出错的,死循环吧。}else if(data.sdp){console.log("start",data);data.id="start";data.videourl="rtsp://iom:123456@192.168.1.134:554/cam/realmonitor?channel=1&subtype=0";//data.sdpOffer=data.sdp;}console.info("kurento data",data);}var json=JSON.stringify(data);console.log("send json",json,data);this.mSocket.send(json);},

这里还留下一个问题,videourl如何传到这里,测试整个过程先写死了。

这里的两个data.id设置对应于服务端的start和onIceCandidate处理。

四、处理接收指令

而服务端处理后发送过来的消息又在HandleIncomingSignaling里面处理,把index.js里面的代码抄过来,改一下startResponse的部分,对接上原来的 this.CreateAnswer(description) : this.RecAnswer(description) 部分。

                InnerAWebRtcPeer.prototype.HandleIncomingSignaling = function() {/*InnerAWebRtcPeer.HandleIncomingSignaling @  awrtc.js:3464InnerAWebRtcPeer.Update    @  awrtc.js:3457InnerMediaPeer.Update  @  awrtc.js:5694InnerWebRtcNetwork.CheckSignalingState @  awrtc.js:3878InnerWebRtcNetwork.Update  @  awrtc.js:3823InnerBrowserMediaNetwork.Update    @  awrtc.js:5942InnerAWebRtcCall.Update*/for (; this.mIncomingSignalingQueue.Count() > 0;) {var data = this.mIncomingSignalingQueue.Dequeue();console.info('Received message: ' + data);t = Helper.tryParseInt(data);if (null != t) {console.error("InnerWebRtcNetwork.prototype.HandleIncomingSignaling",data,t);this.mDidSendRandomNumber && (t < this.mRandomNumerSent ? (SLog.L("Signaling negotiation complete. Starting signaling."), this.StartSignaling()) : t == this.mRandomNumerSent ? this.NegotiateSignaling() : SLog.L("Signaling negotiation complete. Waiting for signaling."));}else {var parsedMessage = JSON.parse(data);if(parsedMessage.id){ //[kurento]switch (parsedMessage.id) {case 'startResponse'://startResponse(parsedMessage);var answer={type: 'answer',sdp: parsedMessage.sdpAnswer}//console.error(">>>>>>>>>  parsedMessage.sdpAnswer",answer);var description = new RTCSessionDescription(answer);console.error(">>>>>>>>>  startResponse Answer",description);"offer" == description.type ? this.CreateAnswer(description) : this.RecAnswer(description);//this.CreateAnswer(description);break;case 'error':if (state == I_AM_STARTING) {setState(I_CAN_START);}onError('Error message from server: ' + parsedMessage.message);break;case 'playEnd':playEnd();break;case 'videoInfo'://showVideoData(parsedMessage);// {"id":"videoInfo","isSeekable":false,"initSeekable":0,"endSeekable":0,"videoDuration":0}break;case 'iceCandidate'://这部分不处理也可以// console.log(">>>>>>>>>  parsedMessage",parsedMessage,parsedMessage.candidate);// var candidate = new RTCIceCandidate(parsedMessage.candidate);// console.log(">>>>>>>>>  candidate",candidate);// if (null != candidate) {//     var pro = this.mPeer.addIceCandidate(candidate);//     pro.then(function() {}),//         pro.//         catch(function(error) {//             Debug.LogError(error)//         })// }break;case 'seek':console.log (parsedMessage.message);break;case 'position':document.getElementById("videoPosition").value = parsedMessage.position;break;case 'iceCandidate':break;default:if (state == I_AM_STARTING) {setState(I_CAN_START);}onError('Unrecognized message', parsedMessage);}}else{ //原来的var answer = parsedMessage;console.log(">>>>>>>>>  InnerAWebRtcPeer.prototype.HandleIncomingSignaling",answer);if (answer.sdp) {var description = new RTCSessionDescription(answer);console.log(">>>>>>>>>  Answer",description);"offer" == description.type ? this.CreateAnswer(description) : this.RecAnswer(description)} else {var candidate = new RTCIceCandidate(answer);console.log(">>>>>>>>>  addIceCandidate",candidate);if (null != candidate) {var pro = this.mPeer.addIceCandidate(candidate);pro.then(function() {}),pro.catch(function(error) {Debug.LogError(error)})}}}}}},

然后................................就好了,可以播放视频了。

其实我对整个webrtc视频连接的过程的理解是懵懵懂懂的,大概知道几个关键步骤,具体细节的话有些地方还是不懂,如果自己写一个原型代码的话会更加深理解。

//todo:写个js连接websocket的原型代码

这个修改过程相当于把两个有一定兼容性的机器连接起来,这里的连接的依据就是双方是基于webrtc的规则来的。把客户端(awrtc)和服务端(kurento-player)的相关处理过程修改一下,把两者接起来。

五、暂停等指令接口

服务端提供了几个控制接口

其实就是发送响应的id,客户端这边封装一下

                        InnerBrowserWebRtcCall.prototype.SendObject = function(obj) {console.log(">>>>>>>>>>>>>>>>>> BrowserWebRtcCall.SendObject",obj);this.mNetwork.mSignalingNetwork.SendObject(obj);},InnerBrowserWebRtcCall.prototype.Stop = function() {//[kurento]this.SendObject({id:"stop"});this.DisposeInternal();},InnerBrowserWebRtcCall.prototype.Resume = function() {//[kurento]this.SendObject({id:"resume"});},InnerBrowserWebRtcCall.prototype.Pause = function() {//[kurento]this.SendObject({id:"pause"});},
        $('#btnStop').click(function(){call.Stop();});$('#btnPause').click(function(){call.Pause();});$('#btnResume').click(function(){call.Resume();});

六、Unity播放

前面的js代码的修改,都是为了放到unity里面。

把awrtc.js代码拷贝到awrtc.jspre,在awrtc_unity.jslib里面加上想要的接口

  Unity_Kurento_GetVideo: function(a,b){var serverUrl=Pointer_stringify(a);var videoUrl=Pointer_stringify(b);console.log("------- Unity_Kurento_GetVideo",serverUrl,videoUrl);return awrtc.CAPI_Kurento_GetVideo(serverUrl,videoUrl);},

Unity里面则是

        [DllImport("__Internal")]public static extern InitState Unity_Kurento_GetVideo(string serverUrl,string videoUrl);

前面加了个VideoUrl参数,Unity里面需要相应调整。

        public class NetworkConfigEx : NetworkConfig{public string VideoUrl { get; set; }}

WebRtcVideo.CreateNetworkConfig:

        NetworkConfigEx netConfig = new NetworkConfigEx();if (string.IsNullOrEmpty(uIceServer) == false)netConfig.IceServers.Add(new IceServer(uIceServer, uIceServerUser, uIceServerPassword));if (string.IsNullOrEmpty(uIceServer2) == false)netConfig.IceServers.Add(new IceServer(uIceServer2));uSignalingUrl = mUi.InputUrl.text;videoUrl = mUi.VideoUrls.options[mUi.VideoUrls.value].text;Debug.Log("uSignalingUrl:"+ uSignalingUrl);Debug.Log("videoUrl:" + videoUrl);netConfig.SignalingUrl = uSignalingUrl;netConfig.VideoUrl = videoUrl;

BrowserMediaNetwork:

            string conf = "{\"IceServers\":" + iceServersJson.ToString() + ", \"SignalingUrl\":\"" + signalingUrl + "\", \"IsConference\":\"" + false + "\"}";if (lNetConfig is NetworkConfigEx){NetworkConfigEx ex = lNetConfig as NetworkConfigEx;conf = "{\"IceServers\":" + iceServersJson.ToString() + ", \"SignalingUrl\":\"" + signalingUrl +"\", \"VideoUrl\":\"" + ex.VideoUrl + "\", \"IsConference\":\"" + false + "\"}";}SLog.L("Creating BrowserMediaNetwork config: " + conf, this.GetType().Name);mReference = CAPI.Unity_MediaNetwork_Create(conf);

界面上再加上一个videoUrl的输入框,打包webgl测试,可以播放。

七、Unity帧率问题(视频分辨率)

发现播放一会,帧率就变成了2-4,同时在打印信息中发现视频分辨率有几次变化,最后变成1920*1080了。

公司有两个摄像头一个是1920*1080,一个是1280*720,低分辨率播放视频时的帧率在45-50,可能是分辨率问题。

另外发现,js里面的核心代码context.drawImage()实际上是可以修改分辨率的,参考:前端JS利用canvas的drawImage()对图片进行压缩

原本是mVideoElement.videoWidth的,改成用mVideoElement.width,使用video的大小来获取图片。

                            InnerBrowserMediaStream.prototype.CreateFrame = function() {// console.log("InnerBrowserMediaStream.prototype.CreateFrame",//     this.mVideoElement.videoWidth,this.mVideoElement.videoHeight,//     this.mVideoElement.width,this.mVideoElement.height);//var width=this.mVideoElement.videoWidth;//var height=this.mVideoElement.videoHeight;var width=this.mVideoElement.width;var height=this.mVideoElement.height;this.mCanvasElement.width = width;this.mCanvasElement.height = height;var context = this.mCanvasElement.getContext("2d");context.clearRect(0, 0, this.mCanvasElement.width, this.mCanvasElement.height);context.drawImage(this.mVideoElement, 0, 0,width,height);try {var data = context.getImageData(0, 0, this.mCanvasElement.width, this.mCanvasElement.height).data,array = new Uint8Array(data.buffer);return new RawFrame(array, this.mCanvasElement.width, this.mCanvasElement.height)} catch(error) { (array = new Uint8Array(this.mCanvasElement.width * this.mCanvasElement.height * 4)).fill(255, 0, array.length - 1);var frame = new RawFrame(array, this.mCanvasElement.width, this.mCanvasElement.height);return SLog.LogWarning("Firefox workaround: Refused access to the remote video buffer. Retrying next frame..."),this.DestroyCanvas(),this.mCanvasElement = this.SetupCanvas(),frame}},

在传入配置信息中,加上分辨率的设置。

             var config={SignalingUrl:"ws://192.168.1.150:8444/player",VideoUrl:"rtsp://iom:123456@192.168.1.134:554/cam/realmonitor?channel=1&subtype=0",VideoWidth:tmp[0],VideoHeight:tmp[1]};console.log('config',config);call=awrtc.CAPI_Kurento_GetVideo(JSON.stringify(config));
                function func_CAPI_Kurento_GetVideo(configJson) {//[kurento]console.log("func_CAPI_Kurento_GetVideo",configJson);var config=JSON.parse(configJson);console.log("config",config);BrowserMediaStream.DEBUG_SHOW_ELEMENTS=true;//在网页中显示视频AWebRtcPeer.SourceType="kurento";//原本用这个区分代码的var browserCall=new BrowserWebRtcCall(config);

然后就可以在播放时设置分辨率了。

--------------------------------------------------------------------

测试了几种分辨率,发现改成4:3的分辨率时获取到的图片和Video里面的视频不一样,拉伸了一些,16:9的则是一样的,这里可能需要处理一下。

普屏4:3 320*240 640*480
宽屏16:9 480*272 640*360 672*378 720*480 1024*600 1280*720 1920*1080

现在相当于默认都要拉伸的,以后需要的话可以增加不同的方式。

--------------------------------------------------------------------

关键的Unity测试结果,从结论上讲,确实是受分辨率影响的,在1280*720以下的分辨率的帧率还能接受(30-60),再清晰一些则帧率下降到1-10了,不能接受。但是这里有个前提是这里的刷新时用FixedUpdate,0.02s一次。改成用InvokeRepeating的方式,0.1s一次,则就算是1920*1080的分辨率也播放,帧率20-40,还能接受。而1920*1080,0.2s一次,则可以达到40-50。

总之不能用FixedUpdata,没必要,看监控视频不是玩游戏,不需要0.02s刷新一次。0.1-0.2就可以了。

实际上应该根据UI界面的大小和视频本身分辨率,界面大小小于视频分辨率的话,使用界面的分辨率,大于的话使用视频本身的分辨率。

根据界面UI大小设置视频分辨率:

if (Config.resolution == "AutoUI"){//Config.resolution = mUi.uNoCameraTexture.width + "*" + mUi.uNoCameraTexture.height;RectTransform rectT = GetComponent<RectTransform>();var rect = rectT.rect;//ShowHtmlElement.ShowElement(rect,"video1");Config.resolution = (int)rect.width + "*" + (int)rect.height;Debug.Log("AutoUI:" + Config.resolution);}

-------------------------------------------------------------------

八、播放多个摄像头视频

整理了一下代码,原来的代码里面UI和控制代码在一起的,实际上UI部分只有一个RawImage是必要的。

直接将代码整理一下,和RawImage一起做成一个预设。再复制一下,修改复制后的摄像头地址。

打包webgl,测试。可以。

当然帧率还是受分辨率影响。

九、显示Video(并设置位置)

前面的在Unity里面播放WebRtc的方式的核心是,用一个隐藏的(Html5的)Video来播放WebRtc,通过和js交互,获取Video的图片,并不断刷新。相比于直接只是Video播放多了获取和显示在Unity中的两步,一定程度上会影响Unity内的帧率,还有视频的质量。不过这种的优点是效果上和Unity完美结合,通过Material还能再三维物体表面播放视频。

但是对于不需要再三维物体上显示的需求来说,在Unity里面也只是在一个界面上显示视频而已,直接可以把Video显示出来,并调整位置,放到Unity前面,达到看起来和Unity界面重叠一致的效果。

相关参考资料:UnityWebGL调研(7) 修改打包模板,UnityWebGL调研(5) 和网页交互

十、js核心原型代码

视频服务器(8) Kurento[3] unity客户端相关推荐

  1. 【游戏开发实战】用Go语言写一个服务器,实现与Unity客户端通信(Golang | Unity | Socket | 通信 | 教程 | 附工程源码)

    文章目录 一.前言 二.Go开发环境搭建(Windows系统) 1.安装Go命令行工具 2.创建GoWorkspace目录 3.配置GOPATH环境变量 4.配置GOPROXY代理 5.安装VSCod ...

  2. 视频服务器(6) Kurento[1] rtsp2webrtc

    目录 一.安装Kurento 二.播放rtsp调研 三.播放RTSP实现 四.wsl(ubuntu)安装使用 官网:https://www.kurento.org/ 参考:Kurento流媒体开发环境 ...

  3. Kurento搭建音视频服务器

    Kurento搭建视频通话(基于WebRTC) 本文介绍 环境准备 安装KMS 体验官方java-demo 搭建JAVA环境 下载官方demo 运行demo之一(一对一视频对话) 官方其他案例 这篇文 ...

  4. 视频教程-Unity客户端框架设计PureMVC篇视频课程(上)-Unity3D

    Unity客户端框架设计PureMVC篇视频课程(上) 二十多年的软件开发与教学经验IT技术布道者,资深软件工程师.具备深厚编程语言经验,在国内上市企业做项目经理.研发经理,熟悉企业大型软件运作管理过 ...

  5. 视频教程-Unity客户端框架设计PureMVC篇视频课程(下)-Unity3D

    Unity客户端框架设计PureMVC篇视频课程(下) 二十多年的软件开发与教学经验IT技术布道者,资深软件工程师.具备深厚编程语言经验,在国内上市企业做项目经理.研发经理,熟悉企业大型软件运作管理过 ...

  6. unity响应服务器消息,[从零开始的Unity网络同步] 5.服务器将状态同步给客户端(状态缓存,状态插值,估算帧)...

    在上一篇文章中,已经可以在服务器上直接根据服务器自己的操作指令,模拟得出结果,修改球的位置了,接下来,将要考虑如何将服务器模拟的位置如何同步到客户端. 1.服务器向客户端发送单位实体(Entity)状 ...

  7. v2视频服务器退出系统怎么启动,V2视频会议系统入门操作手册.doc

    V2视频会议系统入门操作手册 登陆方式 打开IE(浏览器),用户访问服务器地址00,进入V2 Conference系统主界面. 首次登录视频会议服务器,系统会自动提示客户端下载安装客户端插件,用户也可 ...

  8. 如何测试网络视频服务器(DVS)

    打开搜索引擎或相关专业杂志,就能看到很多品牌的网络视频服务器的广告充斥,用目不暇接来形容绝不为过.这么多的选择,究竟哪一种产品才是真正的高品质, 哪一种产品才是适合自己的需求呢,我想,光凭广告的一面之 ...

  9. 流媒体(视频)服务器调研

    这篇文章主要向大家介绍流媒体(视频)服务器调研,主要内容包括基础应用.实用技巧.原理机制等方面,希望对大家有所帮助. 标签:javascriptphphtmljavapythonlinuxandroi ...

最新文章

  1. Spring mvc环境搭建
  2. 邮件内容被分析抛售,你的企业邮箱安全吗?
  3. js中内置对象Math()常用方法笔记
  4. python教程:getattr函数和hasattr函数的用法
  5. 2018-2019-2 网络对抗技术 20165202 Exp9 Web安全基础
  6. 手风琴案例jquery写法
  7. 职教云自动签到网页版开源源码
  8. 一个想法照进现实-《IT连》创业项目:万事开头难
  9. 梦想,没想到这么快就实现了!
  10. 游戏蛮牛Egret游戏引擎视频教程
  11. 基于C++的图片压缩设计与实现
  12. java 无理数_《数学分析原理》笔记之——无理数的引入
  13. MATLAB启动慢解决措施
  14. BaseQuickAdapter设置条目点击事件无效
  15. 私钥,公钥,钱包地址,助记词,keyStore的区别
  16. 七年交易经验,倾囊分享中长线交易秘诀
  17. B2B电商的几大核心问题
  18. 前端使用html2canvas插件进行截图
  19. 搭建java开发环境
  20. 后端优化1(SLAM十四讲ch10)-BA

热门文章

  1. 台达伺服位置控制的应用和调试
  2. IP地址的分类 十进制与二进制的转换
  3. web CSS 颜色渐变代码
  4. 《我和我的祖国》之《夺冠》:所有人都在欢呼,只有他在哭......
  5. 一篇对于了解我自己,挖掘我自己,从而成长的文章
  6. hyperterminal使用教程_hyperterminal 教程
  7. CSDN 博客已式微?
  8. python-requests请求超时解决方案
  9. 2021-2022 ACM-ICPC Latin American Regional Programming Contest 题解
  10. 第4章 虚拟机性能监控与故障处理工具