TeaWeb/shared/js/ui/modal/video-source/Controller.tsx
2020-11-29 21:00:59 +01:00

336 lines
No EOL
13 KiB
TypeScript

import {Registry} from "tc-shared/events";
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
import {ModalVideoSourceEvents} from "tc-shared/ui/modal/video-source/Definitions";
import {ModalVideoSource} from "tc-shared/ui/modal/video-source/Renderer";
import {getVideoDriver, VideoPermissionStatus, VideoSource} from "tc-shared/video/VideoSource";
import {LogCategory, logError} from "tc-shared/log";
import {VideoBroadcastType} from "tc-shared/connection/VideoConnection";
type VideoSourceRef = { source: VideoSource };
/**
* @param type The video type which should be prompted
* @param selectDefault If we're trying to select a source on default
* @param quickSelect If we want to quickly select a source and instantly use it.
* This option is only useable for screen sharing.
*/
export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, selectDefault: boolean, quickSelect: boolean) : Promise<VideoSource> {
const refSource: VideoSourceRef = {
source: undefined
};
const events = new Registry<ModalVideoSourceEvents>();
events.enableDebug("video-source-select");
initializeController(events, refSource, type, quickSelect);
const modal = spawnReactModal(ModalVideoSource, events, type);
modal.events.on("destroy", () => {
events.fire("notify_destroy");
events.destroy();
});
events.on(["action_start", "action_cancel"], () => {
modal.destroy();
});
modal.show().then(() => {
if(type === "screen" && getVideoDriver().screenQueryAvailable()) {
events.fire_react("action_toggle_screen_capture_device_select", { shown: true });
} else if(selectDefault) {
events.fire("action_select_source", { id: undefined });
}
});
await new Promise(resolve => {
if(type === "screen" && quickSelect) {
events.on("notify_video_preview", event => {
if(event.status.status === "preview") {
/* we've successfully selected something */
modal.destroy();
}
});
}
modal.events.one(["destroy", "close"], resolve);
});
return refSource.source;
}
type SourceConstraints = { width?: number, height?: number, frameRate?: number };
function initializeController(events: Registry<ModalVideoSourceEvents>, currentSourceRef: VideoSourceRef, type: VideoBroadcastType, quickSelect: boolean) {
let currentSource: VideoSource | string;
let currentConstraints: SourceConstraints;
/* preselected current source id */
let currentSourceId: string;
/* fallback current source name if "currentSource" is empty */
let fallbackCurrentSourceName: string;
const notifyStartButton = () => {
events.fire_react("notify_start_button", { enabled: typeof currentSource === "object" })
};
const notifyDeviceList = () => {
const driver = getVideoDriver();
driver.getDevices().then(devices => {
if(devices === false) {
if(driver.getPermissionStatus() === VideoPermissionStatus.SystemDenied) {
events.fire_react("notify_device_list", { status: { status: "error", reason: "no-permissions" } });
} else {
events.fire_react("notify_device_list", { status: { status: "error", reason: "request-permissions" } });
}
} else {
events.fire_react("notify_device_list", {
status: {
status: "success",
devices: devices.map(e => { return { id: e.id, displayName: e.name }}),
selectedDeviceId: currentSourceId,
fallbackSelectedDeviceName: fallbackCurrentSourceName
}
});
}
});
}
const notifyScreenCaptureDevices = () => {
const driver = getVideoDriver();
driver.queryScreenCaptureDevices().then(devices => {
events.fire_react("notify_screen_capture_devices", { devices: { status: "success", devices: devices }});
}).catch(error => {
if(typeof error !== "string") {
logError(LogCategory.VIDEO, tr("Failed to query screen capture devices: %o"), error);
error = tr("lookup the console");
}
events.fire_react("notify_screen_capture_devices", { devices: { status: "error", reason: error }});
})
}
const notifyVideoPreview = () => {
const driver = getVideoDriver();
switch (driver.getPermissionStatus()) {
case VideoPermissionStatus.SystemDenied:
events.fire_react("notify_video_preview", { status: { status: "error", reason: "no-permissions" }});
break;
case VideoPermissionStatus.UserDenied:
events.fire_react("notify_video_preview", { status: { status: "error", reason: "request-permissions" }});
break;
case VideoPermissionStatus.Granted:
if(typeof currentSource === "string") {
events.fire_react("notify_video_preview", { status: {
status: "error",
reason: "custom",
message: currentSource
}});
} else if(currentSource) {
events.fire_react("notify_video_preview", { status: {
status: "preview",
stream: currentSource.getStream()
}});
} else {
events.fire_react("notify_video_preview", { status: { status: "none" }});
}
break;
}
};
const notifyCurrentSource = () => {
if(typeof currentSource === "object") {
events.fire_react("notify_source", {
state: {
type: "selected",
deviceId: currentSource.getId(),
name: currentSource?.getName() || fallbackCurrentSourceName
}
});
} else if(typeof currentSource === "string") {
events.fire_react("notify_source", {
state: {
type: "errored",
error: currentSource
}
});
} else {
events.fire_react("notify_source", {
state: {
type: "none"
}
});
}
}
const notifySettingDimension = () => {
if(typeof currentSource === "object") {
const videoTrack = currentSource.getStream().getVideoTracks()[0];
const settings = videoTrack.getSettings();
const capabilities = "getCapabilities" in videoTrack ? videoTrack.getCapabilities() : undefined;
events.fire_react("notify_setting_dimension", {
setting: {
minWidth: capabilities?.width ? capabilities.width.min : 1,
maxWidth: capabilities?.width ? capabilities.width.max : settings.width,
minHeight: capabilities?.height ? capabilities.height.min : 1,
maxHeight: capabilities?.height ? capabilities.height.max : settings.height,
originalWidth: settings.width,
originalHeight: settings.height,
currentWidth: settings.width,
currentHeight: settings.height
}
});
} else {
events.fire_react("notify_setting_dimension", { setting: undefined });
}
};
const notifySettingFramerate = () => {
if(typeof currentSource === "object") {
const videoTrack = currentSource.getStream().getVideoTracks()[0];
const settings = videoTrack.getSettings();
const capabilities = "getCapabilities" in videoTrack ? videoTrack.getCapabilities() : undefined;
const round = (value: number) => Math.round(value * 100) / 100;
events.fire_react("notify_settings_framerate", {
frameRate: {
min: round(capabilities?.frameRate ? capabilities.frameRate.min : 1),
max: round(capabilities?.frameRate ? capabilities.frameRate.max : settings.frameRate),
original: round(settings.frameRate)
}
});
} else {
events.fire_react("notify_settings_framerate", { frameRate: undefined });
}
};
const setCurrentSource = (source: VideoSource | string | undefined) => {
if(typeof currentSource === "object") {
currentSource.deref();
}
currentConstraints = {};
currentSource = source;
notifyVideoPreview();
notifyStartButton();
notifyCurrentSource();
notifySettingDimension();
notifySettingFramerate();
}
events.on("query_source", () => notifyCurrentSource());
events.on("query_device_list", () => notifyDeviceList());
events.on("query_screen_capture_devices", () => notifyScreenCaptureDevices());
events.on("query_video_preview", () => notifyVideoPreview());
events.on("query_start_button", () => notifyStartButton());
events.on("query_setting_dimension", () => notifySettingDimension());
events.on("query_setting_framerate", () => notifySettingFramerate());
events.on("action_request_permissions", () => {
getVideoDriver().requestPermissions().then(result => {
if(typeof result === "object") {
currentSourceId = result.getId() + " --";
fallbackCurrentSourceName = result.getName();
notifyDeviceList();
setCurrentSource(result);
} else {
/* the device list will already be updated due to the notify_permissions_changed event */
}
});
});
events.on("action_select_source", event => {
const driver = getVideoDriver();
if(type === "camera") {
currentSourceId = event.id;
fallbackCurrentSourceName = tr("loading...");
notifyDeviceList();
driver.createVideoSource(currentSourceId).then(stream => {
fallbackCurrentSourceName = stream.getName();
setCurrentSource(stream);
}).catch(error => {
fallbackCurrentSourceName = "invalid device";
if(typeof error === "string") {
setCurrentSource(error);
} else {
logError(LogCategory.GENERAL, tr("Failed to open video device %s: %o"), event.id, error);
setCurrentSource(tr("Failed to open video device (Lookup the console)"));
}
});
} else if(driver.screenQueryAvailable() && typeof event.id === "undefined") {
events.fire_react("action_toggle_screen_capture_device_select", { shown: true });
} else {
currentSourceId = undefined;
fallbackCurrentSourceName = tr("loading...");
driver.createScreenSource(event.id, quickSelect).then(stream => {
setCurrentSource(stream);
fallbackCurrentSourceName = stream?.getName() || tr("No stream");
}).catch(error => {
fallbackCurrentSourceName = "screen capture failed";
if(typeof error === "string") {
setCurrentSource(error);
} else {
logError(LogCategory.GENERAL, tr("Failed to open screen capture device %s: %o"), event.id, error);
setCurrentSource(tr("Failed to open screen capture device (Lookup the console)"));
}
});
}
});
events.on("action_cancel", () => {
currentSourceRef.source = undefined;
});
if(type === "camera") {
/* only the camara requires a device list */
events.on("notify_destroy", getVideoDriver().getEvents().on("notify_permissions_changed", () => {
if(getVideoDriver().getPermissionStatus() !== VideoPermissionStatus.Granted) {
currentSourceId = undefined;
fallbackCurrentSourceName = undefined;
notifyDeviceList();
/* implicitly updates the start button */
setCurrentSource(undefined);
} else {
notifyDeviceList();
notifyVideoPreview();
notifyStartButton();
}
}));
}
events.on("notify_destroy", () => {
if(typeof currentSource === "object") {
currentSource.deref();
}
});
events.on("action_start", () => {
if(typeof currentSource === "object") {
currentSourceRef.source = currentSource.ref();
}
})
const applyConstraints = async () => {
if(typeof currentSource === "object") {
const videoTrack = currentSource.getStream().getVideoTracks()[0];
if(!videoTrack) { return; }
await videoTrack.applyConstraints(currentConstraints);
}
};
events.on("action_setting_dimension", event => {
currentConstraints.height = event.height;
currentConstraints.width = event.width;
applyConstraints().then(undefined);
});
events.on("action_setting_framerate", event => {
currentConstraints.frameRate = event.frameRate;
applyConstraints().then(undefined);
});
}