diff --git a/packages/rocketchat-webrtc/WebRTCClass.coffee b/packages/rocketchat-webrtc/WebRTCClass.coffee
deleted file mode 100644
index a957e411d7cc..000000000000
--- a/packages/rocketchat-webrtc/WebRTCClass.coffee
+++ /dev/null
@@ -1,823 +0,0 @@
-emptyFn = ->
- # empty
-
-class WebRTCTransportClass
- debug: false
-
- log: ->
- if @debug is true
- console.log.apply(console, arguments)
-
- constructor: (@webrtcInstance) ->
- @callbacks = {}
-
- RocketChat.Notifications.onRoom @webrtcInstance.room, 'webrtc', (type, data) =>
- @log 'WebRTCTransportClass - onRoom', type, data
-
- switch type
- when 'status'
- if @callbacks['onRemoteStatus']?.length > 0
- fn(data) for fn in @callbacks['onRemoteStatus']
-
- onUserStream: (type, data) ->
- if data.room isnt @webrtcInstance.room then return
- @log 'WebRTCTransportClass - onUser', type, data
-
- switch type
- when 'call'
- if @callbacks['onRemoteCall']?.length > 0
- fn(data) for fn in @callbacks['onRemoteCall']
-
- when 'join'
- if @callbacks['onRemoteJoin']?.length > 0
- fn(data) for fn in @callbacks['onRemoteJoin']
-
- when 'candidate'
- if @callbacks['onRemoteCandidate']?.length > 0
- fn(data) for fn in @callbacks['onRemoteCandidate']
-
- when 'description'
- if @callbacks['onRemoteDescription']?.length > 0
- fn(data) for fn in @callbacks['onRemoteDescription']
-
- startCall: (data) ->
- @log 'WebRTCTransportClass - startCall', @webrtcInstance.room, @webrtcInstance.selfId
- RocketChat.Notifications.notifyUsersOfRoom @webrtcInstance.room, 'webrtc', 'call',
- from: @webrtcInstance.selfId
- room: @webrtcInstance.room
- media: data.media
- monitor: data.monitor
-
- joinCall: (data) ->
- @log 'WebRTCTransportClass - joinCall', @webrtcInstance.room, @webrtcInstance.selfId
- if data.monitor is true
- RocketChat.Notifications.notifyUser data.to, 'webrtc', 'join',
- from: @webrtcInstance.selfId
- room: @webrtcInstance.room
- media: data.media
- monitor: data.monitor
- else
- RocketChat.Notifications.notifyUsersOfRoom @webrtcInstance.room, 'webrtc', 'join',
- from: @webrtcInstance.selfId
- room: @webrtcInstance.room
- media: data.media
- monitor: data.monitor
-
- sendCandidate: (data) ->
- data.from = @webrtcInstance.selfId
- data.room = @webrtcInstance.room
- @log 'WebRTCTransportClass - sendCandidate', data
- RocketChat.Notifications.notifyUser data.to, 'webrtc', 'candidate', data
-
- sendDescription: (data) ->
- data.from = @webrtcInstance.selfId
- data.room = @webrtcInstance.room
- @log 'WebRTCTransportClass - sendDescription', data
- RocketChat.Notifications.notifyUser data.to, 'webrtc', 'description', data
-
- sendStatus: (data) ->
- @log 'WebRTCTransportClass - sendStatus', data, @webrtcInstance.room
- data.from = @webrtcInstance.selfId
- RocketChat.Notifications.notifyRoom @webrtcInstance.room, 'webrtc', 'status', data
-
- onRemoteCall: (fn) ->
- @callbacks['onRemoteCall'] ?= []
- @callbacks['onRemoteCall'].push fn
-
- onRemoteJoin: (fn) ->
- @callbacks['onRemoteJoin'] ?= []
- @callbacks['onRemoteJoin'].push fn
-
- onRemoteCandidate: (fn) ->
- @callbacks['onRemoteCandidate'] ?= []
- @callbacks['onRemoteCandidate'].push fn
-
- onRemoteDescription: (fn) ->
- @callbacks['onRemoteDescription'] ?= []
- @callbacks['onRemoteDescription'].push fn
-
- onRemoteStatus: (fn) ->
- @callbacks['onRemoteStatus'] ?= []
- @callbacks['onRemoteStatus'].push fn
-
-
-class WebRTCClass
- config:
- iceServers: []
-
- debug: false
-
- transportClass: WebRTCTransportClass
-
-
- ###
- @param seldId {String}
- @param room {String}
- ###
- constructor: (@selfId, @room) ->
- @config.iceServers = []
-
- servers = RocketChat.settings.get("WebRTC_Servers")
- if servers?.trim() isnt ''
- servers = servers.replace /\s/g, ''
- servers = servers.split ','
- for server in servers
- server = server.split '@'
- serverConfig =
- urls: server.pop()
-
- if server.length is 1
- server = server[0].split ':'
- serverConfig.username = decodeURIComponent(server[0])
- serverConfig.credential = decodeURIComponent(server[1])
-
- @config.iceServers.push serverConfig
-
- @peerConnections = {}
-
- @remoteItems = new ReactiveVar []
- @remoteItemsById = new ReactiveVar {}
- @callInProgress = new ReactiveVar false
- @audioEnabled = new ReactiveVar true
- @videoEnabled = new ReactiveVar true
- @overlayEnabled = new ReactiveVar false
- @screenShareEnabled = new ReactiveVar false
- @localUrl = new ReactiveVar
-
- @active = false
- @remoteMonitoring = false
- @monitor = false
- @autoAccept = false
-
- @navigator = undefined
- userAgent = navigator.userAgent.toLocaleLowerCase();
- if userAgent.indexOf('electron') isnt -1
- @navigator = 'electron'
- else if userAgent.indexOf('chrome') isnt -1
- @navigator = 'chrome'
- else if userAgent.indexOf('firefox') isnt -1
- @navigator = 'firefox'
- else if userAgent.indexOf('safari') isnt -1
- @navigator = 'safari'
-
- @screenShareAvailable = @navigator in ['chrome', 'firefox', 'electron']
-
- @media =
- video: false
- audio: true
-
- @transport = new @transportClass @
-
- @transport.onRemoteCall @onRemoteCall.bind @
- @transport.onRemoteJoin @onRemoteJoin.bind @
- @transport.onRemoteCandidate @onRemoteCandidate.bind @
- @transport.onRemoteDescription @onRemoteDescription.bind @
- @transport.onRemoteStatus @onRemoteStatus.bind @
-
- Meteor.setInterval @checkPeerConnections.bind(@), 1000
-
- # Meteor.setInterval @broadcastStatus.bind(@), 1000
-
- log: ->
- if @debug is true
- console.log.apply(console, arguments)
-
- onError: ->
- console.error.apply(console, arguments)
-
- checkPeerConnections: ->
- for id, peerConnection of @peerConnections
- if peerConnection.iceConnectionState not in ['connected', 'completed'] and peerConnection.createdAt + 5000 < Date.now()
- @stopPeerConnection id
-
- updateRemoteItems: ->
- items = []
- itemsById = {}
-
- for id, peerConnection of @peerConnections
- for remoteStream in peerConnection.getRemoteStreams()
- item =
- id: id
- url: URL.createObjectURL(remoteStream)
- state: peerConnection.iceConnectionState
-
- switch peerConnection.iceConnectionState
- when 'checking'
- item.stateText = 'Connecting...'
-
- when 'connected', 'completed'
- item.stateText = 'Connected'
- item.connected = true
-
- when 'disconnected'
- item.stateText = 'Disconnected'
-
- when 'failed'
- item.stateText = 'Failed'
-
- when 'closed'
- item.stateText = 'Closed'
-
- items.push item
- itemsById[id] = item
-
- @remoteItems.set items
- @remoteItemsById.set itemsById
-
- resetCallInProgress: ->
- @callInProgress.set false
-
- broadcastStatus: ->
- if @active isnt true or @monitor is true or @remoteMonitoring is true then return
-
- remoteConnections = []
- for id, peerConnection of @peerConnections
- remoteConnections.push
- id: id
- media: peerConnection.remoteMedia
-
- @transport.sendStatus
- media: @media
- remoteConnections: remoteConnections
-
- ###
- @param data {Object}
- from {String}
- media {Object}
- remoteConnections {Array[Object]}
- id {String}
- media {Object}
- ###
- onRemoteStatus: (data) ->
- # @log 'onRemoteStatus', arguments
-
- @callInProgress.set true
-
- Meteor.clearTimeout @callInProgressTimeout
- @callInProgressTimeout = Meteor.setTimeout @resetCallInProgress.bind(@), 2000
-
- if @active isnt true then return
-
- remoteConnections = [{id: data.from, media: data.media}].concat data.remoteConnections
-
- for remoteConnection in remoteConnections
- if remoteConnection.id isnt @selfId and not @peerConnections[remoteConnection.id]?
- @log 'reconnecting with', remoteConnection.id
- @onRemoteJoin
- from: remoteConnection.id
- media: remoteConnection.media
-
- ###
- @param id {String}
- ###
- getPeerConnection: (id) ->
- return @peerConnections[id] if @peerConnections[id]?
-
- peerConnection = new RTCPeerConnection @config
-
- peerConnection.createdAt = Date.now()
- peerConnection.remoteMedia = {}
-
- @peerConnections[id] = peerConnection
-
- eventNames = [
- 'icecandidate'
- 'addstream'
- 'removestream'
- 'iceconnectionstatechange'
- 'datachannel'
- 'identityresult'
- 'idpassertionerror'
- 'idpvalidationerror'
- 'negotiationneeded'
- 'peeridentity'
- 'signalingstatechange'
- ]
-
- for eventName in eventNames
- peerConnection.addEventListener eventName, (e) =>
- @log id, e.type, e
-
- peerConnection.addEventListener 'icecandidate', (e) =>
- if not e.candidate?
- return
-
- @transport.sendCandidate
- to: id
- candidate:
- candidate: e.candidate.candidate
- sdpMLineIndex: e.candidate.sdpMLineIndex
- sdpMid: e.candidate.sdpMid
-
- peerConnection.addEventListener 'addstream', (e) =>
- @updateRemoteItems()
-
- peerConnection.addEventListener 'removestream', (e) =>
- @updateRemoteItems()
-
- peerConnection.addEventListener 'iceconnectionstatechange', (e) =>
- if peerConnection.iceConnectionState in ['disconnected', 'closed'] and peerConnection is @peerConnections[id]
- @stopPeerConnection id
- Meteor.setTimeout =>
- if Object.keys(@peerConnections).length is 0
- @stop()
- , 3000
-
- @updateRemoteItems()
-
- return peerConnection
-
- _getUserMedia: (media, onSuccess, onError) ->
- onSuccessLocal = (stream) ->
- if AudioContext? and stream.getAudioTracks().length > 0
- audioContext = new AudioContext
- source = audioContext.createMediaStreamSource(stream)
-
- volume = audioContext.createGain()
- source.connect(volume)
- peer = audioContext.createMediaStreamDestination()
- volume.connect(peer)
- volume.gain.value = 0.6
-
- stream.removeTrack(stream.getAudioTracks()[0])
- stream.addTrack(peer.stream.getAudioTracks()[0])
- stream.volume = volume
-
- this.audioContext = audioContext
-
- onSuccess(stream)
-
- navigator.getUserMedia media, onSuccessLocal, onError
-
-
- getUserMedia: (media, onSuccess, onError=@onError) ->
- if media.desktop isnt true
- @_getUserMedia media, onSuccess, onError
- return
-
- if @screenShareAvailable isnt true
- console.log 'Screen share is not avaliable'
- return
-
- getScreen = (audioStream) =>
- if document.cookie.indexOf("rocketchatscreenshare=chrome") is -1 and not window.rocketchatscreenshare? and @navigator isnt 'electron'
- refresh = ->
- swal
- type: "warning"
- title: TAPi18n.__ "Refresh_your_page_after_install_to_enable_screen_sharing"
-
- swal
- type: "warning"
- title: TAPi18n.__ "Screen_Share"
- text: TAPi18n.__ "You_need_install_an_extension_to_allow_screen_sharing"
- html: true
- showCancelButton: true
- confirmButtonText: TAPi18n.__ "Install_Extension"
- cancelButtonText: TAPi18n.__ "Cancel"
- , (isConfirm) =>
- if isConfirm
- if @navigator is 'chrome'
- url = 'https://chrome.google.com/webstore/detail/rocketchat-screen-share/nocfbnnmjnndkbipkabodnheejiegccf'
- try
- chrome.webstore.install url, refresh, ->
- window.open(url)
- refresh()
- catch e
- window.open(url)
- refresh()
- else if @navigator is 'firefox'
- window.open('https://addons.mozilla.org/en-GB/firefox/addon/rocketchat-screen-share/')
- refresh()
-
- return onError(false)
-
- getScreenSuccess = (stream) =>
- if audioStream?
- stream.addTrack(audioStream.getAudioTracks()[0])
- onSuccess(stream)
-
- if @navigator is 'firefox'
- media =
- audio: media.audio
- video:
- mozMediaSource: 'window'
- mediaSource: 'window'
- @_getUserMedia media, getScreenSuccess, onError
- else
- ChromeScreenShare.getSourceId @navigator, (id) =>
- media =
- audio: false
- video:
- mandatory:
- chromeMediaSource: 'desktop'
- chromeMediaSourceId: id
- maxWidth: 1280
- maxHeight: 720
-
- @_getUserMedia media, getScreenSuccess, onError
-
- if @navigator is 'firefox' or not media.audio? or media.audio is false
- getScreen()
- else
- getAudioSuccess = (audioStream) =>
- getScreen(audioStream)
-
- getAudioError = =>
- getScreen()
-
- @_getUserMedia {audio: media.audio}, getAudioSuccess, getAudioError
-
-
- ###
- @param callback {Function}
- ###
- getLocalUserMedia: (callback) ->
- @log 'getLocalUserMedia', arguments
-
- if @localStream?
- return callback null, @localStream
-
- onSuccess = (stream) =>
- @localStream = stream
- @localUrl.set URL.createObjectURL(stream)
-
- @videoEnabled.set @media.video is true
- @audioEnabled.set @media.audio is true
-
- for id, peerConnection of @peerConnections
- peerConnection.addStream stream
-
- callback null, @localStream
-
- onError = (error) =>
- callback false
- @onError error
-
- @getUserMedia @media, onSuccess, onError
-
-
- ###
- @param id {String}
- ###
- stopPeerConnection: (id) ->
- peerConnection = @peerConnections[id]
- if not peerConnection? then return
-
- delete @peerConnections[id]
- peerConnection.close()
-
- @updateRemoteItems()
-
- stopAllPeerConnections: ->
- for id, peerConnection of @peerConnections
- @stopPeerConnection id
- window.audioContext?.close()
-
- setAudioEnabled: (enabled=true) ->
- if @localStream?
- if enabled is true and @media.audio isnt true
- delete @localStream
- @media.audio = true
- @getLocalUserMedia =>
- @stopAllPeerConnections()
- @joinCall()
- else
- @localStream.getAudioTracks().forEach (audio) -> audio.enabled = enabled
- @audioEnabled.set enabled
-
- disableAudio: ->
- @setAudioEnabled false
-
- enableAudio: ->
- @setAudioEnabled true
-
- setVideoEnabled: (enabled=true) ->
- if @localStream?
- if enabled is true and @media.video isnt true
- delete @localStream
- @media.video = true
- @getLocalUserMedia =>
- @stopAllPeerConnections()
- @joinCall()
- else
- @localStream.getVideoTracks().forEach (video) -> video.enabled = enabled
- @videoEnabled.set enabled
-
- disableScreenShare: ->
- @setScreenShareEnabled false
-
- enableScreenShare: ->
- @setScreenShareEnabled true
-
- setScreenShareEnabled: (enabled=true) ->
- if @localStream?
- @media.desktop = enabled
- delete @localStream
- @getLocalUserMedia (err) =>
- if err?
- return
- @screenShareEnabled.set enabled
- @stopAllPeerConnections()
- @joinCall()
-
- disableVideo: ->
- @setVideoEnabled false
-
- enableVideo: ->
- @setVideoEnabled true
-
- stop: ->
- @active = false
- @monitor = false
- @remoteMonitoring = false
- if @localStream? and typeof @localStream isnt 'undefined'
- @localStream.getTracks().forEach (track) ->
- track.stop()
- @localUrl.set undefined
- delete @localStream
-
- @stopAllPeerConnections()
-
-
- ###
- @param media {Object}
- audio {Boolean}
- video {Boolean}
- ###
- startCall: (media={}) ->
- @log 'startCall', arguments
- @media = media
- @getLocalUserMedia =>
- @active = true
- @transport.startCall
- media: @media
-
- startCallAsMonitor: (media={}) ->
- @log 'startCallAsMonitor', arguments
- @media = media
- @active = true
- @monitor = true
- @transport.startCall
- media: @media
- monitor: true
-
-
- ###
- @param data {Object}
- from {String}
- monitor {Boolean}
- media {Object}
- audio {Boolean}
- video {Boolean}
- ###
- onRemoteCall: (data) ->
- if @autoAccept is true
- FlowRouter.goToRoomById data.room
- Meteor.defer =>
- @joinCall
- to: data.from
- monitor: data.monitor
- media: data.media
- return
-
- fromUsername = Meteor.users.findOne(data.from)?.username
- subscription = ChatSubscription.findOne({rid: data.room})
-
- if data.monitor is true
- icon = 'eye'
- title = "Monitor call from #{fromUsername}"
- else if subscription?.t is 'd'
- if data.media?.video
- icon = 'videocam'
- title = "Direct video call from #{fromUsername}"
- else
- icon = 'phone'
- title = "Direct audio call from #{fromUsername}"
- else
- if data.media?.video
- icon = 'videocam'
- title = "Group video call from #{subscription.name}"
- else
- icon = 'phone'
- title = "Group audio call from #{subscription.name}"
-
- swal
- title: "#{title}"
- text: "Do you want to accept?"
- html: true
- showCancelButton: true
- confirmButtonText: "Yes"
- cancelButtonText: "No"
- , (isConfirm) =>
- if isConfirm
- FlowRouter.goToRoomById data.room
- Meteor.defer =>
- @joinCall
- to: data.from
- monitor: data.monitor
- media: data.media
- else
- @stop()
-
-
- ###
- @param data {Object}
- to {String}
- monitor {Boolean}
- media {Object}
- audio {Boolean}
- video {Boolean}
- desktop {Boolean}
- ###
- joinCall: (data={}) ->
- if data.media?.audio?
- @media.audio = data.media.audio
-
- if data.media?.video?
- @media.video = data.media.video
-
- data.media = @media
-
- @log 'joinCall', arguments
- @getLocalUserMedia =>
- @remoteMonitoring = data.monitor
- @active = true
- @transport.joinCall(data)
-
-
- ###
- @param data {Object}
- from {String}
- monitor {Boolean}
- media {Object}
- audio {Boolean}
- video {Boolean}
- desktop {Boolean}
- ###
- onRemoteJoin: (data) ->
- if @active isnt true then return
-
- @log 'onRemoteJoin', arguments
-
- peerConnection = @getPeerConnection data.from
-
- # needsRefresh = false
- # if peerConnection.iceConnectionState isnt 'new'
- # needsAudio = data.media.audio is true and peerConnection.remoteMedia.audio isnt true
- # needsVideo = data.media.video is true and peerConnection.remoteMedia.video isnt true
- # needsRefresh = needsAudio or needsVideo or data.media.desktop isnt peerConnection.remoteMedia.desktop
-
- # if peerConnection.signalingState is "have-local-offer" or needsRefresh
- if peerConnection.signalingState isnt "checking"
- @stopPeerConnection data.from
- peerConnection = @getPeerConnection data.from
-
- if peerConnection.iceConnectionState isnt 'new'
- return
-
- peerConnection.remoteMedia = data.media
-
- peerConnection.addStream @localStream if @localStream
-
- onOffer = (offer) =>
- onLocalDescription = =>
- @transport.sendDescription
- to: data.from
- type: 'offer'
- ts: peerConnection.createdAt
- media: @media
- description:
- sdp: offer.sdp
- type: offer.type
-
- peerConnection.setLocalDescription(new RTCSessionDescription(offer), onLocalDescription, @onError)
-
- if data.monitor is true
- peerConnection.createOffer onOffer, @onError,
- mandatory:
- OfferToReceiveAudio: data.media.audio
- OfferToReceiveVideo: data.media.video
- else
- peerConnection.createOffer(onOffer, @onError)
-
-
- ###
- @param data {Object}
- from {String}
- ts {Integer}
- description {String}
- ###
- onRemoteOffer: (data) ->
- if @active isnt true then return
-
- @log 'onRemoteOffer', arguments
- peerConnection = @getPeerConnection data.from
-
- if peerConnection.signalingState in ["have-local-offer", "stable"] and peerConnection.createdAt < data.ts
- @stopPeerConnection data.from
- peerConnection = @getPeerConnection data.from
-
- if peerConnection.iceConnectionState isnt 'new'
- return
-
- peerConnection.setRemoteDescription new RTCSessionDescription(data.description)
-
- try peerConnection.addStream @localStream if @localStream
-
- onAnswer = (answer) =>
- onLocalDescription = =>
- @transport.sendDescription
- to: data.from
- type: 'answer'
- ts: peerConnection.createdAt
- description:
- sdp: answer.sdp
- type: answer.type
-
- peerConnection.setLocalDescription(new RTCSessionDescription(answer), onLocalDescription, @onError)
-
- peerConnection.createAnswer(onAnswer, @onError)
-
-
- ###
- @param data {Object}
- to {String}
- from {String}
- candidate {RTCIceCandidate JSON encoded}
- ###
- onRemoteCandidate: (data) ->
- if @active isnt true then return
- if data.to isnt @selfId then return
-
- @log 'onRemoteCandidate', arguments
- peerConnection = @getPeerConnection data.from
-
- if peerConnection.iceConnectionState not in ["closed", "failed", "disconnected", "completed"]
- peerConnection.addIceCandidate new RTCIceCandidate(data.candidate)
-
-
- ###
- @param data {Object}
- to {String}
- from {String}
- type {String} [offer, answer]
- description {RTCSessionDescription JSON encoded}
- ts {Integer}
- media {Object}
- audio {Boolean}
- video {Boolean}
- desktop {Boolean}
- ###
- onRemoteDescription: (data) ->
- if @active isnt true then return
- if data.to isnt @selfId then return
-
- @log 'onRemoteDescription', arguments
- peerConnection = @getPeerConnection data.from
-
- if data.type is 'offer'
- peerConnection.remoteMedia = data.media
- @onRemoteOffer
- from: data.from
- ts: data.ts
- description: data.description
- else
- peerConnection.setRemoteDescription new RTCSessionDescription(data.description)
-
-
-WebRTC = new class
- constructor: ->
- @instancesByRoomId = {}
-
- getInstanceByRoomId: (roomId) ->
- subscription = ChatSubscription.findOne({rid: roomId})
- if not subscription
- return
-
- enabled = false
- switch subscription.t
- when 'd'
- enabled = RocketChat.settings.get('WebRTC_Enable_Direct')
- when 'p'
- enabled = RocketChat.settings.get('WebRTC_Enable_Private')
- when 'c'
- enabled = RocketChat.settings.get('WebRTC_Enable_Channel')
-
- if enabled is false
- return
-
- if not @instancesByRoomId[roomId]?
- @instancesByRoomId[roomId] = new WebRTCClass Meteor.userId(), roomId
-
- return @instancesByRoomId[roomId]
-
-
-Meteor.startup ->
- Tracker.autorun ->
- if Meteor.userId()
- RocketChat.Notifications.onUser 'webrtc', (type, data) =>
- if not data.room? then return
-
- webrtc = WebRTC.getInstanceByRoomId(data.room)
-
- webrtc.transport.onUserStream type, data
diff --git a/packages/rocketchat-webrtc/client/WebRTCClass.js b/packages/rocketchat-webrtc/client/WebRTCClass.js
new file mode 100644
index 000000000000..dfbccbd4391f
--- /dev/null
+++ b/packages/rocketchat-webrtc/client/WebRTCClass.js
@@ -0,0 +1,992 @@
+/* globals chrome, ChromeScreenShare */
+class WebRTCTransportClass {
+ constructor(webrtcInstance) {
+ this.debug = false;
+ this.webrtcInstance = webrtcInstance;
+ this.callbacks = {};
+ RocketChat.Notifications.onRoom(this.webrtcInstance.room, 'webrtc', (type, data) => {
+ const onRemoteStatus = this.callbacks['onRemoteStatus'];
+ this.log('WebRTCTransportClass - onRoom', type, data);
+ switch (type) {
+ case 'status':
+ if (onRemoteStatus && onRemoteStatus.length) {
+ onRemoteStatus.forEach(fn => fn(data));
+ }
+ }
+ });
+ }
+
+ log() {
+ if (this.debug === true) {
+ console.log.apply(console, arguments);
+ }
+ }
+
+ onUserStream(type, data) {
+ if (data.room !== this.webrtcInstance.room) {
+ return;
+ }
+ this.log('WebRTCTransportClass - onUser', type, data);
+ const onRemoteCall = this.callbacks['onRemoteCall'];
+ const onRemoteJoin = this.callbacks['onRemoteJoin'];
+ const onRemoteCandidate = this.callbacks['onRemoteCandidate'];
+ const onRemoteDescription = this.callbacks['onRemoteDescription'];
+
+ switch (type) {
+ case 'call':
+ if (onRemoteCall && onRemoteCall.length) {
+ onRemoteCall.forEach(fn => fn(data));
+ }
+ break;
+ case 'join':
+ if (onRemoteJoin && onRemoteJoin.length) {
+ onRemoteJoin.forEach(fn => fn(data));
+ }
+ break;
+ case 'candidate':
+ if (onRemoteCandidate && onRemoteCandidate.length) {
+ onRemoteCandidate.forEach(fn => fn(data));
+ }
+ break;
+ case 'description':
+ if (onRemoteDescription && onRemoteDescription.length) {
+ onRemoteDescription.forEach(fn => fn(data));
+ }
+ }
+ }
+
+ startCall(data) {
+ this.log('WebRTCTransportClass - startCall', this.webrtcInstance.room, this.webrtcInstance.selfId);
+ RocketChat.Notifications.notifyUsersOfRoom(this.webrtcInstance.room, 'webrtc', 'call', {
+ from: this.webrtcInstance.selfId,
+ room: this.webrtcInstance.room,
+ media: data.media,
+ monitor: data.monitor
+ });
+ }
+
+ joinCall(data) {
+ this.log('WebRTCTransportClass - joinCall', this.webrtcInstance.room, this.webrtcInstance.selfId);
+ if (data.monitor === true) {
+ RocketChat.Notifications.notifyUser(data.to, 'webrtc', 'join', {
+ from: this.webrtcInstance.selfId,
+ room: this.webrtcInstance.room,
+ media: data.media,
+ monitor: data.monitor
+ });
+ } else {
+ RocketChat.Notifications.notifyUsersOfRoom(this.webrtcInstance.room, 'webrtc', 'join', {
+ from: this.webrtcInstance.selfId,
+ room: this.webrtcInstance.room,
+ media: data.media,
+ monitor: data.monitor
+ });
+ }
+ }
+
+ sendCandidate(data) {
+ data.from = this.webrtcInstance.selfId;
+ data.room = this.webrtcInstance.room;
+ this.log('WebRTCTransportClass - sendCandidate', data);
+ RocketChat.Notifications.notifyUser(data.to, 'webrtc', 'candidate', data);
+ }
+
+ sendDescription(data) {
+ data.from = this.webrtcInstance.selfId;
+ data.room = this.webrtcInstance.room;
+ this.log('WebRTCTransportClass - sendDescription', data);
+ RocketChat.Notifications.notifyUser(data.to, 'webrtc', 'description', data);
+ }
+
+ sendStatus(data) {
+ this.log('WebRTCTransportClass - sendStatus', data, this.webrtcInstance.room);
+ data.from = this.webrtcInstance.selfId;
+ RocketChat.Notifications.notifyRoom(this.webrtcInstance.room, 'webrtc', 'status', data);
+ }
+
+ onRemoteCall(fn) {
+ const callbacks = this.callbacks;
+ if (callbacks['onRemoteCall'] == null) {
+ callbacks['onRemoteCall'] = [];
+ }
+ callbacks['onRemoteCall'].push(fn);
+ }
+
+ onRemoteJoin(fn) {
+ const callbacks = this.callbacks;
+ if (callbacks['onRemoteJoin'] == null) {
+ callbacks['onRemoteJoin'] = [];
+ }
+ callbacks['onRemoteJoin'].push(fn);
+ }
+
+ onRemoteCandidate(fn) {
+ const callbacks = this.callbacks;
+ if (callbacks['onRemoteCandidate'] == null) {
+ callbacks['onRemoteCandidate'] = [];
+ }
+ callbacks['onRemoteCandidate'].push(fn);
+ }
+
+ onRemoteDescription(fn) {
+ const callbacks = this.callbacks;
+ if (callbacks['onRemoteDescription'] == null) {
+ callbacks['onRemoteDescription'] = [];
+ }
+ callbacks['onRemoteDescription'].push(fn);
+ }
+
+ onRemoteStatus(fn) {
+ const callbacks = this.callbacks;
+ if (callbacks['onRemoteStatus'] == null) {
+ callbacks['onRemoteStatus'] = [];
+ }
+ callbacks['onRemoteStatus'].push(fn);
+ }
+
+
+
+}
+
+class WebRTCClass {
+ /*
+ @param seldId {String}
+ @param room {String}
+ */
+
+ constructor(selfId, room) {
+ this.config = {
+ iceServers: []
+ };
+ this.debug = false;
+ this.TransportClass = WebRTCTransportClass;
+ this.selfId = selfId;
+ this.room = room;
+ let servers = RocketChat.settings.get('WebRTC_Servers');
+ if (servers && servers.trim() !== '') {
+ servers = servers.replace(/\s/g, '');
+ servers = servers.split(',');
+
+ servers.forEach(server => {
+ server = server.split('@');
+ const serverConfig = {
+ urls: server.pop()
+ };
+ if (server.length === 1) {
+ server = server[0].split(':');
+ serverConfig.username = decodeURIComponent(server[0]);
+ serverConfig.credential = decodeURIComponent(server[1]);
+ }
+ this.config.iceServers.push(serverConfig);
+ });
+ }
+ this.peerConnections = {};
+ this.remoteItems = new ReactiveVar([]);
+ this.remoteItemsById = new ReactiveVar({});
+ this.callInProgress = new ReactiveVar(false);
+ this.audioEnabled = new ReactiveVar(true);
+ this.videoEnabled = new ReactiveVar(true);
+ this.overlayEnabled = new ReactiveVar(false);
+ this.screenShareEnabled = new ReactiveVar(false);
+ this.localUrl = new ReactiveVar;
+ this.active = false;
+ this.remoteMonitoring = false;
+ this.monitor = false;
+ this.autoAccept = false;
+ this.navigator = undefined;
+ const userAgent = navigator.userAgent.toLocaleLowerCase();
+
+ if (userAgent.indexOf('electron') !== -1) {
+ this.navigator = 'electron';
+ } else if (userAgent.indexOf('chrome') !== -1) {
+ this.navigator = 'chrome';
+ } else if (userAgent.indexOf('firefox') !== -1) {
+ this.navigator = 'firefox';
+ } else if (userAgent.indexOf('safari') !== -1) {
+ this.navigator = 'safari';
+ }
+ const nav = this.navigator;
+ this.screenShareAvailable = nav === 'chrome' || nav === 'firefox' || nav === 'electron';
+ this.media = {
+ video: false,
+ audio: true
+ };
+ this.transport = new this.TransportClass(this);
+ this.transport.onRemoteCall(this.onRemoteCall.bind(this));
+ this.transport.onRemoteJoin(this.onRemoteJoin.bind(this));
+ this.transport.onRemoteCandidate(this.onRemoteCandidate.bind(this));
+ this.transport.onRemoteDescription(this.onRemoteDescription.bind(this));
+ this.transport.onRemoteStatus(this.onRemoteStatus.bind(this));
+ Meteor.setInterval(this.checkPeerConnections.bind(this), 1000);
+
+ //Meteor.setInterval(this.broadcastStatus.bind(@), 1000);
+ }
+
+ log() {
+ if (this.debug === true) {
+ console.log.apply(console, arguments);
+ }
+ }
+
+ onError() {
+ console.error.apply(console, arguments);
+ }
+
+ checkPeerConnections() {
+ const peerConnections = this.peerConnections;
+ Object.keys(peerConnections).forEach(id => {
+ const peerConnection = peerConnections[id];
+ if (peerConnection.iceConnectionState !== 'connected' && peerConnection.iceConnectionState !== 'completed' && peerConnection.createdAt + 5000 < Date.now()) {
+ this.stopPeerConnection(id);
+ }
+ });
+ }
+
+ updateRemoteItems() {
+ const items = [];
+ const itemsById = {};
+ const peerConnections = this.peerConnections;
+
+ Object.keys(peerConnections).forEach(id => {
+ const peerConnection = peerConnections[id];
+
+ peerConnection.getRemoteStreams().forEach(remoteStream => {
+ const item = {
+ id,
+ url: URL.createObjectURL(remoteStream),
+ state: peerConnection.iceConnectionState
+ };
+ switch (peerConnection.iceConnectionState) {
+ case 'checking':
+ item.stateText = 'Connecting...';
+ break;
+ case 'connected':
+ case 'completed':
+ item.stateText = 'Connected';
+ item.connected = true;
+ break;
+ case 'disconnected':
+ item.stateText = 'Disconnected';
+ break;
+ case 'failed':
+ item.stateText = 'Failed';
+ break;
+ case 'closed':
+ item.stateText = 'Closed';
+ }
+ items.push(item);
+ itemsById[id] = item;
+ });
+ });
+ this.remoteItems.set(items);
+ this.remoteItemsById.set(itemsById);
+ }
+
+ resetCallInProgress() {
+ this.callInProgress.set(false);
+ }
+
+ broadcastStatus() {
+ if (this.active !== true || this.monitor === true || this.remoteMonitoring === true) {
+ return;
+ }
+ const remoteConnections = [];
+ const peerConnections = this.peerConnections;
+ Object.keys(peerConnections).forEach(id => {
+ const peerConnection = peerConnections[id];
+ remoteConnections.push({
+ id,
+ media: peerConnection.remoteMedia
+ });
+ });
+
+ this.transport.sendStatus({
+ media: this.media,
+ remoteConnections
+ });
+ }
+
+
+ /*
+ @param data {Object}
+ from {String}
+ media {Object}
+ remoteConnections {Array[Object]}
+ id {String}
+ media {Object}
+ */
+
+ onRemoteStatus(data) {
+ //this.log(onRemoteStatus, arguments);
+ this.callInProgress.set(true);
+ Meteor.clearTimeout(this.callInProgressTimeout);
+ this.callInProgressTimeout = Meteor.setTimeout(this.resetCallInProgress.bind(this), 2000);
+ if (this.active !== true) {
+ return;
+ }
+ const remoteConnections = [{
+ id: data.from,
+ media: data.media
+ },
+ ...data.remoteConnections];
+
+ remoteConnections.forEach(remoteConnection => {
+ if (remoteConnection.id !== this.selfId && (this.peerConnections[remoteConnection.id] == null)) {
+ this.log('reconnecting with', remoteConnection.id);
+ this.onRemoteJoin({
+ from: remoteConnection.id,
+ media: remoteConnection.media
+ });
+ }
+ });
+ }
+
+
+ /*
+ @param id {String}
+ */
+
+ getPeerConnection(id) {
+ if (this.peerConnections[id] != null) {
+ return this.peerConnections[id];
+ }
+ const peerConnection = new RTCPeerConnection(this.config);
+
+ peerConnection.createdAt = Date.now();
+ peerConnection.remoteMedia = {};
+ this.peerConnections[id] = peerConnection;
+ const eventNames = ['icecandidate', 'addstream', 'removestream', 'iceconnectionstatechange', 'datachannel', 'identityresult', 'idpassertionerror', 'idpvalidationerror', 'negotiationneeded', 'peeridentity', 'signalingstatechange'];
+
+ eventNames.forEach(eventName => {
+ peerConnection.addEventListener(eventName, (e) => {
+ this.log(id, e.type, e);
+ });
+ });
+
+ peerConnection.addEventListener('icecandidate', (e) => {
+ if (e.candidate == null) {
+ return;
+ }
+ this.transport.sendCandidate({
+ to: id,
+ candidate: {
+ candidate: e.candidate.candidate,
+ sdpMLineIndex: e.candidate.sdpMLineIndex,
+ sdpMid: e.candidate.sdpMid
+ }
+ });
+ });
+ peerConnection.addEventListener('addstream', () => {
+ this.updateRemoteItems();
+ });
+ peerConnection.addEventListener('removestream', () => {
+ this.updateRemoteItems();
+ });
+ peerConnection.addEventListener('iceconnectionstatechange', () => {
+ if ((peerConnection.iceConnectionState === 'disconnected' || peerConnection.iceConnectionState === 'closed') && peerConnection === this.peerConnections[id]) {
+ this.stopPeerConnection(id);
+ Meteor.setTimeout(() => {
+ if (Object.keys(this.peerConnections).length === 0) {
+ this.stop();
+ }
+ }, 3000);
+ }
+ this.updateRemoteItems();
+ });
+ return peerConnection;
+ }
+
+ _getUserMedia(media, onSuccess, onError) {
+ const onSuccessLocal = function(stream) {
+ if (AudioContext && stream.getAudioTracks().length > 0) {
+ const audioContext = new AudioContext;
+ const source = audioContext.createMediaStreamSource(stream);
+ const volume = audioContext.createGain();
+ source.connect(volume);
+ const peer = audioContext.createMediaStreamDestination();
+ volume.connect(peer);
+ volume.gain.value = 0.6;
+ stream.removeTrack(stream.getAudioTracks()[0]);
+ stream.addTrack(peer.stream.getAudioTracks()[0]);
+ stream.volume = volume;
+ this.audioContext = audioContext;
+ }
+ onSuccess(stream);
+ };
+ navigator.getUserMedia(media, onSuccessLocal, onError);
+ }
+
+ getUserMedia(media, onSuccess, onError = this.onError) {
+ if (media.desktop !== true) {
+ this._getUserMedia(media, onSuccess, onError);
+ return;
+ }
+ if (this.screenShareAvailable !== true) {
+ console.log('Screen share is not avaliable');
+ return;
+ }
+ const getScreen = (audioStream) => {
+ if (document.cookie.indexOf('rocketchatscreenshare=chrome') === -1 && (window.rocketchatscreenshare == null) && this.navigator !== 'electron') {
+ const refresh = function() {
+ swal({
+ type: 'warning',
+ title: TAPi18n.__('Refresh_your_page_after_install_to_enable_screen_sharing')
+ });
+ };
+ swal({
+ type: 'warning',
+ title: TAPi18n.__('Screen_Share'),
+ text: TAPi18n.__('You_need_install_an_extension_to_allow_screen_sharing'),
+ html: true,
+ showCancelButton: true,
+ confirmButtonText: TAPi18n.__('Install_Extension'),
+ cancelButtonText: TAPi18n.__('Cancel')
+ }, (isConfirm) => {
+ if (isConfirm) {
+ if (this.navigator === 'chrome') {
+ const url = 'https://chrome.google.com/webstore/detail/rocketchat-screen-share/nocfbnnmjnndkbipkabodnheejiegccf';
+ try {
+ chrome.webstore.install(url, refresh, function() {
+ window.open(url);
+ refresh();
+ });
+ } catch (_error) {
+ console.log(_error);
+ }
+ } else if (this.navigator === 'firefox') {
+ window.open('https://addons.mozilla.org/en-GB/firefox/addon/rocketchat-screen-share/');
+ refresh();
+ }
+ }
+ });
+ return onError(false);
+ }
+ const getScreenSuccess = (stream) => {
+ if (audioStream != null) {
+ stream.addTrack(audioStream.getAudioTracks()[0]);
+ }
+ onSuccess(stream);
+ };
+ if (this.navigator === 'firefox') {
+ media = {
+ audio: media.audio,
+ video: {
+ mozMediaSource: 'window',
+ mediaSource: 'window'
+ }
+ };
+ this._getUserMedia(media, getScreenSuccess, onError);
+ } else {
+ ChromeScreenShare.getSourceId(this.navigator, (id) => {
+ media = {
+ audio: false,
+ video: {
+ mandatory: {
+ chromeMediaSource: 'desktop',
+ chromeMediaSourceId: id,
+ maxWidth: 1280,
+ maxHeight: 720
+ }
+ }
+ };
+ this._getUserMedia(media, getScreenSuccess, onError);
+ });
+ }
+ };
+ if (this.navigator === 'firefox' || (media.audio == null) || media.audio === false) {
+ getScreen();
+ } else {
+ const getAudioSuccess = (audioStream) => {
+ getScreen(audioStream);
+ };
+ const getAudioError = () => {
+ getScreen();
+ };
+
+ this._getUserMedia({
+ audio: media.audio
+ }, getAudioSuccess, getAudioError);
+ }
+ }
+
+
+ /*
+ @param callback {Function}
+ */
+
+ getLocalUserMedia(callback) {
+ this.log('getLocalUserMedia', arguments);
+ if (this.localStream != null) {
+ return callback(null, this.localStream);
+ }
+ const onSuccess = (stream) => {
+ this.localStream = stream;
+ this.localUrl.set(URL.createObjectURL(stream));
+ this.videoEnabled.set(this.media.video === true);
+ this.audioEnabled.set(this.media.audio === true);
+ const peerConnections = this.peerConnections;
+ Object.keys(peerConnections).forEach(id => {
+ const peerConnection = peerConnections[id];
+ peerConnection.addStream(stream);
+ });
+ callback(null, this.localStream);
+ };
+ const onError = (error) => {
+ callback(false);
+ this.onError(error);
+ };
+ this.getUserMedia(this.media, onSuccess, onError);
+ }
+
+
+ /*
+ @param id {String}
+ */
+
+ stopPeerConnection(id) {
+ const peerConnection = this.peerConnections[id];
+ if (peerConnection == null) {
+ return;
+ }
+ delete this.peerConnections[id];
+ peerConnection.close();
+ this.updateRemoteItems();
+ }
+
+ stopAllPeerConnections() {
+ const peerConnections = this.peerConnections;
+
+ Object.keys(peerConnections).forEach(id => {
+ this.stopPeerConnection(id);
+ });
+
+ window.audioContext && window.audioContext.close();
+ }
+
+ setAudioEnabled(enabled = true) {
+ if (this.localStream != null) {
+ if (enabled === true && this.media.audio !== true) {
+ delete this.localStream;
+ this.media.audio = true;
+ this.getLocalUserMedia(() => {
+ this.stopAllPeerConnections();
+ this.joinCall();
+ });
+ } else {
+ this.localStream.getAudioTracks().forEach(function(audio) {
+ audio.enabled = enabled;
+ });
+ this.audioEnabled.set(enabled);
+ }
+ }
+ }
+
+ disableAudio() {
+ this.setAudioEnabled(false);
+ }
+
+ enableAudio() {
+ this.setAudioEnabled(true);
+ }
+
+ setVideoEnabled(enabled = true) {
+ if (this.localStream != null) {
+ if (enabled === true && this.media.video !== true) {
+ delete this.localStream;
+ this.media.video = true;
+ this.getLocalUserMedia(() => {
+ this.stopAllPeerConnections();
+ this.joinCall();
+ });
+ } else {
+ this.localStream.getVideoTracks().forEach(function(video) {
+ video.enabled = enabled;
+ });
+ this.videoEnabled.set(enabled);
+ }
+ }
+ }
+
+ disableScreenShare() {
+ this.setScreenShareEnabled(false);
+ }
+
+ enableScreenShare() {
+ this.setScreenShareEnabled(true);
+ }
+
+ setScreenShareEnabled(enabled = true) {
+ if (this.localStream != null) {
+ this.media.desktop = enabled;
+ delete this.localStream;
+ this.getLocalUserMedia(err => {
+ if (err != null) {
+ return;
+ }
+ this.screenShareEnabled.set(enabled);
+ this.stopAllPeerConnections();
+ this.joinCall();
+ });
+ }
+ }
+
+ disableVideo() {
+ this.setVideoEnabled(false);
+ }
+
+ enableVideo() {
+ this.setVideoEnabled(true);
+ }
+
+ stop() {
+ this.active = false;
+ this.monitor = false;
+ this.remoteMonitoring = false;
+ if (this.localStream != null && typeof this.localStream !== 'undefined') {
+ this.localStream.getTracks().forEach(track => track.stop());
+ }
+ this.localUrl.set(undefined);
+ delete this.localStream;
+ this.stopAllPeerConnections();
+ }
+
+
+ /*
+ @param media {Object}
+ audio {Boolean}
+ video {Boolean}
+ */
+
+ startCall(media = {}) {
+ this.log('startCall', arguments);
+ this.media = media;
+ this.getLocalUserMedia(() => {
+ this.active = true;
+ this.transport.startCall({
+ media: this.media
+ });
+ });
+ }
+
+ startCallAsMonitor(media = {}) {
+ this.log('startCallAsMonitor', arguments);
+ this.media = media;
+ this.active = true;
+ this.monitor = true;
+ this.transport.startCall({
+ media: this.media,
+ monitor: true
+ });
+ }
+
+
+ /*
+ @param data {Object}
+ from {String}
+ monitor {Boolean}
+ media {Object}
+ audio {Boolean}
+ video {Boolean}
+ */
+
+ onRemoteCall(data) {
+ if (this.autoAccept === true) {
+ FlowRouter.goToRoomById(data.room);
+ Meteor.defer(() => {
+ this.joinCall({
+ to: data.from,
+ monitor: data.monitor,
+ media: data.media
+ });
+ });
+ return;
+ }
+
+ const user = Meteor.users.findOne(data.from);
+ let fromUsername = undefined;
+ if (user && user.username) {
+ fromUsername = user.username;
+ }
+ const subscription = ChatSubscription.findOne({
+ rid: data.room
+ });
+
+ let icon;
+ let title;
+ if (data.monitor === true) {
+ icon = 'eye';
+ title = `Monitor call from ${ fromUsername }`;
+ } else if (subscription && subscription.t === 'd') {
+ if (data.media && data.media.video) {
+ icon = 'videocam';
+ title = `Direct video call from ${ fromUsername }`;
+ } else {
+ icon = 'phone';
+ title = `Direct audio call from ${ fromUsername }`;
+ }
+ } else if (data.media && data.media.video) {
+ icon = 'videocam';
+ title = `Group video call from ${ subscription.name }`;
+ } else {
+ icon = 'phone';
+ title = `Group audio call from ${ subscription.name }`;
+ }
+ swal({
+ title: `${ title }`,
+ text: 'Do you want to accept?',
+ html: true,
+ showCancelButton: true,
+ confirmButtonText: 'Yes',
+ cancelButtonText: 'No'
+ }, (isConfirm) => {
+ if (isConfirm) {
+ FlowRouter.goToRoomById(data.room);
+ Meteor.defer(() => {
+ this.joinCall({
+ to: data.from,
+ monitor: data.monitor,
+ media: data.media
+ });
+ });
+ } else {
+ this.stop();
+ }
+ });
+ }
+
+
+ /*
+ @param data {Object}
+ to {String}
+ monitor {Boolean}
+ media {Object}
+ audio {Boolean}
+ video {Boolean}
+ desktop {Boolean}
+ */
+
+ joinCall(data = {}) {
+ if (data.media && data.media.audio) {
+ this.media.audio = data.media.audio;
+ }
+ if (data.media && data.media.video) {
+ this.media.video = data.media.video;
+ }
+ data.media = this.media;
+ this.log('joinCall', arguments);
+ this.getLocalUserMedia(() => {
+ this.remoteMonitoring = data.monitor;
+ this.active = true;
+ this.transport.joinCall(data);
+ });
+ }
+
+
+ onRemoteJoin(data) {
+ if (this.active !== true) {
+ return;
+ }
+ this.log('onRemoteJoin', arguments);
+ let peerConnection = this.getPeerConnection(data.from);
+
+ // needsRefresh = false
+ // if peerConnection.iceConnectionState isnt 'new'
+ // needsAudio = data.media.audio is true and peerConnection.remoteMedia.audio isnt true
+ // needsVideo = data.media.video is true and peerConnection.remoteMedia.video isnt true
+ // needsRefresh = needsAudio or needsVideo or data.media.desktop isnt peerConnection.remoteMedia.desktop
+
+ // # if peerConnection.signalingState is "have-local-offer" or needsRefresh
+
+ if (peerConnection.signalingState !== 'checking') {
+ this.stopPeerConnection(data.from);
+ peerConnection = this.getPeerConnection(data.from);
+ }
+ if (peerConnection.iceConnectionState !== 'new') {
+ return;
+ }
+ peerConnection.remoteMedia = data.media;
+ if (this.localStream) {
+ peerConnection.addStream(this.localStream);
+ }
+ const onOffer = offer => {
+ const onLocalDescription = () => {
+ this.transport.sendDescription({
+ to: data.from,
+ type: 'offer',
+ ts: peerConnection.createdAt,
+ media: this.media,
+ description: {
+ sdp: offer.sdp,
+ type: offer.type
+ }
+ });
+ };
+
+ peerConnection.setLocalDescription(new RTCSessionDescription(offer), onLocalDescription, this.onError);
+ };
+
+ if (data.monitor === true) {
+ peerConnection.createOffer(onOffer, this.onError, {
+ mandatory: {
+ OfferToReceiveAudio: data.media.audio,
+ OfferToReceiveVideo: data.media.video
+ }
+ });
+ } else {
+ peerConnection.createOffer(onOffer, this.onError);
+ }
+ }
+
+
+
+ onRemoteOffer(data) {
+ if (this.active !== true) {
+ return;
+ }
+
+ this.log('onRemoteOffer', arguments);
+ let peerConnection = this.getPeerConnection(data.from);
+
+ if (['have-local-offer', 'stable'].includes(peerConnection.signalingState) && (peerConnection.createdAt < data.ts)) {
+ this.stopPeerConnection(data.from);
+ peerConnection = this.getPeerConnection(data.from);
+ }
+
+ if (peerConnection.iceConnectionState !== 'new') {
+ return;
+ }
+
+ peerConnection.setRemoteDescription(new RTCSessionDescription(data.description));
+
+ try {
+ if (this.localStream) {
+ peerConnection.addStream(this.localStream);
+ }
+ } catch (error) {
+ console.log(error);
+ }
+
+ const onAnswer = answer => {
+ const onLocalDescription = () => {
+ this.transport.sendDescription({
+ to: data.from,
+ type: 'answer',
+ ts: peerConnection.createdAt,
+ description: {
+ sdp: answer.sdp,
+ type: answer.type
+ }
+ });
+ };
+
+ peerConnection.setLocalDescription(new RTCSessionDescription(answer), onLocalDescription, this.onError);
+ };
+
+ peerConnection.createAnswer(onAnswer, this.onError);
+ }
+
+
+ /*
+ @param data {Object}
+ to {String}
+ from {String}
+ candidate {RTCIceCandidate JSON encoded}
+ */
+
+ onRemoteCandidate(data) {
+ if (this.active !== true) {
+ return;
+ }
+ if (data.to !== this.selfId) {
+ return;
+ }
+ this.log('onRemoteCandidate', arguments);
+ const peerConnection = this.getPeerConnection(data.from);
+ if (peerConnection.iceConnectionState !== 'closed' && peerConnection.iceConnectionState !== 'failed' && peerConnection.iceConnectionState !== 'disconnected' && peerConnection.iceConnectionState !== 'completed') {
+ peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
+ }
+ }
+
+
+ /*
+ @param data {Object}
+ to {String}
+ from {String}
+ type {String} [offer, answer]
+ description {RTCSessionDescription JSON encoded}
+ ts {Integer}
+ media {Object}
+ audio {Boolean}
+ video {Boolean}
+ desktop {Boolean}
+ */
+
+ onRemoteDescription(data) {
+ if (this.active !== true) {
+ return;
+ }
+ if (data.to !== this.selfId) {
+ return;
+ }
+ this.log('onRemoteDescription', arguments);
+ const peerConnection = this.getPeerConnection(data.from);
+ if (data.type === 'offer') {
+ peerConnection.remoteMedia = data.media;
+ this.onRemoteOffer({
+ from: data.from,
+ ts: data.ts,
+ description: data.description
+ });
+ } else {
+ peerConnection.setRemoteDescription(new RTCSessionDescription(data.description));
+ }
+ }
+
+}
+
+const WebRTC = new class {
+ constructor() {
+ this.instancesByRoomId = {};
+ }
+
+ getInstanceByRoomId(roomId) {
+ const subscription = ChatSubscription.findOne({ rid: roomId });
+ if (!subscription) {
+ return;
+ }
+ let enabled = false;
+ switch (subscription.t) {
+ case 'd':
+ enabled = RocketChat.settings.get('WebRTC_Enable_Direct');
+ break;
+ case 'p':
+ enabled = RocketChat.settings.get('WebRTC_Enable_Private');
+ break;
+ case 'c':
+ enabled = RocketChat.settings.get('WebRTC_Enable_Channel');
+ }
+ if (enabled === false) {
+ return;
+ }
+ if (this.instancesByRoomId[roomId] == null) {
+ this.instancesByRoomId[roomId] = new WebRTCClass(Meteor.userId(), roomId);
+ }
+ return this.instancesByRoomId[roomId];
+ }
+};
+
+Meteor.startup(function() {
+ Tracker.autorun(function() {
+ if (Meteor.userId()) {
+ RocketChat.Notifications.onUser('webrtc', (type, data) => {
+ if (data.room == null) {
+ return;
+ }
+ const webrtc = WebRTC.getInstanceByRoomId(data.room);
+ webrtc.transport.onUserStream(type, data);
+ });
+ }
+ });
+});
+
+export {WebRTC};
diff --git a/packages/rocketchat-webrtc/adapter.js b/packages/rocketchat-webrtc/client/adapter.js
similarity index 100%
rename from packages/rocketchat-webrtc/adapter.js
rename to packages/rocketchat-webrtc/client/adapter.js
diff --git a/packages/rocketchat-webrtc/client/screenShare.js b/packages/rocketchat-webrtc/client/screenShare.js
new file mode 100644
index 000000000000..22f9ba2dc951
--- /dev/null
+++ b/packages/rocketchat-webrtc/client/screenShare.js
@@ -0,0 +1,29 @@
+/* globals ChromeScreenShare, fireGlobalEvent */
+this.ChromeScreenShare = {
+ screenCallback: undefined,
+ getSourceId(navigator, callback) {
+ if (callback == null) {
+ throw '"callback" parameter is mandatory.';
+ }
+ ChromeScreenShare.screenCallback = callback;
+ if (navigator === 'electron') {
+ return fireGlobalEvent('get-sourceId', '*');
+ }
+ return window.postMessage('get-sourceId', '*');
+ }
+};
+
+window.addEventListener('message', function(e) {
+ if (e.origin !== window.location.origin) {
+ return;
+ }
+ if (e.data === 'PermissionDeniedError') {
+ if (ChromeScreenShare.screenCallback != null) {
+ return ChromeScreenShare.screenCallback('PermissionDeniedError');
+ }
+ throw new Error('PermissionDeniedError');
+ }
+ if (e.data.sourceId != null) {
+ return typeof ChromeScreenShare.screenCallback === 'function' && ChromeScreenShare.screenCallback(e.data.sourceId);
+ }
+});
diff --git a/packages/rocketchat-webrtc/package.js b/packages/rocketchat-webrtc/package.js
index 53d56d1cde70..608f3d91768e 100644
--- a/packages/rocketchat-webrtc/package.js
+++ b/packages/rocketchat-webrtc/package.js
@@ -7,16 +7,15 @@ Package.describe({
Package.onUse(function(api) {
api.use('rocketchat:lib');
- api.use('coffeescript');
api.use('ecmascript');
api.use('templating', 'client');
+ api.mainModule('client/WebRTCClass.js', 'client');
+ api.addFiles('client/adapter.js', 'client');
+ // api.addFiles(');
+ api.addFiles('client/screenShare.js', 'client');
- api.addFiles('adapter.js', 'client');
- api.addFiles('WebRTCClass.coffee', 'client');
- api.addFiles('screenShare.coffee', 'client');
+ api.addFiles('server/settings.js', 'server');
- api.addFiles('server/settings.coffee', 'server');
-
- api.export('WebRTC');
+ api.export('WebRTC', 'client');
});
diff --git a/packages/rocketchat-webrtc/screenShare.coffee b/packages/rocketchat-webrtc/screenShare.coffee
deleted file mode 100644
index c157e7c045b3..000000000000
--- a/packages/rocketchat-webrtc/screenShare.coffee
+++ /dev/null
@@ -1,27 +0,0 @@
-@ChromeScreenShare =
- screenCallback: undefined
-
- getSourceId: (navigator, callback) ->
- if not callback? then throw '"callback" parameter is mandatory.'
-
- ChromeScreenShare.screenCallback = callback
-
- if navigator is 'electron'
- fireGlobalEvent('get-sourceId', '*')
- else
- window.postMessage('get-sourceId', '*')
-
-window.addEventListener 'message', (e) ->
- if e.origin isnt window.location.origin
- return
-
- # "cancel" button was clicked
- if e.data is 'PermissionDeniedError'
- if ChromeScreenShare.screenCallback?
- return ChromeScreenShare.screenCallback('PermissionDeniedError')
- else
- throw new Error('PermissionDeniedError')
-
- # extension shared temp sourceId
- if e.data.sourceId?
- ChromeScreenShare.screenCallback?(e.data.sourceId)
diff --git a/packages/rocketchat-webrtc/server/settings.coffee b/packages/rocketchat-webrtc/server/settings.coffee
deleted file mode 100644
index 84337ffbc49c..000000000000
--- a/packages/rocketchat-webrtc/server/settings.coffee
+++ /dev/null
@@ -1,5 +0,0 @@
-RocketChat.settings.addGroup 'WebRTC', ->
- @add 'WebRTC_Enable_Channel', false, { type: 'boolean', group: 'WebRTC', public: true}
- @add 'WebRTC_Enable_Private', true , { type: 'boolean', group: 'WebRTC', public: true}
- @add 'WebRTC_Enable_Direct' , true , { type: 'boolean', group: 'WebRTC', public: true}
- @add 'WebRTC_Servers', 'stun:stun.l.google.com:19302, stun:23.21.150.121, team%40rocket.chat:demo@turn:numb.viagenie.ca:3478', { type: 'string', group: 'WebRTC', public: true}
diff --git a/packages/rocketchat-webrtc/server/settings.js b/packages/rocketchat-webrtc/server/settings.js
new file mode 100644
index 000000000000..c32f3103acaf
--- /dev/null
+++ b/packages/rocketchat-webrtc/server/settings.js
@@ -0,0 +1,22 @@
+RocketChat.settings.addGroup('WebRTC', function() {
+ this.add('WebRTC_Enable_Channel', false, {
+ type: 'boolean',
+ group: 'WebRTC',
+ 'public': true
+ });
+ this.add('WebRTC_Enable_Private', true, {
+ type: 'boolean',
+ group: 'WebRTC',
+ 'public': true
+ });
+ this.add('WebRTC_Enable_Direct', true, {
+ type: 'boolean',
+ group: 'WebRTC',
+ 'public': true
+ });
+ return this.add('WebRTC_Servers', 'stun:stun.l.google.com:19302, stun:23.21.150.121, team%40rocket.chat:demo@turn:numb.viagenie.ca:3478', {
+ type: 'string',
+ group: 'WebRTC',
+ 'public': true
+ });
+});