gstreamer/subprojects/gst-examples/webrtc/sendrecv/js/webrtc.js
Nirbheek Chauhan 639f8a24ae 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>
2023-07-19 13:01:49 +00:00

424 lines
14 KiB
JavaScript

/* vim: set sts=4 sw=4 et :
*
* Demo Javascript app for negotiating and streaming a sendrecv webrtc stream
* with a GStreamer app. Runs only in passive mode, i.e., responds to offers
* with answers, exchanges ICE candidates, and streams.
*
* Author: Nirbheek Chauhan <nirbheek@centricular.com>
*/
// Set this to override the automatic detection in websocketServerConnect()
var ws_server;
var ws_port;
// Set this to use a specific peer id instead of a random one
var default_peer_id;
// Override with your own STUN servers if you want
var rtc_configuration = {iceServers: [{urls: "stun:stun.l.google.com:19302"}]};
// The default constraints that will be attempted. Can be overriden by the user.
var default_constraints = {video: true, audio: true};
var connect_attempts = 0;
var peer_connection = new RTCPeerConnection(rtc_configuration);
var send_channel;
var ws_conn;
// Local stream after constraints are approved by the user
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) {
document.getElementById("peer-connect-button").value = value;
}
function wantRemoteOfferer() {
return document.getElementById("remote-offerer").checked;
}
function onConnectClicked() {
if (document.getElementById("peer-connect-button").value == "Disconnect") {
resetState();
return;
}
var id = document.getElementById("peer-connect").value;
if (id == "") {
alert("Peer id must be filled out");
return;
}
ws_conn.send("SESSION " + id);
setConnectButtonState("Disconnect");
}
function onTextKeyPress(e) {
e = e ? e : window.event;
if (e.code == "Enter") {
onConnectClicked();
return false;
}
return true;
}
function getOurId() {
return Math.floor(Math.random() * (9000 - 10) + 10).toString();
}
function resetState() {
// This will call onServerClose()
ws_conn.close();
}
function handleIncomingError(error) {
setError("ERROR: " + error);
resetState();
}
function getVideoElement() {
var div = document.getElementById("video");
var video_tag = document.createElement("video");
video_tag.textContent = "Your browser doesn't support video";
video_tag.autoplay = true;
video_tag.playsinline = true;
div.appendChild(video_tag);
return video_tag
}
function setStatus(text) {
console.log(text);
var span = document.getElementById("status")
// Don't set the status if it already contains an error
if (!span.classList.contains('error'))
span.textContent = text;
}
function setError(text) {
console.error(text);
var span = document.getElementById("status")
span.textContent = text;
span.classList.add('error');
}
function resetVideo() {
// Release the webcam and mic
if (local_stream) {
local_stream.then(stream => {
if (stream) {
stream.getTracks().forEach(function (track) { track.stop(); });
}
});
local_stream = null;
}
// Remove all video players
document.getElementById("video").innerHTML = "";
}
function onIncomingSDP(sdp) {
try {
// An offer may come in while we are busy processing SRD(answer).
// 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;
}
isSettingRemoteAnswerPending = sdp.type == "answer";
peer_connection.setRemoteDescription(sdp).then(() => {
setStatus("Remote SDP set");
isSettingRemoteAnswerPending = false;
if (sdp.type == "offer") {
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 by incoming SDP offer, send answer to peer
function onLocalDescription(desc) {
if (desc.type != "answer") {
console.warn("Expected SDP answer, received: " + desc.type);
}
console.log("Got local description: " + JSON.stringify(desc));
peer_connection.setLocalDescription(desc).then(() => {
var dsc = peer_connection.localDescription;
setStatus("Sending SDP " + desc.type);
ws_conn.send(JSON.stringify({'sdp': desc}));
});
}
// ICE candidate received from peer, add it to the peer connection
function onIncomingICE(ice) {
var candidate = new RTCIceCandidate(ice);
peer_connection.addIceCandidate(candidate).catch(setError);
}
function onServerMessage(event) {
console.log("Received " + event.data);
switch (event.data) {
case "HELLO":
setStatus("Registered with server, waiting for call");
return;
case "SESSION_OK":
setStatus("Starting negotiation");
if (wantRemoteOfferer()) {
ws_conn.send("OFFER_REQUEST");
setStatus("Sent OFFER_REQUEST, waiting for offer");
return;
}
if (!callCreateTriggered) {
createCall();
setStatus("Created peer connection for call, waiting for SDP");
}
return;
case "OFFER_REQUEST":
// The peer wants us to set up and then send an offer
if (!callCreateTriggered)
createCall();
return;
default:
if (event.data.startsWith("ERROR")) {
handleIncomingError(event.data);
return;
}
// Handle incoming JSON SDP and ICE messages
try {
msg = JSON.parse(event.data);
} catch (e) {
if (e instanceof SyntaxError) {
handleIncomingError("Error parsing incoming JSON: " + event.data);
} else {
handleIncomingError("Unknown error parsing response: " + event.data);
}
return;
}
// Incoming JSON signals the beginning of a call
if (!callCreateTriggered)
createCall(msg);
if (msg.sdp != null) {
onIncomingSDP(msg.sdp);
} else if (msg.ice != null) {
onIncomingICE(msg.ice);
} else {
handleIncomingError("Unknown incoming JSON: " + msg);
}
}
}
function onServerClose(event) {
setStatus('Disconnected from server');
resetVideo();
if (peer_connection) {
peer_connection.close();
peer_connection = new RTCPeerConnection(rtc_configuration);
}
callCreateTriggered = false;
// Reset after a second
window.setTimeout(websocketServerConnect, 1000);
}
function onServerError(event) {
setError("Unable to connect to server, did you add an exception for the certificate?")
// Retry after 3 seconds
window.setTimeout(websocketServerConnect, 3000);
}
function getLocalStream() {
var constraints;
var textarea = document.getElementById('constraints');
try {
constraints = JSON.parse(textarea.value);
} catch (e) {
console.error(e);
setError('ERROR parsing constraints: ' + e.message + ', using default constraints');
constraints = default_constraints;
}
console.log(JSON.stringify(constraints));
// Add local stream
if (navigator.mediaDevices.getUserMedia) {
return navigator.mediaDevices.getUserMedia(constraints);
} else {
errorUserMediaHandler();
}
}
function websocketServerConnect() {
connect_attempts++;
if (connect_attempts > 3) {
setError("Too many connection attempts, aborting. Refresh page to try again");
return;
}
// Clear errors in the status span
var span = document.getElementById("status");
span.classList.remove('error');
span.textContent = '';
// Populate constraints
var textarea = document.getElementById('constraints');
if (textarea.value == '')
textarea.value = JSON.stringify(default_constraints);
// Fetch the peer id to use
peer_id = default_peer_id || getOurId();
ws_port = ws_port || '8443';
if (window.location.protocol.startsWith ("file")) {
ws_server = ws_server || "127.0.0.1";
} else if (window.location.protocol.startsWith ("http")) {
ws_server = ws_server || window.location.hostname;
} else {
throw new Error ("Don't know how to connect to the signalling server with uri" + window.location);
}
var ws_url = 'wss://' + ws_server + ':' + ws_port
setStatus("Connecting to server " + ws_url);
ws_conn = new WebSocket(ws_url);
/* When connected, immediately register with the server */
ws_conn.addEventListener('open', (event) => {
document.getElementById("peer-id").textContent = peer_id;
ws_conn.send('HELLO ' + peer_id);
setStatus("Registering with server");
setConnectButtonState("Connect");
// Reset connection attempts because we connected successfully
connect_attempts = 0;
});
ws_conn.addEventListener('error', onServerError);
ws_conn.addEventListener('message', onServerMessage);
ws_conn.addEventListener('close', onServerClose);
}
function errorUserMediaHandler() {
setError("Browser doesn't support getUserMedia!");
}
const handleDataChannelOpen = (event) =>{
console.log("dataChannel.OnOpen", event);
};
const handleDataChannelMessageReceived = (event) =>{
console.log("dataChannel.OnMessage:", event, event.data.type);
setStatus("Received data channel message");
if (typeof event.data === 'string' || event.data instanceof String) {
console.log('Incoming string message: ' + event.data);
textarea = document.getElementById("text")
textarea.value = textarea.value + '\n' + event.data
} else {
console.log('Incoming data message');
}
send_channel.send("Hi! (from browser)");
};
const handleDataChannelError = (error) =>{
console.log("dataChannel.OnError:", error);
};
const handleDataChannelClose = (event) =>{
console.log("dataChannel.OnClose", event);
};
function onDataChannel(event) {
setStatus("Data channel created");
let receiveChannel = event.channel;
receiveChannel.onopen = handleDataChannelOpen;
receiveChannel.onmessage = handleDataChannelMessageReceived;
receiveChannel.onerror = handleDataChannelError;
receiveChannel.onclose = handleDataChannelClose;
}
function createCall() {
callCreateTriggered = true;
console.log('Configuring RTCPeerConnection');
send_channel = peer_connection.createDataChannel('label', null);
send_channel.onopen = handleDataChannelOpen;
send_channel.onmessage = handleDataChannelMessageReceived;
send_channel.onerror = handleDataChannelError;
send_channel.onclose = handleDataChannelClose;
peer_connection.ondatachannel = onDataChannel;
peer_connection.ontrack = ({track, streams}) => {
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) => {
// We have a candidate, send it to the remote party with the
// same uuid
if (event.candidate == null) {
console.log("ICE Candidate was null, done");
return;
}
ws_conn.send(JSON.stringify({'ice': event.candidate}));
};
peer_connection.oniceconnectionstatechange = (event) => {
if (peer_connection.iceConnectionState == "connected") {
setStatus("ICE gathering complete");
}
};
// let the "negotiationneeded" event trigger offer generation
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");
}