Webrtc 为什么从不调用调用者的轨道?

Webrtc why is ontrack never called for the caller?

我正在使用 this webrtc example 的修改版本。

更新:这似乎实际上是 code sample I'm using. 中的一个错误,如果我正确地设置它,我仍然只能在呼叫方获得本地视频。任何解决此问题的帮助将不胜感激。

唯一的区别是发送给远程客户端的报价稍晚一些。即:"Let the person know I'm ready to meet, then send it"

远程对等方(被调用方?)可以完美地看到两个流。

本地调用者只能看到自己,不会调用ontrack。

本地和远程对等点都显示稳定连接,就像我说的,远程对等点工作完美。

发送 ice candidates 应该双向进行吗?因为我觉得是。我是 webrtc 的新手,这让我很惊讶。

"$ ('#ReadyModalButton').click " 是发送呼叫提议的内容。

    var myPeerConnection = null; // RTCPeerConnection
    var transceiver = null; // RTCRtpTransceiver
    var webcamStream = null; // MediaStream from webcam
    var remoteUser = null;
    var mediaConstraints = {
      audio: true, // We want an audio track
      video: true ,
    };

    function log (text) {
      var time = new Date ();

      console.log ('[' + time.toLocaleTimeString () + '] ' + text);
    }
    function log_error (text) {
      var time = new Date ();

      console.trace ('[' + time.toLocaleTimeString () + '] ' + text);
    }

    async function createPeerConnection () {
      log ('Setting up a connection...');

      // Create an RTCPeerConnection which knows to use our chosen
      // STUN server.
      var configuration = {
        offerToReceiveAudio: true,
        offerToReceiveVideo: true
    }
      myPeerConnection = new RTCPeerConnection ({
        configuration:configuration,
        iceServers: [
          {
            urls: 'turn:...',
            username: '...',
            credential: '...',
          },
          {
            urls: [
              'stun:stun.l.google.com:19302',
              'stun:stun1.l.google.com:19302',
              'stun:stun2.l.google.com:19302',
              'stun:stun3.l.google.com:19302',
            ],
          },
        ],
      });

      // Set up event handlers for the ICE negotiation process.

      myPeerConnection.onicecandidate = handleICECandidateEvent;
      myPeerConnection.oniceconnectionstatechange = handleICEConnectionStateChangeEvent;
      myPeerConnection.onicegatheringstatechange = handleICEGatheringStateChangeEvent;
      myPeerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
      myPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent;
      myPeerConnection.ontrack = handleTrackEvent;
    }

    // Called by the WebRTC layer to let us know when it's time to
    // begin, resume, or restart ICE negotiation.

    async function handleNegotiationNeededEvent () {
      log ('*** Negotiation needed');

      try {
        log ('---> Creating offer');
        const offer = await myPeerConnection.createOffer ();

        // If the connection hasn't yet achieved the "stable" state,
        // return to the caller. Another negotiationneeded event
        // will be fired when the state stabilizes.

        if (myPeerConnection.signalingState != 'stable') {
          log ("     -- The connection isn't stable yet; postponing...");
          return;
        }

        // Establish the offer as the local peer's current
        // description.
        await myPeerConnection.setLocalDescription (offer);

        // Send the offer to the remote peer.

        log ('---> Sending the offer to the remote peer');
        if (remoteUser == null) {
          alert ('remote user is null.');
        }

        sendMessage (
          ws,
          JSON.stringify ({
            remoteUser: remoteUser,
            handler: 'relayOffer',
            callback: 'handleVideoOfferMsg',
            sdp: myPeerConnection.localDescription.toJSON (),
          })
        );
      } catch (err) {
        log (
          '*** The following error occurred while handling the negotiationneeded event:'
        );
        reportError (err);
      }
    }

    // Called by the WebRTC layer when events occur on the media tracks
    // on our WebRTC call. This includes when streams are added to and
    // removed from the call.
    //
    // track events include the following fields:
    //
    // RTCRtpReceiver       receiver
    // MediaStreamTrack     track
    // MediaStream[]        streams
    // RTCRtpTransceiver    transceiver
    //
    // In our case, we're just taking the first stream found and attaching
    // it to the <video> element for incoming media.

    function handleTrackEvent (event) {
      log ('*** Track event');
      document.getElementById ('received_video').srcObject = event.streams[0];
      document.getElementById ('hangup-button').disabled = false;
    }

    // Handles |icecandidate| events by forwarding the specified
    // ICE candidate (created by our local ICE agent) to the other
    // peer through the signaling server.

    function handleICECandidateEvent (event) {
      if (event.candidate) {
        log ('*** Outgoing ICE candidate');

        visitId = getVisitId ();
        if (visitId == null) {
          alert ('No visit ID provided.');
          return;
        }
        if (remoteUser == null) {
          alert ('remote user is null.');
        }
        sendMessage (
          ws,
          JSON.stringify ({
            handler: 'newIceCandidate',
            callback: 'handleNewICECandidateMsg',
            remoteUser: remoteUser,
            candidate: event.candidate.toJSON (),
          })
        );
      }
    }

    // Handle |iceconnectionstatechange| events. This will detect
    // when the ICE connection is closed, failed, or disconnected.
    //
    // This is called when the state of the ICE agent changes.

    function handleICEConnectionStateChangeEvent (event) {
      log (
        '*** ICE connection state changed to ' + myPeerConnection.iceConnectionState
      );

      switch (myPeerConnection.iceConnectionState) {
        case 'closed':
        case 'failed':
        case 'disconnected':
          closeVideoCall ();
          break;
      }
    }

    // Set up a |signalingstatechange| event handler. This will detect when
    // the signaling connection is closed.
    //
    // NOTE: This will actually move to the new RTCPeerConnectionState enum
    // returned in the property RTCPeerConnection.connectionState when
    // browsers catch up with the latest version of the specification!

    function handleSignalingStateChangeEvent (event) {
      log (
        '*** WebRTC signaling state changed to: ' + myPeerConnection.signalingState
      );
      switch (myPeerConnection.signalingState) {
        case 'closed':
          closeVideoCall ();
          break;
      }
    }

    // Handle the |icegatheringstatechange| event. This lets us know what the
    // ICE engine is currently working on: "new" means no networking has happened
    // yet, "gathering" means the ICE engine is currently gathering candidates,
    // and "complete" means gathering is complete. Note that the engine can
    // alternate between "gathering" and "complete" repeatedly as needs and
    // circumstances change.
    //
    // We don't need to do anything when this happens, but we log it to the
    // console so you can see what's going on when playing with the sample.

    function handleICEGatheringStateChangeEvent (event) {
      log (
        '*** ICE gathering state changed to: ' + myPeerConnection.iceGatheringState
      );
    }

    // Close the RTCPeerConnection and reset variables so that the user can
    // make or receive another call if they wish. This is called both
    // when the user hangs up, the other user hangs up, or if a connection
    // failure is detected.

    function closeVideoCall () {
      var localVideo = document.getElementById ('local_video');

      log ('Closing the call');

      // Close the RTCPeerConnection

      if (myPeerConnection) {
        log ('--> Closing the peer connection');

        // Disconnect all our event listeners; we don't want stray events
        // to interfere with the hangup while it's ongoing.

        myPeerConnection.ontrack = null;
        myPeerConnection.onnicecandidate = null;
        myPeerConnection.oniceconnectionstatechange = null;
        myPeerConnection.onsignalingstatechange = null;
        myPeerConnection.onicegatheringstatechange = null;
        myPeerConnection.onnotificationneeded = null;

        // Stop all transceivers on the connection

        myPeerConnection.getTransceivers ().forEach (transceiver => {
          transceiver.stop ();
        });

        // Stop the webcam preview as well by pausing the <video>
        // element, then stopping each of the getUserMedia() tracks
        // on it.

        if (localVideo.srcObject) {
          localVideo.pause ();
          localVideo.srcObject.getTracks ().forEach (track => {
            track.stop ();
          });
        }

        // Close the peer connection

        myPeerConnection.close ();
        myPeerConnection = null;
        webcamStream = null;
      }

      // Disable the hangup button

      document.getElementById ('hangup-button').disabled = true;
      targetUsername = null;
    }

    // Handle the "hang-up" message, which is sent if the other peer
    // has hung up the call or otherwise disconnected.

    function handleHangUpMsg (msg) {
      log ('*** Received hang up notification from other peer');

      closeVideoCall ();
    }

    // Hang up the call by closing our end of the connection, then
    // sending a "hang-up" message to the other peer (keep in mind that
    // the signaling is done on a different connection). This notifies
    // the other peer that the connection should be terminated and the UI
    // returned to the "no call in progress" state.

    function hangUpCall () {
      closeVideoCall ();
      if (remoteUser == null) {
        alert ('remote user is null.');
      }
      sendToServer ({
        remoteUser: remoteUser,
        handler: 'hangupCall',
      });
    }

    // Handle a click on an item in the user list by inviting the clicked
    // user to video chat. Note that we don't actually send a message to
    // the callee here -- calling RTCPeerConnection.addTrack() issues
    // a |notificationneeded| event, so we'll let our handler for that
    // make the offer.
    /*
    async function invite(evt) {
      log("Starting to prepare an invitation");
      if (myPeerConnection) {
        alert("You can't start a call because you already have one open!");
      } else {
        var clickedUsername = evt.target.textContent;

        // Don't allow users to call themselves, because weird.

        if (clickedUsername === myUsername) {
          alert("I'm afraid I can't let you talk to yourself. That would be weird.");
          return;
        }

        // Record the username being called for future reference

        targetUsername = clickedUsername;
        log("Inviting user " + targetUsername);

        // Call createPeerConnection() to create the RTCPeerConnection.
        // When this returns, myPeerConnection is our RTCPeerConnection
        // and webcamStream is a stream coming from the camera. They are
        // not linked together in any way yet.

        log("Setting up connection to invite user: " + targetUsername);
        createPeerConnection();

        // Get access to the webcam stream and attach it to the
        // "preview" box (id "local_video").

        try {
          webcamStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
          document.getElementById("local_video").srcObject = webcamStream;
        } catch(err) {
          handleGetUserMediaError(err);
          return;
        }

        // Add the tracks from the stream to the RTCPeerConnection

        try {
          webcamStream.getTracks().forEach(
            transceiver = track => myPeerConnection.addTransceiver(track, {streams: [webcamStream]})
          );
        } catch(err) {
          handleGetUserMediaError(err);
        }
      }
    }
    */
    // Accept an offer to video chat. We configure our local settings,
    // create our RTCPeerConnection, get and attach our local camera
    // stream, then create and send an answer to the caller.

    async function handleVideoOfferMsg (data) {
      msg = data.sdp;
      // If we're not already connected, create an RTCPeerConnection
      // to be linked to the caller.
      log ('Received video chat offer');
      if (!myPeerConnection) {
        createPeerConnection ();
      }
      // Get the webcam stream if we don't already have it

      if (!webcamStream) {
        try {
          webcamStream = await navigator.mediaDevices.getUserMedia (
            mediaConstraints
          );
        } catch (err) {
          handleGetUserMediasError (err);
          return;
        }
      }
        document.getElementById ('local_video').srcObject = webcamStream;

        // Add the camera stream to the RTCPeerConnection
        console.log( webcamStream
          .getTracks ())
        try {
          webcamStream
            .getTracks ()
            .forEach (
              (transceiver = track =>
                myPeerConnection.addTransceiver (track, {streams: [webcamStream]}))
            );
        } catch (err) {
          handleGetUserMediaError (err);
        }

      // We need to set the remote description to the received SDP offer
      // so that our local WebRTC layer knows how to talk to the caller.
      try {
        var desc = new RTCSessionDescription ({sdp: msg.sdp, type: msg.type});
      } catch (e) {
        log ('msg.sdp error ' + e);
        console.log (msg.sdp);
      }
      log ('Remote Description added');
      // If the connection isn't stable yet, wait for it...

      if (myPeerConnection.signalingState != 'stable') {
        log ("  - But the signaling state isn't stable, so triggering rollback");

        // Set the local and remove descriptions for rollback; don't proceed
        // until both return.
        await Promise.all ([
          myPeerConnection.setLocalDescription ({type: 'rollback'}),
          myPeerConnection.setRemoteDescription (desc),
        ]);
        return;
      } else {
        log ('  - Setting remote description');
        await myPeerConnection.setRemoteDescription (desc);
      }


      log ('---> Creating and sending answer to caller');

      await myPeerConnection.setLocalDescription (
        await myPeerConnection.createAnswer ()
      );
    console.log( myPeerConnection.localDescription)
      if (remoteUser == null) {
        alert ('remote user is null.');
      }

      sendMessage (
        ws,
        JSON.stringify ({
          remoteUser: remoteUser,
          handler: 'respondToOffer',
          sdp: myPeerConnection.localDescription.toJSON (),
          callback: 'handleVideoAnswerMsg',
        })
      );
    }

    // Responds to the "video-answer" message sent to the caller
    // once the callee has decided to accept our request to talk.

    async function handleVideoAnswerMsg (data) {
      log ('*** Call recipient has accepted our call');
      msg = data.sdp;
      // Configure the remote description, which is the SDP payload
      // in our "video-answer" message.
      try {
        var desc = new RTCSessionDescription ({sdp: msg.sdp, type: msg.type});
      } catch (e) {
        log ('msg.sdp error ' + e);
        console.log (msg.sdp);
      }
      await myPeerConnection.setRemoteDescription (desc).catch (reportError);
    }

    // A new ICE candidate has been received from the other peer. Call
    // RTCPeerConnection.addIceCandidate() to send it along to the
    // local ICE framework.

    async function handleNewICECandidateMsg (msg) {
      if (typeof msg.event.sdpMid === undefined) {
        msg.event.sdpMid = null;
      }
      if (typeof msg.event.sdpMLineIndex === undefined) {
        msg.event.sdpMLineIndex = null;
      }
      if (typeof msg.event.usernameFragment === undefined) {
        msg.event.usernameFragment = null;
      }
      var candidate = new RTCIceCandidate ({
        candidate: msg.event.candidate,
        sdpMid: msg.event.sdpMid,
        sdpMLineIndex: msg.event.sdpMLineIndex,
        usernameFragment: msg.event.usernameFragment,
      });

      //log("*** Adding received ICE candidate: " + JSON.stringify(candidate));
      log ('*** Adding received ICE candidate');
      try {
        await myPeerConnection.addIceCandidate (candidate);
      } catch (err) {
        reportError (err);
      }
    }

    // Handle errors which occur when trying to access the local media
    // hardware; that is, exceptions thrown by getUserMedia(). The two most
    // likely scenarios are that the user has no camera and/or microphone
    // or that they declined to share their equipment when prompted. If
    // they simply opted not to share their media, that's not really an
    // error, so we won't present a message in that situation.

    function handleGetUserMediaError (e) {
      log_error (e);
      switch (e.name) {
        case 'NotFoundError':
          alert (
            'Unable to open your call because no camera and/or microphone' +
              'were found.'
          );
          break;
        case 'SecurityError':
        case 'PermissionDeniedError':
          // Do nothing; this is the same as the user canceling the call.
          break;
        default:
          alert ('Error opening your camera and/or microphone: ' + e.message);
          break;
      }

      // Make sure we shut down our end of the RTCPeerConnection so we're
      // ready to try again.

      closeVideoCall ();
    }

    // Handles reporting errors. Currently, we just dump stuff to console but
    // in a real-world application, an appropriate (and user-friendly)
    // error message should be displayed.

    function reportError (errMessage) {
      log_error (`Error ${errMessage.name}: ${errMessage.message}`);
    }

