这段时间帮朋友做一个简单的HTML的webrtc连接,单对单,一端推流,一端拉流,东西极其简单,偏偏还遇到了几个坑,这里写出来供大家参考,顺便分享下代码。
要建立webrtc连接,首先需要一个模块帮助连接双方交换SDP和ICE,通常我们采用一个websocket服务器来做这个事,不过受限朋友原来代码,考虑用一个简单的HTTP服务来做这个事,连tomcat容器都不用,服务器用java写的,就是对每一个流名建立一个两个消息队列,A发的消息放A队列,B发的消息放B队列,A和B会有个定时器定时获取消息,如果没有消息就会收到nomessage, 放核心代码代码参考:
public class HttpServer implements Runnable {
static public void StartHttpServer(Integer port) {
Executors.newSingleThreadScheduledExecutor().execute(new HttpServer(port));
}
@Override
public void run() {
ServerSocket server=null;
// TODO Auto-generated method stub
ExecutorService ex=Executors.newFixedThreadPool(200);
try {
//建立服务器监听端口
server=new ServerSocket(port_);
while(true) {
Socket s=server.accept();
//然后把接收到socket传给SocketServer并执行该线程
ex.execute(new SocketServer(s));
}
} catch (IOException e) {
e.printStackTrace();
}
}
private class SocketServer implements Runnable{
Socket s;
private byte[] header;
//构造方法
public SocketServer(Socket s) {
this.s=s;
}
@SuppressWarnings("unused")
private String getHeader(Integer datalen) {
String header="HTTP/1.0 200 OK\r\n"+
"Server: RtcServer 1.0\r\n"+
"Content-length: "+datalen+"\r\n"+
"Content-type: text/json\r\n"+
"Access-Control-Allow-Origin:*\r\n"+
"Access-Control-Allow-Methods:*\r\n"+
"Access-Control-Allow-Headers:*\r\n\r\n";
return header;
}
@Override
public void run() {
// TODO Auto-generated method stub
//输入输出流
OutputStream os=null;
InputStream in=null;
try
{ //打开socket输入流,并转为BufferedReader流
in=s.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(in));
//接收第一行,得到请求路径
String strRequest=br.readLine();
do{
String newline=br.readLine();
if (newline==null || newline.length()==0) {
break;
}
//strRequest += "\r\n" +newline;
}while (true);
String stretcontent = ProccessRequest(strRequest);
if(stretcontent==null)
stretcontent="";
//打开socket对象的输出流,写入响应头
os = s.getOutputStream();
String heaner = getHeader(stretcontent.length());
os.write(heaner.getBytes("ASCII"));
os.write(stretcontent.getBytes("ASCII"));
os.flush();
//如果os流没有关闭的话,浏览器会以为内容还没传输完成,将一直显示不了内容
os.close();
}catch (IOException e) {
e.printStackTrace();
}
}
private String ProccessRequest(String strRequest)
{
String strRetContent = "{\"retcode\":\"000010\"}";
if(strRequest==null) {
return strRetContent;
}
int begin = strRequest.indexOf("GET /");
if(begin==-1) {
return strRetContent;
}
int end = strRequest.indexOf("HTTP/");
if(end==-1) {
return strRetContent;
}
String strReqCmd=strRequest.substring(begin+5, end);
if(strReqCmd==null) {
return strRetContent;
}
int key = strReqCmd.indexOf("?");
String strcmd = strReqCmd;
if(key!=-1) {
strcmd = strReqCmd.substring(0, key).toLowerCase().trim();
}
String parastr = strReqCmd.substring(key+1, strReqCmd.length()).trim();
if(strcmd.contains("rtc_sdp") ){
return ProccessRtcSdpMsg(parastr);
}
else if(strcmd.contains("rtc_icefinish")) {
return ProccessRtcIceFinshMsg(parastr);
}
else if(strcmd.contains("rtc_ice")) {
return ProccessRtcIceMsg(parastr);
}
else if(strcmd.contains("rtc_get")) {
return ProccessRtcGetMsg(parastr);
}
else if(strcmd.contains("start_stream")) {
return ProccessStartStream(parastr);
}
else {
}
return strRetContent;
}
}
推流端定时器定时获取消息,收到startstream消息,就开始创建connect和offer sdp, 外部只要调用connectToRtcChannel,参数为服务器地址,流的名称,video控件即可,Js代码:
var pcConfig = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]};
var pcOptions = {
optional: [
{DtlsSrtpKeyAgreement: true}
]
}
var mediaConstraints = {'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': true }};
RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription;
RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate;
getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
function connectToRtcChannel(server, channelname, videoelement)
{
var channelobj={__server:server, __channelname: channelname, __videoele:videoelement,pc:null}
setTimeout(getNewMessage,1000,server, channelname,channelobj);
return channelobj;
}
function getNewMessage(server, channelname, channelobj){
$.ajax({
type: "GET",
url: server+'/rtc_get?from=server&channel='+channelname,
dataType: 'json',
success: function (data) {
if(data['msgtype'] == 'remotesdp'){
var sdpstr = decodeURIComponent(data['sdp']);
var sdp = JSON.parse(sdpstr);
channelobj.pc.setRemoteDescription(new RTCSessionDescription(sdp), onRemoteSdpSucces, onRemoteSdpError);
setTimeout(getNewMessage,500,server, channelname,channelobj);
}
else if(data['msgtype'] == 'remoteice'){
var icestr = decodeURIComponent(data['ice']);
var ice = JSON.parse(icestr);
var candidate = new RTCIceCandidate({sdpMLineIndex: ice.sdpMLineIndex, candidate: ice.candidate,sdpMid:ice.sdpMid});
channelobj.pc.addIceCandidate(candidate, aic_success_cb, aic_failure_cb);
setTimeout(getNewMessage,500,server, channelname,channelobj);
}
else if(data['msgtype'] == 'icefinish'){
return;
}
else if(data['msgtype'] == 'statstream'){
connectToRtcChannelInner(server,channelname,channelobj);
setTimeout(getNewMessage,500,server, channelname,channelobj);
}
else if(data['msgtype'] == 'nomessage'){
setTimeout(getNewMessage,1000,server, channelname,channelobj);
}
}
});
}
function connectToRtcChannelInner(server, channelname, channelobj)
{
var pc = new RTCPeerConnection(pcConfig, pcOptions);
channelobj.pc=pc;
pc.onicecandidate = function(event) {
if (event.candidate) {
var candidate = {
sdpMLineIndex: event.candidate.sdpMLineIndex,
sdpMid: event.candidate.sdpMid,
candidate: event.candidate.candidate
};
var data = JSON.stringify(candidate);
console.log("onicecandidate ",data);
sendChannelIceCandidate(server, channelname, data);
} else {
console.log("End of candidates.");
sendChannelIceCandidateFinish(server,channelname);
}
};
pc.onconnecting = onSessionConnecting;
pc.onopen = onSessionOpened;
pc.ontrack = onRemoteStreamAdded;
pc.onremovestream = onRemoteStreamRemoved;
navigator.getUserMedia({ video: true, audio: true },
stream => {
//window.stream = stream;
for (const track of stream.getTracks()) {
channelobj.pc.addTrack(track, stream);
}
channelobj.__videoele.srcObject = stream;
channelobj.__videoele.play();
channelobj.pc.createOffer(function(sessionDescription) {
console.log("Create answer:", sessionDescription);
pc.setLocalDescription(sessionDescription,sld_success_cb,sld_failure_cb);
var data = JSON.stringify(sessionDescription);
sendChannelOfferSdp(server, channelname, data);
console.log(data);
},
function(error) { // error
console.log("Create answer error:", error);
}, mediaConstraints);
},
err => {
console.log(err);
})
return pc;
}
function sendChannelOfferSdp(server, channelname, sdp)
{
$.ajax({
type: "GET",
url: server+'/rtc_sdp?from=server&channel='+channelname+'&sdp='+encodeURIComponent(sdp),
dataType: 'text',
success: function (data) {
//setTimeout(getNewMessage,1000,server, channelname,pc);
}
});
}
function sendChannelIceCandidate(server, channelname, ice)
{
$.ajax({
type: "GET",
url: server+'/rtc_ice?from=server&channel='+channelname+'&ice='+ encodeURIComponent(ice),
dataType: 'text',
success: function (data) {
//setTimeout(getNewMessage,1000,server, channelname,pc);
}
});
}
function sendChannelIceCandidateFinish(server, channelname)
{
$.ajax({
type: "GET",
url: server+'/rtc_icefinish?from=server&channel='+channelname,
dataType: 'text',
success: function (data) {
//setTimeout(getNewMessage,1000,server, channelname,pc);
}
});
}
拉流端连到服务器,定时获取消息,收到SDP后设置并创建answer即可:
function connectToRtcChannel(server, channelname, videoshow)
{
var channelobj={__server:server, __channelname: channelname, __videoele:videoshow,pc:null}
$.ajax({
type: "GET",
url: server+'/start_stream?channel='+channelname,
dataType: 'text',
success: function (data) {
}
});
setTimeout(getNewMessage,500,server, channelname,channelobj);
return channelobj;
}
//-------------------------inner function----------------------------
var pcConfig = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]};
var pcOptions = {
optional: [
{DtlsSrtpKeyAgreement: true}
]
}
var mediaConstraints = {'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': true }};
RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription;
RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate;
getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
function getNewMessage(server, channelname, channelobj){
$.ajax({
type: "GET",
url: server+'/rtc_get?channel='+channelname,
dataType: 'json',
success: function (data) {
if(data.msgtype == 'remotesdp'){
var sdpstr = decodeURIComponent(data['sdp']);
var sdp = JSON.parse(sdpstr);
connectToRtcChannelInner(server,channelname,channelobj,sdp);
setTimeout(getNewMessage,500,server, channelname,channelobj);
}
else if(data.msgtype == 'remoteice'){
var icestr = decodeURIComponent(data['ice']);
var ice = JSON.parse(icestr);
var candidate = new RTCIceCandidate({sdpMLineIndex: ice.sdpMLineIndex, candidate: ice.candidate,sdpMid:ice.sdpMid});
channelobj.pc.addIceCandidate(candidate, aic_success_cb, aic_failure_cb);
setTimeout(getNewMessage,500,server, channelname,channelobj);
}
else if(data.msgtype == 'icefinish'){
return;
}
else if(data.msgtype == 'error'){
}
else if(data.msgtype == 'nomessage'){
setTimeout(getNewMessage,1000,server, channelname,channelobj);
}
}
});
}
function connectToRtcChannelInner(server, channelname, channelobj,sdp)
{
var pc = new RTCPeerConnection(pcConfig, pcOptions);
channelobj.pc = pc;
pc.onicecandidate = function(event) {
if (event.candidate) {
var candidate = {
sdpMLineIndex: event.candidate.sdpMLineIndex,
sdpMid: event.candidate.sdpMid,
candidate: event.candidate.candidate
};
var data = JSON.stringify(candidate);
console.log("onicecandidate ",data);
sendChannelIceCandidate(server, channelname,channelobj, data);
} else {
console.log("End of candidates.");
sendChannelIceCandidateFinish(server,channelname);
}
};
pc.ontrack = function(event){
if(event.track.kind=='video'){
channelobj.__videoele.srcObject = event.streams[0];
channelobj.__videoele.play();
}
};
pc.onconnecting = onSessionConnecting;
pc.onopen = onSessionOpened;
pc.onremovestream = onRemoteStreamRemoved;
pc.setRemoteDescription(new RTCSessionDescription(sdp), onRemoteSdpSucces, onRemoteSdpError);
pc.createAnswer(function(sessionDescription) {
console.log("Create answer:", sessionDescription);
pc.setLocalDescription(sessionDescription,sld_success_cb,sld_failure_cb);
var data = JSON.stringify(sessionDescription);
sendChannelOfferSdp(server, channelname, channelobj, data);
console.log(data);
},
function(error) { // error
console.log("Create answer error:", error);
}, mediaConstraints);
return pc;
}
function onRemoteStreamAdded(event) {
console.log("Remote stream added:");
}
function onSessionConnecting(message) {
console.log("Session connecting.");
}
function onSessionOpened(message) {
console.log("Session opened.");
}
function onRemoteStreamRemoved(event) {
console.log("Remote stream removed.");
}
function onRemoteSdpError(event) {
console.error('onRemoteSdpError', event.name, event.message);
}
function onRemoteSdpSucces() {
console.log('onRemoteSdpSucces');
}
function sld_success_cb() {
console.log("setLocalDescription success");
}
function sld_failure_cb() {
console.log("setLocalDescription failed");
}
function aic_success_cb() {
console.log("addIceCandidate success.");
}
function aic_failure_cb() {
console.log("addIceCandidate failed");
}
function sendChannelOfferSdp(server, channelname, channelobj, sdp)
{
$.ajax({
type: "GET",
url: server+'/rtc_sdp?channel='+channelname+'&sdp='+encodeURIComponent(sdp),
dataType: 'text',
success: function (data) {
//setTimeout(getNewMessage,1000,server, channelname,channelobj);
}
});
}
function sendChannelIceCandidate(server, channelname, channelobj, ice)
{
$.ajax({
type: "GET",
url: server+'/rtc_ice?channel='+channelname+'&ice='+ encodeURIComponent(ice),
dataType: 'text',
success: function (data) {
//setTimeout(getNewMessage,1000,server, channelname,channelobj);
}
});
}
function sendChannelIceCandidateFinish(server, channelname)
{
$.ajax({
type: "GET",
url: server+'/rtc_icefinish?channel='+channelname,
dataType: 'text',
success: function (data) {
//setTimeout(getNewMessage,1000,server, channelname,pc);
}
});
}
遇到的坑1:
推流端一定要在getUserMedia函数回调里才能调用createOffer, 跟在getUserMedia调用后面createOffer,可能sdp都发出去了,stream还没有add
遇到的坑2:
RTCPeerConnection的onaddstream回调已经废弃,使用ontrack代替了,网上一些比较老的例子还在用onstreamadd
遇到的坑3:
SDP和ICE是区分大小写的,服务器里把收到的消息做了个toLowerCase,导致sdp设置失败,失败原因也莫名其妙