webrtc/js: Support renegotiation during a call correctly

When a video track is muted, hide the video element to differentiate
it from a track that is stuck because we stopped receiving RTP data.
Show it again when it is unmuted.

When a video track is removed, remove the video element. It will be
re-added on renegotiation.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/5045>
This commit is contained in:
Nirbheek Chauhan 2023-07-17 10:19:03 +05:30 committed by GStreamer Marge Bot
parent 57b6c743ef
commit 639f8a24ae

View file

@ -18,11 +18,16 @@ var rtc_configuration = {iceServers: [{urls: "stun:stun.l.google.com:19302"}]};
var default_constraints = {video: true, audio: true}; var default_constraints = {video: true, audio: true};
var connect_attempts = 0; var connect_attempts = 0;
var peer_connection; var peer_connection = new RTCPeerConnection(rtc_configuration);
var send_channel; var send_channel;
var ws_conn; var ws_conn;
// Promise for local stream after constraints are approved by the user // Local stream after constraints are approved by the user
var local_stream_promise; var local_stream = null;
// keep track of some negotiation state to prevent races and errors
var callCreateTriggered = false;
var makingOffer = false;
var isSettingRemoteAnswerPending = false;
function setConnectButtonState(value) { function setConnectButtonState(value) {
document.getElementById("peer-connect-button").value = value; document.getElementById("peer-connect-button").value = value;
@ -98,46 +103,70 @@ function setError(text) {
function resetVideo() { function resetVideo() {
// Release the webcam and mic // Release the webcam and mic
if (local_stream_promise) if (local_stream) {
local_stream_promise.then(stream => { local_stream.then(stream => {
if (stream) { if (stream) {
stream.getTracks().forEach(function (track) { track.stop(); }); stream.getTracks().forEach(function (track) { track.stop(); });
} }
}); });
local_stream = null;
}
// Remove all video players // Remove all video players
document.getElementById("video").innerHTML = ""; document.getElementById("video").innerHTML = "";
} }
// SDP offer received from peer, set remote description and create an answer
function onIncomingSDP(sdp) { function onIncomingSDP(sdp) {
peer_connection.setRemoteDescription(sdp).then(() => { try {
setStatus("Remote SDP set"); // An offer may come in while we are busy processing SRD(answer).
if (sdp.type != "offer") // In this case, we will be in "stable" by the time the offer is processed
// so it is safe to chain it on our Operations Chain now.
const readyForOffer =
!makingOffer &&
(peer_connection.signalingState == "stable" || isSettingRemoteAnswerPending);
const offerCollision = sdp.type == "offer" && !readyForOffer;
if (offerCollision) {
return; return;
setStatus("Got SDP offer"); }
local_stream_promise.then((stream) => { isSettingRemoteAnswerPending = sdp.type == "answer";
setStatus("Got local stream, creating answer"); peer_connection.setRemoteDescription(sdp).then(() => {
peer_connection.createAnswer() setStatus("Remote SDP set");
.then(onLocalDescription).catch(setError); isSettingRemoteAnswerPending = false;
}).catch(setError); if (sdp.type == "offer") {
}).catch(setError); setStatus("Got SDP offer, waiting for getUserMedia to complete");
local_stream.then((stream) => {
setStatus("getUserMedia to completed, setting local description");
peer_connection.setLocalDescription().then(() => {
let desc = peer_connection.localDescription;
console.log("Got local description: " + JSON.stringify(desc));
setStatus("Sending SDP " + desc.type);
ws_conn.send(JSON.stringify({'sdp': desc}));
if (peer_connection.iceConnectionState == "connected") {
setStatus("SDP " + desc.type + " sent, ICE connected, all looks OK");
}
});
});
}
});
} catch (err) {
handleIncomingError(err);
}
} }
// Local description was set, send it to peer // Local description was set by incoming SDP offer, send answer to peer
function onLocalDescription(desc) { function onLocalDescription(desc) {
if (desc.type != "answer") {
console.warn("Expected SDP answer, received: " + desc.type);
}
console.log("Got local description: " + JSON.stringify(desc)); console.log("Got local description: " + JSON.stringify(desc));
peer_connection.setLocalDescription(desc).then(function() { peer_connection.setLocalDescription(desc).then(() => {
var dsc = peer_connection.localDescription;
setStatus("Sending SDP " + desc.type); setStatus("Sending SDP " + desc.type);
sdp = {'sdp': peer_connection.localDescription} ws_conn.send(JSON.stringify({'sdp': desc}));
ws_conn.send(JSON.stringify(sdp));
}); });
} }
function generateOffer() {
peer_connection.createOffer().then(onLocalDescription).catch(setError);
}
// ICE candidate received from peer, add it to the peer connection // ICE candidate received from peer, add it to the peer connection
function onIncomingICE(ice) { function onIncomingICE(ice) {
var candidate = new RTCIceCandidate(ice); var candidate = new RTCIceCandidate(ice);
@ -157,13 +186,15 @@ function onServerMessage(event) {
setStatus("Sent OFFER_REQUEST, waiting for offer"); setStatus("Sent OFFER_REQUEST, waiting for offer");
return; return;
} }
if (!peer_connection) if (!callCreateTriggered) {
createCall(null).then (generateOffer); createCall();
setStatus("Created peer connection for call, waiting for SDP");
}
return; return;
case "OFFER_REQUEST": case "OFFER_REQUEST":
// The peer wants us to set up and then send an offer // The peer wants us to set up and then send an offer
if (!peer_connection) if (!callCreateTriggered)
createCall(null).then (generateOffer); createCall();
return; return;
default: default:
if (event.data.startsWith("ERROR")) { if (event.data.startsWith("ERROR")) {
@ -183,7 +214,7 @@ function onServerMessage(event) {
} }
// Incoming JSON signals the beginning of a call // Incoming JSON signals the beginning of a call
if (!peer_connection) if (!callCreateTriggered)
createCall(msg); createCall(msg);
if (msg.sdp != null) { if (msg.sdp != null) {
@ -202,8 +233,9 @@ function onServerClose(event) {
if (peer_connection) { if (peer_connection) {
peer_connection.close(); peer_connection.close();
peer_connection = null; peer_connection = new RTCPeerConnection(rtc_configuration);
} }
callCreateTriggered = false;
// Reset after a second // Reset after a second
window.setTimeout(websocketServerConnect, 1000); window.setTimeout(websocketServerConnect, 1000);
@ -268,19 +300,14 @@ function websocketServerConnect() {
ws_conn.send('HELLO ' + peer_id); ws_conn.send('HELLO ' + peer_id);
setStatus("Registering with server"); setStatus("Registering with server");
setConnectButtonState("Connect"); setConnectButtonState("Connect");
// Reset connection attempts because we connected successfully
connect_attempts = 0;
}); });
ws_conn.addEventListener('error', onServerError); ws_conn.addEventListener('error', onServerError);
ws_conn.addEventListener('message', onServerMessage); ws_conn.addEventListener('message', onServerMessage);
ws_conn.addEventListener('close', onServerClose); ws_conn.addEventListener('close', onServerClose);
} }
function onRemoteTrack(event) {
var videoElem = getVideoElement();
if (event.track.kind === 'audio')
videoElem.style = 'display: none;';
videoElem.srcObject = new MediaStream([event.track]);
}
function errorUserMediaHandler() { function errorUserMediaHandler() {
setError("Browser doesn't support getUserMedia!"); setError("Browser doesn't support getUserMedia!");
} }
@ -320,30 +347,36 @@ function onDataChannel(event) {
receiveChannel.onclose = handleDataChannelClose; receiveChannel.onclose = handleDataChannelClose;
} }
function createCall(msg) { function createCall() {
// Reset connection attempts because we connected successfully callCreateTriggered = true;
connect_attempts = 0; console.log('Configuring RTCPeerConnection');
console.log('Creating RTCPeerConnection');
peer_connection = new RTCPeerConnection(rtc_configuration);
send_channel = peer_connection.createDataChannel('label', null); send_channel = peer_connection.createDataChannel('label', null);
send_channel.onopen = handleDataChannelOpen; send_channel.onopen = handleDataChannelOpen;
send_channel.onmessage = handleDataChannelMessageReceived; send_channel.onmessage = handleDataChannelMessageReceived;
send_channel.onerror = handleDataChannelError; send_channel.onerror = handleDataChannelError;
send_channel.onclose = handleDataChannelClose; send_channel.onclose = handleDataChannelClose;
peer_connection.ondatachannel = onDataChannel; peer_connection.ondatachannel = onDataChannel;
peer_connection.ontrack = onRemoteTrack;
/* Send our video/audio to the other peer */
local_stream_promise = getLocalStream().then((stream) => {
console.log('Adding local stream');
peer_connection.addStream(stream);
return stream;
}).catch(setError);
if (msg != null && !msg.sdp) { peer_connection.ontrack = ({track, streams}) => {
console.log("WARNING: First message wasn't an SDP message!?"); console.log("ontrack triggered");
} var videoElem = getVideoElement();
if (event.track.kind === 'audio')
videoElem.style.display = 'none';
videoElem.srcObject = streams[0];
videoElem.srcObject.addEventListener('mute', (e) => {
console.log("track muted, hiding video element");
videoElem.style.display = 'none';
});
videoElem.srcObject.addEventListener('unmute', (e) => {
console.log("track unmuted, showing video element");
videoElem.style.display = 'block';
});
videoElem.srcObject.addEventListener('removetrack', (e) => {
console.log("track removed, removing video element");
videoElem.remove();
});
};
peer_connection.onicecandidate = (event) => { peer_connection.onicecandidate = (event) => {
// We have a candidate, send it to the remote party with the // We have a candidate, send it to the remote party with the
@ -354,10 +387,38 @@ function createCall(msg) {
} }
ws_conn.send(JSON.stringify({'ice': event.candidate})); ws_conn.send(JSON.stringify({'ice': event.candidate}));
}; };
peer_connection.oniceconnectionstatechange = (event) => {
if (peer_connection.iceConnectionState == "connected") {
setStatus("ICE gathering complete");
}
};
if (msg != null) // let the "negotiationneeded" event trigger offer generation
setStatus("Created peer connection for call, waiting for SDP"); peer_connection.onnegotiationneeded = async () => {
setStatus("Negotiation needed");
if (wantRemoteOfferer())
return;
try {
makingOffer = true;
await peer_connection.setLocalDescription();
let desc = peer_connection.localDescription;
setStatus("Sending SDP " + desc.type);
ws_conn.send(JSON.stringify({'sdp': desc}));
} catch (err) {
handleIncomingError(err);
} finally {
makingOffer = false;
}
};
/* Send our video/audio to the other peer */
local_stream = getLocalStream().then((stream) => {
console.log('Adding local stream');
for (const track of stream.getTracks()) {
peer_connection.addTrack(track, stream);
}
return stream;
}).catch(setError);
setConnectButtonState("Disconnect"); setConnectButtonState("Disconnect");
return local_stream_promise;
} }