[英]WebRTC can't answer an offer with multiple tracks in the same connection
在收到报价后,我正在尝试与同一连接中的两个视频轨道建立 WebRTC 连接。
来电者在接听电话时收不到被叫方添加的所有视频轨道。 但是,呼叫者可以启动提供两个或更多视频轨道的连接。
这就是调用者(发送者)正在做的事情:
const senderStreams = [localStream1];
senderStreams.forEach((stream) => {
stream.getVideoTracks().forEach((track) => sender.addTrack(track, stream));
});
这就是被调用者(接收者)正在做的事情:
const receiverStreams = [localStream2, localStream3]
receiver.onsignalingstatechange = async () => {
if (receiver.signalingState === "have-remote-offer") {
receiverStreams.forEach((stream) => {
stream
.getVideoTracks()
.forEach((track) => receiver.addTrack(track, stream));
});
const answer = await receiver.createAnswer();
await receiver.setLocalDescription(answer);
await sender.setRemoteDescription(answer);
}
};
调用者(发送者)应该收到两个跟踪事件:
sender.ontrack = (e) => {
console.log(`Sender received track:`, e.track.id);
// ...
};
这是完整的 POC 实现:
"use strict"; let localStream1, localStream2, localStream3; let sender, receiver; main(); function main() { const btnOffer1 = document.getElementById("btnOffer1"); const btnOffer2 = document.getElementById("btnOffer2"); const buttons = document.querySelector(".buttons"); btnOffer1.addEventListener("click", () => { startCall(1); buttons.remove(); }); btnOffer2.addEventListener("click", () => { startCall(2); buttons.remove(); }); } function startCall(offerOptionNum) { localStream1 = createCanvasStream(); localStream2 = createCanvasStream(); localStream3 = createCanvasStream(); const senderStreams = offerOptionNum === 1? [localStream1]: [localStream1, localStream2]; const receiverStreams = offerOptionNum === 1? [localStream2, localStream3]: [localStream3]; document.getElementById("senderTotalLocalTracks").innerText = senderStreams.length; document.getElementById("receiverTotalLocalTracks").innerText = receiverStreams.length; sender = new RTCPeerConnection(); sender.onicecandidate = (e) => onIceCandidate(sender, e); receiver = new RTCPeerConnection(); receiver.onicecandidate = (e) => onIceCandidate(receiver, e); sender.onconnectionstatechange = () => onConnectionStateChange(sender); receiver.onconnectionstatechange = () => onConnectionStateChange(receiver); sender.onsignalingstatechange = async() => { console.log(`${getName(sender)} Signaling state: ${sender.signalingState}`); if (sender.signalingState === "have-local-offer") { await receiver.setRemoteDescription(sender.localDescription); } }; sender.onnegotiationneeded = async() => { await sender.setLocalDescription(await sender.createOffer()); }; receiver.onsignalingstatechange = async() => { console.log( `${getName(receiver)} Signaling state: ${receiver.signalingState}` ); if (receiver.signalingState === "have-remote-offer") { receiverStreams.forEach((stream) => { stream.getVideoTracks().forEach((track) => receiver.addTrack(track, stream)); }); const answer = await receiver.createAnswer(); await receiver.setLocalDescription(answer); await sender.setRemoteDescription(answer); } }; sender.ontrack = (e) => { console.log(`${getName(sender)} received track:`, e.track.id); const el = document.getElementById("senderTotalRemoteTracks"); el.innerText = Number(el.innerText) + 1; }; receiver.ontrack = (e) => { console.log(`${getName(receiver)} received track:`, e.track.id); const el = document.getElementById("receiverTotalRemoteTracks"); el.innerText = Number(el.innerText) + 1; }; senderStreams.forEach((stream) => { stream.getVideoTracks().forEach((track) => sender.addTrack(track, stream)); }); } function createCanvasStream() { const canvas = Object.assign( document.createElement("canvas", { width: 640, height: 480, }) ); const ctx = canvas.getContext("2d"); const stream = canvas.captureStream(1); const drawInCanvas = () => ctx.fillRect(0, 0, canvas.width, canvas.height); drawInCanvas(); setInterval(() => { drawInCanvas(); }, 1000); return stream; } async function onIceCandidate(pc, event) { if (event.candidate) { try { await getOtherPc(pc).addIceCandidate(event.candidate); } catch (error) { console.error(error, event.candidate); } } } function onConnectionStateChange(pc) { if (pc) { console.log(`${getName(pc)} Connection state: ${pc.connectionState}`); } } function getName(pc) { return pc === sender? "Sender": "Receiver"; } function getOtherPc(pc) { return pc === sender? receiver: sender; }
html, body { margin: 0; font-family: system-ui, sans-serif; color: #222; background: #f8f8f8; } input, textarea { font-size: 1em; box-sizing: border-box; padding: 6px 8px; } button, code, kbd, pre { font-size: 1em; } code, kbd, pre { font-family: "Menlo", "Monaco", monospace; border-radius: 3px; box-sizing: border-box; padding: 2px 4px 1px 4px; background: rgba(0, 0, 0, 0.1); } pre { padding: 8px 12px; } p { line-height: 1.5em; } a { color: #222; } a:hover { color: #666; }.cards { display: flex; }.buttons { display: flex; }
<,DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width. initial-scale=1:0"> <title>Document</title> <link rel="stylesheet" href="https.//unpkg.com/blocks.css/dist/blocks.min:css" /> </head> <body> <div class="cards"> <div class="card fixed block sender"> <h2>sender</h2> <p>Remote tracks: <span id="senderTotalRemoteTracks">0</span></p> <p>Local tracks: <span id="senderTotalLocalTracks">0</span></p> </div> <div class="card fixed block receiver"> <h2>receiver</h2> <p>Remote tracks: <span id="receiverTotalRemoteTracks">0</span></p> <p>Local tracks: <span id="receiverTotalLocalTracks">0</span></p> </div> </div> <div class="buttons"> <button class="block accent" id="btnOffer1">Offer 1 track / Receive 2 tracks</button> <button class="block" id="btnOffer2">Offer 2 tracks / Receive 1 track</button> </div> </body> </html>
那可能吗? 我究竟做错了什么?
显然,这是一个 WebRTC 重新协商的案例。
向接收方添加一个轨道将触发其negotiationneeded
需要事件。
根据文档,
这既发生在连接的初始设置期间,也发生在更改通信环境需要重新配置连接的任何时候。
所以我修改了POC来支持双向协商过程,如下:
async function onNegotiationNeeded(pc) {
console.log(`${getName(pc)} negotiationneeded event`);
await pc.setLocalDescription(await pc.createOffer());
}
async function onSignalingStateChange(pc) {
console.log(`${getName(pc)} Signaling state: ${pc.signalingState}`);
const otherPc = getOtherPc(pc);
if (pc.signalingState === "have-local-offer") {
await otherPc.setRemoteDescription(pc.localDescription);
} else if (pc.signalingState === "have-remote-offer") {
if (pc === receiver) {
receiverStreams.forEach((stream) => {
stream.getVideoTracks().forEach((track) => pc.addTrack(track, stream));
});
}
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
await otherPc.setRemoteDescription(answer);
}
}
sender.onnegotiationneeded = () => onNegotiationNeeded(sender);
receiver.onnegotiationneeded = () => onNegotiationNeeded(receiver);
sender.onsignalingstatechange = () => onSignalingStateChange(sender);
receiver.onsignalingstatechange = () => onSignalingStateChange(receiver);
这是完整的实现:
"use strict"; let localStream1, localStream2, localStream3; let sender, receiver; let senderStreams = [], receiverStreams = []; main(); function main() { const btnOffer1 = document.getElementById("btnOffer1"); const btnOffer2 = document.getElementById("btnOffer2"); const buttons = document.querySelector(".buttons"); const btnTryAgain = document.getElementById("btnTryAgain"); btnOffer1.addEventListener("click", () => { startCall(1); buttons.remove(); }); btnOffer2.addEventListener("click", () => { startCall(2); buttons.remove(); }); btnTryAgain.addEventListener("click", () => { window.location.reload(); }); } function startCall(offerOptionNum) { localStream1 = createCanvasStream(); localStream2 = createCanvasStream(); localStream3 = createCanvasStream(); senderStreams = offerOptionNum === 1? [localStream1]: [localStream1, localStream2]; receiverStreams = offerOptionNum === 1? [localStream2, localStream3]: [localStream3]; document.getElementById("senderTotalLocalTracks").innerText = senderStreams.length; document.getElementById("receiverTotalLocalTracks").innerText = receiverStreams.length; sender = new RTCPeerConnection(); sender.onicecandidate = (e) => onIceCandidate(sender, e); receiver = new RTCPeerConnection(); receiver.onicecandidate = (e) => onIceCandidate(receiver, e); sender.onconnectionstatechange = () => onConnectionStateChange(sender); receiver.onconnectionstatechange = () => onConnectionStateChange(receiver); sender.onnegotiationneeded = () => onNegotiationNeeded(sender); receiver.onnegotiationneeded = () => onNegotiationNeeded(receiver); sender.onsignalingstatechange = () => onSignalingStateChange(sender); receiver.onsignalingstatechange = () => onSignalingStateChange(receiver); sender.ontrack = (e) => { console.log(`${getName(sender)} received track id:`, e.track.id); const el = document.getElementById("senderTotalRemoteTracks"); el.innerText = Number(el.innerText) + 1; }; receiver.ontrack = (e) => { console.log(`${getName(receiver)} received track id:`, e.track.id); const el = document.getElementById("receiverTotalRemoteTracks"); el.innerText = Number(el.innerText) + 1; }; senderStreams.forEach((stream) => { stream.getVideoTracks().forEach((track) => sender.addTrack(track, stream)); }); } function createCanvasStream() { const canvas = Object.assign( document.createElement("canvas", { width: 640, height: 480, }) ); const ctx = canvas.getContext("2d"); const stream = canvas.captureStream(1); const drawInCanvas = () => ctx.fillRect(0, 0, canvas.width, canvas.height); drawInCanvas(); setInterval(() => { drawInCanvas(); }, 1000); return stream; } async function onIceCandidate(pc, event) { if (event.candidate) { try { await getOtherPc(pc).addIceCandidate(event.candidate); } catch (error) { console.error(error, event.candidate); } } } function onConnectionStateChange(pc) { if (pc) { console.log(`${getName(pc)} Connection state: ${pc.connectionState}`); } } async function onNegotiationNeeded(pc) { console.log(`${getName(pc)} negotiationneeded event`); await pc.setLocalDescription(await pc.createOffer()); } async function onSignalingStateChange(pc) { console.log(`${getName(pc)} Signaling state: ${pc.signalingState}`); const otherPc = getOtherPc(pc); if (pc.signalingState === "have-local-offer") { await otherPc.setRemoteDescription(pc.localDescription); } else if (pc.signalingState === "have-remote-offer") { if (pc === receiver) { receiverStreams.forEach((stream) => { stream.getVideoTracks().forEach((track) => pc.addTrack(track, stream)); }); } const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); await otherPc.setRemoteDescription(answer); } } function getName(pc) { return pc === sender? "Sender": "Receiver"; } function getOtherPc(pc) { return pc === sender? receiver: sender; }
html, body { margin: 0; font-family: system-ui, sans-serif; color: #222; background: #f8f8f8; } input, textarea { font-size: 1em; box-sizing: border-box; padding: 6px 8px; } button, code, kbd, pre { font-size: 1em; } code, kbd, pre { font-family: "Menlo", "Monaco", monospace; border-radius: 3px; box-sizing: border-box; padding: 2px 4px 1px 4px; background: rgba(0, 0, 0, 0.1); } pre { padding: 8px 12px; } p { line-height: 1.5em; } a { color: #222; } a:hover { color: #666; }
<,DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width. initial-scale=1.0"> <title>Document</title> <link rel="stylesheet" href="reset:css" /> <link rel="stylesheet" href="https.//unpkg.com/blocks.css/dist/blocks.min.css" /> <style>:cards { display; flex. }:buttons { display; flex: } </style> </head> <body> <div class="cards"> <div class="card fixed block sender"> <h2>sender</h2> <p>Remote tracks: <span id="senderTotalRemoteTracks">0</span></p> <p>Local tracks: <span id="senderTotalLocalTracks">0</span></p> </div> <div class="card fixed block receiver"> <h2>receiver</h2> <p>Remote tracks: <span id="receiverTotalRemoteTracks">0</span></p> <p>Local tracks. <span id="receiverTotalLocalTracks">0</span></p> </div> </div> <div class="buttons"> <button class="block accent" id="btnOffer1">Offer 1 track / Receive 2 tracks</button> <button class="block" id="btnOffer2">Offer 2 tracks / Receive 1 track</button> </div> <button class="block" id="btnTryAgain">Try again</button> <script src="main.js" async></script> </body> </html>
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.