Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
This commit is contained in:
Daniel Calviño Sánchez 2019-12-12 08:24:53 +01:00 коммит произвёл Joas Schilling
Родитель e122cc77fd
Коммит 966d10a038
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 7076EA9751AACDDA
4 изменённых файлов: 706 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,54 @@
/**
*
* @copyright Copyright (c) 2019, Daniel Calviño Sánchez (danxuliu@gmail.com)
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import CallParticipantModel from './CallParticipantModel'
export default function CallParticipantCollection() {
this.callParticipantModels = []
}
CallParticipantCollection.prototype = {
add: function(options) {
const callParticipantModel = new CallParticipantModel(options)
this.callParticipantModels.push(callParticipantModel)
return callParticipantModel
},
get: function(peerId) {
return this.callParticipantModels.find(function(callParticipantModel) {
return callParticipantModel.attributes.peerId === peerId
})
},
remove: function(peerId) {
const index = this.callParticipantModels.findIndex(function(callParticipantModel) {
return callParticipantModel.attributes.peerId === peerId
})
if (index !== -1) {
this.callParticipantModels.splice(index, 1)
}
},
}

Просмотреть файл

@ -0,0 +1,249 @@
/**
*
* @copyright Copyright (c) 2019, Daniel Calviño Sánchez (danxuliu@gmail.com)
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
export const ConnectionState = {
NEW: 'new',
CHECKING: 'checking',
CONNECTED: 'connected',
COMPLETED: 'completed',
DISCONNECTED: 'disconnected',
DISCONNECTED_LONG: 'disconnected-long', // Talk specific
FAILED: 'failed',
FAILED_NO_RESTART: 'failed-no-restart', // Talk specific
CLOSED: 'closed',
}
export default function CallParticipantModel(options) {
this.attributes = {
peerId: null,
// "undefined" is used for values not known yet; "null" or "false"
// are used for known but negative/empty values.
userId: undefined,
name: undefined,
connectionState: ConnectionState.NEW,
stream: null,
audioAvailable: undefined,
speaking: undefined,
videoAvailable: undefined,
screen: null,
}
this.set('peerId', options.peerId)
this._webRtc = options.webRtc
this._handlePeerStreamAddedBound = this._handlePeerStreamAdded.bind(this)
this._handlePeerStreamRemovedBound = this._handlePeerStreamRemoved.bind(this)
this._handleNickBound = this._handleNick.bind(this)
this._handleMuteBound = this._handleMute.bind(this)
this._handleUnmuteBound = this._handleUnmute.bind(this)
this._handleExtendedIceConnectionStateChangeBound = this._handleExtendedIceConnectionStateChange.bind(this)
this._handleChannelMessageBound = this._handleChannelMessage.bind(this)
this._webRtc.on('peerStreamAdded', this._handlePeerStreamAddedBound)
this._webRtc.on('peerStreamRemoved', this._handlePeerStreamRemovedBound)
this._webRtc.on('nick', this._handleNickBound)
this._webRtc.on('mute', this._handleMuteBound)
this._webRtc.on('unmute', this._handleUnmuteBound)
this._webRtc.on('channelMessage', this._handleChannelMessageBound)
}
CallParticipantModel.prototype = {
get: function(key) {
return this.attributes[key]
},
set: function(key, value) {
this.attributes[key] = value
},
_handlePeerStreamAdded: function(peer) {
if (this._peer === peer) {
this.set('stream', this._peer.stream || null)
// "peer.nick" is set only for users and when the MCU is not used.
if (this._peer.nick !== undefined) {
this.set('name', this._peer.nick)
}
} else if (this._screenPeer === peer) {
this.set('screen', this._screenPeer.stream || null)
}
},
_handlePeerStreamRemoved: function(peer) {
if (this._peer === peer) {
this.set('stream', null)
this.set('audioAvailable', undefined)
this.set('speaking', undefined)
this.set('videoAvailable', undefined)
} else if (this._screenPeer === peer) {
this.set('screen', null)
}
},
_handleNick: function(data) {
if (!this._peer || this._peer.id !== data.id) {
return
}
this.set('userId', data.userid || null)
this.set('name', data.name || null)
},
_handleMute: function(data) {
if (!this._peer || this._peer.id !== data.id) {
return
}
if (data.name === 'video') {
this.set('videoAvailable', false)
} else {
this.set('audioAvailable', false)
this.set('speaking', false)
}
},
_handleUnmute: function(data) {
if (!this._peer || this._peer.id !== data.id) {
return
}
if (data.name === 'video') {
this.set('videoAvailable', true)
} else {
this.set('audioAvailable', true)
}
},
_handleChannelMessage: function(peer, label, data) {
if (!this._peer || this._peer.id !== peer.id) {
return
}
if (label !== 'status') {
return
}
if (data.type === 'speaking') {
this.set('speaking', true)
} else if (data.type === 'stoppedSpeaking') {
this.set('speaking', false)
}
},
setPeer: function(peer) {
if (peer && this.get('peerId') !== peer.id) {
console.warn('Mismatch between stored peer ID and ID of given peer: ', this.get('peerId'), peer.id)
}
if (this._peer) {
this._peer.off('extendedIceConnectionStateChange', this._handleExtendedIceConnectionStateChangeBound)
}
this._peer = peer
// Special case when the participant has no streams.
if (!this._peer) {
this.set('connectionState', ConnectionState.COMPLETED)
this.set('audioAvailable', false)
this.set('speaking', false)
this.set('videoAvailable', false)
return
}
// Reset state that depends on the Peer object.
this._handleExtendedIceConnectionStateChange(this._peer.pc.iceConnectionState)
this._handlePeerStreamAdded(this._peer)
this._peer.on('extendedIceConnectionStateChange', this._handleExtendedIceConnectionStateChangeBound)
},
_handleExtendedIceConnectionStateChange: function(extendedIceConnectionState) {
// Ensure that the name is set, as when the MCU is not used it will
// not be set later for registered users without microphone nor
// camera.
const setNameForUserFromPeerNick = function() {
if (this._peer.nick !== undefined) {
this.set('name', this._peer.nick)
}
}.bind(this)
switch (extendedIceConnectionState) {
case 'new':
this.set('connectionState', ConnectionState.NEW)
this.set('audioAvailable', undefined)
this.set('speaking', undefined)
this.set('videoAvailable', undefined)
break
case 'checking':
this.set('connectionState', ConnectionState.CHECKING)
this.set('audioAvailable', undefined)
this.set('speaking', undefined)
this.set('videoAvailable', undefined)
break
case 'connected':
this.set('connectionState', ConnectionState.CONNECTED)
setNameForUserFromPeerNick()
break
case 'completed':
this.set('connectionState', ConnectionState.COMPLETED)
setNameForUserFromPeerNick()
break
case 'disconnected':
this.set('connectionState', ConnectionState.DISCONNECTED)
break
case 'disconnected-long':
this.set('connectionState', ConnectionState.DISCONNECTED_LONG)
break
case 'failed':
this.set('connectionState', ConnectionState.FAILED)
break
case 'failed-no-restart':
this.set('connectionState', ConnectionState.FAILED_NO_RESTART)
break
case 'closed':
this.set('connectionState', ConnectionState.CLOSED)
break
default:
console.error('Unexpected (extended) ICE connection state: ', extendedIceConnectionState)
}
},
setScreenPeer: function(screenPeer) {
if (this.get('peerId') !== screenPeer.id) {
console.warn('Mismatch between stored peer ID and ID of given screen peer: ', this.get('peerId'), screenPeer.id)
}
this._screenPeer = screenPeer
// Reset state that depends on the screen Peer object.
this._handlePeerStreamAdded(this._screenPeer)
},
setUserId: function(userId) {
this.set('userId', userId)
},
}

Просмотреть файл

@ -0,0 +1,54 @@
/**
*
* @copyright Copyright (c) 2019, Daniel Calviño Sánchez (danxuliu@gmail.com)
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
export default function LocalCallParticipantModel() {
this.attributes = {
peerId: null,
guestName: null,
}
}
LocalCallParticipantModel.prototype = {
set: function(key, value) {
this.attributes[key] = value
},
setWebRtc: function(webRtc) {
this._webRtc = webRtc
this.set('peerId', this._webRtc.connection.getSessionid())
this.set('guestName', null)
},
setGuestName: function(guestName) {
if (!this._webRtc) {
throw new Error('WebRtc not initialized yet')
}
this.set('guestName', guestName)
this._webRtc.sendDirectlyToAll('status', 'nickChanged', guestName)
},
}

Просмотреть файл

@ -0,0 +1,349 @@
/**
*
* @copyright Copyright (c) 2019, Daniel Calviño Sánchez (danxuliu@gmail.com)
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
export default function LocalMediaModel() {
this.attributes = {
localStream: null,
audioAvailable: false,
audioEnabled: false,
speaking: false,
speakingWhileMuted: false,
currentVolume: -100,
volumeThreshold: -100,
videoAvailable: false,
videoEnabled: false,
localScreen: null,
}
this._handlers = []
this._handleLocalStreamBound = this._handleLocalStream.bind(this)
this._handleLocalStreamRequestFailedBound = this._handleLocalStreamRequestFailed.bind(this)
this._handleLocalStreamStoppedBound = this._handleLocalStreamStopped.bind(this)
this._handleAudioOnBound = this._handleAudioOn.bind(this)
this._handleAudioOffBound = this._handleAudioOff.bind(this)
this._handleVolumeChangeBound = this._handleVolumeChange.bind(this)
this._handleSpeakingBound = this._handleSpeaking.bind(this)
this._handleStoppedSpeakingBound = this._handleStoppedSpeaking.bind(this)
this._handleSpeakingWhileMutedBound = this._handleSpeakingWhileMuted.bind(this)
this._handleStoppedSpeakingWhileMutedBound = this._handleStoppedSpeakingWhileMuted.bind(this)
this._handleVideoOnBound = this._handleVideoOn.bind(this)
this._handleVideoOffBound = this._handleVideoOff.bind(this)
this._handleLocalScreenBound = this._handleLocalScreen.bind(this)
this._handleLocalScreenStoppedBound = this._handleLocalScreenStopped.bind(this)
}
LocalMediaModel.prototype = {
get: function(key) {
return this.attributes[key]
},
set: function(key, value) {
this.attributes[key] = value
this._trigger('change:' + key, [value])
},
on: function(event, handler) {
if (!this._handlers.hasOwnProperty(event)) {
this._handlers[event] = [handler]
} else {
this._handlers[event].push(handler)
}
},
_trigger: function(event, args) {
let handlers = this._handlers[event]
if (!handlers) {
return
}
args.unshift(this)
handlers = handlers.slice(0)
for (let i = 0; i < handlers.length; i++) {
const handler = handlers[i]
handler.apply(handler, args)
}
},
getWebRtc: function() {
return this._webRtc
},
setWebRtc: function(webRtc) {
if (this._webRtc && this._webRtc.webrtc) {
this._webRtc.webrtc.off('localStream', this._handleLocalStreamBound)
this._webRtc.webrtc.off('localStreamRequestFailed', this._handleLocalStreamRequestFailedBound)
this._webRtc.webrtc.off('localStreamStopped', this._handleLocalStreamStoppedBound)
this._webRtc.webrtc.off('audioOn', this._handleAudioOnBound)
this._webRtc.webrtc.off('audioOff', this._handleAudioOffBound)
this._webRtc.webrtc.off('volumeChange', this._handleVolumeChangeBound)
this._webRtc.webrtc.off('speaking', this._handleSpeakingBound)
this._webRtc.webrtc.off('stoppedSpeaking', this._handleStoppedSpeakingBound)
this._webRtc.webrtc.off('speakingWhileMuted', this._handleSpeakingWhileMutedBound)
this._webRtc.webrtc.off('stoppedSpeakingWhileMuted', this._handleStoppedSpeakingWhileMutedBound)
this._webRtc.webrtc.off('videoOn', this._handleVideoOnBound)
this._webRtc.webrtc.off('videoOff', this._handleVideoOffBound)
this._webRtc.webrtc.off('localScreen', this._handleLocalScreenBound)
this._webRtc.webrtc.off('localScreenStopped', this._handleLocalScreenStoppedBound)
}
this._webRtc = webRtc
this.set('localStream', null)
this.set('audioAvailable', false)
this.set('audioEnabled', false)
this.set('speaking', false)
this.set('speakingWhileMuted', false)
this.set('currentVolume', -100)
this.set('volumeThreshold', -100)
this.set('videoAvailable', false)
this.set('videoEnabled', false)
this.set('localScreen', null)
this._webRtc.webrtc.on('localStream', this._handleLocalStreamBound)
this._webRtc.webrtc.on('localStreamRequestFailed', this._handleLocalStreamRequestFailedBound)
this._webRtc.webrtc.on('localStreamStopped', this._handleLocalStreamStoppedBound)
this._webRtc.webrtc.on('audioOn', this._handleAudioOnBound)
this._webRtc.webrtc.on('audioOff', this._handleAudioOffBound)
this._webRtc.webrtc.on('volumeChange', this._handleVolumeChangeBound)
this._webRtc.webrtc.on('speaking', this._handleSpeakingBound)
this._webRtc.webrtc.on('stoppedSpeaking', this._handleStoppedSpeakingBound)
this._webRtc.webrtc.on('speakingWhileMuted', this._handleSpeakingWhileMutedBound)
this._webRtc.webrtc.on('stoppedSpeakingWhileMuted', this._handleStoppedSpeakingWhileMutedBound)
this._webRtc.webrtc.on('videoOn', this._handleVideoOnBound)
this._webRtc.webrtc.on('videoOff', this._handleVideoOffBound)
this._webRtc.webrtc.on('localScreen', this._handleLocalScreenBound)
this._webRtc.webrtc.on('localScreenStopped', this._handleLocalScreenStoppedBound)
},
_handleLocalStream: function(configuration, localStream) {
// Although there could be several local streams active at the same
// time (if the local media is started again before stopping it
// first) the methods to control them ("mute", "unmute",
// "pauseVideo" and "resumeVideo") act on all the streams, it is not
// possible to control them individually. Also all local streams
// are transmitted when a Peer is created, but if another local
// stream is then added it will not be automatically added to the
// Peer. As it is not well supported and there is also no need to
// use several local streams for now it is assumed that only one
// local stream will be active at the same time.
this.set('localStream', localStream)
this._setInitialMediaState(configuration)
},
_handleLocalStreamRequestFailed: function() {
this.set('localStream', null)
this._setInitialMediaState({ audio: false, video: false })
},
_setInitialMediaState: function(configuration) {
if (configuration.audio !== false) {
this.set('audioAvailable', true)
if (this.get('audioEnabled')) {
this.enableAudio()
} else {
this.disableAudio()
}
} else {
this.set('audioEnabled', false)
this.set('audioAvailable', false)
}
if (configuration.video !== false) {
this.set('videoAvailable', true)
if (this.get('videoEnabled')) {
this.enableVideo()
} else {
this.disableVideo()
}
} else {
this.set('videoEnabled', false)
this.set('videoAvailable', false)
}
},
_handleLocalStreamStopped: function(localStream) {
if (this.get('localStream') !== localStream) {
return
}
this.set('localStream', null)
},
_handleAudioOn: function() {
if (!this.get('audioAvailable')) {
return
}
this.set('audioEnabled', true)
},
_handleAudioOff: function() {
if (!this.get('audioAvailable')) {
return
}
this.set('audioEnabled', false)
},
_handleVolumeChange: function(currentVolume, volumeThreshold) {
if (!this.get('audioAvailable')) {
return
}
this.set('currentVolume', currentVolume)
this.set('volumeThreshold', volumeThreshold)
},
_handleSpeaking: function() {
if (!this.get('audioAvailable')) {
return
}
this.set('speaking', true)
},
_handleStoppedSpeaking: function() {
if (!this.get('audioAvailable')) {
return
}
this.set('speaking', false)
},
_handleSpeakingWhileMuted: function() {
if (!this.get('audioAvailable')) {
return
}
this.set('speakingWhileMuted', true)
},
_handleStoppedSpeakingWhileMuted: function() {
if (!this.get('audioAvailable')) {
return
}
this.set('speakingWhileMuted', false)
},
_handleVideoOn: function() {
if (!this.get('videoAvailable')) {
return
}
this.set('videoEnabled', true)
},
_handleVideoOff: function() {
if (!this.get('videoAvailable')) {
return
}
this.set('videoEnabled', false)
},
_handleLocalScreen: function(screen) {
this.set('localScreen', screen)
},
_handleLocalScreenStopped: function() {
this.set('localScreen', null)
},
enableAudio: function() {
if (!this._webRtc) {
throw new Error('WebRtc not initialized yet')
}
if (!this.get('audioAvailable')) {
return
}
this._webRtc.unmute()
},
disableAudio: function() {
if (!this._webRtc) {
throw new Error('WebRtc not initialized yet')
}
if (!this.get('audioAvailable')) {
// Ensure that the audio will be disabled once available.
this.set('audioEnabled', false)
return
}
this._webRtc.mute()
},
enableVideo: function() {
if (!this._webRtc) {
throw new Error('WebRtc not initialized yet')
}
if (!this.get('videoAvailable')) {
return
}
this._webRtc.resumeVideo()
},
disableVideo: function() {
if (!this._webRtc) {
throw new Error('WebRtc not initialized yet')
}
if (!this.get('videoAvailable')) {
// Ensure that the video will be disabled once available.
this.set('videoEnabled', false)
return
}
this._webRtc.pauseVideo()
},
shareScreen: function(mode, callback) {
if (!this._webRtc) {
throw new Error('WebRtc not initialized yet')
}
this._webRtc.shareScreen(mode, callback)
},
stopSharingScreen: function() {
if (!this._webRtc) {
throw new Error('WebRtc not initialized yet')
}
this._webRtc.stopScreenShare()
},
}