简体   繁体   English

WebRTC 无法在同一连接中回答多个轨道的报价

[英]WebRTC can't answer an offer with multiple tracks in the same connection

I'm trying to establish a WebRTC connection with two video tracks in the same connection, just after receiving an offer.在收到报价后,我正在尝试与同一连接中的两个视频轨道建立 WebRTC 连接。

The caller can't receive all the video tracks added by the callee when answering the call.来电者在接听电话时收不到被叫方添加的所有视频轨道。 However, the caller can start a connection offering two or more video tracks.但是,呼叫者可以启动提供两个或更多视频轨道的连接。

That's what the caller (sender) is doing:这就是调用者(发送者)正在做的事情:

const senderStreams = [localStream1];

senderStreams.forEach((stream) => {
  stream.getVideoTracks().forEach((track) => sender.addTrack(track, stream));
});

That's what the callee (receiver) is doing:这就是被调用者(接收者)正在做的事情:

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);
  }
};

The caller (sender) should receive back two track events:调用者(发送者)应该收到两个跟踪事件:

sender.ontrack = (e) => {
  console.log(`Sender received track:`, e.track.id);
  
  // ...
};

Here is the full POC implementation:这是完整的 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>

Is that possible?那可能吗? What am I doing wrong?我究竟做错了什么?

Apparently, this is a case of WebRTC renegotiation.显然,这是一个 WebRTC 重新协商的案例。

Adding one more track to the receiver's side will fire its negotiationneeded event.向接收方添加一个轨道将触发其negotiationneeded需要事件。

According to the docs ,根据文档

This occurs both during the initial setup of the connection as well as any time a change to the communication environment requires reconfiguring the connection.这既发生在连接的初始设置期间,也发生在更改通信环境需要重新配置连接的任何时候。

So I modified the POC to support a bidirectional negotiation process, as follows:所以我修改了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);

Here's the full implementation:这是完整的实现:

 "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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM