bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/video-provider/service.js

381 lines
11 KiB
JavaScript
Raw Normal View History

import { Tracker } from 'meteor/tracker';
2019-11-28 21:13:06 +08:00
import { Session } from 'meteor/session';
import Settings from '/imports/ui/services/settings';
import Auth from '/imports/ui/services/auth';
2019-11-28 21:13:06 +08:00
import Meetings from '/imports/api/meetings';
import Users from '/imports/api/users';
import VideoStreams from '/imports/api/video-streams';
2018-04-09 22:39:27 +08:00
import UserListService from '/imports/ui/components/user-list/service';
2019-11-28 21:13:06 +08:00
import { makeCall } from '/imports/ui/services/api';
import { notify } from '/imports/ui/services/notification';
import { monitorVideoConnection } from '/imports/utils/stats';
import browser from 'browser-detect';
2019-12-19 01:44:56 +08:00
import getFromUserSettings from '/imports/ui/services/users-settings';
2019-11-28 21:13:06 +08:00
import logger from '/imports/startup/client/logger';
const CAMERA_PROFILES = Meteor.settings.public.kurento.cameraProfiles;
2019-12-19 01:44:56 +08:00
const MULTIPLE_CAMERAS = Meteor.settings.public.app.enableMultipleCameras;
const SKIP_VIDEO_PREVIEW = Meteor.settings.public.kurento.skipVideoPreview;
2017-09-01 23:26:57 +08:00
2019-11-28 21:13:06 +08:00
const SFU_URL = Meteor.settings.public.kurento.wsUrl;
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const ENABLE_NETWORK_MONITORING = Meteor.settings.public.networkMonitoring.enableNetworkMonitoring;
2019-11-28 21:13:06 +08:00
const TOKEN = '_';
class VideoService {
constructor() {
this.defineProperties({
2019-11-28 21:13:06 +08:00
isConnecting: false,
isConnected: false,
});
2019-12-19 01:44:56 +08:00
this.skipVideoPreview = getFromUserSettings('bbb_skip_video_preview', false) || SKIP_VIDEO_PREVIEW;
this.userParameterProfile = getFromUserSettings(
'bbb_preferred_camera_profile',
(CAMERA_PROFILES.filter(i => i.default) || {}).id
);
const BROWSER_RESULTS = browser();
this.isMobile = BROWSER_RESULTS.mobile || BROWSER_RESULTS.os.includes('Android');
this.isSafari = BROWSER_RESULTS.name === 'safari';
this.numberOfDevices = 0;
this.updateNumberOfDevices = this.updateNumberOfDevices.bind(this);
// Safari doesn't support ondevicechange
if (!this.isSafari) {
navigator.mediaDevices.ondevicechange = (event) => this.updateNumberOfDevices();
}
this.updateNumberOfDevices();
}
2017-09-20 11:12:10 +08:00
defineProperties(obj) {
Object.keys(obj).forEach((key) => {
const privateKey = `_${key}`;
this[privateKey] = {
value: obj[key],
tracker: new Tracker.Dependency(),
};
2017-09-20 11:12:10 +08:00
Object.defineProperty(this, key, {
set: (value) => {
this[privateKey].value = value;
this[privateKey].tracker.changed();
},
get: () => {
this[privateKey].tracker.depend();
return this[privateKey].value;
},
});
});
}
2017-09-01 23:26:57 +08:00
updateNumberOfDevices() {
navigator.mediaDevices.enumerateDevices().then(devices => {
const deviceIds = [];
devices.forEach(d => {
if (d.kind === 'videoinput' && !deviceIds.includes(d.deviceId)) {
deviceIds.push(d.deviceId);
}
});
this.numberOfDevices = deviceIds.length;
});
}
2019-11-28 21:13:06 +08:00
joinVideo(deviceId) {
this.deviceId = deviceId;
this.isConnecting = true;
}
joinedVideo() {
this.isConnected = true;
}
exitVideo() {
2019-11-28 21:13:06 +08:00
if (this.isConnected) {
logger.info({
logCode: 'video_provider_unsharewebcam',
}, `Sending unshare all ${Auth.userID} webcams notification to meteor`);
const streams = VideoStreams.find(
{
meetingId: Auth.meetingID,
userId: Auth.userID,
}, { fields: { stream: 1 } },
).fetch();
streams.forEach(s => this.sendUserUnshareWebcam(s.stream));
this.exitedVideo();
}
}
exitedVideo() {
2019-11-28 21:13:06 +08:00
this.isConnecting = false;
this.deviceId = null;
this.isConnected = false;
}
stopVideo(cameraId) {
2019-11-28 21:13:06 +08:00
const streams = VideoStreams.find(
{
meetingId: Auth.meetingID,
userId: Auth.userID,
}, { fields: { stream: 1 } },
).fetch().length;
this.sendUserUnshareWebcam(cameraId);
if (streams < 2) {
// If the user had less than 2 streams, set as a full disconnection
this.exitedVideo();
}
}
getSharedDevices() {
const devices = VideoStreams.find(
{
meetingId: Auth.meetingID,
userId: Auth.userID,
}, { fields: { deviceId: 1 } },
).fetch().map(vs => vs.deviceId);
return devices;
}
2019-11-28 21:13:06 +08:00
sendUserShareWebcam(cameraId) {
makeCall('userShareWebcam', cameraId);
}
2019-11-28 21:13:06 +08:00
sendUserUnshareWebcam(cameraId) {
makeCall('userUnshareWebcam', cameraId);
}
2019-11-28 21:13:06 +08:00
getAuthenticatedURL() {
return Auth.authenticateURL(SFU_URL);
}
2018-04-10 02:28:54 +08:00
2019-11-28 21:13:06 +08:00
getVideoStreams() {
const streams = VideoStreams.find(
{ meetingId: Auth.meetingID },
2019-09-06 04:03:22 +08:00
{
fields: {
2019-11-28 21:13:06 +08:00
userId: 1, stream: 1, name: 1,
2019-09-06 04:03:22 +08:00
},
2019-11-28 21:13:06 +08:00
},
).fetch();
2019-11-28 21:13:06 +08:00
const connectingStream = this.getConnectingStream(streams);
2019-11-28 21:13:06 +08:00
if (connectingStream) streams.push(connectingStream);
2019-11-28 21:13:06 +08:00
return streams.map(vs => ({
cameraId: vs.stream,
userId: vs.userId,
name: vs.name,
})).sort(UserListService.sortUsersByName);
}
2019-11-28 21:13:06 +08:00
getConnectingStream(streams) {
let connectingStream;
2018-04-10 02:28:54 +08:00
2019-11-28 21:13:06 +08:00
if (this.isConnecting) {
if (this.deviceId) {
const stream = this.buildStreamName(Auth.userID, this.deviceId);
if (!this.hasStream(streams, stream) && !this.isUserLocked()) {
connectingStream = {
stream,
userId: Auth.userID,
name: Auth.fullname,
};
} else {
// Connecting stream is already stored at database
this.deviceId = null;
this.isConnecting = false;
}
} else {
logger.error({
logCode: 'video_provider_missing_deviceid',
}, 'Could not retrieve a valid deviceId');
}
}
2018-04-10 02:28:54 +08:00
2019-11-28 21:13:06 +08:00
return connectingStream;
2018-04-10 02:28:54 +08:00
}
2019-11-28 21:13:06 +08:00
buildStreamName(userId, deviceId) {
return `${userId}${TOKEN}${deviceId}`;
2018-03-22 22:14:54 +08:00
}
2019-11-28 21:13:06 +08:00
hasVideoStream() {
const videoStreams = VideoStreams.findOne({ userId: Auth.userID },
{ fields: {} });
return !!videoStreams;
}
hasStream(streams, stream) {
return streams.find(s => s.stream === stream);
}
disableCam() {
const m = Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { 'lockSettingsProps.disableCam': 1 } });
return m.lockSettingsProps ? m.lockSettingsProps.disableCam : false;
2018-03-22 22:14:54 +08:00
}
hideUserList() {
const m = Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { 'lockSettingsProps.hideUserList': 1 } });
return m.lockSettingsProps ? m.lockSettingsProps.hideUserList : false;
}
2019-11-28 21:13:06 +08:00
getInfo() {
const m = Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { 'voiceProp.voiceConf': 1 } });
const voiceBridge = m.voiceProp ? m.voiceProp.voiceConf : null;
return {
userId: Auth.userID,
userName: Auth.fullname,
meetingId: Auth.meetingID,
sessionToken: Auth.sessionToken,
voiceBridge,
};
}
2019-12-19 01:44:56 +08:00
getMyStream(deviceId) {
const videoStream = VideoStreams.findOne(
{
meetingId: Auth.meetingID,
userId: Auth.userID,
deviceId: deviceId
}, { fields: { stream: 1 } }
);
return videoStream ? videoStream.stream : null;
}
2019-11-28 21:13:06 +08:00
isUserLocked() {
return !!Users.findOne({
userId: Auth.userID,
locked: true,
role: { $ne: ROLE_MODERATOR },
}, { fields: {} }) && this.disableCam();
}
2019-11-28 21:13:06 +08:00
lockUser() {
if (this.isConnected) {
this.exitVideo();
}
}
2019-11-28 21:13:06 +08:00
isLocalStream(cameraId) {
return cameraId.startsWith(Auth.userID);
}
playStart(cameraId) {
if (this.isLocalStream(cameraId)) {
this.sendUserShareWebcam(cameraId);
this.joinedVideo();
}
}
getCameraProfile() {
const profileId = Session.get('WebcamProfileId') || '';
const cameraProfile = CAMERA_PROFILES.find(profile => profile.id === profileId)
|| CAMERA_PROFILES.find(profile => profile.default)
|| CAMERA_PROFILES[0];
const deviceId = Session.get('WebcamDeviceId');
if (deviceId) {
cameraProfile.constraints = cameraProfile.constraints || {};
cameraProfile.constraints.deviceId = { exact: deviceId };
}
return cameraProfile;
}
addCandidateToPeer(peer, candidate, cameraId) {
peer.addIceCandidate(candidate, (error) => {
if (error) {
// Just log the error. We can't be sure if a candidate failure on add is
// fatal or not, so that's why we have a timeout set up for negotiations
// and listeners for ICE state transitioning to failures, so we won't
// act on it here
logger.error({
logCode: 'video_provider_addicecandidate_error',
extraInfo: {
cameraId,
error,
},
}, `Adding ICE candidate failed for ${cameraId} due to ${error.message}`);
}
});
}
2019-11-28 21:13:06 +08:00
processInboundIceQueue(peer, cameraId) {
while (peer.inboundIceQueue.length) {
const candidate = peer.inboundIceQueue.shift();
this.addCandidateToPeer(peer, candidate, cameraId);
}
}
onBeforeUnload() {
this.exitVideo();
}
isDisabled() {
const { viewParticipantsWebcams } = Settings.dataSaving;
return this.isUserLocked() || this.isConnecting || !viewParticipantsWebcams;
}
2017-09-01 23:26:57 +08:00
2019-11-28 21:13:06 +08:00
getRole(isLocal) {
return isLocal ? 'share' : 'viewer';
}
2019-12-19 01:44:56 +08:00
getSkipVideoPreview(fromInterface) {
return this.skipVideoPreview && !fromInterface;
}
getUserParameterProfile() {
return this.userParameterProfile;
}
isMultipleCamerasEnabled() {
// Multiple cameras shouldn't be enabled with video preview skipping
// Mobile shouldn't be able to share more than one camera at the same time
// Safari needs to implement devicechange event for safe device control
return MULTIPLE_CAMERAS &&
!this.skipVideoPreview &&
!this.isMobile &&
!this.isSafari &&
this.numberOfDevices > 1;
2019-12-19 01:44:56 +08:00
}
monitor(conn) {
if (ENABLE_NETWORK_MONITORING) monitorVideoConnection(conn);
}
}
const videoService = new VideoService();
2017-09-01 23:26:57 +08:00
export default {
exitVideo: () => videoService.exitVideo(),
2019-11-28 21:13:06 +08:00
joinVideo: deviceId => videoService.joinVideo(deviceId),
stopVideo: cameraId => videoService.stopVideo(cameraId),
2019-11-28 21:13:06 +08:00
getVideoStreams: () => videoService.getVideoStreams(),
getInfo: () => videoService.getInfo(),
2019-12-19 01:44:56 +08:00
getMyStream: deviceId => videoService.getMyStream(deviceId),
2019-11-28 21:13:06 +08:00
isUserLocked: () => videoService.isUserLocked(),
lockUser: () => videoService.lockUser(),
getAuthenticatedURL: () => videoService.getAuthenticatedURL(),
isLocalStream: cameraId => videoService.isLocalStream(cameraId),
hasVideoStream: () => videoService.hasVideoStream(),
isDisabled: () => videoService.isDisabled(),
playStart: cameraId => videoService.playStart(cameraId),
getCameraProfile: () => videoService.getCameraProfile(),
addCandidateToPeer: (peer, candidate, cameraId) => videoService.addCandidateToPeer(peer, candidate, cameraId),
processInboundIceQueue: (peer, cameraId) => videoService.processInboundIceQueue(peer, cameraId),
getRole: isLocal => videoService.getRole(isLocal),
getSharedDevices: () => videoService.getSharedDevices(),
2019-12-19 01:44:56 +08:00
getSkipVideoPreview: fromInterface => videoService.getSkipVideoPreview(fromInterface),
getUserParameterProfile: () => videoService.getUserParameterProfile(),
isMultipleCamerasEnabled: () => videoService.isMultipleCamerasEnabled(),
monitor: conn => videoService.monitor(conn),
2019-11-28 21:13:06 +08:00
onBeforeUnload: () => videoService.onBeforeUnload(),
notify: message => notify(message, 'error', 'video'),
2017-09-01 23:26:57 +08:00
};