mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-22 17:51:16 +00:00
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:
parent
57b6c743ef
commit
639f8a24ae
1 changed files with 118 additions and 57 deletions
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue