videoconference/js/conference-phone.js
06fbd764
 /**
  *  Copyright (C) 2021  Double Bastion LLC
  *
  *  This file is part of Roundpin, which is licensed under the
  *  GNU Affero General Public License Version 3.0. The license terms
  *  are detailed in the "LICENSE.txt" file located in the root directory.
  *
  *  This is a modified version of the original file "cyber_mega_phone.js",
  *  first modified in 2020. The copyright notice for the original
  *  content follows.
  */
 ///////////////////////////////////////////////////////////////////////////////
 //  Cyber Mega Phone 2K
 //  Copyright (C) 2017 Digium, Inc.
 //
 //  This program is free software, distributed under the terms of the
 //  MIT License. See the LICENSE file at the top of the source tree.
 ///////////////////////////////////////////////////////////////////////////////
 
 'use_strict';
 
 let isFirefox = typeof InstallTrigger !== 'undefined';
 let isChrome = !!window.chrome && !!window.chrome;
 let currentSession = null;
 
 function ConferencePhone(id, name, password, host, StunServer, register, audio=true, video=true) {
 
 	EasyEvent.call(this);
 
 	this.id = id;
 	this.name = name;
 	this.password = password;
 	this.host = host;
         this.StunServer = StunServer;
 	this.register = register;
 
 	this.audio = audio;
 	this.video = video;
 
 	this._locals = new Streams();
 	this._locals.bubble("streamAdded", this);
 	this._locals.bubble("streamRemoved", this);
 
 	this._remotes = new Streams();
 	this._remotes.bubble("streamAdded", this);
 	this._remotes.bubble("streamRemoved", this);
 };
 
 ConferencePhone.prototype = Object.create(EasyEvent.prototype);
 ConferencePhone.prototype.constructor = ConferencePhone;
 
 // This was taken from the WebRTC unified transition guide located at
 // https://docs.google.com/document/d/1-ZfikoUtoJa9k-GZG1daN0BU3IjIanQ_JSscHxQesvU/edit
 function isUnifiedPlanDefault() {
 	// Safari supports addTransceiver() but not Unified Plan when
 	// currentDirection is not defined.
 	if (!('currentDirection' in RTCRtpTransceiver.prototype))
 		return false;
 
 	// If Unified Plan is supported, addTransceiver() should not throw.
 	const tempPc = new RTCPeerConnection();
 	let canAddTransceiver = false;
 	try {
 		tempPc.addTransceiver('audio');
 		canAddTransceiver = true;
 	} catch (e) {
 	}
 
 	tempPc.close();
 	return canAddTransceiver;
 }
 
 ConferencePhone.prototype.connect = function () {
 	if (this._ua) {
 		this._ua.start(); // Just reconnect
 		return;
 	}
 
 	let that = this;
 
 	let socket = new JsSIP.WebSocketInterface('wss://' + this.host + ':8089/ws');
 	let uri = 'sip:' + this.id + '@' + this.host;
 
 	let config = {
 		sockets: [ socket ],
 		uri: uri,
 		contact_uri: uri,
 		username: this.name ? this.name : this.id,
 		password: this.password,
 		register: this.register,
 		register_expires : 300,
                 sessionDescriptionHandlerFactoryOptions: {
                     peerConnectionOptions : {
                         alwaysAcquireMediaFirst: true,
                         iceCheckingTimeout: 500,
                         rtcConfiguration: {
                             iceServers : [
                                 { urls: "stun:" + this.StunServer }
                             ]
                         }
                     }
                 }
 	};
 
 	this._unified = isUnifiedPlanDefault();
 
 	this._ua = new JsSIP.UA(config);
 
 	function bubble (obj, name) {
 		obj.on(name, function (data) {
 			that.raise(name, data);
 		});
 	};
 
 	bubble(this._ua, 'connected');
 	bubble(this._ua, 'disconnected');
 	bubble(this._ua, 'registered');
 	bubble(this._ua, 'unregistered');
 	bubble(this._ua, 'registrationFailed');
 
 	this._ua.on('newRTCSession', function (data) {
 
                 currentSession = data.session;
 
 		let rtc = data.session;
 		rtc.interop = new SdpInterop.InteropChrome();
 
 		console.log('new session - ' + rtc.direction + ' - ' + rtc);
 
 		rtc.on("confirmed", function () {
 			// ACK was received
 			let streams = rtc.connection.getLocalStreams();
 			for (let i = 0; i < streams.length; ++i) {
 				console.log('confirmed: adding local stream ' + streams[i].id);
 				streams[i].local = true;
 				that._locals.add(streams[i]);
 			}
 		});
 
 		rtc.on("sdp", function (data) {
 			if (isFirefox && data.originator === 'remote') {
 				data.sdp = data.sdp.replace(/actpass/g, 'active');
 			} else if (isChrome && !that._unified) {
 				let desc = new RTCSessionDescription({type:data.type, sdp:data.sdp});
 				if (data.originator === 'local') {
 					converted = rtc.interop.toUnifiedPlan(desc);
 				} else {
 					converted = rtc.interop.toPlanB(desc);
 				}
 
 				data.sdp = converted.sdp;
 			}
 		});
 
 		bubble(rtc, 'muted');
 		bubble(rtc, 'unmuted');
 		bubble(rtc, 'failed');
 		bubble(rtc, 'ended');
 
 		rtc.connection.ontrack = function (event) {
 
 			console.log('ontrack: ' + event.track.kind + ' - ' + event.track.id +
 						' stream ' + event.streams[0].id);
 
                         $("#video-view"+event.streams[0].id+"").show();
 
 			if (event.track.kind == 'video') {
 				event.track.enabled = false;
 			}
 
 			for (let i = 0; i < event.streams.length; ++i) {
 				event.streams[i].local = false;
 				that._remotes.add(event.streams[i]);
 			}
 
 			event.track.onended = function() {
 
                                 $("#video-view"+event.streams[0].id+"").hide();
 			};
 		};
 
 		rtc.connection.onremovestream = function (event) {
 			console.log('onremovestream: ' + event.stream.id);
 			that._remotes.remove(event.stream);
 		};
 
 		if (data.originator === "remote") {
 			that.raise('incoming', data.request.ruri.toAor());
 		}
 	});
 
 	this._ua.start();
 };
 
 ConferencePhone.prototype.disconnect = function () {
 	this._locals.removeAll();
 	this._remotes.removeAll();
 	if (this._ua) {
 		this._ua.stop();
 	}
 };
 
 ConferencePhone.prototype.answer = function () {
 
 	if (!this._ua) {
 		return;
 	}
 
 	let options = {
 		'mediaConstraints': { 'audio': this.audio, 'video': this.video }
 	};
 
 	this._rtc.answer(options);
 };
 
 ConferencePhone.prototype.call = function (exten) {
 
 	if (!this._ua || !exten) {
 		return;
 	}
 
 	let options = {
 		'mediaConstraints': { 'audio': this.audio, 'video': this.video }
         };
 
 	if (exten.startsWith('sip:')) {
 		this._rtc = this._ua.call(exten, options);
 	} else {
 		this._rtc = this._ua.call('sip:' + exten + '@' + this.host, options);
 	}
 };
 
 ConferencePhone.prototype.terminate = function () {
 	this._locals.removeAll();
 	this._remotes.removeAll();
 	if (this._ua) {
 		this._rtc.terminate();
 	}
 };
 
 ConferencePhone.prototype.ShareScreen = function () {
 
     var localStream = new MediaStream();
     var pc = currentSession.connection;
 
     var screenShareConstraints = { video: true, audio: false };
 
     navigator.mediaDevices.getDisplayMedia(screenShareConstraints).then(function(newStream) {
 
             var newMediaTrack = newStream.getVideoTracks()[0];
             pc.getSenders().forEach(function (RTCRtpSender) {
                 if (RTCRtpSender.track && RTCRtpSender.track.kind == "video") {
                     RTCRtpSender.replaceTrack(newMediaTrack);
                     localStream.addTrack(newMediaTrack);
                 }
             });
 
             var localVideo = $('[id*="locVideo"]').get(0);
             localVideo.autoplay = true;
             localVideo.srcObject = localStream;
 
             var VidView = $('[id*="video-view"]').get(0);
             VidView.append(localVideo);
             $('[id*="new-media-view"]').get(0).append(VidView);
           
     }).catch(function() { console.error(e); });
 }
 
 ConferencePhone.prototype.ShareVideo = function () {
 
     var localStream = new MediaStream();
     var pc = currentSession.connection;
 
     var videoShareConstraints = { video: true, audio: true };
     navigator.mediaDevices.getUserMedia(videoShareConstraints).then(function(newStream) {
 
             var newMediaTrack = newStream.getVideoTracks()[0];
             pc.getSenders().forEach(function (RTCRtpSender) {
                 if (RTCRtpSender.track && RTCRtpSender.track.kind == "video") {
                     RTCRtpSender.replaceTrack(newMediaTrack);
                     localStream.addTrack(newMediaTrack);
                 }
             });
 
             var localVideo = $('[id*="locVideo"]').get(0);
             localVideo.autoplay = true;
             localVideo.srcObject = localStream;
 
             var VidView = $('[id*="video-view"]').get(0);
             VidView.append(localVideo);
             $('[id*="new-media-view"]').get(0).append(VidView);
          
     }).catch(function() { console.error(e); });
 }
 
 ConferencePhone.prototype.dtmfSend = function (numPressed) {
     $("#dialText").val(numPressed);
     var pc = currentSession.connection;
     var dtmfSender = pc.getSenders()[0].dtmf;
     dtmfSender.insertDTMF(numPressed);
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 
 function mute(stream, options) {
 
 	function setTracks(tracks, val) {
 		if (!tracks) {
 			return;
 		}
 
 		for (let i = 0; i < tracks.length; ++i) {
 			if (tracks[i].enabled == val) {
 				tracks[i].enabled = !val;
 			}
 		}
 	};
 
 	options = options || { audio: true, video: true };
 
 	if (typeof options.audio != 'undefined') {
 		setTracks(stream.getAudioTracks(), options.audio);
 	}
 
 	if (typeof options.video != 'undefined') {
 		setTracks(stream.getVideoTracks(), options.video);
 	}
 }
 
 function unmute(stream, options) {
 	let opts = options || { audio: false, video: false };
 	mute(stream, opts);
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 
 function Streams () {
 	EasyEvent.call(this);
 	this._streams = [];
 };
 
 Streams.prototype = Object.create(EasyEvent.prototype);
 Streams.prototype.constructor = Streams;
 
 Streams.prototype.add = function (stream) {
 	if (this._streams.indexOf(stream) == -1) {
 		this._streams.push(stream);
 		console.log('Streams: added ' + stream.id);
 		this.raise('streamAdded', stream);
 	}
 };
 
 Streams.prototype.remove = function (stream) {
 	let index = typeof stream == 'number' ? stream : this._streams.indexOf(stream);
 
 	if (index == -1) {
 		return;
 	}
 
 	let removed = this._streams.splice(index, 1);
 	for (let i = 0; i < removed.length; ++i) {
 		console.log('Streams: removed ' + removed[i].id);
 		this.raise('streamRemoved', removed[i]);
 	}
 };
 
 Streams.prototype.removeAll = function () {
 	for (let i = this._streams.length - 1; i >= 0 ; --i) {
 		this.remove(i);
 	}
 };
 
 ///////////////////////////////////////////////////////////////////////////////
 
 function EasyEvent () {
 	this._events = {};
 };
 
 EasyEvent.prototype.handle = function (name, fun) {
 	if (name in this._events) {
 		this._events[name].push(fun);
 	} else {
 		this._events[name] = [fun];
 	}
 };
 
 EasyEvent.prototype.raise = function (name) {
 	if (name in this._events) {
 		for (let i = 0; i < this._events[name].length; ++i) {
 			this._events[name][i].apply(this,
 					Array.prototype.slice.call(arguments, 1));
 		}
 	}
 };
 
 EasyEvent.prototype.bubble = function (name, obj) {
 	this.handle(name, function (data) {
 		obj.raise(name, data);
 	});
 };
 
 EasyEvent.prototype.raiseForEach = function (name, array) {
 	if (name in this._events) {
 		for (let i = 0; i < array.length; ++i) {
 			this.raise(name, array[i], i);
 		}
 	}
 };