/** * 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); } } };