如何在 Chrome 中为 WebRTC 呼叫者设置远程描述而不会出错?
How to set remote description for a WebRTC caller in Chrome without errors?
希望逻辑没有漏洞
第 1 步: 来电者创建报价
步骤 2: 调用者设置 localDescription
第 3 步: 调用者将描述发送给被调用者
//---------------------------------------- ----------//
第 4 步: 被叫方接收报价设置远程描述
第 5 步: 被叫方创建应答
第 6 步: 被叫方设置本地描述
第 7 步: 被叫方将描述发送给来电方
//---------------------------------------- ----------//
第 8 步: 调用方收到应答并设置远程描述
这是上面的代码
const socket = io();
const constraints = {
audio: true,
video: true
};
const configuration = {
iceServers: [{
"url": "stun:23.21.150.121"
}, {
"url": "stun:stun.l.google.com:19302"
}]
};
const selfView = $('#selfView')[0];
const remoteView = $('#remoteView')[0];
var pc = new RTCPeerConnection(configuration);
pc.onicecandidate = ({
candidate
}) => {
socket.emit('message', {
to: $('#remote').val(),
candidate: candidate
});
};
pc.onnegotiationneeded = async () => {
try {
await pc.setLocalDescription(await pc.createOffer());
socket.emit('message', {
to: $('#remote').val(),
desc: pc.localDescription
});
} catch (err) {
console.error(err);
}
};
pc.ontrack = (event) => {
// don't set srcObject again if it is already set.
if (remoteView.srcObject) return;
remoteView.srcObject = event.streams[0];
};
socket.on('message', async ({
from,
desc,
candidate
}) => {
$('#remote').val(from);
try {
if (desc) {
// if we get an offer, we need to reply with an answer
if (desc.type === 'offer') {
await pc.setRemoteDescription(desc);
const stream = await navigator.mediaDevices.getUserMedia(constraints);
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
selfView.srcObject = stream;
await pc.setLocalDescription(await pc.createAnswer());
console.log(pc.localDescription);
socket.emit({
to: from,
desc: pc.localDescription
});
} else if (desc.type === 'answer') {
await pc.setRemoteDescription(desc).catch(err => console.log(err));
} else {
console.log('Unsupported SDP type.');
}
} else if (candidate) {
await pc.addIceCandidate(new RTCIceCandidate(candidate)).catch(err => console.log(err));
}
} catch (err) {
console.error(err);
}
});
async function start() {
try {
// get local stream, show it in self-view and add it to be sent
const stream = await requestUserMedia(constraints);
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
attachMediaStream(selfView, stream);
} catch (err) {
console.error(err);
}
}
socket.on('id', (data) => {
$('#myid').text(data.id);
});
// this function is called once the caller hits connect after inserting the unique id of the callee
async function connect() {
try {
await pc.setLocalDescription(await pc.createOffer());
socket.emit('message', {
to: $('#remote').val(),
desc: pc.localDescription
});
} catch (err) {
console.error(err);
}
}
socket.on('error', data => {
console.log(data);
});
现在这段代码在执行步骤 8
时抛出错误
DOMException: Failed to execute 'setRemoteDescription' on
'RTCPeerConnection': Failed to set remote offer sdp: Called in wrong
state: kHaveLocalOffer
DOMException: Failed to execute 'addIceCandidate' on
'RTCPeerConnection': Error processing ICE candidate
尝试调试但未发现逻辑或代码中的任何缺陷。注意到 pc
对象具有 localDescription
和 currentLocalDescription
的一件奇怪的事情,我认为创建答案的被调用者必须同时具有描述类型 answer
而是显示localDescription
为 offer
并且 currentLocalDescription
类型为 answer
.
我不知道它是否应该像我初学者那样表现。
提前致谢。
您的代码是正确的。这是一个长期 bug in Chrome 和 negotiationneeded
.
我在 a fiddle 中对其进行了检测(右键单击并打开相邻的两个 windows,然后单击调用其中一个)。
在 Firefox 中,它可以工作。报价方协商一次,因为您一次添加了两条轨道(video/audio):
negotiating in stable
onmessage answer
并且,在回答者方面,您在 'stable'
状态之外添加的曲目将添加到答案中:
onmessage offer
adding audio track
adding video track
但在 Chrome 中,它坏了,在提供者上发射 negotiationneeded
两次,每添加一个曲目一次:
negotiating in stable
negotiating in stable
onmessage offer
DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection':
Failed to set remote offer sdp: Called in wrong state: kHaveLocalOffer
onmessage offer
DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection':
Failed to set remote offer sdp: Called in wrong state: kHaveLocalOffer
onmessage offer
DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection':
Failed to set remote offer sdp: Called in wrong state: kHaveLocalOffer
并在应答端触发 negotiationneeded
两次,甚至不在 'stable'
状态:
onmessage offer
adding audio track
adding video track
negotiating in have-remote-offer
negotiating in have-remote-offer
onmessage offer
DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection':
Failed to set remote offer sdp: Called in wrong state: kHaveLocalOffer
这些额外的事件导致两端出现相互状态错误的严重破坏。
具体来说,Chrome 在这里违反了 spec 的两个部分:
"Queue a task" 来触发这个事件。 "queueing prevents negotiationneeded from firing prematurely, in the common situation where multiple modifications to connection are being made at once."
如果连接的信号状态不是 "stable"
,则中止这些步骤[以触发事件]。
解决方法
解决两个 Chrome 错误需要(为简洁起见使用async
/await
):
let negotiating = false;
pc.onnegotiationneeded = async e => {
try {
if (negotiating || pc.signalingState != "stable") return;
negotiating = true;
/* Your async/await-using code goes here */
} finally {
negotiating = false;
}
}
希望逻辑没有漏洞
第 1 步: 来电者创建报价
步骤 2: 调用者设置 localDescription
第 3 步: 调用者将描述发送给被调用者
//---------------------------------------- ----------//
第 4 步: 被叫方接收报价设置远程描述
第 5 步: 被叫方创建应答
第 6 步: 被叫方设置本地描述
第 7 步: 被叫方将描述发送给来电方
//---------------------------------------- ----------//
第 8 步: 调用方收到应答并设置远程描述
这是上面的代码
const socket = io();
const constraints = {
audio: true,
video: true
};
const configuration = {
iceServers: [{
"url": "stun:23.21.150.121"
}, {
"url": "stun:stun.l.google.com:19302"
}]
};
const selfView = $('#selfView')[0];
const remoteView = $('#remoteView')[0];
var pc = new RTCPeerConnection(configuration);
pc.onicecandidate = ({
candidate
}) => {
socket.emit('message', {
to: $('#remote').val(),
candidate: candidate
});
};
pc.onnegotiationneeded = async () => {
try {
await pc.setLocalDescription(await pc.createOffer());
socket.emit('message', {
to: $('#remote').val(),
desc: pc.localDescription
});
} catch (err) {
console.error(err);
}
};
pc.ontrack = (event) => {
// don't set srcObject again if it is already set.
if (remoteView.srcObject) return;
remoteView.srcObject = event.streams[0];
};
socket.on('message', async ({
from,
desc,
candidate
}) => {
$('#remote').val(from);
try {
if (desc) {
// if we get an offer, we need to reply with an answer
if (desc.type === 'offer') {
await pc.setRemoteDescription(desc);
const stream = await navigator.mediaDevices.getUserMedia(constraints);
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
selfView.srcObject = stream;
await pc.setLocalDescription(await pc.createAnswer());
console.log(pc.localDescription);
socket.emit({
to: from,
desc: pc.localDescription
});
} else if (desc.type === 'answer') {
await pc.setRemoteDescription(desc).catch(err => console.log(err));
} else {
console.log('Unsupported SDP type.');
}
} else if (candidate) {
await pc.addIceCandidate(new RTCIceCandidate(candidate)).catch(err => console.log(err));
}
} catch (err) {
console.error(err);
}
});
async function start() {
try {
// get local stream, show it in self-view and add it to be sent
const stream = await requestUserMedia(constraints);
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
attachMediaStream(selfView, stream);
} catch (err) {
console.error(err);
}
}
socket.on('id', (data) => {
$('#myid').text(data.id);
});
// this function is called once the caller hits connect after inserting the unique id of the callee
async function connect() {
try {
await pc.setLocalDescription(await pc.createOffer());
socket.emit('message', {
to: $('#remote').val(),
desc: pc.localDescription
});
} catch (err) {
console.error(err);
}
}
socket.on('error', data => {
console.log(data);
});
现在这段代码在执行步骤 8
时抛出错误DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote offer sdp: Called in wrong state: kHaveLocalOffer
DOMException: Failed to execute 'addIceCandidate' on 'RTCPeerConnection': Error processing ICE candidate
尝试调试但未发现逻辑或代码中的任何缺陷。注意到 pc
对象具有 localDescription
和 currentLocalDescription
的一件奇怪的事情,我认为创建答案的被调用者必须同时具有描述类型 answer
而是显示localDescription
为 offer
并且 currentLocalDescription
类型为 answer
.
提前致谢。
您的代码是正确的。这是一个长期 bug in Chrome 和 negotiationneeded
.
我在 a fiddle 中对其进行了检测(右键单击并打开相邻的两个 windows,然后单击调用其中一个)。
在 Firefox 中,它可以工作。报价方协商一次,因为您一次添加了两条轨道(video/audio):
negotiating in stable
onmessage answer
并且,在回答者方面,您在 'stable'
状态之外添加的曲目将添加到答案中:
onmessage offer
adding audio track
adding video track
但在 Chrome 中,它坏了,在提供者上发射 negotiationneeded
两次,每添加一个曲目一次:
negotiating in stable
negotiating in stable
onmessage offer
DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection':
Failed to set remote offer sdp: Called in wrong state: kHaveLocalOffer
onmessage offer
DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection':
Failed to set remote offer sdp: Called in wrong state: kHaveLocalOffer
onmessage offer
DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection':
Failed to set remote offer sdp: Called in wrong state: kHaveLocalOffer
并在应答端触发 negotiationneeded
两次,甚至不在 'stable'
状态:
onmessage offer
adding audio track
adding video track
negotiating in have-remote-offer
negotiating in have-remote-offer
onmessage offer
DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection':
Failed to set remote offer sdp: Called in wrong state: kHaveLocalOffer
这些额外的事件导致两端出现相互状态错误的严重破坏。
具体来说,Chrome 在这里违反了 spec 的两个部分:
"Queue a task" 来触发这个事件。 "queueing prevents negotiationneeded from firing prematurely, in the common situation where multiple modifications to connection are being made at once."
如果连接的信号状态不是
"stable"
,则中止这些步骤[以触发事件]。
解决方法
解决两个 Chrome 错误需要(为简洁起见使用async
/await
):
let negotiating = false;
pc.onnegotiationneeded = async e => {
try {
if (negotiating || pc.signalingState != "stable") return;
negotiating = true;
/* Your async/await-using code goes here */
} finally {
negotiating = false;
}
}