diff --git a/ChangeLog.md b/ChangeLog.md index 19acf68e..9435c704 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,9 @@ # Changelog: +* **12.12.20** + - Improved screen sharing and camara selection + - Showing the echoed own stream from the server instead of the local one + - Fixed a few minor issues with the auto url tokenizer + * **09.12.20** - Fixed the private messages unread indicator - Properly updating the private message unread count diff --git a/shared/js/connection/rtc/Connection.ts b/shared/js/connection/rtc/Connection.ts index d53312b5..389719ce 100644 --- a/shared/js/connection/rtc/Connection.ts +++ b/shared/js/connection/rtc/Connection.ts @@ -11,6 +11,7 @@ import {SdpCompressor, SdpProcessor} from "./SdpUtils"; import {ErrorCode} from "tc-shared/connection/ErrorCode"; import {WhisperTarget} from "tc-shared/voice/VoiceWhisper"; import {globalAudioContext} from "tc-backend/audio/player"; +import * as sdpTransform from "sdp-transform"; const kSdpCompressionMode = 1; @@ -200,6 +201,7 @@ class CommandHandler extends AbstractCommandHandler { sdp: sdp, type: "answer" }).then(() => { + this.handle["cachedRemoteSessionDescription"] = sdp; this.handle["peerRemoteDescriptionReceived"] = true; this.handle.applyCachedRemoteIceCandidates(); }).catch(error => { @@ -207,6 +209,7 @@ class CommandHandler extends AbstractCommandHandler { this.handle["handleFatalError"](tr("Failed to set the remote description (answer)"), true); }); } else if(data.mode === "offer") { + this.handle["cachedRemoteSessionDescription"] = sdp; this.handle["peer"].setRemoteDescription({ sdp: sdp, type: "offer" @@ -498,6 +501,8 @@ export class RTCConnection { private peerRemoteDescriptionReceived: boolean; private cachedRemoteIceCandidates: { candidate: RTCIceCandidate, mediaLine: number }[]; + private cachedRemoteSessionDescription: string; + private currentTracks: {[T in RTCSourceTrackType]: MediaStreamTrack | undefined} = { "audio-whisper": undefined, "video-screen": undefined, @@ -592,6 +597,7 @@ export class RTCConnection { } this.peerRemoteDescriptionReceived = false; this.cachedRemoteIceCandidates = []; + this.cachedRemoteSessionDescription = undefined; clearTimeout(this.connectTimeout); Object.keys(this.currentTransceiver).forEach(key => this.currentTransceiver[key] = undefined); @@ -625,7 +631,7 @@ export class RTCConnection { } } - async setTrackSource(type: RTCSourceTrackType, source: MediaStreamTrack | null) { + async setTrackSource(type: RTCSourceTrackType, source: MediaStreamTrack | null) : Promise { switch (type) { case "audio": case "audio-whisper": @@ -642,8 +648,9 @@ export class RTCConnection { return; } - this.currentTracks[type] = source; + const oldTrack = this.currentTracks[type] = source; await this.updateTracks(); + return oldTrack; } /** @@ -791,13 +798,17 @@ export class RTCConnection { iceServers: [{ urls: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"] }] }); + const kAddGenericTransceiver = false; + if(this.audioSupport) { this.currentTransceiver["audio"] = this.peer.addTransceiver("audio"); this.currentTransceiver["audio-whisper"] = this.peer.addTransceiver("audio"); /* add some other transceivers for later use */ - for(let i = 0; i < 8; i++) { - this.peer.addTransceiver("audio"); + for(let i = 0; i < 8 && kAddGenericTransceiver; i++) { + const transceiver = this.peer.addTransceiver("audio"); + /* we only want to received on that and don't share any bandwidth limits */ + transceiver.direction = "recvonly"; } } @@ -805,8 +816,10 @@ export class RTCConnection { this.currentTransceiver["video-screen"] = this.peer.addTransceiver("video"); /* add some other transceivers for later use */ - for(let i = 0; i < 4; i++) { - this.peer.addTransceiver("video"); + for(let i = 0; i < 4 && kAddGenericTransceiver; i++) { + const transceiver = this.peer.addTransceiver("video"); + /* we only want to received on that and don't share any bandwidth limits */ + transceiver.direction = "recvonly"; } this.peer.onicecandidate = event => this.handleLocalIceCandidate(event.candidate); @@ -855,6 +868,20 @@ export class RTCConnection { } await this.currentTransceiver[type].sender.replaceTrack(target); + if(target) { + console.error("Setting sendrecv from %o", this.currentTransceiver[type].direction, this.currentTransceiver[type].currentDirection); + this.currentTransceiver[type].direction = "sendrecv"; + } else if(type === "video" || type === "video-screen") { + /* + * We don't need to stop & start the audio transceivers every time we're toggling the stream state. + * This would be a much overall cost than just keeping it going. + * + * The video streams instead are not toggling that much and since they split up the bandwidth between them, + * we've to shut them down if they're no needed. This not only allows the one stream to take full advantage + * of the bandwidth it also reduces resource usage. + */ + //this.currentTransceiver[type].direction = "recvonly"; + } logTrace(LogCategory.WEBRTC, "Replaced track for %o (Fallback: %o)", type, target === fallback); } } @@ -1005,7 +1032,7 @@ export class RTCConnection { logTrace(LogCategory.WEBRTC, tr("Peer signalling state changed to %s"), this.peer.signalingState); } private handleNegotiationNeeded() { - logWarn(LogCategory.WEBRTC, tr("Local peer needs negotiation, but we don't support client sideded negotiation.")); + logWarn(LogCategory.WEBRTC, tr("Local peer needs negotiation, but that's not supported that.")); } private handlePeerConnectionStateChanged() { logTrace(LogCategory.WEBRTC, tr("Peer connection state changed to %s"), this.peer.connectionState); diff --git a/shared/js/connection/rtc/SdpUtils.ts b/shared/js/connection/rtc/SdpUtils.ts index e9bc3f15..6a429ebf 100644 --- a/shared/js/connection/rtc/SdpUtils.ts +++ b/shared/js/connection/rtc/SdpUtils.ts @@ -1,5 +1,4 @@ import * as sdpTransform from "sdp-transform"; -import {MediaDescription} from "sdp-transform"; import { tr } from "tc-shared/i18n/localize"; interface SdpCodec { @@ -49,9 +48,10 @@ export class SdpProcessor { rtcpFb: [ "nack", "nack pli", "ccm fir", "transport-cc" ], //42001f | Original: 42e01f fmtp: { - "level-asymmetry-allowed": 1, "packetization-mode": 1, "profile-level-id": "42e01f", "max-br": 25000, "max-fr": 30, - "x-google-max-bitrate": 22 * 1000, - "x-google-start-bitrate": 22 * 1000, + "level-asymmetry-allowed": 1, + "packetization-mode": 1, + "profile-level-id": "42e01f", + "max-fr": 30, } } ]; @@ -83,27 +83,10 @@ export class SdpProcessor { const sdp = sdpTransform.parse(sdpString); this.rtpRemoteChannelMapping = SdpProcessor.generateRtpSSrcMapping(sdp); - /* Fix for Firefox to acknowledge the max bandwidth */ - for(const media of sdp.media) { - if(media.type !== "video") { continue; } - if(media.bandwidth?.length > 0) { continue; } - - const config = media.fmtp.find(e => e.config.indexOf("x-google-start-bitrate") !== -1); - if(!config) { continue; } - - const bitrate = config.config.split(";").find(e => e.startsWith("x-google-start-bitrate="))?.substr(23); - if(!bitrate) { continue; } - - media.bandwidth = [{ - type: "AS", - limit: bitrate - }]; - } - return sdpTransform.write(sdp); } - processOutgoingSdp(sdpString: string, mode: "offer" | "answer") : string { + processOutgoingSdp(sdpString: string, _mode: "offer" | "answer") : string { const sdp = sdpTransform.parse(sdpString); /* apply the "root" fingerprint to each media, FF fix */ @@ -195,10 +178,6 @@ export class SdpProcessor { } media.payloads = media.rtp.map(e => e.payload).join(" "); - media.bandwidth = [{ - type: "AS", - limit: 12000 - }] } } } diff --git a/shared/js/connection/rtc/video/Connection.ts b/shared/js/connection/rtc/video/Connection.ts index ad9ea6c6..5d9453fd 100644 --- a/shared/js/connection/rtc/video/Connection.ts +++ b/shared/js/connection/rtc/video/Connection.ts @@ -222,10 +222,6 @@ export class RtpVideoConnection implements VideoConnection { } async startBroadcasting(type: VideoBroadcastType, source: VideoSource) : Promise { - if(this.broadcasts[type]) { - this.stopBroadcasting(type); - } - const videoTracks = source.getStream().getVideoTracks(); if(videoTracks.length === 0) { throw tr("missing video stream track"); @@ -237,7 +233,7 @@ export class RtpVideoConnection implements VideoConnection { failedReason: undefined, active: true }; - this.events.fire("notify_local_broadcast_state_changed", { oldState: VideoBroadcastState.Stopped, newState: VideoBroadcastState.Initializing, broadcastType: type }); + this.events.fire("notify_local_broadcast_state_changed", { oldState: this.broadcasts[type].state || VideoBroadcastState.Stopped, newState: VideoBroadcastState.Initializing, broadcastType: type }); try { await this.rtcConnection.setTrackSource(type === "camera" ? "video" : "video-screen", videoTracks[0]); diff --git a/shared/js/events/ClientGlobalControlHandler.ts b/shared/js/events/ClientGlobalControlHandler.ts index 432376a2..8a025667 100644 --- a/shared/js/events/ClientGlobalControlHandler.ts +++ b/shared/js/events/ClientGlobalControlHandler.ts @@ -192,7 +192,8 @@ export function initialize(event_registry: Registry) createErrorModal(tr("You're not connected"), tr("You're not connected to any server!")).open(); return; } - spawnVideoSourceSelectModal(event.broadcastType, true, !!event.quickSelect).then(async source => { + + spawnVideoSourceSelectModal(event.broadcastType, event.quickSelect ? "quick" : "default", event.defaultDevice).then(async source => { if(!source) { return; } try { diff --git a/shared/js/events/GlobalEvents.ts b/shared/js/events/GlobalEvents.ts index d8e6c5da..a72c0b62 100644 --- a/shared/js/events/GlobalEvents.ts +++ b/shared/js/events/GlobalEvents.ts @@ -29,7 +29,13 @@ export interface ClientGlobalControlEvents { videoUrl: string, handlerId: string }, - action_toggle_video_broadcasting: { connection: ConnectionHandler, enabled: boolean, broadcastType: VideoBroadcastType, quickSelect?: boolean } + action_toggle_video_broadcasting: { + connection: ConnectionHandler, + enabled: boolean, + broadcastType: VideoBroadcastType, + quickSelect?: boolean, + defaultDevice?: string + } /* some more specific window openings */ action_open_window_connect: { diff --git a/shared/js/text/bbcode.tsx b/shared/js/text/bbcode.tsx index 7d0501cd..30672e26 100644 --- a/shared/js/text/bbcode.tsx +++ b/shared/js/text/bbcode.tsx @@ -14,7 +14,7 @@ export const allowedBBCodes = [ "u", "underlined", "s", "strikethrough", "color", "bgcolor", - "url", + "url", "img", "code", "i-code", "icode", "sub", "sup", @@ -31,7 +31,6 @@ export const allowedBBCodes = [ "tr", "td", "th", "yt", "youtube", - "img", "quote" ]; diff --git a/shared/js/text/bbcode/image.tsx b/shared/js/text/bbcode/image.tsx index aca17136..8ab57cdd 100644 --- a/shared/js/text/bbcode/image.tsx +++ b/shared/js/text/bbcode/image.tsx @@ -7,11 +7,12 @@ import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; import {copy_to_clipboard} from "tc-shared/utils/helpers"; import * as image_preview from "tc-shared/ui/frames/image_preview"; -const regexImage = /^(?:https?):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g; +export const regexImage = /^(?:https?):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g; function loadImageForElement(element: HTMLImageElement) { - if(!element.hasAttribute("x-image-url")) + if(!element.hasAttribute("x-image-url")) { return; + } const url = decodeURIComponent(element.getAttribute("x-image-url") || ""); element.removeAttribute("x-image-url"); @@ -65,13 +66,15 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { let target; let content = rendererText.render(element); if (!element.options) { - target = content; - } else - target = element.options; + target = content?.trim(); + } else { + target = element.options?.trim(); + } regexImage.lastIndex = 0; - if (!regexImage.test(target)) + if (!regexImage.test(target)) { return {"[img]" + content + "[/img]"}; + } return (
diff --git a/shared/js/text/chat.ts b/shared/js/text/chat.ts index 2b115f11..62c77365 100644 --- a/shared/js/text/chat.ts +++ b/shared/js/text/chat.ts @@ -4,6 +4,8 @@ import {renderMarkdownAsBBCode} from "../text/markdown"; import {escapeBBCode} from "../text/bbcode"; import {parse as parseBBCode} from "vendor/xbbcode/parser"; import {TagElement} from "vendor/xbbcode/elements"; +import * as React from "react"; +import {regexImage} from "tc-shared/text/bbcode/image"; interface UrlKnifeUrl { value: { @@ -26,6 +28,7 @@ function bbcodeLinkUrls(message: string, ignore: { start: number, end: number }[ /* we want to go through the urls from the back to the front */ urls.sort((a, b) => b.index.start - a.index.start); + outerLoop: for(const url of urls) { if(ignore.findIndex(range => range.start <= url.index.start && range.end >= url.index.end) !== -1) { continue; @@ -36,6 +39,23 @@ function bbcodeLinkUrls(message: string, ignore: { start: number, end: number }[ const urlPath = message.substring(url.index.start, url.index.end); let bbcodeUrl; + regexImage.lastIndex = 0; + if (regexImage.test(urlPath)) { + for(const suffix of [ + ".jpeg", ".jpg", + ".png", ".gif", ".tiff", + ".bmp", + ".webp", + ".svg" + ]) { + if(urlPath.endsWith(suffix)) { + /* It's an image. Images will be rendered by the client automatically. */ + continue outerLoop; + } + } + } + + let colonIndex = urlPath.indexOf(":"); if(colonIndex === -1 || colonIndex + 2 < urlPath.length || urlPath[colonIndex + 1] !== "/" || urlPath[colonIndex + 2] !== "/") { bbcodeUrl = "[url=https://" + urlPath + "]" + urlPath + "[/url]"; @@ -89,6 +109,7 @@ export function preprocessChatMessageForSend(message: string) : string { break; } } + console.error("Message: %s; No Parse: %s", message, noParseRanges); message = bbcodeLinkUrls(message, noParseRanges); } diff --git a/shared/js/tree/ChannelTree.tsx b/shared/js/tree/ChannelTree.tsx index a60f3c17..caa8cb9d 100644 --- a/shared/js/tree/ChannelTree.tsx +++ b/shared/js/tree/ChannelTree.tsx @@ -394,21 +394,20 @@ export class ChannelTree { client.channelTree = this; } - /* for debug purposes, the server might send back the own audio/video stream */ - if(!isLocalClient || __build.mode === "debug") { + if(!isLocalClient) { const voiceConnection = this.client.serverConnection.getVoiceConnection(); try { client.setVoiceClient(voiceConnection.registerVoiceClient(client.clientId())); } catch (error) { logError(LogCategory.AUDIO, tr("Failed to register a voice client for %d: %o"), client.clientId(), error); } + } - const videoConnection = this.client.serverConnection.getVideoConnection(); - try { - client.setVideoClient(videoConnection.registerVideoClient(client.clientId())); - } catch (error) { - logError(LogCategory.VIDEO, tr("Failed to register a video client for %d: %o"), client.clientId(), error); - } + const videoConnection = this.client.serverConnection.getVideoConnection(); + try { + client.setVideoClient(videoConnection.registerVideoClient(client.clientId())); + } catch (error) { + logError(LogCategory.VIDEO, tr("Failed to register a video client for %d: %o"), client.clientId(), error); } } diff --git a/shared/js/ui/frames/control-bar/Button.scss b/shared/js/ui/frames/control-bar/Button.scss index fe0293bc..e144d979 100644 --- a/shared/js/ui/frames/control-bar/Button.scss +++ b/shared/js/ui/frames/control-bar/Button.scss @@ -167,19 +167,14 @@ html:root { right: 0; } - :global { - .icon, .icon-container, .icon_em { - vertical-align: middle; - margin-right: 5px; - } + .iconContainer { + margin-right: .25em; - .icon-empty, .icon_empty { - flex-shrink: 0; - flex-grow: 0; + display: flex; + flex-direction: column; - height: 16px; - width: 16px; - } + flex-shrink: 0; + flex-grow: 0; } .dropdownEntry { @@ -204,6 +199,10 @@ html:root { .icon, .arrow { flex-grow: 0; flex-shrink: 0; + + display: flex; + flex-direction: column; + justify-content: center; } .arrow { diff --git a/shared/js/ui/frames/control-bar/Controller.ts b/shared/js/ui/frames/control-bar/Controller.ts index e34c3616..d5d5dd94 100644 --- a/shared/js/ui/frames/control-bar/Controller.ts +++ b/shared/js/ui/frames/control-bar/Controller.ts @@ -3,7 +3,7 @@ import { Bookmark, ControlBarEvents, ControlBarMode, - HostButtonInfo, + HostButtonInfo, VideoDeviceInfo, VideoState } from "tc-shared/ui/frames/control-bar/Definitions"; import {server_connections} from "tc-shared/ConnectionManager"; @@ -24,6 +24,7 @@ import {LogCategory, logWarn} from "tc-shared/log"; import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal"; import {VideoBroadcastState, VideoBroadcastType, VideoConnectionStatus} from "tc-shared/connection/VideoConnection"; import { tr } from "tc-shared/i18n/localize"; +import {getVideoDriver} from "tc-shared/video/VideoSource"; class InfoController { private readonly mode: ControlBarMode; @@ -61,7 +62,7 @@ class InfoController { this.sendVideoState("camera"); })); events.push(bookmarkEvents.on("notify_bookmarks_updated", () => this.sendBookmarks())); - + events.push(getVideoDriver().getEvents().on("notify_device_list_changed", () => this.sendCameraList())) if(this.mode === "main") { events.push(server_connections.events().on("notify_active_handler_changed", event => this.setConnectionHandler(event.newHandler))); } @@ -268,6 +269,26 @@ class InfoController { this.events.fire_react("notify_video_state", { state: state, broadcastType: type }); } + + public sendCameraList() { + let devices: VideoDeviceInfo[] = []; + const driver = getVideoDriver(); + driver.getDevices().then(result => { + if(result === false || result.length === 0) { + return; + } + + this.events.fire_react("notify_camera_list", { + devices: result.map(e => { + return { + name: e.name, + id: e.id + }; + }) + }); + }) + this.events.fire_react("notify_camera_list", { devices: devices }); + } } export function initializePopoutControlBarController(events: Registry, handler: ConnectionHandler) { @@ -294,6 +315,7 @@ export function initializeControlBarController(events: Registry infoHandler.sendSubscribeState()); events.on("query_host_button", () => infoHandler.sendHostButton()); events.on("query_video_state", event => infoHandler.sendVideoState(event.broadcastType)); + events.on("query_camera_list", () => infoHandler.sendCameraList()); events.on("action_connection_connect", event => global_client_actions.fire("action_open_window_connect", { newTab: event.newTab })); events.on("action_connection_disconnect", event => { @@ -378,7 +400,14 @@ export function initializeControlBarController(events: Registry { if(infoHandler.getCurrentHandler()) { - global_client_actions.fire("action_toggle_video_broadcasting", { connection: infoHandler.getCurrentHandler(), broadcastType: event.broadcastType, enabled: event.enable }); + /* TODO: Just update the stream and don't "rebroadcast" */ + global_client_actions.fire("action_toggle_video_broadcasting", { + connection: infoHandler.getCurrentHandler(), + broadcastType: event.broadcastType, + enabled: event.enable, + quickSelect: event.quickStart, + defaultDevice: event.deviceId + }); } else { createErrorModal(tr("Missing connection handler"), tr("Cannot start video broadcasting with a missing connection handler")).open(); } diff --git a/shared/js/ui/frames/control-bar/Definitions.ts b/shared/js/ui/frames/control-bar/Definitions.ts index b1b15e75..0562f462 100644 --- a/shared/js/ui/frames/control-bar/Definitions.ts +++ b/shared/js/ui/frames/control-bar/Definitions.ts @@ -8,6 +8,7 @@ export type AwayState = { locallyAway: boolean, globallyAway: "partial" | "full" export type MicrophoneState = "enabled" | "disabled" | "muted"; export type VideoState = "enabled" | "disabled" | "unavailable" | "unsupported" | "disconnected"; export type HostButtonInfo = { title?: string, target?: string, url: string }; +export type VideoDeviceInfo = { name: string, id: string }; export interface ControlBarEvents { action_connection_connect: { newTab: boolean }, @@ -21,7 +22,8 @@ export interface ControlBarEvents { action_toggle_subscribe: { subscribe: boolean }, action_toggle_query: { show: boolean }, action_query_manage: {}, - action_toggle_video: { broadcastType: VideoBroadcastType, enable: boolean } + action_toggle_video: { broadcastType: VideoBroadcastType, enable: boolean, quickStart?: boolean, deviceId?: string }, + action_manage_video: { broadcastType: VideoBroadcastType } query_mode: {}, query_connection_state: {}, @@ -33,6 +35,7 @@ export interface ControlBarEvents { query_query_state: {}, query_host_button: {}, query_video_state: { broadcastType: VideoBroadcastType }, + query_camera_list: {} notify_mode: { mode: ControlBarMode } notify_connection_state: { state: ConnectionState }, @@ -44,6 +47,7 @@ export interface ControlBarEvents { notify_query_state: { shown: boolean }, notify_host_button: { button: HostButtonInfo | undefined }, notify_video_state: { broadcastType: VideoBroadcastType, state: VideoState }, + notify_camera_list: { devices: VideoDeviceInfo[] } notify_destroy: {} } \ No newline at end of file diff --git a/shared/js/ui/frames/control-bar/DropDown.tsx b/shared/js/ui/frames/control-bar/DropDown.tsx index d8f763f1..28cbb98e 100644 --- a/shared/js/ui/frames/control-bar/DropDown.tsx +++ b/shared/js/ui/frames/control-bar/DropDown.tsx @@ -15,11 +15,11 @@ export interface DropdownEntryProperties { children?: React.ReactElement[] } -const LocalIconRenderer = (props: { icon?: string | RemoteIconInfo }) => { +const LocalIconRenderer = (props: { icon?: string | RemoteIconInfo, className?: string }) => { if(!props.icon || typeof props.icon === "string") { - return + return } else { - return ; + return ; } } @@ -30,7 +30,7 @@ export class DropdownEntry extends ReactComponentBase - + {this.props.text}
@@ -41,7 +41,7 @@ export class DropdownEntry extends ReactComponentBase - + {this.props.text}
); diff --git a/shared/js/ui/frames/control-bar/Renderer.tsx b/shared/js/ui/frames/control-bar/Renderer.tsx index 189ee7a7..e751c196 100644 --- a/shared/js/ui/frames/control-bar/Renderer.tsx +++ b/shared/js/ui/frames/control-bar/Renderer.tsx @@ -6,7 +6,7 @@ import { ControlBarEvents, ControlBarMode, HostButtonInfo, - MicrophoneState, + MicrophoneState, VideoDeviceInfo, VideoState } from "tc-shared/ui/frames/control-bar/Definitions"; import * as React from "react"; @@ -205,6 +205,32 @@ const AwayButton = () => { ); }; +const VideoDeviceList = React.memo(() => { + const events = useContext(Events); + const [ devices, setDevices ] = useState(() => { + events.fire("query_camera_list"); + return []; + }); + events.reactUse("notify_camera_list", event => setDevices(event.devices)); + + if(devices.length === 0) { + return null; + } + + return ( + <> +
+ {devices.map(device => ( + events.fire("action_toggle_video", {enable: true, broadcastType: "camera", deviceId: device.id, quickStart: true})} + /> + ))} + + ); +}); + const VideoButton = (props: { type: VideoBroadcastType }) => { const events = useContext(Events); @@ -221,34 +247,59 @@ const VideoButton = (props: { type: VideoBroadcastType }) => { let tooltip = props.type === "camera" ? tr("Video broadcasting not supported") : tr("Screen sharing not supported"); let modalTitle = props.type === "camera" ? tr("Video broadcasting unsupported") : tr("Screen sharing unsupported"); let modalBody = props.type === "camera" ? tr("Video broadcasting isn't supported by the target server.") : tr("Screen sharing isn't supported by the target server."); - return + ); } case "unavailable": { let tooltip = props.type === "camera" ? tr("Video broadcasting not available") : tr("Screen sharing not available"); let modalTitle = props.type === "camera" ? tr("Video broadcasting unavailable") : tr("Screen sharing unavailable"); let modalBody = props.type === "camera" ? tr("Video broadcasting isn't available right now.") : tr("Screen sharing isn't available right now."); - return + ); } case "disconnected": case "disabled": { let tooltip = props.type === "camera" ? tr("Start video broadcasting") : tr("Start screen sharing"); - return + ); } case "enabled": { let tooltip = props.type === "camera" ? tr("Stop video broadcasting") : tr("Stop screen sharing"); - return + ); } } } @@ -403,12 +454,12 @@ export const ControlBar2 = (props: { events: Registry, classNa items.push(); items.push(
); } - items.push(); items.push(); items.push(); items.push(); items.push(); items.push(
); + items.push(); items.push(); items.push(); items.push(
); diff --git a/shared/js/ui/frames/video/Controller.ts b/shared/js/ui/frames/video/Controller.ts index fb707e2e..4b272c61 100644 --- a/shared/js/ui/frames/video/Controller.ts +++ b/shared/js/ui/frames/video/Controller.ts @@ -4,7 +4,12 @@ import * as ReactDOM from "react-dom"; import {ChannelVideoRenderer} from "tc-shared/ui/frames/video/Renderer"; import {Registry} from "tc-shared/events"; import {ChannelVideoEvents, kLocalVideoId} from "tc-shared/ui/frames/video/Definitions"; -import {VideoBroadcastState, VideoBroadcastType, VideoConnection} from "tc-shared/connection/VideoConnection"; +import { + VideoBroadcastState, + VideoBroadcastType, + VideoClient, + VideoConnection +} from "tc-shared/connection/VideoConnection"; import {ClientEntry, ClientType, LocalClientEntry, MusicClientEntry} from "tc-shared/tree/Client"; import {LogCategory, logError, logWarn} from "tc-shared/log"; import {tr} from "tc-shared/i18n/localize"; @@ -69,6 +74,7 @@ class RemoteClientVideoController implements ClientVideoController { const videoClient = this.client.getVideoClient(); if(videoClient) { + this.initializeVideoClient(videoClient); events.push(videoClient.getEvents().on("notify_broadcast_state_changed", event => { console.error("Broadcast state changed: %o - %o - %o", event.broadcastType, VideoBroadcastState[event.oldState], VideoBroadcastState[event.newState]); if(event.newState === VideoBroadcastState.Stopped || event.oldState === VideoBroadcastState.Stopped) { @@ -81,6 +87,18 @@ class RemoteClientVideoController implements ClientVideoController { } } + protected initializeVideoClient(videoClient: VideoClient) { + this.eventListenerVideoClient.push(videoClient.getEvents().on("notify_broadcast_state_changed", event => { + console.error("Broadcast state changed: %o - %o - %o", event.broadcastType, VideoBroadcastState[event.oldState], VideoBroadcastState[event.newState]); + if(event.newState === VideoBroadcastState.Stopped || event.oldState === VideoBroadcastState.Stopped) { + /* we've a new broadcast which hasn't been dismissed yet */ + this.dismissed[event.broadcastType] = false; + } + this.notifyVideo(); + this.notifyMuteState(); + })); + } + destroy() { this.eventListenerVideoClient?.forEach(callback => callback()); this.eventListenerVideoClient = undefined; @@ -231,7 +249,20 @@ class LocalVideoController extends RemoteClientVideoController { super(client, eventRegistry, kLocalVideoId); const videoConnection = client.channelTree.client.serverConnection.getVideoConnection(); - this.eventListener.push(videoConnection.getEvents().on("notify_local_broadcast_state_changed", () => this.notifyVideo())); + this.eventListener.push(videoConnection.getEvents().on("notify_local_broadcast_state_changed", () => { + this.notifyVideo(); + })); + } + + protected initializeVideoClient(videoClient: VideoClient) { + super.initializeVideoClient(videoClient); + + this.eventListenerVideoClient.push(videoClient.getEvents().on("notify_broadcast_state_changed", event => { + if(event.newState === VideoBroadcastState.Available) { + /* we want to watch our own broadcast */ + videoClient.joinBroadcast(event.broadcastType).then(undefined); + } + })) } isBroadcasting() { @@ -239,14 +270,11 @@ class LocalVideoController extends RemoteClientVideoController { return videoConnection.isBroadcasting("camera") || videoConnection.isBroadcasting("screen"); } - async getStatistics(target: VideoBroadcastType) { - - } - protected isVideoActive(): boolean { return true; } + /* protected getBroadcastState(target: VideoBroadcastType): VideoBroadcastState { const videoConnection = this.client.channelTree.client.serverConnection.getVideoConnection(); return videoConnection.getBroadcastingState(target); @@ -256,6 +284,7 @@ class LocalVideoController extends RemoteClientVideoController { const videoConnection = this.client.channelTree.client.serverConnection.getVideoConnection(); return videoConnection.getBroadcastingSource(target)?.getStream(); } + */ } class ChannelVideoController { @@ -540,6 +569,10 @@ class ChannelVideoController { if(channel) { const clients = channel.channelClientsOrdered(); for(const client of clients) { + if(client instanceof LocalClientEntry) { + continue; + } + if(!this.clientVideos[client.clientId()]) { /* should not be possible (Is only possible for the local client) */ continue; diff --git a/shared/js/ui/modal/permission/ModalPermissionEditor.tsx b/shared/js/ui/modal/permission/ModalPermissionEditor.tsx index fe371044..7d4ce987 100644 --- a/shared/js/ui/modal/permission/ModalPermissionEditor.tsx +++ b/shared/js/ui/modal/permission/ModalPermissionEditor.tsx @@ -321,17 +321,16 @@ class PermissionEditorModal extends InternalModal { renderBody() { return (
- -
- - -
-
- - -
-
+
+ + +
+ +
+ + +
); } diff --git a/shared/js/ui/modal/video-source/Controller.tsx b/shared/js/ui/modal/video-source/Controller.tsx index 530b55e5..8dea6720 100644 --- a/shared/js/ui/modal/video-source/Controller.tsx +++ b/shared/js/ui/modal/video-source/Controller.tsx @@ -6,42 +6,48 @@ import {getVideoDriver, VideoPermissionStatus, VideoSource} from "tc-shared/vide import {LogCategory, logError} from "tc-shared/log"; import {VideoBroadcastType} from "tc-shared/connection/VideoConnection"; -type VideoSourceRef = { source: VideoSource }; +type SourceConstraints = { width?: number, height?: number, frameRate?: number }; /** * @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. + * @param selectMode + * @param defaultDeviceId */ -export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, selectDefault: boolean, quickSelect: boolean) : Promise { - const refSource: VideoSourceRef = { - source: undefined - }; +export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, selectMode: "quick" | "default" | "none", defaultDeviceId?: string) : Promise { + const controller = new VideoSourceController(type); - const events = new Registry(); - events.enableDebug("video-source-select"); - initializeController(events, refSource, type, quickSelect); + if(selectMode === "quick") { + /* We need the modal itself for the native client in order to present the window selector */ + if(type === "camera" || __build.target === "web") { + /* Try to get the default device. If we succeeded directly return that */ + if(await controller.selectSource(defaultDeviceId)) { + const source = controller.getCurrentSource()?.ref(); + controller.destroy(); + + return source; + } + } + } + + const modal = spawnReactModal(ModalVideoSource, controller.events, type); + controller.events.on(["action_start", "action_cancel"], () => modal.destroy()); - 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 }); + if(selectMode === "default" || selectMode === "quick") { + if(type === "screen" && getVideoDriver().screenQueryAvailable()) { + controller.events.fire_react("action_toggle_screen_capture_device_select", { shown: true }); + } else { + controller.selectSource(defaultDeviceId); + } } }); + let refSource: { source: VideoSource } = { source: undefined }; + controller.events.on("action_start", () => refSource.source = controller.getCurrentSource()?.ref()); + await new Promise(resolve => { - if(type === "screen" && quickSelect) { - events.on("notify_video_preview", event => { + if(type === "screen" && selectMode === "quick") { + controller.events.on("notify_video_preview", event => { if(event.status.status === "preview") { /* we've successfully selected something */ modal.destroy(); @@ -52,106 +58,287 @@ export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, sele modal.events.one(["destroy", "close"], resolve); }); + controller.destroy(); return refSource.source; } -type SourceConstraints = { width?: number, height?: number, frameRate?: number }; +class VideoSourceController { + readonly events: Registry; + private readonly type: VideoBroadcastType; -function initializeController(events: Registry, currentSourceRef: VideoSourceRef, type: VideoBroadcastType, quickSelect: boolean) { - let currentSource: VideoSource | string; - let currentConstraints: SourceConstraints; + private currentSource: VideoSource | string; + private currentConstraints: SourceConstraints; /* preselected current source id */ - let currentSourceId: string; + private currentSourceId: string; + /* fallback current source name if "currentSource" is empty */ - let fallbackCurrentSourceName: string; + private fallbackCurrentSourceName: string; - const notifyStartButton = () => { - events.fire_react("notify_start_button", { enabled: typeof currentSource === "object" }) - }; + constructor(type: VideoBroadcastType) { + this.type = type; + this.events = new Registry(); + this.events.enableDebug("video-source-select"); - const notifyDeviceList = () => { + + this.events.on("query_source", () => this.notifyCurrentSource()); + this.events.on("query_device_list", () => this.notifyDeviceList()); + this.events.on("query_screen_capture_devices", () => this.notifyScreenCaptureDevices()); + this.events.on("query_video_preview", () => this.notifyVideoPreview()); + this.events.on("query_start_button", () => this.notifyStartButton()); + this.events.on("query_setting_dimension", () => this.notifySettingDimension()); + this.events.on("query_setting_framerate", () => this.notifySettingFramerate()); + + this.events.on("action_request_permissions", () => { + getVideoDriver().requestPermissions().then(result => { + if(typeof result === "object") { + this.currentSourceId = result.getId() + " --"; + this.fallbackCurrentSourceName = result.getName(); + this.notifyDeviceList(); + + this.setCurrentSource(result); + } else { + /* the device list will already be updated due to the notify_permissions_changed event */ + } + }); + }); + + this.events.on("action_select_source", event => { + const driver = getVideoDriver(); + + if(type === "camera") { + this.currentSourceId = event.id; + this.fallbackCurrentSourceName = tr("loading..."); + this.notifyDeviceList(); + + driver.createVideoSource(this.currentSourceId).then(stream => { + this.fallbackCurrentSourceName = stream.getName(); + this.setCurrentSource(stream); + }).catch(error => { + this.fallbackCurrentSourceName = "invalid device"; + if(typeof error === "string") { + this.setCurrentSource(error); + } else { + logError(LogCategory.GENERAL, tr("Failed to open video device %s: %o"), event.id, error); + this.setCurrentSource(tr("Failed to open video device (Lookup the console)")); + } + }); + } else if(driver.screenQueryAvailable() && typeof event.id === "undefined") { + this.events.fire_react("action_toggle_screen_capture_device_select", { shown: true }); + } else { + this.currentSourceId = undefined; + this.fallbackCurrentSourceName = tr("loading..."); + driver.createScreenSource(event.id, false).then(stream => { + this.setCurrentSource(stream); + this.fallbackCurrentSourceName = stream?.getName() || tr("No stream"); + }).catch(error => { + this.fallbackCurrentSourceName = "screen capture failed"; + if(typeof error === "string") { + this.setCurrentSource(error); + } else { + logError(LogCategory.GENERAL, tr("Failed to open screen capture device %s: %o"), event.id, error); + this.setCurrentSource(tr("Failed to open screen capture device (Lookup the console)")); + } + }); + } + }); + + this.events.on("action_cancel", () => { + this.setCurrentSource(undefined); + }); + + if(type === "camera") { + /* only the camara requires a device list */ + this.events.on("notify_destroy", getVideoDriver().getEvents().on("notify_permissions_changed", () => { + if(getVideoDriver().getPermissionStatus() !== VideoPermissionStatus.Granted) { + this.currentSourceId = undefined; + this.fallbackCurrentSourceName = undefined; + this.notifyDeviceList(); + + /* implicitly updates the start button */ + this.setCurrentSource(undefined); + } else { + this.notifyDeviceList(); + this.notifyVideoPreview(); + this.notifyStartButton(); + } + })); + } + + const applyConstraints = async () => { + if(typeof this.currentSource === "object") { + const videoTrack = this.currentSource.getStream().getVideoTracks()[0]; + if(!videoTrack) { return; } + + await videoTrack.applyConstraints(this.currentConstraints); + } + }; + + this.events.on("action_setting_dimension", event => { + this.currentConstraints.height = event.height; + this.currentConstraints.width = event.width; + applyConstraints().then(undefined); + }); + + this.events.on("action_setting_framerate", event => { + this.currentConstraints.frameRate = event.frameRate; + applyConstraints().then(undefined); + }); + } + + destroy() { + if(typeof this.currentSource === "object") { + this.currentSource.deref(); + this.currentSource = undefined; + } + + this.events.fire("notify_destroy"); + this.events.destroy(); + } + + setCurrentSource(source: VideoSource | string | undefined) { + if(typeof this.currentSource === "object") { + this.currentSource.deref(); + } + + this.currentConstraints = {}; + this.currentSource = source; + this.notifyVideoPreview(); + this.notifyStartButton(); + this.notifyCurrentSource(); + this.notifySettingDimension(); + this.notifySettingFramerate(); + } + + async selectSource(sourceId: string) : Promise { + const driver = getVideoDriver(); + + let streamPromise: Promise; + if(this.type === "camera") { + this.currentSourceId = sourceId; + this.fallbackCurrentSourceName = tr("loading..."); + this.notifyDeviceList(); + + streamPromise = driver.createVideoSource(this.currentSourceId); + } else if(driver.screenQueryAvailable() && typeof sourceId === "undefined") { + /* TODO: What the hack is this?! */ + this.events.fire_react("action_toggle_screen_capture_device_select", { shown: true }); + return; + } else { + this.currentSourceId = undefined; + this.fallbackCurrentSourceName = tr("loading..."); + streamPromise = driver.createScreenSource(sourceId, false); + } + + try { + const stream = await streamPromise; + this.setCurrentSource(stream); + this.fallbackCurrentSourceName = stream?.getName() || tr("No stream"); + + return !!stream; + } catch (error) { + this.fallbackCurrentSourceName = tr("failed to attach to device"); + if(typeof error === "string") { + this.setCurrentSource(error); + } else { + logError(LogCategory.GENERAL, tr("Failed to open capture device %s: %o"), sourceId, error); + this.setCurrentSource(tr("Failed to open capture device (Lookup the console)")); + } + + return false; + } + } + + getCurrentSource() : VideoSource | undefined { + return typeof this.currentSource === "object" ? this.currentSource : undefined; + } + + private notifyStartButton() { + this.events.fire_react("notify_start_button", { enabled: typeof this.currentSource === "object" }) + } + + private 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" } }); + this.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" } }); + this.events.fire_react("notify_device_list", { status: { status: "error", reason: "request-permissions" } }); } } else { - events.fire_react("notify_device_list", { + this.events.fire_react("notify_device_list", { status: { status: "success", devices: devices.map(e => { return { id: e.id, displayName: e.name }}), - selectedDeviceId: currentSourceId, - fallbackSelectedDeviceName: fallbackCurrentSourceName + selectedDeviceId: this.currentSourceId, + fallbackSelectedDeviceName: this.fallbackCurrentSourceName } }); } }); } - const notifyScreenCaptureDevices = () => { + private notifyScreenCaptureDevices(){ const driver = getVideoDriver(); driver.queryScreenCaptureDevices().then(devices => { - events.fire_react("notify_screen_capture_devices", { devices: { status: "success", devices: devices }}); + this.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 }}); + this.events.fire_react("notify_screen_capture_devices", { devices: { status: "error", reason: error }}); }) } - const notifyVideoPreview = () => { + private notifyVideoPreview(){ const driver = getVideoDriver(); switch (driver.getPermissionStatus()) { case VideoPermissionStatus.SystemDenied: - events.fire_react("notify_video_preview", { status: { status: "error", reason: "no-permissions" }}); + this.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" }}); + this.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() - }}); + if(typeof this.currentSource === "string") { + this.events.fire_react("notify_video_preview", { status: { + status: "error", + reason: "custom", + message: this.currentSource + }}); + } else if(this.currentSource) { + this.events.fire_react("notify_video_preview", { status: { + status: "preview", + stream: this.currentSource.getStream() + }}); } else { - events.fire_react("notify_video_preview", { status: { status: "none" }}); + this.events.fire_react("notify_video_preview", { status: { status: "none" }}); } break; } }; - const notifyCurrentSource = () => { - if(typeof currentSource === "object") { - events.fire_react("notify_source", { + private notifyCurrentSource(){ + if(typeof this.currentSource === "object") { + this.events.fire_react("notify_source", { state: { type: "selected", - deviceId: currentSource.getId(), - name: currentSource?.getName() || fallbackCurrentSourceName + deviceId: this.currentSource.getId(), + name: this.currentSource?.getName() || this.fallbackCurrentSourceName } }); - } else if(typeof currentSource === "string") { - events.fire_react("notify_source", { + } else if(typeof this.currentSource === "string") { + this.events.fire_react("notify_source", { state: { type: "errored", - error: currentSource + error: this.currentSource } }); } else { - events.fire_react("notify_source", { + this.events.fire_react("notify_source", { state: { type: "none" } @@ -159,13 +346,13 @@ function initializeController(events: Registry, currentS } } - const notifySettingDimension = () => { - if(typeof currentSource === "object") { - const videoTrack = currentSource.getStream().getVideoTracks()[0]; + private notifySettingDimension(){ + if(typeof this.currentSource === "object") { + const videoTrack = this.currentSource.getStream().getVideoTracks()[0]; const settings = videoTrack.getSettings(); const capabilities = "getCapabilities" in videoTrack ? videoTrack.getCapabilities() : undefined; - events.fire_react("notify_setting_dimension", { + this.events.fire_react("notify_setting_dimension", { setting: { minWidth: capabilities?.width ? capabilities.width.min : 1, maxWidth: capabilities?.width ? capabilities.width.max : settings.width, @@ -181,18 +368,18 @@ function initializeController(events: Registry, currentS } }); } else { - events.fire_react("notify_setting_dimension", { setting: undefined }); + this.events.fire_react("notify_setting_dimension", { setting: undefined }); } }; - const notifySettingFramerate = () => { - if(typeof currentSource === "object") { - const videoTrack = currentSource.getStream().getVideoTracks()[0]; + notifySettingFramerate() { + if(typeof this.currentSource === "object") { + const videoTrack = this.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", { + this.events.fire_react("notify_settings_framerate", { frameRate: { min: round(capabilities?.frameRate ? capabilities.frameRate.min : 1), max: round(capabilities?.frameRate ? capabilities.frameRate.max : settings.frameRate), @@ -200,137 +387,7 @@ function initializeController(events: Registry, currentS } }); } else { - events.fire_react("notify_settings_framerate", { frameRate: undefined }); + this.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); - }); } \ No newline at end of file diff --git a/shared/js/ui/react-elements/Icon.tsx b/shared/js/ui/react-elements/Icon.tsx index a7d4bac5..d41e72e2 100644 --- a/shared/js/ui/react-elements/Icon.tsx +++ b/shared/js/ui/react-elements/Icon.tsx @@ -12,7 +12,7 @@ export const IconRenderer = (props: { if(!props.icon) { return
; } else if(typeof props.icon === "string") { - return
; + return
; } else { throw "JQuery icons are not longer supported"; }