async function renderVideoPage (videoId) {
  if (getVideoId === undefined || getVideoId === null) {
    toastr.options.closeButton = true;
    toastr.options.timeOut = 5000;
    toastr.error (
      'Cound not determine your visit ID. Please try again.',
      'Warning'
    );
    window.location.hash = '#';
    return false;
  }
  $ ('#videoPage').removeClass ('hiddenPage');
  //**********************
  //Starting a peer connection
  //**********************

  //getting local video stream
  console.log ('Requesting local stream');
  if (myPeerConnection) {
    alert ("You can't start a call because you already have one open!");
  } else {
    // Record the username being called for future reference

    createPeerConnection ();

    // Get access to the webcam stream and attach it to the
    // "preview" box (id "local_video").
    if (!webcamStream) {
      try {
        webcamStream = await navigator.mediaDevices.getUserMedia (
          mediaConstraints
        );
        document.getElementById ('local_video').srcObject = webcamStream;
      } catch (err) {
        handleGetUserMediaError (err);
        return;
      }
    }


  }
}


$ ('#ReadyModalButton').click (function () {
    //This will send the offer.
    console.log( webcamStream
      .getTracks ())
    try {
      webcamStream
        .getTracks ()
        .forEach (
          (transceiver = track =>
            myPeerConnection.addTransceiver (track, {streams: [webcamStream]}))
        );
    } catch (err) {
      handleGetUserMediaError (err);
    }
    toastr.options.closeButton = true;
    toastr.options.timeOut = 5000;
    toastr.info ('Attempting to establish secure connection.', 'Please Hold');

  });

(我不能给出正确的答案,因为我不知道正确的规格) 我也有这个问题。 原因似乎是 addTransceiver()。 OfferUser addTransceiver() 是安全的,但是如果 AnswerUser 在 setRemoteDescription() 之前使用 addTransceiver() 向 peerConnection 添加轨道,它似乎是一个与 setRemoteDescription() 无关的收发器。

我能想到两种可能的解决方案。 1. 如果 AnswerUser 使用 peerConnection.addTrack() 而不是 addTransceiver() 那么它就可以工作。 2. 完成一次协商后,通过getTransceivers()获取收发器,添加轨道并改变方向,然后再次进行协商。