2004 lines
43 KiB
JavaScript
2004 lines
43 KiB
JavaScript
import protooClient from 'protoo-client';
|
|
import * as mediasoupClient from 'mediasoup-client';
|
|
import Logger from './Logger';
|
|
import hark from 'hark';
|
|
import ScreenShare from './ScreenShare';
|
|
import { getProtooUrl } from './urlFactory';
|
|
import * as cookiesManager from './cookiesManager';
|
|
import * as requestActions from './redux/requestActions';
|
|
import * as stateActions from './redux/stateActions';
|
|
import {
|
|
turnServers,
|
|
requestTimeout,
|
|
transportOptions
|
|
} from '../config';
|
|
|
|
const logger = new Logger('RoomClient');
|
|
|
|
const ROOM_OPTIONS =
|
|
{
|
|
requestTimeout : requestTimeout,
|
|
transportOptions : transportOptions,
|
|
turnServers : turnServers
|
|
};
|
|
|
|
const VIDEO_CONSTRAINS =
|
|
{
|
|
width : { ideal: 1280 },
|
|
aspectRatio : 1.334
|
|
};
|
|
|
|
export default class RoomClient
|
|
{
|
|
constructor(
|
|
{ roomId, peerName, displayName, device, useSimulcast, produce, dispatch, getState })
|
|
{
|
|
logger.debug(
|
|
'constructor() [roomId:"%s", peerName:"%s", displayName:"%s", device:%s]',
|
|
roomId, peerName, displayName, device.flag);
|
|
|
|
const protooUrl = getProtooUrl(peerName, roomId);
|
|
const protooTransport = new protooClient.WebSocketTransport(protooUrl);
|
|
|
|
// window element to external login site
|
|
this._loginWindow;
|
|
|
|
// Closed flag.
|
|
this._closed = false;
|
|
|
|
// Whether we should produce.
|
|
this._produce = produce;
|
|
|
|
// Whether simulcast should be used.
|
|
this._useSimulcast = useSimulcast;
|
|
|
|
// Redux store dispatch function.
|
|
this._dispatch = dispatch;
|
|
|
|
// Redux store getState function.
|
|
this._getState = getState;
|
|
|
|
// This device
|
|
this._device = device;
|
|
|
|
// My peer name.
|
|
this._peerName = peerName;
|
|
|
|
// protoo-client Peer instance.
|
|
this._protoo = new protooClient.Peer(protooTransport);
|
|
|
|
// mediasoup-client Room instance.
|
|
this._room = new mediasoupClient.Room(ROOM_OPTIONS);
|
|
this._room.roomId = roomId;
|
|
|
|
// Transport for sending.
|
|
this._sendTransport = null;
|
|
|
|
// Transport for receiving.
|
|
this._recvTransport = null;
|
|
|
|
// Local mic mediasoup Producer.
|
|
this._micProducer = null;
|
|
|
|
// Local webcam mediasoup Producer.
|
|
this._webcamProducer = null;
|
|
|
|
// Map of webcam MediaDeviceInfos indexed by deviceId.
|
|
// @type {Map<String, MediaDeviceInfos>}
|
|
this._webcams = new Map();
|
|
|
|
this._audioDevices = new Map();
|
|
|
|
// Local Webcam. Object with:
|
|
// - {MediaDeviceInfo} [device]
|
|
// - {String} [resolution] - 'qvga' / 'vga' / 'hd'.
|
|
this._webcam = {
|
|
device : null,
|
|
resolution : 'hd'
|
|
};
|
|
|
|
this._audioDevice = {
|
|
device : null
|
|
};
|
|
|
|
this._screenSharing = ScreenShare.create();
|
|
|
|
this._screenSharingProducer = null;
|
|
|
|
this._join({ displayName, device });
|
|
}
|
|
|
|
close()
|
|
{
|
|
if (this._closed)
|
|
return;
|
|
|
|
this._closed = true;
|
|
|
|
logger.debug('close()');
|
|
|
|
// Leave the mediasoup Room.
|
|
this._room.leave();
|
|
|
|
// Close protoo Peer (wait a bit so mediasoup-client can send
|
|
// the 'leaveRoom' notification).
|
|
setTimeout(() => this._protoo.close(), 250);
|
|
|
|
this._dispatch(stateActions.setRoomState('closed'));
|
|
}
|
|
|
|
login()
|
|
{
|
|
const url = `/login?roomId=${this._room.roomId}&peerName=${this._peerName}`;
|
|
|
|
this._loginWindow = window.open(url, 'loginWindow');
|
|
}
|
|
|
|
logout()
|
|
{
|
|
window.location = '/logout';
|
|
}
|
|
|
|
closeLoginWindow()
|
|
{
|
|
this._loginWindow.close();
|
|
}
|
|
|
|
changeDisplayName(displayName)
|
|
{
|
|
logger.debug('changeDisplayName() [displayName:"%s"]', displayName);
|
|
|
|
// Store in cookie.
|
|
cookiesManager.setUser({ displayName });
|
|
|
|
return this._protoo.send('change-display-name', { displayName })
|
|
.then(() =>
|
|
{
|
|
this._dispatch(
|
|
stateActions.setDisplayName(displayName));
|
|
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
text : 'Display name changed'
|
|
}));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('changeDisplayName() | failed: %o', error);
|
|
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `Could not change display name: ${error}`
|
|
}));
|
|
|
|
// We need to refresh the component for it to render the previous
|
|
// displayName again.
|
|
this._dispatch(stateActions.setDisplayName());
|
|
});
|
|
}
|
|
|
|
changeProfilePicture(picture)
|
|
{
|
|
logger.debug('changeProfilePicture() [picture: "%s"]', picture);
|
|
|
|
this._protoo.send('change-profile-picture', { picture }).catch((error) =>
|
|
{
|
|
logger.error('shareProfilePicure() | failed: %o', error);
|
|
});
|
|
}
|
|
|
|
sendChatMessage(chatMessage)
|
|
{
|
|
logger.debug('sendChatMessage() [chatMessage:"%s"]', chatMessage);
|
|
|
|
return this._protoo.send('chat-message', { chatMessage })
|
|
.catch((error) =>
|
|
{
|
|
logger.error('sendChatMessage() | failed: %o', error);
|
|
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `Could not send chat: ${error}`
|
|
}));
|
|
});
|
|
}
|
|
|
|
sendFile(file)
|
|
{
|
|
logger.debug('sendFile() [file: %o]', file);
|
|
|
|
return this._protoo.send('send-file', { file })
|
|
.catch((error) =>
|
|
{
|
|
logger.error('sendFile() | failed: %o', error);
|
|
|
|
this._dispatch(requestActions.notify({
|
|
typ: 'error',
|
|
text: 'An error occurred while sharing a file'
|
|
}));
|
|
});
|
|
}
|
|
|
|
getChatHistory()
|
|
{
|
|
logger.debug('getChatHistory()');
|
|
|
|
return this._protoo.send('chat-history', {})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('getChatHistory() | failed: %o', error);
|
|
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `Could not get chat history: ${error}`
|
|
}));
|
|
});
|
|
}
|
|
|
|
muteMic()
|
|
{
|
|
logger.debug('muteMic()');
|
|
|
|
this._micProducer.pause();
|
|
}
|
|
|
|
unmuteMic()
|
|
{
|
|
logger.debug('unmuteMic()');
|
|
|
|
this._micProducer.resume();
|
|
}
|
|
|
|
installExtension()
|
|
{
|
|
logger.debug('installExtension()');
|
|
|
|
return new Promise((resolve, reject) =>
|
|
{
|
|
window.addEventListener('message', _onExtensionMessage, false);
|
|
// eslint-disable-next-line no-undef
|
|
chrome.webstore.install(null, _successfulInstall, _failedInstall);
|
|
function _onExtensionMessage({ data })
|
|
{
|
|
if (data.type === 'ScreenShareInjected')
|
|
{
|
|
logger.debug('installExtension() | installation succeeded');
|
|
|
|
return resolve();
|
|
}
|
|
}
|
|
|
|
function _failedInstall(reason)
|
|
{
|
|
window.removeEventListener('message', _onExtensionMessage);
|
|
|
|
return reject(
|
|
new Error('Failed to install extension: %s', reason));
|
|
}
|
|
|
|
function _successfulInstall()
|
|
{
|
|
logger.debug('installExtension() | installation accepted');
|
|
}
|
|
})
|
|
.then(() =>
|
|
{
|
|
// This should be handled better
|
|
this._dispatch(stateActions.setScreenCapabilities(
|
|
{
|
|
canShareScreen : this._room.canSend('video'),
|
|
needExtension : false
|
|
}));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('installExtension() | failed: %o', error);
|
|
});
|
|
}
|
|
|
|
enableScreenSharing()
|
|
{
|
|
logger.debug('enableScreenSharing()');
|
|
|
|
this._dispatch(
|
|
stateActions.setScreenShareInProgress(true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
return this._setScreenShareProducer();
|
|
})
|
|
.then(() =>
|
|
{
|
|
this._dispatch(
|
|
stateActions.setScreenShareInProgress(false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('enableScreenSharing() | failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setScreenShareInProgress(false));
|
|
});
|
|
}
|
|
|
|
enableWebcam()
|
|
{
|
|
logger.debug('enableWebcam()');
|
|
|
|
// Store in cookie.
|
|
cookiesManager.setDevices({ webcamEnabled: true });
|
|
|
|
this._dispatch(
|
|
stateActions.setWebcamInProgress(true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
return this._updateWebcams();
|
|
})
|
|
.then(() =>
|
|
{
|
|
return this._setWebcamProducer();
|
|
})
|
|
.then(() =>
|
|
{
|
|
this._dispatch(
|
|
stateActions.setWebcamInProgress(false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('enableWebcam() | failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setWebcamInProgress(false));
|
|
});
|
|
}
|
|
|
|
disableScreenSharing()
|
|
{
|
|
logger.debug('disableScreenSharing()');
|
|
|
|
this._dispatch(
|
|
stateActions.setScreenShareInProgress(true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
this._screenSharingProducer.close();
|
|
|
|
this._dispatch(
|
|
stateActions.setScreenShareInProgress(false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('disableScreenSharing() | failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setScreenShareInProgress(false));
|
|
});
|
|
}
|
|
|
|
disableWebcam()
|
|
{
|
|
logger.debug('disableWebcam()');
|
|
|
|
// Store in cookie.
|
|
cookiesManager.setDevices({ webcamEnabled: false });
|
|
|
|
this._dispatch(
|
|
stateActions.setWebcamInProgress(true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
this._webcamProducer.close();
|
|
|
|
this._dispatch(
|
|
stateActions.setWebcamInProgress(false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('disableWebcam() | failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setWebcamInProgress(false));
|
|
});
|
|
}
|
|
|
|
changeAudioDevice(deviceId)
|
|
{
|
|
logger.debug('changeAudioDevice() [deviceId: %s]', deviceId);
|
|
|
|
this._dispatch(
|
|
stateActions.setAudioInProgress(true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
this._audioDevice.device = this._audioDevices.get(deviceId);
|
|
|
|
logger.debug(
|
|
'changeAudioDevice() | new selected webcam [device:%o]',
|
|
this._audioDevice.device);
|
|
})
|
|
.then(() =>
|
|
{
|
|
const { device } = this._audioDevice;
|
|
|
|
if (!device)
|
|
throw new Error('no audio devices');
|
|
|
|
logger.debug('changeAudioDevice() | calling getUserMedia()');
|
|
|
|
return navigator.mediaDevices.getUserMedia(
|
|
{
|
|
audio :
|
|
{
|
|
deviceId : { exact: device.deviceId }
|
|
}
|
|
});
|
|
})
|
|
.then((stream) =>
|
|
{
|
|
const track = stream.getAudioTracks()[0];
|
|
|
|
return this._micProducer.replaceTrack(track)
|
|
.then((newTrack) =>
|
|
{
|
|
track.stop();
|
|
|
|
return newTrack;
|
|
});
|
|
})
|
|
.then((newTrack) =>
|
|
{
|
|
this._dispatch(
|
|
stateActions.setProducerTrack(this._micProducer.id, newTrack));
|
|
|
|
return this._updateAudioDevices();
|
|
})
|
|
.then(() =>
|
|
{
|
|
this._dispatch(
|
|
stateActions.setAudioInProgress(false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('changeAudioDevice() failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setAudioInProgress(false));
|
|
});
|
|
}
|
|
|
|
changeWebcam(deviceId)
|
|
{
|
|
logger.debug('changeWebcam() [deviceId: %s]', deviceId);
|
|
|
|
this._dispatch(
|
|
stateActions.setWebcamInProgress(true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
this._webcam.device = this._webcams.get(deviceId);
|
|
|
|
logger.debug(
|
|
'changeWebcam() | new selected webcam [device:%o]',
|
|
this._webcam.device);
|
|
|
|
// Reset video resolution to HD.
|
|
this._webcam.resolution = 'hd';
|
|
})
|
|
.then(() =>
|
|
{
|
|
const { device } = this._webcam;
|
|
|
|
if (!device)
|
|
throw new Error('no webcam devices');
|
|
|
|
logger.debug('changeWebcam() | calling getUserMedia()');
|
|
|
|
return navigator.mediaDevices.getUserMedia(
|
|
{
|
|
video :
|
|
{
|
|
deviceId : { exact: device.deviceId },
|
|
...VIDEO_CONSTRAINS
|
|
}
|
|
});
|
|
})
|
|
.then((stream) =>
|
|
{
|
|
const track = stream.getVideoTracks()[0];
|
|
|
|
return this._webcamProducer.replaceTrack(track)
|
|
.then((newTrack) =>
|
|
{
|
|
track.stop();
|
|
|
|
return newTrack;
|
|
});
|
|
})
|
|
.then((newTrack) =>
|
|
{
|
|
this._dispatch(
|
|
stateActions.setProducerTrack(this._webcamProducer.id, newTrack));
|
|
|
|
return this._updateWebcams();
|
|
})
|
|
.then(() =>
|
|
{
|
|
this._dispatch(
|
|
stateActions.setWebcamInProgress(false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('changeWebcam() failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setWebcamInProgress(false));
|
|
});
|
|
}
|
|
|
|
changeWebcamResolution()
|
|
{
|
|
logger.debug('changeWebcamResolution()');
|
|
|
|
let oldResolution;
|
|
let newResolution;
|
|
|
|
this._dispatch(
|
|
stateActions.setWebcamInProgress(true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
oldResolution = this._webcam.resolution;
|
|
|
|
switch (oldResolution)
|
|
{
|
|
case 'qvga':
|
|
newResolution = 'vga';
|
|
break;
|
|
case 'vga':
|
|
newResolution = 'hd';
|
|
break;
|
|
case 'hd':
|
|
newResolution = 'qvga';
|
|
break;
|
|
}
|
|
|
|
this._webcam.resolution = newResolution;
|
|
})
|
|
.then(() =>
|
|
{
|
|
const { device } = this._webcam;
|
|
|
|
logger.debug('changeWebcamResolution() | calling getUserMedia()');
|
|
|
|
return navigator.mediaDevices.getUserMedia(
|
|
{
|
|
video :
|
|
{
|
|
deviceId : { exact: device.deviceId },
|
|
...VIDEO_CONSTRAINS
|
|
}
|
|
});
|
|
})
|
|
.then((stream) =>
|
|
{
|
|
const track = stream.getVideoTracks()[0];
|
|
|
|
return this._webcamProducer.replaceTrack(track)
|
|
.then((newTrack) =>
|
|
{
|
|
track.stop();
|
|
|
|
return newTrack;
|
|
});
|
|
})
|
|
.then((newTrack) =>
|
|
{
|
|
this._dispatch(
|
|
stateActions.setProducerTrack(this._webcamProducer.id, newTrack));
|
|
|
|
this._dispatch(
|
|
stateActions.setWebcamInProgress(false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('changeWebcamResolution() failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setWebcamInProgress(false));
|
|
|
|
this._webcam.resolution = oldResolution;
|
|
});
|
|
}
|
|
|
|
mutePeerAudio(peerName)
|
|
{
|
|
logger.debug('mutePeerAudio() [peerName:"%s"]', peerName);
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerAudioInProgress(peerName, true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
for (const peer of this._room.peers)
|
|
{
|
|
if (peer.name === peerName)
|
|
{
|
|
for (const consumer of peer.consumers)
|
|
{
|
|
if (consumer.appData.source !== 'mic')
|
|
continue;
|
|
|
|
consumer.pause('mute-audio');
|
|
}
|
|
}
|
|
}
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerAudioInProgress(peerName, false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('mutePeerAudio() failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerAudioInProgress(peerName, false));
|
|
});
|
|
}
|
|
|
|
unmutePeerAudio(peerName)
|
|
{
|
|
logger.debug('unmutePeerAudio() [peerName:"%s"]', peerName);
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerAudioInProgress(peerName, true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
for (const peer of this._room.peers)
|
|
{
|
|
if (peer.name === peerName)
|
|
{
|
|
for (const consumer of peer.consumers)
|
|
{
|
|
if (consumer.appData.source !== 'mic' || !consumer.supported)
|
|
continue;
|
|
|
|
consumer.resume();
|
|
}
|
|
}
|
|
}
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerAudioInProgress(peerName, false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('unmutePeerAudio() failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerAudioInProgress(peerName, false));
|
|
});
|
|
}
|
|
|
|
pausePeerVideo(peerName)
|
|
{
|
|
logger.debug('pausePeerVideo() [peerName:"%s"]', peerName);
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerVideoInProgress(peerName, true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
for (const peer of this._room.peers)
|
|
{
|
|
if (peer.name === peerName)
|
|
{
|
|
for (const consumer of peer.consumers)
|
|
{
|
|
if (consumer.appData.source !== 'webcam')
|
|
continue;
|
|
|
|
consumer.pause('pause-video');
|
|
}
|
|
}
|
|
}
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerVideoInProgress(peerName, false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('pausePeerVideo() failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerVideoInProgress(peerName, false));
|
|
});
|
|
}
|
|
|
|
resumePeerVideo(peerName)
|
|
{
|
|
logger.debug('resumePeerVideo() [peerName:"%s"]', peerName);
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerVideoInProgress(peerName, true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
for (const peer of this._room.peers)
|
|
{
|
|
if (peer.name === peerName)
|
|
{
|
|
for (const consumer of peer.consumers)
|
|
{
|
|
if (consumer.appData.source !== 'webcam' || !consumer.supported)
|
|
continue;
|
|
|
|
consumer.resume();
|
|
}
|
|
}
|
|
}
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerVideoInProgress(peerName, false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('resumePeerVideo() failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerVideoInProgress(peerName, false));
|
|
});
|
|
}
|
|
|
|
pausePeerScreen(peerName)
|
|
{
|
|
logger.debug('pausePeerScreen() [peerName:"%s"]', peerName);
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerScreenInProgress(peerName, true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
for (const peer of this._room.peers)
|
|
{
|
|
if (peer.name === peerName)
|
|
{
|
|
for (const consumer of peer.consumers)
|
|
{
|
|
if (consumer.appData.source !== 'screen')
|
|
continue;
|
|
|
|
consumer.pause('pause-screen');
|
|
}
|
|
}
|
|
}
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerScreenInProgress(peerName, false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('pausePeerScreen() failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerScreenInProgress(peerName, false));
|
|
});
|
|
}
|
|
|
|
resumePeerScreen(peerName)
|
|
{
|
|
logger.debug('resumePeerScreen() [peerName:"%s"]', peerName);
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerScreenInProgress(peerName, true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
for (const peer of this._room.peers)
|
|
{
|
|
if (peer.name === peerName)
|
|
{
|
|
for (const consumer of peer.consumers)
|
|
{
|
|
if (consumer.appData.source !== 'screen' || !consumer.supported)
|
|
continue;
|
|
|
|
consumer.resume();
|
|
}
|
|
}
|
|
}
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerScreenInProgress(peerName, false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('resumePeerScreen() failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerScreenInProgress(peerName, false));
|
|
});
|
|
}
|
|
|
|
enableAudioOnly()
|
|
{
|
|
logger.debug('enableAudioOnly()');
|
|
|
|
this._dispatch(
|
|
stateActions.setAudioOnlyInProgress(true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
if (this._webcamProducer)
|
|
this._webcamProducer.close();
|
|
|
|
for (const peer of this._room.peers)
|
|
{
|
|
for (const consumer of peer.consumers)
|
|
{
|
|
if (consumer.kind !== 'video')
|
|
continue;
|
|
|
|
consumer.pause('audio-only-mode');
|
|
}
|
|
}
|
|
|
|
this._dispatch(
|
|
stateActions.setAudioOnlyState(true));
|
|
|
|
this._dispatch(
|
|
stateActions.setAudioOnlyInProgress(false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('enableAudioOnly() failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setAudioOnlyInProgress(false));
|
|
});
|
|
}
|
|
|
|
disableAudioOnly()
|
|
{
|
|
logger.debug('disableAudioOnly()');
|
|
|
|
this._dispatch(
|
|
stateActions.setAudioOnlyInProgress(true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
if (!this._webcamProducer && this._room.canSend('video'))
|
|
return this.enableWebcam();
|
|
})
|
|
.then(() =>
|
|
{
|
|
for (const peer of this._room.peers)
|
|
{
|
|
for (const consumer of peer.consumers)
|
|
{
|
|
if (consumer.kind !== 'video' || !consumer.supported)
|
|
continue;
|
|
|
|
consumer.resume();
|
|
}
|
|
}
|
|
|
|
this._dispatch(
|
|
stateActions.setAudioOnlyState(false));
|
|
|
|
this._dispatch(
|
|
stateActions.setAudioOnlyInProgress(false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('disableAudioOnly() failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setAudioOnlyInProgress(false));
|
|
});
|
|
}
|
|
|
|
sendRaiseHandState(state)
|
|
{
|
|
logger.debug('sendRaiseHandState: ', state);
|
|
|
|
this._dispatch(
|
|
stateActions.setMyRaiseHandStateInProgress(true));
|
|
|
|
return this._protoo.send('raisehand-message', { raiseHandState: state })
|
|
.then(() =>
|
|
{
|
|
this._dispatch(
|
|
stateActions.setMyRaiseHandState(state));
|
|
this._dispatch(
|
|
stateActions.setMyRaiseHandStateInProgress(false));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('sendRaiseHandState() | failed: %o', error);
|
|
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `Could not change raise hand state: ${error}`
|
|
}));
|
|
|
|
// We need to refresh the component for it to render changed state
|
|
this._dispatch(stateActions.setMyRaiseHandState(!state));
|
|
this._dispatch(
|
|
stateActions.setMyRaiseHandStateInProgress(false));
|
|
});
|
|
}
|
|
|
|
restartIce()
|
|
{
|
|
logger.debug('restartIce()');
|
|
|
|
this._dispatch(
|
|
stateActions.setRestartIceInProgress(true));
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
this._room.restartIce();
|
|
|
|
// Make it artificially longer.
|
|
setTimeout(() =>
|
|
{
|
|
this._dispatch(
|
|
stateActions.setRestartIceInProgress(false));
|
|
}, 500);
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('restartIce() failed: %o', error);
|
|
|
|
this._dispatch(
|
|
stateActions.setRestartIceInProgress(false));
|
|
});
|
|
}
|
|
|
|
_join({ displayName, device })
|
|
{
|
|
this._dispatch(stateActions.setRoomState('connecting'));
|
|
|
|
this._protoo.on('open', () =>
|
|
{
|
|
logger.debug('protoo Peer "open" event');
|
|
|
|
this._joinRoom({ displayName, device });
|
|
});
|
|
|
|
this._protoo.on('disconnected', () =>
|
|
{
|
|
logger.warn('protoo Peer "disconnected" event');
|
|
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : 'WebSocket disconnected'
|
|
}));
|
|
|
|
// Leave Room.
|
|
try { this._room.remoteClose({ cause: 'protoo disconnected' }); }
|
|
catch (error) {}
|
|
|
|
this._dispatch(stateActions.setRoomState('connecting'));
|
|
});
|
|
|
|
this._protoo.on('close', () =>
|
|
{
|
|
if (this._closed)
|
|
return;
|
|
|
|
logger.warn('protoo Peer "close" event');
|
|
|
|
this.close();
|
|
});
|
|
|
|
this._protoo.on('request', (request, accept, reject) =>
|
|
{
|
|
logger.debug(
|
|
'_handleProtooRequest() [method:%s, data:%o]',
|
|
request.method, request.data);
|
|
|
|
switch (request.method)
|
|
{
|
|
case 'mediasoup-notification':
|
|
{
|
|
accept();
|
|
|
|
const notification = request.data;
|
|
|
|
this._room.receiveNotification(notification);
|
|
|
|
break;
|
|
}
|
|
|
|
case 'active-speaker':
|
|
{
|
|
accept();
|
|
|
|
const { peerName } = request.data;
|
|
|
|
this._dispatch(
|
|
stateActions.setRoomActiveSpeaker(peerName));
|
|
|
|
break;
|
|
}
|
|
|
|
case 'display-name-changed':
|
|
{
|
|
accept();
|
|
|
|
// eslint-disable-next-line no-shadow
|
|
const { peerName, displayName, oldDisplayName } = request.data;
|
|
|
|
// NOTE: Hack, we shouldn't do this, but this is just a demo.
|
|
const peer = this._room.getPeerByName(peerName);
|
|
|
|
if (!peer)
|
|
{
|
|
logger.error('peer not found');
|
|
|
|
break;
|
|
}
|
|
|
|
peer.appData.displayName = displayName;
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerDisplayName(displayName, peerName));
|
|
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
text : `${oldDisplayName} is now ${displayName}`
|
|
}));
|
|
|
|
break;
|
|
}
|
|
|
|
case 'profile-picture-changed':
|
|
{
|
|
accept();
|
|
|
|
const { peerName, picture } = request.data;
|
|
|
|
this._dispatch(stateActions.setPeerPicture(peerName, picture));
|
|
|
|
break;
|
|
}
|
|
|
|
// This means: server wants to change MY user information
|
|
case 'auth':
|
|
{
|
|
logger.debug('got auth event from server', request.data);
|
|
accept();
|
|
|
|
this.changeDisplayName(request.data.name);
|
|
|
|
this.changeProfilePicture(request.data.picture);
|
|
this._dispatch(stateActions.setPicture(request.data.picture));
|
|
this._dispatch(stateActions.loggedIn());
|
|
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
text : `Authenticated successfully: ${request.data}`
|
|
}
|
|
));
|
|
|
|
this.closeLoginWindow();
|
|
|
|
break;
|
|
}
|
|
|
|
case 'raisehand-message':
|
|
{
|
|
accept();
|
|
const { peerName, raiseHandState } = request.data;
|
|
|
|
logger.debug('Got raiseHandState from "%s"', peerName);
|
|
|
|
this._dispatch(
|
|
stateActions.setPeerRaiseHandState(peerName, raiseHandState));
|
|
break;
|
|
}
|
|
|
|
case 'chat-message-receive':
|
|
{
|
|
accept();
|
|
|
|
const { peerName, chatMessage } = request.data;
|
|
|
|
logger.debug('Got chat from "%s"', peerName);
|
|
|
|
this._dispatch(
|
|
stateActions.addResponseMessage({ ...chatMessage, peerName }));
|
|
|
|
const toolAreaState = this._getState().toolarea;
|
|
|
|
// Notify about the new file, unless the chat is open.
|
|
if (chatMessage.file && !(toolAreaState.toolAreaOpen && toolAreaState.currentToolTab === 'chat'))
|
|
{
|
|
this._dispatch(
|
|
requestActions.notify({ text: `${chatMessage.name} shared a file` })
|
|
);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case 'chat-history-receive':
|
|
{
|
|
accept();
|
|
|
|
const { chatHistory } = request.data;
|
|
|
|
if (chatHistory.length > 0)
|
|
{
|
|
logger.debug('Got chat history');
|
|
this._dispatch(
|
|
stateActions.addChatHistory(chatHistory));
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case 'file-receive':
|
|
{
|
|
accept();
|
|
|
|
const payload = request.data.file;
|
|
|
|
this._dispatch(stateActions.addFile(payload));
|
|
|
|
this._dispatch(requestActions.notify({
|
|
text: `${payload.name} shared a file`
|
|
}));
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
logger.error('unknown protoo method "%s"', request.method);
|
|
|
|
reject(404, 'unknown method');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
_joinRoom({ displayName, device })
|
|
{
|
|
logger.debug('_joinRoom()');
|
|
|
|
// NOTE: We allow rejoining (room.join()) the same mediasoup Room when Protoo
|
|
// WebSocket re-connects, so we must clean existing event listeners. Otherwise
|
|
// they will be called twice after the reconnection.
|
|
this._room.removeAllListeners();
|
|
|
|
this._room.on('close', (originator, appData) =>
|
|
{
|
|
if (originator === 'remote')
|
|
{
|
|
logger.warn('mediasoup Peer/Room remotely closed [appData:%o]', appData);
|
|
|
|
this._dispatch(stateActions.setRoomState('closed'));
|
|
|
|
return;
|
|
}
|
|
});
|
|
|
|
this._room.on('request', (request, callback, errback) =>
|
|
{
|
|
logger.debug(
|
|
'sending mediasoup request [method:%s]:%o', request.method, request);
|
|
|
|
this._protoo.send('mediasoup-request', request)
|
|
.then(callback)
|
|
.catch(errback);
|
|
});
|
|
|
|
this._room.on('notify', (notification) =>
|
|
{
|
|
logger.debug(
|
|
'sending mediasoup notification [method:%s]:%o',
|
|
notification.method, notification);
|
|
|
|
this._protoo.send('mediasoup-notification', notification)
|
|
.catch((error) =>
|
|
{
|
|
logger.warn('could not send mediasoup notification:%o', error);
|
|
});
|
|
});
|
|
|
|
this._room.on('newpeer', (peer) =>
|
|
{
|
|
logger.debug(
|
|
'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer);
|
|
|
|
this._handlePeer(peer);
|
|
});
|
|
|
|
this._room.join(this._peerName, { displayName, device })
|
|
.then(() =>
|
|
{
|
|
// Create Transport for sending.
|
|
this._sendTransport =
|
|
this._room.createTransport('send', { media: 'SEND_MIC_WEBCAM' });
|
|
|
|
this._sendTransport.on('close', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'Transport "close" event [originator:%s]', originator);
|
|
});
|
|
|
|
// Create Transport for receiving.
|
|
this._recvTransport =
|
|
this._room.createTransport('recv', { media: 'RECV' });
|
|
|
|
this._recvTransport.on('close', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'receiving Transport "close" event [originator:%s]', originator);
|
|
});
|
|
})
|
|
.then(() =>
|
|
{
|
|
// Set our media capabilities.
|
|
this._dispatch(stateActions.setMediaCapabilities(
|
|
{
|
|
canSendMic : this._room.canSend('audio'),
|
|
canSendWebcam : this._room.canSend('video')
|
|
}));
|
|
this._dispatch(stateActions.setScreenCapabilities(
|
|
{
|
|
canShareScreen : this._room.canSend('video') &&
|
|
this._screenSharing.isScreenShareAvailable(),
|
|
needExtension : this._screenSharing.needExtension()
|
|
}));
|
|
})
|
|
.then(() =>
|
|
{
|
|
// Don't produce if explicitely requested to not to do it.
|
|
if (!this._produce)
|
|
return;
|
|
|
|
// NOTE: Don't depend on this Promise to continue (so we don't do return).
|
|
Promise.resolve()
|
|
// Add our mic.
|
|
.then(() =>
|
|
{
|
|
if (!this._room.canSend('audio'))
|
|
return;
|
|
|
|
this._setMicProducer()
|
|
.catch(() => {});
|
|
})
|
|
// Add our webcam (unless the cookie says no).
|
|
.then(() =>
|
|
{
|
|
if (!this._room.canSend('video'))
|
|
return;
|
|
|
|
const devicesCookie = cookiesManager.getDevices();
|
|
|
|
if (!devicesCookie || devicesCookie.webcamEnabled)
|
|
this.enableWebcam();
|
|
});
|
|
})
|
|
.then(() =>
|
|
{
|
|
this._dispatch(stateActions.setRoomState('connected'));
|
|
|
|
// Clean all the existing notifcations.
|
|
this._dispatch(stateActions.removeAllNotifications());
|
|
|
|
this.getChatHistory();
|
|
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
text : 'You are in the room',
|
|
timeout : 5000
|
|
}));
|
|
|
|
const peers = this._room.peers;
|
|
|
|
for (const peer of peers)
|
|
{
|
|
this._handlePeer(peer, { notify: false });
|
|
}
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('_joinRoom() failed:%o', error);
|
|
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `Could not join the room: ${error.toString()}`
|
|
}));
|
|
|
|
this.close();
|
|
});
|
|
}
|
|
|
|
_setMicProducer()
|
|
{
|
|
if (!this._room.canSend('audio'))
|
|
{
|
|
return Promise.reject(
|
|
new Error('cannot send audio'));
|
|
}
|
|
|
|
if (this._micProducer)
|
|
{
|
|
return Promise.reject(
|
|
new Error('mic Producer already exists'));
|
|
}
|
|
|
|
let producer;
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
logger.debug('_setMicProducer() | calling _updateAudioDevices()');
|
|
|
|
return this._updateAudioDevices();
|
|
})
|
|
.then(() =>
|
|
{
|
|
logger.debug('_setMicProducer() | calling getUserMedia()');
|
|
|
|
return navigator.mediaDevices.getUserMedia({ audio: true });
|
|
})
|
|
.then((stream) =>
|
|
{
|
|
const track = stream.getAudioTracks()[0];
|
|
|
|
producer = this._room.createProducer(track, null, { source: 'mic' });
|
|
|
|
// No need to keep original track.
|
|
track.stop();
|
|
|
|
// Send it.
|
|
return producer.send(this._sendTransport);
|
|
})
|
|
.then(() =>
|
|
{
|
|
this._micProducer = producer;
|
|
|
|
this._dispatch(stateActions.addProducer(
|
|
{
|
|
id : producer.id,
|
|
source : 'mic',
|
|
locallyPaused : producer.locallyPaused,
|
|
remotelyPaused : producer.remotelyPaused,
|
|
track : producer.track,
|
|
codec : producer.rtpParameters.codecs[0].name
|
|
}));
|
|
|
|
producer.on('close', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'mic Producer "close" event [originator:%s]', originator);
|
|
|
|
this._micProducer = null;
|
|
this._dispatch(stateActions.removeProducer(producer.id));
|
|
});
|
|
|
|
producer.on('pause', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'mic Producer "pause" event [originator:%s]', originator);
|
|
|
|
this._dispatch(stateActions.setProducerPaused(producer.id, originator));
|
|
});
|
|
|
|
producer.on('resume', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'mic Producer "resume" event [originator:%s]', originator);
|
|
|
|
this._dispatch(stateActions.setProducerResumed(producer.id, originator));
|
|
});
|
|
|
|
producer.on('handled', () =>
|
|
{
|
|
logger.debug('mic Producer "handled" event');
|
|
});
|
|
|
|
producer.on('unhandled', () =>
|
|
{
|
|
logger.debug('mic Producer "unhandled" event');
|
|
});
|
|
})
|
|
.then(() =>
|
|
{
|
|
const stream = new MediaStream;
|
|
|
|
logger.debug('_setMicProducer() succeeded');
|
|
stream.addTrack(producer.track);
|
|
if (!stream.getAudioTracks()[0])
|
|
throw new Error('_setMicProducer(): given stream has no audio track');
|
|
producer.hark = hark(stream, { play: false });
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
producer.hark.on('volume_change', (dBs, threshold) =>
|
|
{
|
|
// The exact formula to convert from dBs (-100..0) to linear (0..1) is:
|
|
// Math.pow(10, dBs / 20)
|
|
// However it does not produce a visually useful output, so let exagerate
|
|
// it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to
|
|
// minimize component renderings.
|
|
let volume = Math.round(Math.pow(10, dBs / 85) * 10);
|
|
|
|
if (volume === 1)
|
|
volume = 0;
|
|
|
|
if (volume !== producer.volume)
|
|
{
|
|
producer.volume = volume;
|
|
this._dispatch(stateActions.setProducerVolume(producer.id, volume));
|
|
}
|
|
});
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('_setMicProducer() failed:%o', error);
|
|
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
text : `Mic producer failed: ${error.name}:${error.message}`
|
|
}));
|
|
|
|
if (producer)
|
|
producer.close();
|
|
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
_setScreenShareProducer()
|
|
{
|
|
if (!this._room.canSend('video'))
|
|
{
|
|
return Promise.reject(
|
|
new Error('cannot send screen'));
|
|
}
|
|
|
|
let producer;
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
const available = this._screenSharing.isScreenShareAvailable() &&
|
|
!this._screenSharing.needExtension();
|
|
|
|
if (!available)
|
|
throw new Error('screen sharing not available');
|
|
|
|
logger.debug('_setScreenShareProducer() | calling getUserMedia()');
|
|
|
|
return this._screenSharing.start({
|
|
width : 1280,
|
|
height : 720,
|
|
frameRate : 3
|
|
});
|
|
})
|
|
.then((stream) =>
|
|
{
|
|
const track = stream.getVideoTracks()[0];
|
|
|
|
producer = this._room.createProducer(
|
|
track, { simulcast: false }, { source: 'screen' });
|
|
|
|
// No need to keep original track.
|
|
track.stop();
|
|
|
|
// Send it.
|
|
return producer.send(this._sendTransport);
|
|
})
|
|
.then(() =>
|
|
{
|
|
this._screenSharingProducer = producer;
|
|
|
|
this._dispatch(stateActions.addProducer(
|
|
{
|
|
id : producer.id,
|
|
source : 'screen',
|
|
deviceLabel : 'screen',
|
|
type : 'screen',
|
|
locallyPaused : producer.locallyPaused,
|
|
remotelyPaused : producer.remotelyPaused,
|
|
track : producer.track,
|
|
codec : producer.rtpParameters.codecs[0].name
|
|
}));
|
|
|
|
producer.on('close', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'webcam Producer "close" event [originator:%s]', originator);
|
|
|
|
this._screenSharingProducer = null;
|
|
this._dispatch(stateActions.removeProducer(producer.id));
|
|
});
|
|
|
|
producer.on('trackended', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'webcam Producer "trackended" event [originator:%s]', originator);
|
|
|
|
this.disableScreenSharing();
|
|
});
|
|
|
|
producer.on('pause', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'webcam Producer "pause" event [originator:%s]', originator);
|
|
|
|
this._dispatch(stateActions.setProducerPaused(producer.id, originator));
|
|
});
|
|
|
|
producer.on('resume', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'webcam Producer "resume" event [originator:%s]', originator);
|
|
|
|
this._dispatch(stateActions.setProducerResumed(producer.id, originator));
|
|
});
|
|
|
|
producer.on('handled', () =>
|
|
{
|
|
logger.debug('webcam Producer "handled" event');
|
|
});
|
|
|
|
producer.on('unhandled', () =>
|
|
{
|
|
logger.debug('webcam Producer "unhandled" event');
|
|
});
|
|
})
|
|
.then(() =>
|
|
{
|
|
logger.debug('_setScreenShareProducer() succeeded');
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('_setScreenShareProducer() failed:%o', error);
|
|
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
text : `Screen share producer failed: ${error.name}:${error.message}`
|
|
}));
|
|
|
|
if (producer)
|
|
producer.close();
|
|
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
_setWebcamProducer()
|
|
{
|
|
if (!this._room.canSend('video'))
|
|
{
|
|
return Promise.reject(
|
|
new Error('cannot send video'));
|
|
}
|
|
|
|
if (this._webcamProducer)
|
|
{
|
|
return Promise.reject(
|
|
new Error('webcam Producer already exists'));
|
|
}
|
|
|
|
let producer;
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
const { device } = this._webcam;
|
|
|
|
if (!device)
|
|
throw new Error('no webcam devices');
|
|
|
|
logger.debug('_setWebcamProducer() | calling getUserMedia()');
|
|
|
|
return navigator.mediaDevices.getUserMedia(
|
|
{
|
|
video :
|
|
{
|
|
deviceId : { exact: device.deviceId },
|
|
...VIDEO_CONSTRAINS
|
|
}
|
|
});
|
|
})
|
|
.then((stream) =>
|
|
{
|
|
const track = stream.getVideoTracks()[0];
|
|
|
|
producer = this._room.createProducer(
|
|
track, { simulcast: this._useSimulcast }, { source: 'webcam' });
|
|
|
|
// No need to keep original track.
|
|
track.stop();
|
|
|
|
// Send it.
|
|
return producer.send(this._sendTransport);
|
|
})
|
|
.then(() =>
|
|
{
|
|
this._webcamProducer = producer;
|
|
|
|
const { device } = this._webcam;
|
|
|
|
this._dispatch(stateActions.addProducer(
|
|
{
|
|
id : producer.id,
|
|
source : 'webcam',
|
|
deviceLabel : device.label,
|
|
type : this._getWebcamType(device),
|
|
locallyPaused : producer.locallyPaused,
|
|
remotelyPaused : producer.remotelyPaused,
|
|
track : producer.track,
|
|
codec : producer.rtpParameters.codecs[0].name
|
|
}));
|
|
|
|
producer.on('close', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'webcam Producer "close" event [originator:%s]', originator);
|
|
|
|
this._webcamProducer = null;
|
|
this._dispatch(stateActions.removeProducer(producer.id));
|
|
});
|
|
|
|
producer.on('pause', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'webcam Producer "pause" event [originator:%s]', originator);
|
|
|
|
this._dispatch(stateActions.setProducerPaused(producer.id, originator));
|
|
});
|
|
|
|
producer.on('resume', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'webcam Producer "resume" event [originator:%s]', originator);
|
|
|
|
this._dispatch(stateActions.setProducerResumed(producer.id, originator));
|
|
});
|
|
|
|
producer.on('handled', () =>
|
|
{
|
|
logger.debug('webcam Producer "handled" event');
|
|
});
|
|
|
|
producer.on('unhandled', () =>
|
|
{
|
|
logger.debug('webcam Producer "unhandled" event');
|
|
});
|
|
})
|
|
.then(() =>
|
|
{
|
|
logger.debug('_setWebcamProducer() succeeded');
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error('_setWebcamProducer() failed:%o', error);
|
|
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
text : `Webcam producer failed: ${error.name}:${error.message}`
|
|
}));
|
|
|
|
if (producer)
|
|
producer.close();
|
|
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
_updateAudioDevices()
|
|
{
|
|
logger.debug('_updateAudioDevices()');
|
|
|
|
// Reset the list.
|
|
this._audioDevices = new Map();
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
logger.debug('_updateAudioDevices() | calling enumerateDevices()');
|
|
|
|
return navigator.mediaDevices.enumerateDevices();
|
|
})
|
|
.then((devices) =>
|
|
{
|
|
for (const device of devices)
|
|
{
|
|
if (device.kind !== 'audioinput')
|
|
continue;
|
|
|
|
device.value = device.deviceId;
|
|
|
|
this._audioDevices.set(device.deviceId, device);
|
|
}
|
|
})
|
|
.then(() =>
|
|
{
|
|
const array = Array.from(this._audioDevices.values());
|
|
const len = array.length;
|
|
const currentAudioDeviceId =
|
|
this._audioDevice.device ? this._audioDevice.device.deviceId : undefined;
|
|
|
|
logger.debug('_updateAudioDevices() [audiodevices:%o]', array);
|
|
|
|
if (len === 0)
|
|
this._audioDevice.device = null;
|
|
else if (!this._audioDevices.has(currentAudioDeviceId))
|
|
this._audioDevice.device = array[0];
|
|
|
|
this._dispatch(
|
|
stateActions.setCanChangeWebcam(this._webcams.size >= 2));
|
|
|
|
this._dispatch(
|
|
stateActions.setCanChangeAudioDevice(len >= 2));
|
|
if (len >= 1)
|
|
this._dispatch(
|
|
stateActions.setAudioDevices(this._audioDevices));
|
|
});
|
|
}
|
|
|
|
_updateWebcams()
|
|
{
|
|
logger.debug('_updateWebcams()');
|
|
|
|
// Reset the list.
|
|
this._webcams = new Map();
|
|
|
|
return Promise.resolve()
|
|
.then(() =>
|
|
{
|
|
logger.debug('_updateWebcams() | calling enumerateDevices()');
|
|
|
|
return navigator.mediaDevices.enumerateDevices();
|
|
})
|
|
.then((devices) =>
|
|
{
|
|
for (const device of devices)
|
|
{
|
|
if (device.kind !== 'videoinput')
|
|
continue;
|
|
|
|
device.value = device.deviceId;
|
|
|
|
this._webcams.set(device.deviceId, device);
|
|
}
|
|
})
|
|
.then(() =>
|
|
{
|
|
const array = Array.from(this._webcams.values());
|
|
const len = array.length;
|
|
const currentWebcamId =
|
|
this._webcam.device ? this._webcam.device.deviceId : undefined;
|
|
|
|
logger.debug('_updateWebcams() [webcams:%o]', array);
|
|
|
|
if (len === 0)
|
|
this._webcam.device = null;
|
|
else if (!this._webcams.has(currentWebcamId))
|
|
this._webcam.device = array[0];
|
|
|
|
this._dispatch(
|
|
stateActions.setCanChangeWebcam(this._webcams.size >= 2));
|
|
|
|
this._dispatch(
|
|
stateActions.setCanChangeWebcam(len >= 2));
|
|
if (len >= 1)
|
|
this._dispatch(
|
|
stateActions.setWebcamDevices(this._webcams));
|
|
});
|
|
}
|
|
|
|
_getWebcamType(device)
|
|
{
|
|
if (/(back|rear)/i.test(device.label))
|
|
{
|
|
logger.debug('_getWebcamType() | it seems to be a back camera');
|
|
|
|
return 'back';
|
|
}
|
|
else
|
|
{
|
|
logger.debug('_getWebcamType() | it seems to be a front camera');
|
|
|
|
return 'front';
|
|
}
|
|
}
|
|
|
|
_handlePeer(peer, { notify = true } = {})
|
|
{
|
|
const displayName = peer.appData.displayName;
|
|
|
|
this._dispatch(stateActions.addPeer(
|
|
{
|
|
name : peer.name,
|
|
displayName : displayName,
|
|
device : peer.appData.device,
|
|
raiseHandState : peer.appData.raiseHandState,
|
|
consumers : []
|
|
}));
|
|
|
|
if (notify)
|
|
{
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
text : `${displayName} joined the room`
|
|
}));
|
|
}
|
|
|
|
for (const consumer of peer.consumers)
|
|
{
|
|
this._handleConsumer(consumer);
|
|
}
|
|
|
|
peer.on('close', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'peer "close" event [name:"%s", originator:%s]',
|
|
peer.name, originator);
|
|
|
|
this._dispatch(stateActions.removePeer(peer.name));
|
|
|
|
if (this._room.joined)
|
|
{
|
|
this._dispatch(requestActions.notify(
|
|
{
|
|
text : `${peer.appData.displayName} left the room`
|
|
}));
|
|
}
|
|
});
|
|
|
|
peer.on('newconsumer', (consumer) =>
|
|
{
|
|
logger.debug(
|
|
'peer "newconsumer" event [name:"%s", id:%s, consumer:%o]',
|
|
peer.name, consumer.id, consumer);
|
|
|
|
this._handleConsumer(consumer);
|
|
});
|
|
}
|
|
|
|
_handleConsumer(consumer)
|
|
{
|
|
const codec = consumer.rtpParameters.codecs[0];
|
|
|
|
this._dispatch(stateActions.addConsumer(
|
|
{
|
|
id : consumer.id,
|
|
peerName : consumer.peer.name,
|
|
source : consumer.appData.source,
|
|
supported : consumer.supported,
|
|
locallyPaused : consumer.locallyPaused,
|
|
remotelyPaused : consumer.remotelyPaused,
|
|
track : null,
|
|
codec : codec ? codec.name : null
|
|
},
|
|
consumer.peer.name)
|
|
);
|
|
|
|
consumer.on('close', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'consumer "close" event [id:%s, originator:%s, consumer:%o]',
|
|
consumer.id, originator, consumer);
|
|
|
|
this._dispatch(stateActions.removeConsumer(
|
|
consumer.id, consumer.peer.name));
|
|
});
|
|
|
|
consumer.on('handled', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'consumer "handled" event [id:%s, originator:%s, consumer:%o]',
|
|
consumer.id, originator, consumer);
|
|
if (consumer.kind === 'audio')
|
|
{
|
|
const stream = new MediaStream;
|
|
|
|
stream.addTrack(consumer.track);
|
|
if (!stream.getAudioTracks()[0])
|
|
throw new Error('consumer.on("handled" | given stream has no audio track');
|
|
|
|
consumer.hark = hark(stream, { play: false });
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
consumer.hark.on('volume_change', (dBs, threshold) =>
|
|
{
|
|
// The exact formula to convert from dBs (-100..0) to linear (0..1) is:
|
|
// Math.pow(10, dBs / 20)
|
|
// However it does not produce a visually useful output, so let exagerate
|
|
// it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to
|
|
// minimize component renderings.
|
|
let volume = Math.round(Math.pow(10, dBs / 85) * 10);
|
|
|
|
if (volume === 1)
|
|
volume = 0;
|
|
|
|
if (volume !== consumer.volume)
|
|
{
|
|
consumer.volume = volume;
|
|
this._dispatch(stateActions.setConsumerVolume(consumer.id, volume));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
consumer.on('pause', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'consumer "pause" event [id:%s, originator:%s, consumer:%o]',
|
|
consumer.id, originator, consumer);
|
|
|
|
this._dispatch(stateActions.setConsumerPaused(consumer.id, originator));
|
|
});
|
|
|
|
consumer.on('resume', (originator) =>
|
|
{
|
|
logger.debug(
|
|
'consumer "resume" event [id:%s, originator:%s, consumer:%o]',
|
|
consumer.id, originator, consumer);
|
|
|
|
this._dispatch(stateActions.setConsumerResumed(consumer.id, originator));
|
|
});
|
|
|
|
consumer.on('effectiveprofilechange', (profile) =>
|
|
{
|
|
logger.debug(
|
|
'consumer "effectiveprofilechange" event [id:%s, consumer:%o, profile:%s]',
|
|
consumer.id, consumer, profile);
|
|
|
|
this._dispatch(stateActions.setConsumerEffectiveProfile(consumer.id, profile));
|
|
});
|
|
|
|
// Receive the consumer (if we can).
|
|
if (consumer.supported)
|
|
{
|
|
// Pause it if video and we are in audio-only mode.
|
|
if (consumer.kind === 'video' && this._getState().me.audioOnly)
|
|
consumer.pause('audio-only-mode');
|
|
|
|
consumer.receive(this._recvTransport)
|
|
.then((track) =>
|
|
{
|
|
this._dispatch(stateActions.setConsumerTrack(consumer.id, track));
|
|
})
|
|
.catch((error) =>
|
|
{
|
|
logger.error(
|
|
'unexpected error while receiving a new Consumer:%o', error);
|
|
});
|
|
}
|
|
}
|
|
}
|