The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Rocket.Chat/packages/rocketchat-webrtc/WebRTCClass.coffee

811 lines
20 KiB

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
if navigator.userAgent.toLocaleLowerCase().indexOf('chrome') > -1
@navigator = 'chrome'
else if navigator.userAgent.toLocaleLowerCase().indexOf('firefox') > -1
@navigator = 'firefox'
else if navigator.userAgent.toLocaleLowerCase().indexOf('safari') > -1
@navigator = 'safari'
@screenShareAvailable = @navigator in ['chrome', 'firefox']
@media =
video: true
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, peerConnections of @peerConnections
remoteConnections.push
id: id
media: peerConnections.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
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?
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'
chrome.webstore.install undefined, refresh, ->
window.open('https://chrome.google.com/webstore/detail/rocketchat-screen-share/nocfbnnmjnndkbipkabodnheejiegccf')
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 (id) =>
console.log 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
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: "<i class='icon-#{icon} alert-icon'></i>#{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 ->
RocketChat.Notifications.onUser 'webrtc', (type, data) =>
if not data.room? then return
webrtc = WebRTC.getInstanceByRoomId(data.room)
webrtc.transport.onUserStream type, data