我正在使用这个 webrtc 示例的修改版本。
更新:这似乎实际上是我正在使用的代码示例中的一个错误。如果我正确设置,我仍然只能在呼叫方获得本地视频。任何解决此问题的帮助将不胜感激。
唯一的区别是对远程客户端的报价稍后发送。即:“让对方知道我已经准备好见面,然后发送”
远程对等方(被调用方?)可以完美地看到两个流。
本地调用者只能看到自己,并且永远不会调用 ontrack。
本地和远程对等点都显示稳定的连接,就像我说的,远程对等点工作完美。
是否应该双向发送候选冰?因为我觉得是。我是 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');
});