工作中遇到要视频直播的需求,前提是不能依赖Flash前端,于是就找到了WebRtc的相关资料.
什么GetUserMedia,RTCPeerConnection,DataChannel我不多说.
简单讲就是谷歌把实时通信层打包进浏览器.而这一套实时通信层又是来源于电信通信领域.
所以浏览器两端交互需要依赖一个叫做信令服务器的东西,来协助两端完成连接.
简单说下流程
以A呼叫B为例
A呼叫B
1.告知Server,我要找B
2.Server查一下有没有B,有就传达,没有就不传达,至于结果需不需要告知A,那全看心情了.反正这东西是自己实现的.WebRTC里就提了一嘴这东西,没具体规范.
3.B得到A的呼叫(准确说叫Offer)
4.B解析Offer,回应(Answer)到Server,Server回给A
5.A收到Answer,解析.
6.这时,A和B就算勾搭上了.剩下的事情就交给他们自己办了.
Offer 和 Answer 都属于SDP.具体规范http://datatracker.ietf.org/doc/draft-nandakumar-rtcweb-sdp/01/
解析这些是交给浏览器做的.
先看Server端的Java实现
package org.rtc.sip;
import java.io.IOException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import com.google.gson.Gson;
@ServerEndpoint(value = "/sipserver")
public class SignalWorker {
private Gson gson = new Gson();
private Session session;
public Session getSession() {
return session;
}
public void setSession(Session session) {
this.session = session;
}
public SignalWorker() {
}
@OnClose
public void onClose(Session session) {
}
@OnError
public void onError(Throwable throwable) {
}
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("收到消息:" + message);
SIPObject sip = gson.fromJson(message, SIPObject.class);
try {
if(sip.getAction().equals("login")){
SIPSessionManager.addNewSession(sip.getFrom(), this);
}else if(sip.getAction().equals("offer")){
SignalWorker sker=SIPSessionManager.getSignalWorker(sip.getTo());
if(sker!=null){
sker.getSession().getBasicRemote().sendText(message);
}
else{
miss();
}
}else if(sip.getAction().equals("answer")){
SignalWorker sker=SIPSessionManager.getSignalWorker(sip.getTo());
if(sker!=null){
sker.getSession().getBasicRemote().sendText(message);
}
else{
miss();
}
}else if(sip.getAction().equals("candidate")){
SignalWorker sker=SIPSessionManager.getSignalWorker(sip.getTo());
if(sker!=null){
sker.getSession().getBasicRemote().sendText(message);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
@OnOpen
public void onOpen(Session session) {
this.session = session;
}
public void miss(){
SIPObject resp=new SIPObject();
resp.setAction("miss");
try {
session.getBasicRemote().sendText(gson.toJson(resp));
} catch (IOException e) {
e.printStackTrace();
}
}
public void send(String msg) {
System.out.println(msg);
session.getAsyncRemote().sendText(msg);
}
}
代码逻辑很简单,就是A->B,B->A,
WebRtc
上面HTML代码逻辑也很简单.
两端Baidu和Google先要登陆在SIPServer上.
Google呼叫baidu是在本地完成了摄像头视频获取后发出的Offer.
整个WebRtc交互用一个JS库屏蔽掉了复杂的代码.
代码只简单实现了功能,未完善.贴出来分享.
/**
*
*/
// define override
var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection
|| window.webkitRTCPeerConnection;
navigator.getUserMedia = navigator.webkitGetUserMedia
|| navigator.mozGetUserMedia;
//ICE SERVER
var iceServer = {
"iceServers": [{
"url": "stun:stun.l.google.com:19302"
}]
};
//LOG
function log(msg,level){
console.log(new Date().toLocaleString()+" "+msg);
}
function RTCSuit(opt) {
this.sipws=opt.sipws;//信令服务器地址
this.id=opt.id||new Date().getTime();//身份信息
this.to=opt.to;
this.conn=null;//websocket对象,connect时初始化
this.sessionId=null;
this.peercn=new RTCPeerConnection(opt.ice||iceServer);
this.peercn.rtc=this;
//init peer
this.peercn.onicecandidate = RTCSuit.prototype.onIceCandidate;
this.peercn.onconnecting = RTCSuit.prototype.onPeerConnecting;
this.peercn.onopen = RTCSuit.prototype.onPeerOpen;
this.peercn.onaddstream = RTCSuit.prototype.onRemotePeerStreamAdd;
this.peercn.onremovestream = RTCSuit.prototype.onPeerStreamRemove;
this.localNode=null;
this.remoteNode=null;
return this;
}
//视频媒体交互
RTCSuit.prototype.bindLocalMedia=function(id, enableAudio, enableVideo,callback)
{
var rtc=this;
navigator.getUserMedia({
"audio" : enableAudio,
"video" : enableVideo
}, function(stream) {//success
var videoNode=document.getElementById(id);
rtc.localNode=videoNode;
videoNode.autoplay=true;
videoNode.src= URL.createObjectURL(stream);
rtc.peercn.addStream(stream);
//触发事件,通知视频流绑定成功
if(typeof(callback)=="function"){
callback(true,stream);
}
log("bind local media success");
},function(error){//error
if(typeof(callback)=="function"){
callback(false,null);
}
});
};
RTCSuit.prototype.bindRemoteMedia=function(id){
var videoNode=document.getElementById(id);
videoNode.autoplay=true;
this.remoteNode=videoNode;
};
//PEER交互
RTCSuit.prototype.sendOffer=function(){
var rtc=this;
if(!rtc.conn.readyState){
setTimeout(function(){rtc.sendOffer()},300);return;
}
this.peercn.createOffer(function(desc){//success
rtc.peercn.setLocalDescription(desc);
rtc.send({action:"offer",sdpWrap:desc});
},function(error){
rtc.onSendOfferError(error);
});
};
RTCSuit.prototype.sendAnswer=function(){
var rtc=this;
if(!rtc.conn.readyState){
setTimeout(function(){rtc.sendOffer()},300);return;
}
this.peercn.createAnswer(function(desc){//success
rtc.peercn.setLocalDescription(desc);
rtc.send({action:"answer",sdpWrap:desc});
},function(error){
});
}
RTCSuit.prototype.onAddStreamToPeer=function(){
log("get here");
};
RTCSuit.prototype.onIceCandidate=function(event){
var rtc=this.rtc;
if (event.candidate) {
rtc.send({action:"candidate",sdpWrap:{
type : "candidate",
label : event.candidate.sdpMLineIndex,
id : event.candidate.sdpMid,
candidate : event.candidate.candidate
}});
} else {
console.log("End of candidates.");
}
};
RTCSuit.prototype.onPeerConnecting=function(){
log(" peer connecting");
};
RTCSuit.prototype.onPeerOpen=function(){
log(" peer open");
};
RTCSuit.prototype.onRemotePeerStreamAdd=function(event){
var url = webkitURL.createObjectURL(event.stream);
this.rtc.remoteNode.src=url;
};
RTCSuit.prototype.onPeerStreamRemove=function(){
};
//信令交互
RTCSuit.prototype.send=function(json){
var rtc=this;
json.from=rtc.id;
json.to=rtc.to;
var smsg=JSON.stringify(json);
if(rtc.conn.readyState)
rtc.conn.send(smsg);
log(rtc.id+" 发送"+smsg);
};
RTCSuit.prototype.connectSip=function(sipws,callback){
sipws=sipws||this.sipws;
var rtc=this;
this.conn=new WebSocket(sipws);
this.conn.onopen=function(){
rtc.send({id:rtc.id,action:"login"});
log("sip server connected");
};
//core msg process
this.conn.onmessage=function(e){
log(rtc.id+" 收到消息:"+e.data);
var msg=e.data;
var json=eval("("+msg+")");
if(typeof(rtc["on"+json.action]) == "function"){
rtc["on"+json.action](json);
}else{
log("error response:"+msg);
}
};
};
RTCSuit.prototype.onoffer=function(json){
this.peercn.setRemoteDescription(new RTCSessionDescription(json.sdpWrap));
this.sendAnswer();
log("recv offer")
};
RTCSuit.prototype.onanswer=function(json){
this.peercn.setRemoteDescription(new RTCSessionDescription(json.sdpWrap));
log("recv answer");
};
RTCSuit.prototype.oncandidate=function(json){
var msg=json.sdpWrap;
var candidate = new RTCIceCandidate({
sdpMLineIndex : msg.label,
candidate : msg.candidate
});
this.peercn.addIceCandidate(candidate);
};
最后代码奉上....环境 tomcat8.0 java 1.8