目录
二、.Binary messages not supported
现在有两种方式:
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部分改造后的。
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
@Override
protected 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 default
var 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:4300
InnerWebsocketNetwork.HandleOutgoingEvents @ awrtc.js:4280
InnerWebsocketNetwork.Flush @ awrtc.js:4343
InnerWebRtcNetwork.Flush @ awrtc.js:3846
InnerBrowserMediaNetwork.Flush @ awrtc.js:5982
InnerAWebRtcCall.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]添加上id
if(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:3464
InnerAWebRtcPeer.Update @ awrtc.js:3457
InnerMediaPeer.Update @ awrtc.js:5694
InnerWebRtcNetwork.CheckSignalingState @ awrtc.js:3878
InnerWebRtcNetwork.Update @ awrtc.js:3823
InnerBrowserMediaNetwork.Update @ awrtc.js:5942
InnerAWebRtcCall.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核心原型代码