我正在尝试实现实时音频/视频组呼叫,但现在我只想为两个参与者获得它。
它不工作,我不明白为什么:(实际上当我用两个不同的帐户同时测试它时,我看到一些错误消息,但它无论如何都有效,但当我在真实的不同网络中与朋友测试时,出现相同的错误消息,但在这种情况下,我们无法听到或看到对方)。
我在Linux上使用Chrome 53 (Ubuntu 16.04)。
错误消息如下,对于发送报价的对等体,浏览器控制台中有6个错误。1:
Failed to create remote session description: OperationError: Failed to set remote offer sdp: Called in wrong state: STATE_SENTOFFER
第二、三、四、五:
addIceCandidate error: OperationError: Error processing ICE candidate
6:
Failed to set local session description: OperationError: CreateAnswer failed because remote_description is not an offer
对于接收要约并发送和回答的对等端,浏览器控制台有1个错误:
Failed to create remote session description: OperationError: Failed to set remote answer sdp: Called in wrong state: STATE_INPROGRESS
如果您想在控制台中查看所有消息,则发送offer的对等体中的消息如下:WebRTC error from offer peer。另一个对等浏览器控制台中有:来自对等回答的WebRTC错误。
HTML文件中重要的代码如下所示(后面我会展示其他带有Javascript代码的文件):
<div class='row'>
<div class='col-xs'>
<div class='box center-xs middle xs'>
<h1>Call CallNameExample</h1>
</div>
</div>
</div>
<div class='row'>
<div class='col-xs'>
<div class='box center-content'>
<button class='btn btn-info btn-37 no-padding circle' id='btnChangeCamStatus'>
<i class='material-icons' id='iconCamOff'>
videocam_off
</i>
<i class='material-icons hidden' id='iconCamOn'>
videocam
</i>
</button>
<button class='btn btn-info btn-37 no-padding circle' id='btnChangeMicStatus'>
<i aria-hidden='true' class='fa fa-microphone-slash' id='iconMicOff'></i>
<i aria-hidden='true' class='fa fa-microphone hidden' id='iconMicOn'></i>
</button>
</div>
</div>
</div>
<div class='row'>
<div class='col-xs'>
<div class='box center-xs middle xs'>
<video autoplay height='200px' id='bigRemoteVideo' width='200px'></video>
</div>
</div>
</div>
<script>
var room = "1"
var localVideo = document.getElementById("localVideo")
var bigRemoteVideo = document.getElementById("bigRemoteVideo")
document.getElementById("btnChangeCamStatus").addEventListener("click", function() {
if (localStream.getVideoTracks()[0].enabled) {
disableCam()
$("#iconCamOff").addClass("hidden")
$("#iconCamOn").removeClass("hidden")
} else {
enableCam()
$("#iconCamOff").removeClass("hidden")
$("#iconCamOn").addClass("hidden")
}
}, false);
document.getElementById("btnChangeMicStatus").addEventListener("click", function() {
if (localStream.getAudioTracks()[0].enabled) {
disableMic()
$("#iconMicOff").addClass("hidden")
$("#iconMicOn").removeClass("hidden")
} else {
enableMic()
$("#iconMicOff").removeClass("hidden")
$("#iconMicOn").addClass("hidden")
}
}, false);
function setLocalVideo(stream) {
localVideo.src = window.URL.createObjectURL(stream)
}
function setRemoteVideo(stream) {
bigRemoteVideo.src = window.URL.createObjectURL(stream)
}
localVideo.addEventListener('loadedmetadata', function() {
console.log('Local video videoWidth: ' + this.videoWidth +
'px, videoHeight: ' + this.videoHeight + 'px');
});
bigRemoteVideo.addEventListener('loadedmetadata', function() {
console.log('Remote video videoWidth: ' + this.videoWidth +
'px, videoHeight: ' + this.videoHeight + 'px');
});
// Starts the party:
(function(){
enableUserMedia()
window.createOrJoin(room)
console.log("Attempted to create or join room: " + room)
}())
</script>
其他Javascript文件包含下面的代码(这里所有的文件放在一起):
var localStream
var mediaConstraints = {video: true, audio: true}
function enableUserMedia(){
console.log('Getting user media with constraints', mediaConstraints);
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia
if (navigator.getUserMedia) {
navigator.getUserMedia(mediaConstraints, gotStream, gotError)
}
window.URL = window.URL || window.webkitURL
function gotStream(stream) {
console.log('Adding local stream.');
setLocalVideo(stream)
localStream = stream;
//sendMessage('got user media');
console.log('got user media');
attachLocalMedia();
}
function gotError(error) {
console.log("navigator.getUserMedia error: ", error);
}
}
function disableCam(){
localStream.getVideoTracks()[0].enabled = false
}
function disableMic(){
localStream.getAudioTracks()[0].enabled = false
}
function enableCam(){
localStream.getVideoTracks()[0].enabled = true
}
function enableMic(){
localStream.getAudioTracks()[0].enabled = true
}
function disableUserMedia(){
localStream.getVideoTracks()[0].stop();
localStream.getAudioTracks()[0].stop();
}
window.onbeforeunload = function() {
sendMessage("bye");
};
function hangup() {
console.log("Hanging up.");
stop();
sendMessage("bye");
}
function handleRemoteHangup() {
console.log("Session terminated.");
stop();
}
function stop() {
disableUserMedia();
pc.close();
console.log("PC STATE: " + pc.signalingState || pc.readyState);
console.log("PC ICE STATE: " + pc.iceConnectionState)
pc = null;
}
var isInitiator = false
var justJoinedRoom = false
var sdpConstraints = { // Set up audio and video regardless of what devices are present.
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': true
}
}
function sendMessage(message){
App.call.message(message);
}
function doCall() {
console.log("Sending offer to peer");
pc.createOffer(sdpConstraints)
.then(setLocalAndSendMessage)
.catch(handleCreateOfferError);
//pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
}
function doAnswer() {
console.log("Sending answer to peer.");
pc.createAnswer()
.then(setLocalAndSendMessage)
.catch(onSetLocalSessionDescriptionError);
}
function setLocalAndSendMessage(sessionDescription) {
console.log("setLocalAndSendMessage sending message" + JSON.stringify(sessionDescription));
pc.setLocalDescription(sessionDescription)
.then(
function(){
onSetLocalSuccess();
sendMessage(sessionDescription);
}
)
.catch(onSetLocalSessionDescriptionError);
}
function onSetLocalSuccess() {
console.log('setLocalDescription complete');
}
function onSetRemoteSuccess() {
console.log('setRemoteDescription complete');
doAnswer();
}
function onSetLocalSessionDescriptionError(error) {
console.error('Failed to set local session description: ' + error.toString())
}
function handleCreateOfferError(event) {
console.error("createOffer() error: " + JSON.stringify(event))
}
function onSetRemoteSessionDescriptionError(error) {
console.error("Failed to create remote session description: " + error.toString())
}
function handleReceivedOffer(message) {
console.log("handleReceivedOffer: " + JSON.stringify(message));
pc.setRemoteDescription(new RTCSessionDescription(message))
.then(onSetRemoteSuccess)
.catch(onSetRemoteSessionDescriptionError)
}
function handleReceivedAnswer(message) {
console.log("handleReceivedAnswer: " + JSON.stringify(message));
pc.setRemoteDescription(new RTCSessionDescription(message))
.then(onSetRemoteSuccess)
.catch(onSetRemoteSessionDescriptionError)
}
function handleReceivedCandidate(label, candidate) {
pc.addIceCandidate(
new RTCIceCandidate({
sdpMLineIndex: label,
candidate: candidate
})
).then(successAddingIceCandidate).catch(errorAddingIceCandidate)
}
function successAddingIceCandidate() { console.log("addIceCandidate successfully") }
function errorAddingIceCandidate(error) { console.error("addIceCandidate error: " + error.toString()) }
var remoteStream
var pc
var pcConfig = {
'iceServers': [{
'url': 'stun:stun.l.google.com:19302'
}, {
'url': 'turn:192.158.29.39:3478?transport=udp',
'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
'username': '28224511:1379330808'
}]
}
function connectionStateCallback(){
var state;
if (pc) {
state = pc.connectionState
console.log("PC CONNECTION state change callback, state: " + state)
}
}
function signalingStateCallback() {
var state;
if (pc) {
state = pc.signalingState || pc.readyState;
console.log("PC SIGNALING state change callback, state: " + state);
}
}
function iceStateCallback() {
var iceState;
if (pc) {
iceState = pc.iceConnectionState;
console.log('PC ICE connection state change callback, state: ' + iceState);
}
}
function createPeerConnection() {
try {
pc = new RTCPeerConnection(pcConfig);
signalingStateCallback();
pc.onsignalingstatechange = signalingStateCallback;
console.log("PC ICE STATE: " + pc.iceConnectionState);
pc.oniceconnectionstatechange = iceStateCallback;
pc.onconnectionstatechange = connectionStateCallback;
pc.onicecandidate = handleIceCandidate;
pc.onaddstream = handleRemoteStreamAdded;
pc.onremovestream = handleRemoteStreamRemoved;
console.log('Created RTCPeerConnnection');
attachLocalMedia();
} catch (e) {
console.error("Failed to create PeerConnection, exception: " + e.toString())
return;
}
}
function handleIceCandidate(event) {
console.log("icecandidate event: " + JSON.stringify(event));
if (event.candidate) {
sendMessage({
type: "candidate",
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate
});
} else {
console.log("End of candidates.");
}
}
function handleRemoteStreamAdded(event) {
console.log("Remote stream added.");
setRemoteVideo(event.stream);
remoteStream = event.stream;
}
function handleRemoteStreamRemoved(event) { //In real life something should be done here but since the point of this website is to learn, this function is not a priority right now.
console.log("Remote stream removed. Event: " + event);
}
function attachLocalMedia() {
if (pc && localStream) {
pc.addStream(localStream)
console.log("Added localStream to pc")
if (justJoinedRoom) {
console.log("call to DOCALL() from attachLocalMedia()")
doCall()
}
}
}
最后是与信令相关的代码。但首先我想澄清一下,我正在用Rails 5做这个网站,并通过ActionCable与WebSockets进行信令,所以通道的CoffeeScript文件(客户端)是这个:
window.createOrJoin = (roomID) ->
App.call = App.cable.subscriptions.create { channel: "CallChannel", room: roomID },
connected: ->
# Called when the subscription is ready for use on the server
createPeerConnection()
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
# Called when there's incoming data on the websocket for this channel
if (data["kindOfData"] == "created")
console.log('Created room ' + data["room"])
window.isInitiator = true # ESTO ME SIRVE SOLO PARA 2 PERSONAS!! # CREO QUE YA NI LO USO
attachLocalMedia()
else if (data["kindOfData"] == "full")
console.log('Room ' + data["room"] + ' is full')
else if (data["kindOfData"] == "join")
console.log('Another peer made a request to join room ' + data["room"])
console.log('This peer is the initiator of room ' + data["room"] + '!')
window.justJoinedRoom = false
else if (data["kindOfData"] == "joined")
console.log('joined: ' + data["room"])
window.justJoinedRoom = true
attachLocalMedia()
else if (data["kindOfData"] == "log")
console.log(data["info"])
else if (data["kindOfData"] == "message") # This client receives a message
console.log("Client received message: " + JSON.stringify(data["message"]));
if (data["message"] == "bye")
handleRemoteHangup()
else if (data["message"]["type"] == "offer")
handleReceivedOffer(data["message"]) # obj with "type" and "sdp"
else if (data["message"]["type"] == "answer")
handleReceivedAnswer(data["message"]) # obj with "type" and "sdp"
else if (data["message"]["type"] == "candidate")
handleReceivedCandidate(data["message"]["label"], data["message"]["candidate"])
message: (data) ->
console.log("Client sending message: " + JSON.stringify(data));
@perform "message", {message: data, room: roomID}
和Ruby(服务器端):
class CallChannel < ApplicationCable::Channel
def subscribed # Action automatically called when a client is subscribed to the channel
stream_from "calls" # calls is a channel in common for everyone # ONLY FOR TESTING!!!
stream_from "calls_room#{params[:room]}_person#{current_user.id}"
@@hashUsersByRoom ||= Hash.new() # { |h,k| h[k] = Set.new }
@@hashRoomsByUser ||= Hash.new() # { |h,k| h[k] = Set.new }
result = createOrJoin(params[:room])
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def message(data)
if data["message"].eql? "bye"
if @@hashUsersByRoom[ data["room"] ] && @@hashUsersByRoom[ data["room"] ].include?( current_user.id )
@@hashUsersByRoom[ data["room"] ].delete( current_user.id )
if @@hashUsersByRoom[ data["room"] ].length() == 0
@@hashUsersByRoom.delete( data["room"] )
Call.find( data["room"] ).update_column("active", false)
end
end
if @@hashRoomsByUser[ current_user.id ] && @@hashRoomsByUser[ current_user.id ].include?( data["room"] )
@@hashRoomsByUser[ current_user.id ].delete( data["room"] )
if @@hashRoomsByUser[ current_user.id ].length() == 0
@@hashRoomsByUser.delete( current_user.id )
end
end
end
ActionCable.server.broadcast "calls_room#{data["room"]}", kindOfData: "log", info: "Client #{current_user.id} said: #{data["message"]}"
ActionCable.server.broadcast "calls_room#{data["room"]}", kindOfData: "message", message: data["message"]
end
private
def createOrJoin(room)
ActionCable.server.broadcast "calls", kindOfData: "log", info: "Received request to create or join room #{room}"
@@hashUsersByRoom[room] ||= Set.new()
ActionCable.server.broadcast "calls", kindOfData: "log", info: "Room #{room} now has #{@@hashUsersByRoom[room].length()} + client(s)"
if @@hashUsersByRoom[room].length == 0
stream_from "calls_room#{room}" # Join the room
@@hashUsersByRoom[ room ] << current_user.id
@@hashRoomsByUser[ current_user.id ] ||= Set.new()
@@hashRoomsByUser[ current_user.id ] << room
ActionCable.server.broadcast "calls", kindOfData: "log", info: "Client ID #{current_user.id} created room #{room}"
ActionCable.server.broadcast "calls_room#{room}_person#{current_user.id}", kindOfData: "created", room: room, user: current_user.id
Call.find(room).update_column("active", true)
elsif ( @@hashUsersByRoom[room].length() < Call.where(:id => room).pluck(:maximumNumberOfParticipants)[0] ) || ( @@hashUsersByRoom[ data["room"] ].include?( current_user.id ) )
ActionCable.server.broadcast "calls", kindOfData: "log", info: "Client ID #{current_user.id} joined room #{room}"
ActionCable.server.broadcast "calls_room#{room}", kindOfData: "join", room: room
stream_from "calls_room#{room}" # Join the room
@@hashUsersByRoom[ room ] << current_user.id
@@hashRoomsByUser[ current_user.id ] ||= Set.new()
@@hashRoomsByUser[ current_user.id ] << room
ActionCable.server.broadcast "calls_room#{room}_person#{current_user.id}", kindOfData: "joined", room: room, user: current_user.id
ActionCable.server.broadcast "calls_room#{room}", kindOfData: "ready"
else # full room
ActionCable.server.broadcast "calls_room#{room}_person#{current_user.id}", kindOfData: "full", room: room
end
end
end
在互联网上搜索,我看到有类似问题的人,但每个人都有不同的原因,没有一个对我的情况有用,但我在某处看到"STATE_INPROGRESS"意味着"Offer/answer exchange completed",因此我无法理解Offer/answer exchange是否已经完成…为什么当我和朋友一起使用时它不起作用?为什么它试图设置更多的远程会话描述在这种情况下(当提供/回答交换应该完成)?所以基本上我的主要问题是:发生了什么,我该如何解决它?
如果你达到了问题的这一部分,谢谢你,我很感激!:)如果你想做多方,你需要为每对参与者一个peerconnection。您当前使用的是单个文件