mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-11-11 21:01:39 +00:00
393 lines
14 KiB
JavaScript
393 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;
|
||
|
// Override with your own STUN servers if you want
|
||
|
var rtc_configuration = {iceServers: [{urls: "stun:stun.l.google.com:19302"},
|
||
|
/* TODO: do not keep these static and in clear text in production,
|
||
|
* and instead use one of the mechanisms discussed in
|
||
|
* https://groups.google.com/forum/#!topic/discuss-webrtc/nn8b6UboqRA
|
||
|
*/
|
||
|
{'urls': 'turn:turn.homeneural.net:3478?transport=udp',
|
||
|
'credential': '1qaz2wsx',
|
||
|
'username': 'test'
|
||
|
}],
|
||
|
/* Uncomment the following line to ensure the turn server is used
|
||
|
* while testing. This should be kept commented out in production,
|
||
|
* as non-relay ice candidates should be preferred
|
||
|
*/
|
||
|
// iceTransportPolicy: "relay",
|
||
|
};
|
||
|
|
||
|
var sessions = {}
|
||
|
|
||
|
/* https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript */
|
||
|
function getOurId() {
|
||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||
|
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||
|
return v.toString(16);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function Uint8ToString(u8a){
|
||
|
var CHUNK_SZ = 0x8000;
|
||
|
var c = [];
|
||
|
for (var i=0; i < u8a.length; i+=CHUNK_SZ) {
|
||
|
c.push(String.fromCharCode.apply(null, u8a.subarray(i, i+CHUNK_SZ)));
|
||
|
}
|
||
|
return c.join("");
|
||
|
}
|
||
|
|
||
|
function Session(our_id, peer_id, closed_callback) {
|
||
|
this.peer_connection = null;
|
||
|
this.ws_conn = null;
|
||
|
this.peer_id = peer_id;
|
||
|
this.our_id = our_id;
|
||
|
this.closed_callback = closed_callback;
|
||
|
|
||
|
this.getVideoElement = function() {
|
||
|
return document.getElementById("stream-" + this.our_id);
|
||
|
};
|
||
|
|
||
|
this.resetState = function() {
|
||
|
if (this.peer_connection) {
|
||
|
this.peer_connection.close();
|
||
|
this.peer_connection = null;
|
||
|
}
|
||
|
var videoElement = this.getVideoElement();
|
||
|
if (videoElement) {
|
||
|
videoElement.pause();
|
||
|
videoElement.src = "";
|
||
|
}
|
||
|
|
||
|
var session_div = document.getElementById("session-" + this.our_id);
|
||
|
if (session_div) {
|
||
|
session_div.parentNode.removeChild(session_div);
|
||
|
}
|
||
|
if (this.ws_conn) {
|
||
|
this.ws_conn.close();
|
||
|
this.ws_conn = null;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
this.handleIncomingError = function(error) {
|
||
|
this.resetState();
|
||
|
this.closed_callback(this.our_id);
|
||
|
};
|
||
|
|
||
|
this.setStatus = function(text) {
|
||
|
console.log(text);
|
||
|
var span = document.getElementById("status-" + this.our_id);
|
||
|
// Don't set the status if it already contains an error
|
||
|
if (!span.classList.contains('error'))
|
||
|
span.textContent = text;
|
||
|
};
|
||
|
|
||
|
this.setError = function(text) {
|
||
|
console.error(text);
|
||
|
console.log(this);
|
||
|
var span = document.getElementById("status-" + this.our_id);
|
||
|
span.textContent = text;
|
||
|
span.classList.add('error');
|
||
|
this.resetState();
|
||
|
this.closed_callback(this.our_id);
|
||
|
};
|
||
|
|
||
|
// Local description was set, send it to peer
|
||
|
this.onLocalDescription = function(desc) {
|
||
|
console.log("Got local description: " + JSON.stringify(desc), this);
|
||
|
var thiz = this;
|
||
|
this.peer_connection.setLocalDescription(desc).then(() => {
|
||
|
this.setStatus("Sending SDP answer");
|
||
|
var sdp = {'peer-id': this.peer_id, 'sdp': this.peer_connection.localDescription.toJSON()}
|
||
|
this.ws_conn.send(JSON.stringify(sdp));
|
||
|
}).catch(function(e) {
|
||
|
thiz.setError(e);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
// SDP offer received from peer, set remote description and create an answer
|
||
|
this.onIncomingSDP = function(sdp) {
|
||
|
this.peer_connection.setRemoteDescription(sdp).then(() => {
|
||
|
this.setStatus("Remote SDP set");
|
||
|
if (sdp.type != "offer")
|
||
|
return;
|
||
|
this.setStatus("Got SDP offer");
|
||
|
this.peer_connection.createAnswer()
|
||
|
.then(this.onLocalDescription.bind(this)).catch(this.setError);
|
||
|
}).catch(this.setError);
|
||
|
};
|
||
|
|
||
|
// ICE candidate received from peer, add it to the peer connection
|
||
|
this.onIncomingICE = function(ice) {
|
||
|
var candidate = new RTCIceCandidate(ice);
|
||
|
this.peer_connection.addIceCandidate(candidate).catch(this.setError);
|
||
|
};
|
||
|
|
||
|
this.onServerMessage = function(event) {
|
||
|
console.log("Received " + event.data);
|
||
|
if (event.data.startsWith("REGISTERED")) {
|
||
|
this.setStatus("Registered with server");
|
||
|
this.connectPeer();
|
||
|
} else if (event.data.startsWith("ERROR")) {
|
||
|
this.handleIncomingError(event.data);
|
||
|
return;
|
||
|
} else {
|
||
|
// Handle incoming JSON SDP and ICE messages
|
||
|
try {
|
||
|
msg = JSON.parse(event.data);
|
||
|
} catch (e) {
|
||
|
if (e instanceof SyntaxError) {
|
||
|
this.handleIncomingError("Error parsing incoming JSON: " + event.data);
|
||
|
} else {
|
||
|
this.handleIncomingError("Unknown error parsing response: " + event.data);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Incoming JSON signals the beginning of a call
|
||
|
if (!this.peer_connection)
|
||
|
this.createCall(msg);
|
||
|
|
||
|
if (msg.sdp != null) {
|
||
|
this.onIncomingSDP(msg.sdp);
|
||
|
} else if (msg.ice != null) {
|
||
|
this.onIncomingICE(msg.ice);
|
||
|
} else {
|
||
|
this.handleIncomingError("Unknown incoming JSON: " + msg);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
this.streamIsPlaying = function(e) {
|
||
|
this.setStatus("Streaming");
|
||
|
};
|
||
|
|
||
|
this.onServerClose = function(event) {
|
||
|
this.resetState();
|
||
|
this.closed_callback(this.our_id);
|
||
|
};
|
||
|
|
||
|
this.onServerError = function(event) {
|
||
|
this.handleIncomingError('Server error');
|
||
|
};
|
||
|
|
||
|
this.websocketServerConnect = function() {
|
||
|
// Clear errors in the status span
|
||
|
var span = document.getElementById("status-" + this.our_id);
|
||
|
span.classList.remove('error');
|
||
|
span.textContent = '';
|
||
|
console.log("Our ID:", this.our_id);
|
||
|
var ws_port = ws_port || '8443';
|
||
|
if (window.location.protocol.startsWith ("file")) {
|
||
|
var ws_server = ws_server || "127.0.0.1";
|
||
|
} else if (window.location.protocol.startsWith ("http")) {
|
||
|
var 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 = 'ws://' + ws_server + ':' + ws_port
|
||
|
this.setStatus("Connecting to server " + ws_url);
|
||
|
this.ws_conn = new WebSocket(ws_url);
|
||
|
/* When connected, immediately register with the server */
|
||
|
this.ws_conn.addEventListener('open', (event) => {
|
||
|
this.ws_conn.send('REGISTER CONSUMER');
|
||
|
this.setStatus("Registering with server");
|
||
|
});
|
||
|
this.ws_conn.addEventListener('error', this.onServerError.bind(this));
|
||
|
this.ws_conn.addEventListener('message', this.onServerMessage.bind(this));
|
||
|
this.ws_conn.addEventListener('close', this.onServerClose.bind(this));
|
||
|
};
|
||
|
|
||
|
this.connectPeer = function() {
|
||
|
this.setStatus("Connecting " + this.peer_id);
|
||
|
|
||
|
this.ws_conn.send("START_SESSION " + this.peer_id);
|
||
|
};
|
||
|
|
||
|
this.onRemoteStreamAdded = function(event) {
|
||
|
var videoTracks = event.stream.getVideoTracks();
|
||
|
var audioTracks = event.stream.getAudioTracks();
|
||
|
|
||
|
if (videoTracks.length > 0) {
|
||
|
console.log('Incoming stream: ' + videoTracks.length + ' video tracks and ' + audioTracks.length + ' audio tracks');
|
||
|
this.getVideoElement().srcObject = event.stream;
|
||
|
this.getVideoElement().play();
|
||
|
} else {
|
||
|
this.handleIncomingError('Stream with unknown tracks added, resetting');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
this.createCall = function(msg) {
|
||
|
console.log('Creating RTCPeerConnection');
|
||
|
|
||
|
this.peer_connection = new RTCPeerConnection(rtc_configuration);
|
||
|
this.peer_connection.onaddstream = this.onRemoteStreamAdded.bind(this);
|
||
|
|
||
|
this.peer_connection.ondatachannel = (event) => {
|
||
|
console.log('Data channel created');
|
||
|
let receive_channel = event.channel;
|
||
|
let buffer = [];
|
||
|
|
||
|
receive_channel.onopen = (event) => {
|
||
|
console.log ("Receive channel opened");
|
||
|
}
|
||
|
receive_channel.onclose = (event) => {
|
||
|
console.log ("Receive channel closed");
|
||
|
}
|
||
|
receive_channel.onerror = (event) => {
|
||
|
console.log ("Error on receive channel", event.data);
|
||
|
}
|
||
|
receive_channel.onmessage = (event) => {
|
||
|
if (typeof event.data === 'string' || event.data instanceof String) {
|
||
|
if (event.data == 'BEGIN_IMAGE')
|
||
|
buffer = [];
|
||
|
else if (event.data == 'END_IMAGE') {
|
||
|
var decoder = new TextDecoder("ascii");
|
||
|
var array_buffer = new Uint8Array(buffer);
|
||
|
var str = decoder.decode(array_buffer);
|
||
|
let img = document.getElementById("image");
|
||
|
img.src = 'data:image/png;base64, ' + str;
|
||
|
}
|
||
|
} else {
|
||
|
var i, len = buffer.length
|
||
|
var view = new DataView(event.data);
|
||
|
for (i = 0; i < view.byteLength; i++) {
|
||
|
buffer[len + i] = view.getUint8(i);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!msg.sdp) {
|
||
|
console.log("WARNING: First message wasn't an SDP message!?");
|
||
|
}
|
||
|
|
||
|
this.peer_connection.onicecandidate = (event) => {
|
||
|
if (event.candidate == null) {
|
||
|
console.log("ICE Candidate was null, done");
|
||
|
return;
|
||
|
}
|
||
|
this.ws_conn.send(JSON.stringify({'ice': event.candidate.toJSON(), 'peer-id': this.peer_id}));
|
||
|
};
|
||
|
|
||
|
this.setStatus("Created peer connection for call, waiting for SDP");
|
||
|
};
|
||
|
|
||
|
document.getElementById("stream-" + this.our_id).addEventListener("playing", this.streamIsPlaying.bind(this), false);
|
||
|
|
||
|
this.websocketServerConnect();
|
||
|
}
|
||
|
|
||
|
function startSession() {
|
||
|
var peer_id = document.getElementById("camera-id").value;
|
||
|
|
||
|
if (peer_id === "") {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
sessions[peer_id] = new Session(peer_id);
|
||
|
}
|
||
|
|
||
|
function session_closed(peer_id) {
|
||
|
sessions[peer_id] = null;
|
||
|
}
|
||
|
|
||
|
function addPeer(peer_id) {
|
||
|
var nav_ul = document.getElementById("camera-list");
|
||
|
var li_str = '<li id="peer-' + peer_id + '"><button class="button button1">' + peer_id + '</button></li>'
|
||
|
|
||
|
nav_ul.insertAdjacentHTML('beforeend', li_str);
|
||
|
var li = document.getElementById("peer-" + peer_id);
|
||
|
li.onclick = function(e) {
|
||
|
var sessions_div = document.getElementById('sessions');
|
||
|
var our_id = getOurId();
|
||
|
var session_div_str = '<div class="session" id="session-' + our_id + '"><video preload="none" class="stream" id="stream-' + our_id + '"></video><p>Status: <span id="status-' + our_id + '">unknown</span></p></div>'
|
||
|
sessions_div.insertAdjacentHTML('beforeend', session_div_str);
|
||
|
sessions[peer_id] = new Session(our_id, peer_id, session_closed);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function clearPeers() {
|
||
|
var nav_ul = document.getElementById("camera-list");
|
||
|
|
||
|
while (nav_ul.firstChild) {
|
||
|
nav_ul.removeChild(nav_ul.firstChild);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function onServerMessage(event) {
|
||
|
console.log("Received " + event.data);
|
||
|
if (event.data.startsWith("REGISTERED ")) {
|
||
|
console.log("Listener registered");
|
||
|
ws_conn.send('LIST');
|
||
|
} else if (event.data.startsWith('LIST')) {
|
||
|
var split = event.data.split(' ');
|
||
|
clearPeers();
|
||
|
|
||
|
for (var i = 1; i < split.length; i++) {
|
||
|
addPeer(split[i]);
|
||
|
}
|
||
|
} else if (event.data.startsWith('ADD PRODUCER')) {
|
||
|
var split = event.data.split(' ');
|
||
|
addPeer(split[2]);
|
||
|
} else if (event.data.startsWith('REMOVE PRODUCER')) {
|
||
|
var split = event.data.split(' ');
|
||
|
var li = document.getElementById("peer-" + split[2]);
|
||
|
li.parentNode.removeChild(li);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
function clearConnection() {
|
||
|
ws_conn.removeEventListener('error', onServerError);
|
||
|
ws_conn.removeEventListener('message', onServerMessage);
|
||
|
ws_conn.removeEventListener('close', onServerClose);
|
||
|
ws_conn = null;
|
||
|
}
|
||
|
|
||
|
function onServerClose(event) {
|
||
|
clearConnection();
|
||
|
clearPeers();
|
||
|
console.log("Close");
|
||
|
window.setTimeout(connect, 1000);
|
||
|
};
|
||
|
|
||
|
function onServerError(event) {
|
||
|
clearConnection();
|
||
|
clearPeers();
|
||
|
console.log("Error", event);
|
||
|
window.setTimeout(connect, 1000);
|
||
|
};
|
||
|
|
||
|
function connect() {
|
||
|
var ws_port = ws_port || '8443';
|
||
|
if (window.location.protocol.startsWith ("file")) {
|
||
|
var ws_server = ws_server || "127.0.0.1";
|
||
|
} else if (window.location.protocol.startsWith ("http")) {
|
||
|
var 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 = 'ws://' + ws_server + ':' + ws_port
|
||
|
console.log("Connecting listener");
|
||
|
ws_conn = new WebSocket(ws_url);
|
||
|
ws_conn.addEventListener('open', (event) => {
|
||
|
ws_conn.send('REGISTER LISTENER');
|
||
|
});
|
||
|
ws_conn.addEventListener('error', onServerError);
|
||
|
ws_conn.addEventListener('message', onServerMessage);
|
||
|
ws_conn.addEventListener('close', onServerClose);
|
||
|
}
|
||
|
|
||
|
function setup() {
|
||
|
connect();
|
||
|
}
|