029c957b22
The 'inactive' event is fired whenever the stream gets inactive (ie it cannot be used anymore), and there are scenarios where that is unexpected behavior and must be handled accordingly. The main example of that is when gUM permissions are revoked by the user via the browser's permission management panel. Since MediaStream/Track inactive events aren't being handled in such scenarios, what actually happens is that the camera just freezes without further indication why. This commit handles those scenarios in both video-preview and video-provider by: - 1) correctly stopping the camera (provider) - 2) surfacing a toast (provider) or error indication (preview)
193 lines
5.5 KiB
JavaScript
Executable File
193 lines
5.5 KiB
JavaScript
Executable File
import Storage from '/imports/ui/services/storage/session';
|
|
import getFromUserSettings from '/imports/ui/services/users-settings';
|
|
import MediaStreamUtils from '/imports/utils/media-stream-utils';
|
|
import VideoService from '/imports/ui/components/video-provider/service';
|
|
import BBBVideoStream from '/imports/ui/services/webrtc-base/bbb-video-stream';
|
|
|
|
const GUM_TIMEOUT = Meteor.settings.public.kurento.gUMTimeout;
|
|
// Unfiltered, includes hidden profiles
|
|
const CAMERA_PROFILES = Meteor.settings.public.kurento.cameraProfiles || [];
|
|
// Filtered, without hidden profiles
|
|
const PREVIEW_CAMERA_PROFILES = CAMERA_PROFILES.filter(p => !p.hidden);
|
|
|
|
const getDefaultProfile = () => {
|
|
return CAMERA_PROFILES.find(profile => profile.id === VideoService.getUserParameterProfile())
|
|
|| CAMERA_PROFILES.find(profile => profile.default)
|
|
|| CAMERA_PROFILES[0];
|
|
}
|
|
|
|
const getCameraProfile = (id) => {
|
|
return CAMERA_PROFILES.find(profile => profile.id === id);
|
|
}
|
|
|
|
// VIDEO_STREAM_STORAGE: Map<deviceId, MediaStream>. Registers WEBCAM streams.
|
|
// Easier to keep track of them. Easier to centralize their referencing.
|
|
// Easier to shuffle them around.
|
|
const VIDEO_STREAM_STORAGE = new Map();
|
|
|
|
const storeStream = (deviceId, stream) => {
|
|
if (!stream) return false;
|
|
|
|
// Check if there's a dangling stream. If there's one and it's active, cool,
|
|
// return false as it's already stored. Otherwised, clean the derelict stream
|
|
// and store the new one
|
|
if (hasStream(deviceId)) {
|
|
const existingStream = getStream(deviceId);
|
|
if (existingStream.active) return false;
|
|
deleteStream(deviceId);
|
|
}
|
|
|
|
VIDEO_STREAM_STORAGE.set(deviceId, stream);
|
|
|
|
// Stream insurance: clean it up if it ends (see the events being listened to below)
|
|
stream.once('inactive', () => {
|
|
deleteStream(deviceId);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
const getStream = (deviceId) => {
|
|
return VIDEO_STREAM_STORAGE.get(deviceId);
|
|
}
|
|
|
|
const hasStream = (deviceId) => {
|
|
return VIDEO_STREAM_STORAGE.has(deviceId);
|
|
}
|
|
|
|
const deleteStream = (deviceId) => {
|
|
const stream = getStream(deviceId);
|
|
if (stream == null) return false;
|
|
MediaStreamUtils.stopMediaStreamTracks(stream);
|
|
return VIDEO_STREAM_STORAGE.delete(deviceId);
|
|
}
|
|
|
|
const promiseTimeout = (ms, promise) => {
|
|
const timeout = new Promise((resolve, reject) => {
|
|
const id = setTimeout(() => {
|
|
clearTimeout(id);
|
|
|
|
const error = {
|
|
name: 'TimeoutError',
|
|
message: 'Promise did not return',
|
|
};
|
|
|
|
reject(error);
|
|
}, ms);
|
|
});
|
|
|
|
return Promise.race([
|
|
promise,
|
|
timeout,
|
|
]);
|
|
};
|
|
|
|
const getSkipVideoPreview = () => {
|
|
const KURENTO_CONFIG = Meteor.settings.public.kurento;
|
|
|
|
const skipVideoPreviewOnFirstJoin = getFromUserSettings(
|
|
'bbb_skip_video_preview_on_first_join',
|
|
KURENTO_CONFIG.skipVideoPreviewOnFirstJoin,
|
|
);
|
|
const skipVideoPreview = getFromUserSettings(
|
|
'bbb_skip_video_preview',
|
|
KURENTO_CONFIG.skipVideoPreview,
|
|
);
|
|
|
|
return (
|
|
(Storage.getItem('isFirstJoin') !== false && skipVideoPreviewOnFirstJoin)
|
|
|| skipVideoPreview
|
|
);
|
|
};
|
|
|
|
// Takes a raw list of media devices of any media type coming enumerateDevices
|
|
// and a deviceId to be prioritized
|
|
// Outputs an object containing:
|
|
// webcams: videoinput media devices, priorityDevice being the first member of the array (if it exists)
|
|
// areLabelled: whether all videoinput devices are labelled
|
|
// areIdentified: whether all videoinput devices have deviceIds
|
|
const digestVideoDevices = (devices, priorityDevice) => {
|
|
const webcams = [];
|
|
let areLabelled = true;
|
|
let areIdentified = true;
|
|
|
|
devices.forEach((device) => {
|
|
if (device.kind === 'videoinput') {
|
|
if (!webcams.some(d => d.deviceId === device.deviceId)) {
|
|
// We found a priority device. Push it to the beginning of the array so we
|
|
// can use it as the "initial device"
|
|
if (priorityDevice && priorityDevice === device.deviceId) {
|
|
webcams.unshift(device);
|
|
} else {
|
|
webcams.push(device);
|
|
}
|
|
|
|
if (!device.label) { areLabelled = false }
|
|
if (!device.deviceId) { areIdentified = false }
|
|
}
|
|
}
|
|
});
|
|
|
|
// Returns the list of devices and whether they are labelled and identified with deviceId
|
|
return {
|
|
webcams,
|
|
areLabelled,
|
|
areIdentified,
|
|
};
|
|
}
|
|
|
|
// Returns a promise that resolves an instance of BBBVideoStream or rejects an *Error
|
|
const doGUM = (deviceId, profile) => {
|
|
// Check if this is an already loaded stream
|
|
if (deviceId && hasStream(deviceId)) {
|
|
return Promise.resolve(getStream(deviceId));
|
|
}
|
|
|
|
const constraints = {
|
|
audio: false,
|
|
video: { ...profile.constraints },
|
|
};
|
|
|
|
if (deviceId) {
|
|
constraints.video.deviceId = { exact: deviceId };
|
|
}
|
|
|
|
const postProcessedgUM = (cts) => {
|
|
return navigator.mediaDevices.getUserMedia(cts).then((stream) => {
|
|
return (new BBBVideoStream(stream));
|
|
});
|
|
};
|
|
|
|
return promiseTimeout(GUM_TIMEOUT, postProcessedgUM(constraints));
|
|
}
|
|
|
|
const terminateCameraStream = (bbbVideoStream, deviceId) => {
|
|
// Cleanup current stream if it wasn't shared/stored
|
|
if (bbbVideoStream && !hasStream(deviceId)) {
|
|
bbbVideoStream.stop()
|
|
}
|
|
}
|
|
|
|
export default {
|
|
PREVIEW_CAMERA_PROFILES,
|
|
CAMERA_PROFILES,
|
|
promiseTimeout,
|
|
changeWebcam: (deviceId) => {
|
|
Session.set('WebcamDeviceId', deviceId);
|
|
},
|
|
webcamDeviceId: () => Session.get('WebcamDeviceId'),
|
|
changeProfile: (profileId) => {
|
|
Session.set('WebcamProfileId', profileId);
|
|
},
|
|
getSkipVideoPreview,
|
|
storeStream,
|
|
getStream,
|
|
hasStream,
|
|
deleteStream,
|
|
digestVideoDevices,
|
|
getDefaultProfile,
|
|
getCameraProfile,
|
|
doGUM,
|
|
terminateCameraStream,
|
|
};
|