UNPKG

js-3dstk

Version:

Universal JavaScript Client for the 3D Streaming Toolkit - Supports Node.js, Browser and React-Native.

447 lines (406 loc) 14.8 kB
const validUrl = require('valid-url'); const _ = require('lodash'); const sdputils = require('./sdputils'); //TODO: READD AUTH TOKEN class ThreeDStreamingClient { constructor({serverUrl, peerConnectionConfig, platform}, WebRTC) { if (!validUrl.isUri(serverUrl)) { throw new Error('Invalid url'); } if (_.isUndefined(WebRTC) || !_.isObject(WebRTC)) { throw new Error('Invalid WebRTC object'); } if (_.isUndefined(platform) || !_.isString(platform) || !_.includes(['browser', 'node', 'react-native'], platform)) { this.platform = 'browser'; // Default to browser } else { this.platform = platform; } this.serverUrl = serverUrl; this.pcConfig = peerConnectionConfig; this.WebRTC = WebRTC; this.myId = -1; this.activePeerId = null; this.signalingConnected = false; this.otherPeers = {}; this.heartBeatIntervalId = null; this.peerConnection = null; this.inputChannel = null; this.repeatLongPoll = false; this.onconnecting = null; this.onopen = null; this.onclose = null; this.onaddstream = null; this.onremovestream = null; this.onupdatepeers = null; } signIn(peerName, {onconnecting = null, onopen = null, onclose = null, onaddstream = null, onremovestream = null, onupdatepeers = null}) { // First part of the hand shake const fetchOptions = { method: 'GET', headers: { 'Peer-Type': 'Client' // Apparently this is useless by @bengreenier } }; this.onconnecting = onconnecting; this.onopen = onopen; this.onclose = onclose; this.onaddstream = onaddstream; this.onremovestream = onremovestream; this.onupdatepeers = onupdatepeers; return fetch(`${this.serverUrl}/sign_in?peer_name=${peerName}`, fetchOptions) .then((response) => response.text()) .then((responseText) => { //TODO: rewrite this parser. var peers = responseText.split('\n'); // parse my id from the sign in response. this.myId = parseInt(peers[0].split(',')[1], 10); // Parse the existing list of peers and update map for (var i = 1; i < peers.length; ++i) { if (peers[i].length > 0) { var parsed = peers[i].split(','); this.otherPeers[parseInt(parsed[1], 10)] = parsed[0]; } } if (_.isFunction(this.onupdatepeers)) { this.onupdatepeers(); } this.signalingConnected = true; }); } disconnect() { this.stopHeartBeat(); this.repeatLongPoll = false; if (this.myId !== -1) { //Tell the other peer we are hanging up if(this.peerConnection !== null && this.peerConnection.iceConnectionState == "connected") { this.disconnectFromCurrentPeer(); } else { this.disconnectFromServer(); } } } //TODO: This is still broken and needs further debugging disconnectFromCurrentPeer() { if(this.peerConnection !== null && this.peerConnection.iceConnectionState == "connected" && this.activePeerId !== null){ //Tell the other peer goodbye this.sendToPeer(this.activePeerId,"BYE"); } return true; } disconnectFromServer() { if(this.signalingConnected == true) { //If not actively streaming, then just sign out fetch(`${this.serverUrl}/sign_out?peer_id=${this.myId}`, { method: 'GET', headers: { 'Peer-Type': 'Client' } }); this.signalingConnected = false; } } // PRIVATE _heartbeatFunc() { const fetchOptions = { method: 'GET', headers: { // 'Peer-Type': 'Client' // Apparently this is useless by @bengreenier } }; // note: we don't really care what the response looks like here, so we don't observe it fetch(`${this.serverUrl}/heartbeat?peer_id=${this.myId}`, fetchOptions); /*if (accessToken) { heartbeatGet.setRequestHeader("Authorization", 'Bearer ' + accessToken); } */ } startHeartbeat() { // Issue heartbeats indefinitely this.heartBeatIntervalId = setInterval(this._heartbeatFunc.bind(this), 5000); } stopHeartBeat() { if (this.heartBeatIntervalId){ clearInterval(this.heartBeatIntervalId); this.heartBeatIntervalId = null; } } getPeerById(id) { if (!_.isNumber(id)){ throw new Error('Invalid Id parameter.'); } if (id in this.otherPeers){ return this.otherPeers[id]; } else { return null; } } getPeerIdByName(name){ if (!_.isString(name)){ throw new Error('Invalid paramter, not a string.'); } return _.findKey(this.otherPeers, function(o) { return o === name; }); } stopPollingSignalingServer() { this.repeatLongPoll = false; } pollSignalingServer(repeat) { if (!_.isUndefined(repeat) && _.isBoolean(repeat)){ this.repeatLongPoll = repeat; } // If repeat long poll is set to false, stop polling. if (this.repeatLongPoll === false) { return; } const fetchOptions = { method: 'GET', headers: { 'Peer-Type': 'Client' // Apparently this is useless by @bengreenier } }; return fetch(`${this.serverUrl}/wait?peer_id=${this.myId}`, fetchOptions) .then((response) => { if (!response.ok) { // console.error(response.statusText); // Disconnect on Internal Server Errors? if (response.status === 500){ // TODO: handle this case. //this.disconnect(); return; } else { this.pollSignalingServer(); } } let pragma = response.headers.get('pragma'); let peer_id = pragma != null && pragma.length ? parseInt(pragma, 10) : null; response.text().then((text) => this._handleMessage(peer_id, text)) .then(() => { this.pollSignalingServer(); }); }).catch((error) => { // Also, this catches any and all errors including errors in the response handler // On all other errors (e.g. timeout), restart the long poll. console.error(error); // restart long poll on timeout. if (this.myId !== -1) { this.pollSignalingServer(); } }); } // PRIVATE _handlePeerListUpdate(peer_id, body) { console.log('Handling PEERLIST_UPDATE message'); var parsed = body.split(','); if (parseInt(parsed[2], 10) !== 0) { console.log('New peer added.'); this.otherPeers[parseInt(parsed[1], 10)] = parsed[0]; } if (_.isFunction(this.onupdatepeers)) { this.onupdatepeers(); } } // PRIVATE _handleOfferMessage(peer_id, body){ //TODO: I MIGHT NEED TO ASK FOR THE STREAM ADD CALLBACK EARLIER THAN JOIN PEER... console.log('Handling OFFER_MESSAGE message'); let mediaConstraints = { 'mandatory': { 'OfferToReceiveAudio': false, 'OfferToReceiveVideo': true } }; let dataJson = JSON.parse(body); if(dataJson['uri'] && dataJson['username'] && dataJson['password']){ console.log('Parsing Turn Credentials from OFFER:', dataJson); var iceServersTemp = [].concat(this.pcConfig['iceServers']); iceServersTemp.push({ 'urls': dataJson['uri'], 'username': dataJson['username'], 'credential': dataJson['password'], 'credentialType': 'password' }); this.pcConfig['iceServers'] = iceServersTemp; } this._createPeerConnection(peer_id); this.peerConnection.setRemoteDescription(new this.WebRTC.RTCSessionDescription(dataJson), () => {console.log('Successfully set remote description');}, (event) => {console.log(`Failed to set remote description on ${event.name}: ${event.message}`);} ); this.peerConnection.createAnswer((sessionDescription) => { console.log('Create answer:', sessionDescription); this.peerConnection.setLocalDescription(sessionDescription); var dataD = JSON.stringify(sessionDescription); this.sendToPeer(peer_id, dataD); }, function (error) { // error console.log('Create answer error:', error); }, mediaConstraints); // type error ); //}, null } // PRIVATE _handleAnswerMessage(peer_id, body){ console.log('Handling ANSWER_MESSAGE message'); var dataJson = JSON.parse(body); console.log('Got answer ', dataJson); this.peerConnection.setRemoteDescription(new this.WebRTC.RTCSessionDescription(dataJson), () => {console.log('Successfully set remote description');}, (event) => {console.log(`Failed to set remote description on ${event.name}: ${event.message}`); }); } // PRIVATE _handleAddIceCandidate(peer_id, body){ console.log('BODDY OF ICE CANDIATE: ' + body); var dataJson = JSON.parse(body); console.log('Adding ICE candiate ', dataJson); var candidate = new this.WebRTC.RTCIceCandidate({ sdpMLineIndex: dataJson.sdpMLineIndex, candidate: dataJson.candidate, sdpMid: dataJson.sdpMid }); if(candidate.sdpMid != null) { this.peerConnection.addIceCandidate(candidate); } /*.then(() => { // Do nothing }).catch(e => { trace("Error: Failure during addIceCandidate() " + e); });*/ } // PRIVATE _handleMessage(peer_id, body){ if (!_.isString(body) || _.isEmpty(body.trim())){ console.log('Received an invalid message'); return; } let messageType = peer_id === this.myId ? 'PEERLIST_UPDATE' : null; messageType = messageType === null && _.isString(body) && body.search('offer') !== -1 ? 'OFFER_MESSAGE' : messageType; messageType = messageType === null && _.isString(body) && body.search('answer') !== -1 ? 'ANSWER_MESSAGE' : messageType; messageType = messageType === null ? 'ADD_ICE_CANDIDATE' : messageType; switch (messageType){ case 'PEERLIST_UPDATE': this._handlePeerListUpdate(peer_id, body); break; case 'OFFER_MESSAGE': this._handleOfferMessage(peer_id, body); break; case 'ANSWER_MESSAGE': this._handleAnswerMessage(peer_id, body); break; case 'ADD_ICE_CANDIDATE': this._handleAddIceCandidate(peer_id, body); break; } } _createPeerConnection(peer_id) { try { // Destroy existing peer connection. This class does not support multiple streams. if (this.peerConnection !== null){ this.peerConnection.close(); this.peerConnection = null; } this.peerConnection = new this.WebRTC.RTCPeerConnection(this.pcConfig); this.peerConnection.onicecandidate = (event) => { if (event.candidate) { var candidate = { sdpMLineIndex: event.candidate.sdpMLineIndex, sdpMid: event.candidate.sdpMid, candidate: event.candidate.candidate }; this.sendToPeer(peer_id, JSON.stringify(candidate)); } else { console.log('End of candidates.'); } }; // Only replace builtin function if we are passed an event handler. if (_.isFunction(this.onconnecting)) { this.peerConnection.onconnecting = this.onconnecting; } if (_.isFunction(this.onopen)) { this.peerConnection.onopen = this.onopen; } if (_.isFunction(this.onaddstream)) { this.peerConnection.onaddstream = this.onaddstream; } if (_.isFunction(this.onremovestream)) { this.peerConnection.onremovestream = this.onremovestream; } if (_.isFunction(this.onclose)) { this.peerConnection.onclose = this.onclose; } this.peerConnection.ondatachannel = (ev) => { this.inputChannel = ev.channel; this.inputChannel.onopen = this._handleSendChannelOpen; this.inputChannel.onclose = this._handleSendChannelClose; }; console.log('Created RTCPeerConnnection with config: ' + JSON.stringify(this.pcConfig)); this.activePeerId = peer_id; return this.peerConnection; } catch (e) { console.log('Failed to create PeerConnection, exception: ' + e.message); } // Explictly set to null if we failed... this.peerConnection = null; return null; } sendToPeer(peer_id, data) { if (this.myId === -1) { // Not connected to signaling server... return null; } if (peer_id === this.myId) { // Can't send a message to myself return null; } /*if (accessToken) { r.setRequestHeader("Authorization", 'Bearer ' + accessToken); }*/ const fetchOptions = { method: 'POST', headers: { 'Peer-Type': 'Client', // Apparently this is useless by @bengreenier 'Content-Type': 'text/plain' }, body: data }; return fetch(`${this.serverUrl}/message?peer_id=${this.myId}&to=${peer_id}`, fetchOptions).catch((reason) => {console.error("Testing"+reason);}); } _handleSendChannelOpen() { console.log('sendChannel opened'); } _handleSendChannelClose(){ console.log('sendChannel closed'); } joinPeer(peer_id, cb) { if (!(peer_id in this.otherPeers)){ throw new Error('Peer Id is not registered'); } // Create peer connection this._createPeerConnection(peer_id, cb); // Create data channel this.inputChannel = this.peerConnection.createDataChannel('inputDataChannel'); this.inputChannel.onopen = this._handleSendChannelOpen; this.inputChannel.onclose = this._handleSendChannelClose; // Create Offer let offerOptions = { offerToReceiveAudio: 0, offerToReceiveVideo: 1 }; var receivedOffer = ''; this.peerConnection.createOffer(offerOptions).then((offer) => { offer.sdp = sdputils.maybePreferCodec(offer.sdp, 'video', 'receive', "H264"); // Set local description this.peerConnection.setLocalDescription(offer); receivedOffer = offer; }).then(() => { // Send offer to signaling server this.sendToPeer(peer_id, JSON.stringify(receivedOffer)); }); // wait for answer & set remote desciption to data supplied by answer return this.peerConnection; } sendInputChannelData(data) { if (this.inputChannel && this.inputChannel.readyState === 'open'){ this.inputChannel.send(JSON.stringify(data)); } } } module.exports = ThreeDStreamingClient;