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 }; export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, selectDefault: boolean) : Promise { const refSource: VideoSourceRef = { source: undefined }; const events = new Registry(); events.enableDebug("video-source-select"); initializeController(events, refSource, type); 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(selectDefault) { events.fire("action_select_source", { id: undefined }); } }); await new Promise(resolve => { modal.events.one(["destroy", "close"], resolve); }); return refSource.source; } type SourceConstraints = { width?: number, height?: number, frameRate?: number }; function initializeController(events: Registry, currentSourceRef: VideoSourceRef, type: VideoBroadcastType) { 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 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_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 { currentSourceId = undefined; fallbackCurrentSourceName = tr("loading..."); driver.createScreenSource().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); }); }