Javascript WebRTC 无法设置远程应答 sdp:在错误状态下调用:kHaveRemoteOffer 并在错误状态下调用:kStable
Javascript WebRTC Failed to set remote answer sdp: Called in wrong state: kHaveRemoteOffer and Called in wrong state: kStable
我无法让我的 WebRTC 代码正常工作。我相信我做的一切都是正确的,但它仍然无法正常工作。为什么这么早就调用 ontrack 有点奇怪,也许它应该是这样的。
该网站使用 javascript 代码,我没有使用服务器代码 post 但那是 WebSockets 连接的地方,它只是一个交换器,您发送到服务器的内容会将相同的信息发送回其他伙伴(陌生人)你也有联系。
服务器代码看起来像这个小示例
private void writeStranger(UserProfile you, String msg) {
UserProfile stranger = you.stranger;
if(stranger != null)
sendMessage(stranger.getWebSocket(), msg);
}
public void sendMessage(WebSocket websocket, String msg) {
try {
websocket.send(msg);
} catch ( WebsocketNotConnectedException e ) {
disconnnectClient(websocket);
}
}
//...
case "ice_candidate":
JSONObject candidatePackage = (JSONObject) packet.get(1);
JSONObject candidate = (JSONObject) candidatePackage.get("candidate");
obj = new JSONObject();
list = new JSONArray();
list.put("iceCandidate");
obj.put("candidate", candidate);
list.put(obj);
System.out.println("Sent = " + list.toString());
writeStranger(you, list.toString()); //send ice candidate to stranger
break;
case "send_answer":
JSONObject sendAnswerPackage = (JSONObject) packet.get(1);
JSONObject answer = (JSONObject) sendAnswerPackage.get("answer");
obj = new JSONObject();
list = new JSONArray();
list.put("getAnswer");
obj.put("answer", answer);
list.put(obj);
System.out.println("Sent = " + list.toString());
writeStranger(you, list.toString()); //send answer to stranger
break;
case "send_offer":
JSONObject offerPackage = (JSONObject) packet.get(1);
JSONObject offer = (JSONObject) offerPackage.get("offer");
obj = new JSONObject();
list = new JSONArray();
list.put("getOffer");
obj.put("offer", offer);
list.put(obj);
System.out.println("Sent = " + list.toString());
writeStranger(you, list.toString()); //send ice candidate to stranger
break;
这是我的输出。
原始文本:https://pastebin.com/raw/FL8g29gG
JSON 彩色:https://pastebin.com/FL8g29gG
我的javascript代码如下
var ws;
var peerConnection, localStream;
var rtc_server = {
iceServers: [
{urls: "stun:stun.l.google.com:19302"},
{urls: "stun:stun.services.mozilla.com"},
{urls: "stun:stun.stunprotocol.org:3478"},
{url: "stun:stun.l.google.com:19302"},
{url: "stun:stun.services.mozilla.com"},
{url: "stun:stun.stunprotocol.org:3478"},
]
}
//offer SDP's tells other peers what you would like
var rtc_media_constraints = {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
};
var rtc_peer_options = {
optional: [
{DtlsSrtpKeyAgreement: true}, //To make Chrome and Firefox to interoperate.
]
}
var PeerConnection = RTCPeerConnection || window.PeerConnection || window.webkitPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
var IceCandidate = RTCIceCandidate || window.mozRTCIceCandidate || window.RTCIceCandidate;
var SessionDescription = RTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription;
var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
function hasSupportForVideoChat() {
return window.RTCPeerConnection && window.RTCIceCandidate && window.RTCSessionDescription && navigator.mediaDevices && navigator.mediaDevices.getUserMedia && (RTCPeerConnection.prototype.addStream || RTCPeerConnection.prototype.addTrack) ? true : false;
}
function loadMyCameraStream() {
if (getUserMedia) {
getUserMedia.call(navigator, { video: {facingMode: "user", aspectRatio: 4 / 3/*height: 272, width: 322*/}, audio: { echoCancellation : true } },
function(localMediaStream) {
//Add my video
$("div#videoBox video#you")[0].muted = true;
$("div#videoBox video#you")[0].autoplay = true;
$("div#videoBox video#you").attr('playsinline', '');
$("div#videoBox video#you").attr('webkit-playsinline', '');
$("div#videoBox video#you")[0].srcObject = localMediaStream;
localStream = localMediaStream;
},
function(e) {
addStatusMsg("Your Video has error : " + e);
}
);
} else {
addStatusMsg("Your browser does not support WebRTC (Camera/Voice chat).");
return;
}
}
function loadStrangerCameraStream() {
if(!hasSupportForVideoChat())
return;
peerConnection = new PeerConnection(rtc_server, rtc_peer_options);
if (peerConnection.addTrack !== undefined)
localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
else
peerConnection.addStream(localStream);
peerConnection.onicecandidate = function(e) {
if (!e || !e.candidate)
return;
ws.send(JSON.stringify(['ice_candidate', {"candidate": e.candidate}]));
};
if (peerConnection.addTrack !== undefined) {
//newer technology
peerConnection.ontrack = function(e) {
//e.streams.forEach(stream => doAddStream(stream));
addStatusMsg("ontrack called");
//Add stranger video
$("div#videoBox video#stranger").attr('playsinline', '');
$("div#videoBox video#stranger").attr('webkit-playsinline', '');
$('div#videoBox video#stranger')[0].srcObject = e.streams[0];
$("div#videoBox video#stranger")[0].autoplay = true;
};
} else {
//older technology
peerConnection.onaddstream = function(e) {
addStatusMsg("onaddstream called");
//Add stranger video
$("div#videoBox video#stranger").attr('playsinline', '');
$("div#videoBox video#stranger").attr('webkit-playsinline', '');
$('div#videoBox video#stranger')[0].srcObject = e.stream;
$("div#videoBox video#stranger")[0].autoplay = true;
};
}
peerConnection.createOffer(
function(offer) {
peerConnection.setLocalDescription(offer, function () {
//both offer and peerConnection.localDescription are the same.
addStatusMsg('createOffer, localDescription: ' + JSON.stringify(peerConnection.localDescription));
//addStatusMsg('createOffer, offer: ' + JSON.stringify(offer));
ws.send(JSON.stringify(['send_offer', {"offer": peerConnection.localDescription}]));
},
function(e) {
addStatusMsg('createOffer, set description error' + e);
});
},
function(e) {
addStatusMsg("createOffer error: " + e);
},
rtc_media_constraints
);
}
function closeStrangerCameraStream() {
$('div#videoBox video#stranger')[0].srcObject = null
if(peerConnection)
peerConnection.close();
}
function iceCandidate(candidate) {
//ICE = Interactive Connectivity Establishment
if(peerConnection)
peerConnection.addIceCandidate(new IceCandidate(candidate));
else
addStatusMsg("peerConnection not created error");
addStatusMsg("Peer Ice Candidate = " + JSON.stringify(candidate));
}
function getAnswer(answer) {
if(!hasSupportForVideoChat())
return;
if(peerConnection) {
peerConnection.setRemoteDescription(new SessionDescription(answer), function() {
console.log("get answer ok");
addStatusMsg("peerConnection, SessionDescription answer is ok");
},
function(e) {
addStatusMsg("peerConnection, SessionDescription fail error: " + e);
});
}
}
function getOffer(offer) {
if(!hasSupportForVideoChat())
return;
addStatusMsg("peerConnection, setRemoteDescription offer: " + JSON.stringify(offer));
if(peerConnection) {
peerConnection.setRemoteDescription(new SessionDescription(offer), function() {
peerConnection.createAnswer(
function(answer) {
peerConnection.setLocalDescription(answer);
addStatusMsg("create answer sent: " + JSON.stringify(answer));
ws.send(JSON.stringify(['send_answer', {"answer": answer}]));
},
function(e) {
addStatusMsg("peerConnection, setRemoteDescription create answer fail: " + e);
}
);
});
}
}
我使用它的网站:https://www.camspark.com/
修复了我自己,我发现这段代码有 2 个问题。
第一个问题是 createOffer() 只能由一个人发送,不能由两个人发送。您必须随机选择执行 createOffer() 的人。
第二个问题是 ICE 候选人你必须为双方创建一个 queue/array,它包含所有传入的 ice_candidates。仅当收到对 createOffer() 的响应并设置来自 createOffer()
响应的 setRemoteDescription
时才执行 peerConnection.addIceCandidate(new IceCandidate(candidate));
。
getAnswer() 和 getOffer() 使用完全相同的代码,但一个是为一个客户接收的,另一个是为另一个客户接收的。当其中一个被触发时,两者都需要刷新 IceCandidates 数组。也许如果有人愿意,你可以将两个函数组合成一个函数,因为代码是相同的。
最终的工作代码如下所示
var ws;
var peerConnection, localStream;
//STUN = (Session Traversal Utilities for NAT)
var rtc_server = {
iceServers: [
{urls: "stun:stun.l.google.com:19302"},
{urls: "stun:stun.services.mozilla.com"},
{urls: "stun:stun.stunprotocol.org:3478"},
{url: "stun:stun.l.google.com:19302"},
{url: "stun:stun.services.mozilla.com"},
{url: "stun:stun.stunprotocol.org:3478"},
]
}
//offer SDP = [Session Description Protocol] tells other peers what you would like
var rtc_media_constraints = {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
};
var rtc_peer_options = {
optional: [
{DtlsSrtpKeyAgreement: true}, //To make Chrome and Firefox to interoperate.
]
}
var finishSDPVideoOffer = false;
var isOfferer = false;
var iceCandidates = [];
var PeerConnection = RTCPeerConnection || window.PeerConnection || window.webkitPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
var IceCandidate = RTCIceCandidate || window.mozRTCIceCandidate || window.RTCIceCandidate;
var SessionDescription = RTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription;
var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
function hasSupportForVideoChat() {
return window.RTCPeerConnection && window.RTCIceCandidate && window.RTCSessionDescription && navigator.mediaDevices && navigator.mediaDevices.getUserMedia && (RTCPeerConnection.prototype.addStream || RTCPeerConnection.prototype.addTrack) ? true : false;
}
function loadMyCameraStream() {
if (getUserMedia) {
getUserMedia.call(navigator, { video: {facingMode: "user", aspectRatio: 4 / 3/*height: 272, width: 322*/}, audio: { echoCancellation : true } },
function(localMediaStream) {
//Add my video
$("div#videoBox video#you")[0].muted = true;
$("div#videoBox video#you")[0].autoplay = true;
$("div#videoBox video#you").attr('playsinline', '');
$("div#videoBox video#you").attr('webkit-playsinline', '');
$("div#videoBox video#you")[0].srcObject = localMediaStream;
localStream = localMediaStream;
},
function(e) {
addStatusMsg("Your Video has error : " + e);
}
);
} else {
addStatusMsg("Your browser does not support WebRTC (Camera/Voice chat).");
return;
}
}
function loadStrangerCameraStream(isOfferer_) {
if(!hasSupportForVideoChat())
return;
//Only add pending ICE Candidates when getOffer() is finished.
finishSDPVideoOfferOrAnswer = false;
iceCandidates = []; //clear ICE Candidates array.
isOfferer = isOfferer_;
peerConnection = new PeerConnection(rtc_server, rtc_peer_options);
if (peerConnection.addTrack !== undefined)
localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
else
peerConnection.addStream(localStream);
peerConnection.onicecandidate = function(e) {
if (!e || !e.candidate)
return;
ws.send(JSON.stringify(['ice_candidate', {"candidate": e.candidate}]));
};
if (peerConnection.addTrack !== undefined) {
//newer technology
peerConnection.ontrack = function(e) {
//e.streams.forEach(stream => doAddStream(stream));
addStatusMsg("ontrack called");
//Add stranger video
$("div#videoBox video#stranger").attr('playsinline', '');
$("div#videoBox video#stranger").attr('webkit-playsinline', '');
$('div#videoBox video#stranger')[0].srcObject = e.streams[0];
$("div#videoBox video#stranger")[0].autoplay = true;
};
} else {
//older technology
peerConnection.onaddstream = function(e) {
addStatusMsg("onaddstream called");
//Add stranger video
$("div#videoBox video#stranger").attr('playsinline', '');
$("div#videoBox video#stranger").attr('webkit-playsinline', '');
$('div#videoBox video#stranger')[0].srcObject = e.stream;
$("div#videoBox video#stranger")[0].autoplay = true;
};
}
if(isOfferer) {
peerConnection.createOffer(
function(offer) {
peerConnection.setLocalDescription(offer, function () {
//both offer and peerConnection.localDescription are the same.
addStatusMsg('createOffer, localDescription: ' + JSON.stringify(peerConnection.localDescription));
//addStatusMsg('createOffer, offer: ' + JSON.stringify(offer));
ws.send(JSON.stringify(['send_offer', {"offer": peerConnection.localDescription}]));
},
function(e) {
addStatusMsg('createOffer, set description error' + e);
});
},
function(e) {
addStatusMsg("createOffer error: " + e);
},
rtc_media_constraints
);
}
}
function closeStrangerCameraStream() {
$('div#videoBox video#stranger')[0].srcObject = null
if(peerConnection)
peerConnection.close();
}
function iceCandidate(candidate) {
//ICE = Interactive Connectivity Establishment
if(!finishSDPVideoOfferOrAnswer) {
iceCandidates.push(candidate);
addStatusMsg("Queued iceCandidate");
return;
}
if(!peerConnection) {
addStatusMsg("iceCandidate peerConnection not created error.");
return;
}
peerConnection.addIceCandidate(new IceCandidate(candidate));
addStatusMsg("Added on time, Peer Ice Candidate = " + JSON.stringify(candidate));
}
function getAnswer(answer) {
if(!hasSupportForVideoChat())
return;
if(!peerConnection) {
addStatusMsg("getAnswer peerConnection not created error.");
return;
}
peerConnection.setRemoteDescription(new SessionDescription(answer), function() {
addStatusMsg("getAnswer SessionDescription answer is ok");
finishSDPVideoOfferOrAnswer = true;
while (iceCandidates.length) {
var candidate = iceCandidates.shift();
try {
peerConnection.addIceCandidate(new IceCandidate(candidate));
addStatusMsg("Adding queued ICE Candidates");
} catch(e) {
addStatusMsg("Error adding queued ICE Candidates error:" + e);
}
}
iceCandidates = [];
},
function(e) {
addStatusMsg("getAnswer SessionDescription fail error: " + e);
});
}
function getOffer(offer) {
if(!hasSupportForVideoChat())
return;
if(!peerConnection) {
addStatusMsg("getOffer peerConnection not created error.");
return;
}
addStatusMsg("getOffer setRemoteDescription offer: " + JSON.stringify(offer));
peerConnection.setRemoteDescription(new SessionDescription(offer), function() {
finishSDPVideoOfferOrAnswer = true;
while (iceCandidates.length) {
var candidate = iceCandidates.shift();
try {
peerConnection.addIceCandidate(new IceCandidate(candidate));
addStatusMsg("Adding queued ICE Candidates");
} catch(e) {
addStatusMsg("Error adding queued ICE Candidates error:" + e);
}
}
iceCandidates = [];
if(!isOfferer) {
peerConnection.createAnswer(
function(answer) {
peerConnection.setLocalDescription(answer);
addStatusMsg("getOffer create answer sent: " + JSON.stringify(answer));
ws.send(JSON.stringify(['send_answer', {"answer": answer}]));
},
function(e) {
addStatusMsg("getOffer setRemoteDescription create answer fail: " + e);
}
);
}
});
}
这是我在服务器端 WebSocket (Java) 服务器上做的补丁。
//JSON
//["connected", {videoChatOfferer: true}]
//["connected", {videoChatOfferer: false}]
JSONObject obj = new JSONObject();
JSONArray list = new JSONArray();
list.put("loadStrangerCameraStream");
obj.put("videoChatOfferer", true); //first guy offerer for WebRTC.
list.put(obj);
server.sendMessage(websocket, list.toString()); //connected to chat partner
obj.put("videoChatOfferer", false); //second guy isn't offerer.
list.put(obj);
server.sendMessage(stranger.getWebSocket(), list.toString()); //connected to chat partner
我无法让我的 WebRTC 代码正常工作。我相信我做的一切都是正确的,但它仍然无法正常工作。为什么这么早就调用 ontrack 有点奇怪,也许它应该是这样的。
该网站使用 javascript 代码,我没有使用服务器代码 post 但那是 WebSockets 连接的地方,它只是一个交换器,您发送到服务器的内容会将相同的信息发送回其他伙伴(陌生人)你也有联系。
服务器代码看起来像这个小示例
private void writeStranger(UserProfile you, String msg) {
UserProfile stranger = you.stranger;
if(stranger != null)
sendMessage(stranger.getWebSocket(), msg);
}
public void sendMessage(WebSocket websocket, String msg) {
try {
websocket.send(msg);
} catch ( WebsocketNotConnectedException e ) {
disconnnectClient(websocket);
}
}
//...
case "ice_candidate":
JSONObject candidatePackage = (JSONObject) packet.get(1);
JSONObject candidate = (JSONObject) candidatePackage.get("candidate");
obj = new JSONObject();
list = new JSONArray();
list.put("iceCandidate");
obj.put("candidate", candidate);
list.put(obj);
System.out.println("Sent = " + list.toString());
writeStranger(you, list.toString()); //send ice candidate to stranger
break;
case "send_answer":
JSONObject sendAnswerPackage = (JSONObject) packet.get(1);
JSONObject answer = (JSONObject) sendAnswerPackage.get("answer");
obj = new JSONObject();
list = new JSONArray();
list.put("getAnswer");
obj.put("answer", answer);
list.put(obj);
System.out.println("Sent = " + list.toString());
writeStranger(you, list.toString()); //send answer to stranger
break;
case "send_offer":
JSONObject offerPackage = (JSONObject) packet.get(1);
JSONObject offer = (JSONObject) offerPackage.get("offer");
obj = new JSONObject();
list = new JSONArray();
list.put("getOffer");
obj.put("offer", offer);
list.put(obj);
System.out.println("Sent = " + list.toString());
writeStranger(you, list.toString()); //send ice candidate to stranger
break;
这是我的输出。
原始文本:https://pastebin.com/raw/FL8g29gG
JSON 彩色:https://pastebin.com/FL8g29gG
我的javascript代码如下
var ws;
var peerConnection, localStream;
var rtc_server = {
iceServers: [
{urls: "stun:stun.l.google.com:19302"},
{urls: "stun:stun.services.mozilla.com"},
{urls: "stun:stun.stunprotocol.org:3478"},
{url: "stun:stun.l.google.com:19302"},
{url: "stun:stun.services.mozilla.com"},
{url: "stun:stun.stunprotocol.org:3478"},
]
}
//offer SDP's tells other peers what you would like
var rtc_media_constraints = {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
};
var rtc_peer_options = {
optional: [
{DtlsSrtpKeyAgreement: true}, //To make Chrome and Firefox to interoperate.
]
}
var PeerConnection = RTCPeerConnection || window.PeerConnection || window.webkitPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
var IceCandidate = RTCIceCandidate || window.mozRTCIceCandidate || window.RTCIceCandidate;
var SessionDescription = RTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription;
var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
function hasSupportForVideoChat() {
return window.RTCPeerConnection && window.RTCIceCandidate && window.RTCSessionDescription && navigator.mediaDevices && navigator.mediaDevices.getUserMedia && (RTCPeerConnection.prototype.addStream || RTCPeerConnection.prototype.addTrack) ? true : false;
}
function loadMyCameraStream() {
if (getUserMedia) {
getUserMedia.call(navigator, { video: {facingMode: "user", aspectRatio: 4 / 3/*height: 272, width: 322*/}, audio: { echoCancellation : true } },
function(localMediaStream) {
//Add my video
$("div#videoBox video#you")[0].muted = true;
$("div#videoBox video#you")[0].autoplay = true;
$("div#videoBox video#you").attr('playsinline', '');
$("div#videoBox video#you").attr('webkit-playsinline', '');
$("div#videoBox video#you")[0].srcObject = localMediaStream;
localStream = localMediaStream;
},
function(e) {
addStatusMsg("Your Video has error : " + e);
}
);
} else {
addStatusMsg("Your browser does not support WebRTC (Camera/Voice chat).");
return;
}
}
function loadStrangerCameraStream() {
if(!hasSupportForVideoChat())
return;
peerConnection = new PeerConnection(rtc_server, rtc_peer_options);
if (peerConnection.addTrack !== undefined)
localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
else
peerConnection.addStream(localStream);
peerConnection.onicecandidate = function(e) {
if (!e || !e.candidate)
return;
ws.send(JSON.stringify(['ice_candidate', {"candidate": e.candidate}]));
};
if (peerConnection.addTrack !== undefined) {
//newer technology
peerConnection.ontrack = function(e) {
//e.streams.forEach(stream => doAddStream(stream));
addStatusMsg("ontrack called");
//Add stranger video
$("div#videoBox video#stranger").attr('playsinline', '');
$("div#videoBox video#stranger").attr('webkit-playsinline', '');
$('div#videoBox video#stranger')[0].srcObject = e.streams[0];
$("div#videoBox video#stranger")[0].autoplay = true;
};
} else {
//older technology
peerConnection.onaddstream = function(e) {
addStatusMsg("onaddstream called");
//Add stranger video
$("div#videoBox video#stranger").attr('playsinline', '');
$("div#videoBox video#stranger").attr('webkit-playsinline', '');
$('div#videoBox video#stranger')[0].srcObject = e.stream;
$("div#videoBox video#stranger")[0].autoplay = true;
};
}
peerConnection.createOffer(
function(offer) {
peerConnection.setLocalDescription(offer, function () {
//both offer and peerConnection.localDescription are the same.
addStatusMsg('createOffer, localDescription: ' + JSON.stringify(peerConnection.localDescription));
//addStatusMsg('createOffer, offer: ' + JSON.stringify(offer));
ws.send(JSON.stringify(['send_offer', {"offer": peerConnection.localDescription}]));
},
function(e) {
addStatusMsg('createOffer, set description error' + e);
});
},
function(e) {
addStatusMsg("createOffer error: " + e);
},
rtc_media_constraints
);
}
function closeStrangerCameraStream() {
$('div#videoBox video#stranger')[0].srcObject = null
if(peerConnection)
peerConnection.close();
}
function iceCandidate(candidate) {
//ICE = Interactive Connectivity Establishment
if(peerConnection)
peerConnection.addIceCandidate(new IceCandidate(candidate));
else
addStatusMsg("peerConnection not created error");
addStatusMsg("Peer Ice Candidate = " + JSON.stringify(candidate));
}
function getAnswer(answer) {
if(!hasSupportForVideoChat())
return;
if(peerConnection) {
peerConnection.setRemoteDescription(new SessionDescription(answer), function() {
console.log("get answer ok");
addStatusMsg("peerConnection, SessionDescription answer is ok");
},
function(e) {
addStatusMsg("peerConnection, SessionDescription fail error: " + e);
});
}
}
function getOffer(offer) {
if(!hasSupportForVideoChat())
return;
addStatusMsg("peerConnection, setRemoteDescription offer: " + JSON.stringify(offer));
if(peerConnection) {
peerConnection.setRemoteDescription(new SessionDescription(offer), function() {
peerConnection.createAnswer(
function(answer) {
peerConnection.setLocalDescription(answer);
addStatusMsg("create answer sent: " + JSON.stringify(answer));
ws.send(JSON.stringify(['send_answer', {"answer": answer}]));
},
function(e) {
addStatusMsg("peerConnection, setRemoteDescription create answer fail: " + e);
}
);
});
}
}
我使用它的网站:https://www.camspark.com/
修复了我自己,我发现这段代码有 2 个问题。
第一个问题是 createOffer() 只能由一个人发送,不能由两个人发送。您必须随机选择执行 createOffer() 的人。
第二个问题是 ICE 候选人你必须为双方创建一个 queue/array,它包含所有传入的 ice_candidates。仅当收到对 createOffer() 的响应并设置来自 createOffer()
响应的 setRemoteDescription
时才执行 peerConnection.addIceCandidate(new IceCandidate(candidate));
。
getAnswer() 和 getOffer() 使用完全相同的代码,但一个是为一个客户接收的,另一个是为另一个客户接收的。当其中一个被触发时,两者都需要刷新 IceCandidates 数组。也许如果有人愿意,你可以将两个函数组合成一个函数,因为代码是相同的。
最终的工作代码如下所示
var ws;
var peerConnection, localStream;
//STUN = (Session Traversal Utilities for NAT)
var rtc_server = {
iceServers: [
{urls: "stun:stun.l.google.com:19302"},
{urls: "stun:stun.services.mozilla.com"},
{urls: "stun:stun.stunprotocol.org:3478"},
{url: "stun:stun.l.google.com:19302"},
{url: "stun:stun.services.mozilla.com"},
{url: "stun:stun.stunprotocol.org:3478"},
]
}
//offer SDP = [Session Description Protocol] tells other peers what you would like
var rtc_media_constraints = {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
};
var rtc_peer_options = {
optional: [
{DtlsSrtpKeyAgreement: true}, //To make Chrome and Firefox to interoperate.
]
}
var finishSDPVideoOffer = false;
var isOfferer = false;
var iceCandidates = [];
var PeerConnection = RTCPeerConnection || window.PeerConnection || window.webkitPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
var IceCandidate = RTCIceCandidate || window.mozRTCIceCandidate || window.RTCIceCandidate;
var SessionDescription = RTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription;
var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
function hasSupportForVideoChat() {
return window.RTCPeerConnection && window.RTCIceCandidate && window.RTCSessionDescription && navigator.mediaDevices && navigator.mediaDevices.getUserMedia && (RTCPeerConnection.prototype.addStream || RTCPeerConnection.prototype.addTrack) ? true : false;
}
function loadMyCameraStream() {
if (getUserMedia) {
getUserMedia.call(navigator, { video: {facingMode: "user", aspectRatio: 4 / 3/*height: 272, width: 322*/}, audio: { echoCancellation : true } },
function(localMediaStream) {
//Add my video
$("div#videoBox video#you")[0].muted = true;
$("div#videoBox video#you")[0].autoplay = true;
$("div#videoBox video#you").attr('playsinline', '');
$("div#videoBox video#you").attr('webkit-playsinline', '');
$("div#videoBox video#you")[0].srcObject = localMediaStream;
localStream = localMediaStream;
},
function(e) {
addStatusMsg("Your Video has error : " + e);
}
);
} else {
addStatusMsg("Your browser does not support WebRTC (Camera/Voice chat).");
return;
}
}
function loadStrangerCameraStream(isOfferer_) {
if(!hasSupportForVideoChat())
return;
//Only add pending ICE Candidates when getOffer() is finished.
finishSDPVideoOfferOrAnswer = false;
iceCandidates = []; //clear ICE Candidates array.
isOfferer = isOfferer_;
peerConnection = new PeerConnection(rtc_server, rtc_peer_options);
if (peerConnection.addTrack !== undefined)
localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
else
peerConnection.addStream(localStream);
peerConnection.onicecandidate = function(e) {
if (!e || !e.candidate)
return;
ws.send(JSON.stringify(['ice_candidate', {"candidate": e.candidate}]));
};
if (peerConnection.addTrack !== undefined) {
//newer technology
peerConnection.ontrack = function(e) {
//e.streams.forEach(stream => doAddStream(stream));
addStatusMsg("ontrack called");
//Add stranger video
$("div#videoBox video#stranger").attr('playsinline', '');
$("div#videoBox video#stranger").attr('webkit-playsinline', '');
$('div#videoBox video#stranger')[0].srcObject = e.streams[0];
$("div#videoBox video#stranger")[0].autoplay = true;
};
} else {
//older technology
peerConnection.onaddstream = function(e) {
addStatusMsg("onaddstream called");
//Add stranger video
$("div#videoBox video#stranger").attr('playsinline', '');
$("div#videoBox video#stranger").attr('webkit-playsinline', '');
$('div#videoBox video#stranger')[0].srcObject = e.stream;
$("div#videoBox video#stranger")[0].autoplay = true;
};
}
if(isOfferer) {
peerConnection.createOffer(
function(offer) {
peerConnection.setLocalDescription(offer, function () {
//both offer and peerConnection.localDescription are the same.
addStatusMsg('createOffer, localDescription: ' + JSON.stringify(peerConnection.localDescription));
//addStatusMsg('createOffer, offer: ' + JSON.stringify(offer));
ws.send(JSON.stringify(['send_offer', {"offer": peerConnection.localDescription}]));
},
function(e) {
addStatusMsg('createOffer, set description error' + e);
});
},
function(e) {
addStatusMsg("createOffer error: " + e);
},
rtc_media_constraints
);
}
}
function closeStrangerCameraStream() {
$('div#videoBox video#stranger')[0].srcObject = null
if(peerConnection)
peerConnection.close();
}
function iceCandidate(candidate) {
//ICE = Interactive Connectivity Establishment
if(!finishSDPVideoOfferOrAnswer) {
iceCandidates.push(candidate);
addStatusMsg("Queued iceCandidate");
return;
}
if(!peerConnection) {
addStatusMsg("iceCandidate peerConnection not created error.");
return;
}
peerConnection.addIceCandidate(new IceCandidate(candidate));
addStatusMsg("Added on time, Peer Ice Candidate = " + JSON.stringify(candidate));
}
function getAnswer(answer) {
if(!hasSupportForVideoChat())
return;
if(!peerConnection) {
addStatusMsg("getAnswer peerConnection not created error.");
return;
}
peerConnection.setRemoteDescription(new SessionDescription(answer), function() {
addStatusMsg("getAnswer SessionDescription answer is ok");
finishSDPVideoOfferOrAnswer = true;
while (iceCandidates.length) {
var candidate = iceCandidates.shift();
try {
peerConnection.addIceCandidate(new IceCandidate(candidate));
addStatusMsg("Adding queued ICE Candidates");
} catch(e) {
addStatusMsg("Error adding queued ICE Candidates error:" + e);
}
}
iceCandidates = [];
},
function(e) {
addStatusMsg("getAnswer SessionDescription fail error: " + e);
});
}
function getOffer(offer) {
if(!hasSupportForVideoChat())
return;
if(!peerConnection) {
addStatusMsg("getOffer peerConnection not created error.");
return;
}
addStatusMsg("getOffer setRemoteDescription offer: " + JSON.stringify(offer));
peerConnection.setRemoteDescription(new SessionDescription(offer), function() {
finishSDPVideoOfferOrAnswer = true;
while (iceCandidates.length) {
var candidate = iceCandidates.shift();
try {
peerConnection.addIceCandidate(new IceCandidate(candidate));
addStatusMsg("Adding queued ICE Candidates");
} catch(e) {
addStatusMsg("Error adding queued ICE Candidates error:" + e);
}
}
iceCandidates = [];
if(!isOfferer) {
peerConnection.createAnswer(
function(answer) {
peerConnection.setLocalDescription(answer);
addStatusMsg("getOffer create answer sent: " + JSON.stringify(answer));
ws.send(JSON.stringify(['send_answer', {"answer": answer}]));
},
function(e) {
addStatusMsg("getOffer setRemoteDescription create answer fail: " + e);
}
);
}
});
}
这是我在服务器端 WebSocket (Java) 服务器上做的补丁。
//JSON
//["connected", {videoChatOfferer: true}]
//["connected", {videoChatOfferer: false}]
JSONObject obj = new JSONObject();
JSONArray list = new JSONArray();
list.put("loadStrangerCameraStream");
obj.put("videoChatOfferer", true); //first guy offerer for WebRTC.
list.put(obj);
server.sendMessage(websocket, list.toString()); //connected to chat partner
obj.put("videoChatOfferer", false); //second guy isn't offerer.
list.put(obj);
server.sendMessage(stranger.getWebSocket(), list.toString()); //connected to chat partner