Some video related changes and bugfixes (requires nightly 18)

master
WolverinDEV 2020-12-12 00:16:17 +01:00 committed by WolverinDEV
parent de527a750d
commit 0457c68a04
19 changed files with 524 additions and 316 deletions

View File

@ -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

View File

@ -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<MediaStreamTrack> {
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);

View File

@ -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
}]
}
}
}

View File

@ -222,10 +222,6 @@ export class RtpVideoConnection implements VideoConnection {
}
async startBroadcasting(type: VideoBroadcastType, source: VideoSource) : Promise<void> {
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]);

View File

@ -192,7 +192,8 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
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 {

View File

@ -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: {

View File

@ -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"
];

View File

@ -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 <React.Fragment key={"er-" + ++reactId}>{"[img]" + content + "[/img]"}</React.Fragment>;
}
return (
<div key={"irc-" + ++reactId} className={"xbbcode-tag-img"}>

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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 {

View File

@ -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<ControlBarEvents>, handler: ConnectionHandler) {
@ -294,6 +315,7 @@ export function initializeControlBarController(events: Registry<ControlBarEvents
events.on("query_subscribe_state", () => 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<ControlBarEvents
});
events.on("action_toggle_video", event => {
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();
}

View File

@ -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: {}
}

View File

@ -15,11 +15,11 @@ export interface DropdownEntryProperties {
children?: React.ReactElement<DropdownEntry>[]
}
const LocalIconRenderer = (props: { icon?: string | RemoteIconInfo }) => {
const LocalIconRenderer = (props: { icon?: string | RemoteIconInfo, className?: string }) => {
if(!props.icon || typeof props.icon === "string") {
return <IconRenderer icon={props.icon as any} key={"fixed-icon"} />
return <IconRenderer icon={props.icon as any} key={"fixed-icon"} className={props.className} />
} else {
return <RemoteIconRenderer icon={getIconManager().resolveIcon(props.icon.iconId, props.icon.serverUniqueId)} key={"remote-icon"} />;
return <RemoteIconRenderer icon={getIconManager().resolveIcon(props.icon.iconId, props.icon.serverUniqueId)} key={"remote-icon"} className={props.className} />;
}
}
@ -30,7 +30,7 @@ export class DropdownEntry extends ReactComponentBase<DropdownEntryProperties, {
if(this.props.children) {
return (
<div className={cssStyle.dropdownEntry} onClick={this.props.onClick} onAuxClick={this.props.onAuxClick} onContextMenu={this.props.onContextMenu}>
<LocalIconRenderer icon={this.props.icon} />
<LocalIconRenderer icon={this.props.icon} className={cssStyle.iconContainer} />
<a className={cssStyle.entryName}>{this.props.text}</a>
<div className={cssStyle.arrow + " " + cssStyle.right} />
<DropdownContainer>
@ -41,7 +41,7 @@ export class DropdownEntry extends ReactComponentBase<DropdownEntryProperties, {
} else {
return (
<div className={cssStyle.dropdownEntry} onClick={this.props.onClick} onAuxClick={this.props.onAuxClick} onContextMenu={this.props.onContextMenu}>
<LocalIconRenderer icon={this.props.icon} />
<LocalIconRenderer icon={this.props.icon} className={cssStyle.iconContainer} />
<a className={cssStyle.entryName}>{this.props.text}</a>
</div>
);

View File

@ -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<VideoDeviceInfo[]>(() => {
events.fire("query_camera_list");
return [];
});
events.reactUse("notify_camera_list", event => setDevices(event.devices));
if(devices.length === 0) {
return null;
}
return (
<>
<hr key={"hr"} />
{devices.map(device => (
<DropdownEntry
text={device.name || tr("Unknown device name")}
key={device.id}
onClick={() => 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 <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={icon} tooltip={tooltip}
key={"unsupported"}
onToggle={() => createErrorModal(modalTitle, modalBody).open()}
/>;
let dropdownText = props.type === "camera" ? tr("Start screen sharing") : tr("Start video broadcasting");
return (
<Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={icon} tooltip={tooltip}
key={"unsupported"}
onToggle={() => createErrorModal(modalTitle, modalBody).open()}
>
<DropdownEntry text={dropdownText} onClick={() => createErrorModal(modalTitle, modalBody).open()} />
</Button>
);
}
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 <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={icon} tooltip={tooltip}
key={"unavailable"}
onToggle={() => createErrorModal(modalTitle, modalBody).open()}/>;
let dropdownText = props.type === "camera" ? tr("Start screen sharing") : tr("Start video broadcasting");
return (
<Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={icon} tooltip={tooltip}
key={"unavailable"}
onToggle={() => createErrorModal(modalTitle, modalBody).open()} >
<DropdownEntry text={dropdownText} onClick={() => createErrorModal(modalTitle, modalBody).open()} />
</Button>
);
}
case "disconnected":
case "disabled": {
let tooltip = props.type === "camera" ? tr("Start video broadcasting") : tr("Start screen sharing");
return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={icon}
onToggle={() => events.fire("action_toggle_video", {enable: true, broadcastType: props.type})}
tooltip={tooltip} key={"enable"}/>;
let dropdownText = props.type === "camera" ? tr("Start screen sharing") : tr("Start video broadcasting");
return (
<Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={icon}
onToggle={() => events.fire("action_toggle_video", {enable: true, broadcastType: props.type, quickStart: true})}
tooltip={tooltip} key={"enable"}>
<DropdownEntry icon={icon} text={dropdownText} onClick={() => events.fire("action_toggle_video", {enable: true, broadcastType: props.type})} />
{props.type === "camera" ? <VideoDeviceList key={"list"} /> : null}
</Button>
);
}
case "enabled": {
let tooltip = props.type === "camera" ? tr("Stop video broadcasting") : tr("Stop screen sharing");
return <Button switched={false} colorTheme={"red"} autoSwitch={false} iconNormal={icon}
onToggle={() => events.fire("action_toggle_video", {enable: false, broadcastType: props.type})}
tooltip={tooltip} key={"disable"}/>;
let dropdownTextManage = props.type === "camera" ? tr("Configure/change screen sharing") : tr("Configure/change video broadcasting");
let dropdownTextStop = props.type === "camera" ? tr("Stop screen sharing") : tr("Stop video broadcasting");
return (
<Button switched={false} colorTheme={"red"} autoSwitch={false} iconNormal={icon}
onToggle={() => events.fire("action_toggle_video", {enable: false, broadcastType: props.type})}
tooltip={tooltip} key={"disable"}>
{/* <DropdownEntry icon={icon} text={dropdownTextManage} onClick={() => events.fire("action_manage_video", { broadcastType: props.type })} /> TODO! */}
<DropdownEntry icon={icon} text={dropdownTextStop} onClick={() => events.fire("action_toggle_video", {enable: false, broadcastType: props.type})} />
<VideoDeviceList />
{props.type === "camera" ? <VideoDeviceList key={"list"} /> : null}
</Button>
);
}
}
}
@ -403,12 +454,12 @@ export const ControlBar2 = (props: { events: Registry<ControlBarEvents>, classNa
items.push(<BookmarkButton key={"bookmarks"} />);
items.push(<div className={cssStyle.divider + " " + cssStyle.hideSmallPopout} key={"divider-1"} />);
}
items.push(<AwayButton key={"away"} />);
items.push(<VideoButton key={"video"} type={"camera"} />);
items.push(<VideoButton key={"screen"} type={"screen"} />);
items.push(<MicrophoneButton key={"microphone"} />);
items.push(<SpeakerButton key={"speaker"} />);
items.push(<div className={cssStyle.divider + " " + cssStyle.hideSmallPopout} key={"divider-2"} />);
items.push(<AwayButton key={"away"} />);
items.push(<SubscribeButton key={"subscribe"} />);
items.push(<QueryButton key={"query"} />);
items.push(<div className={cssStyle.spacer} key={"spacer"} />);

View File

@ -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;

View File

@ -321,17 +321,16 @@ class PermissionEditorModal extends InternalModal {
renderBody() {
return (
<div className={cssStyle.container}>
<ContextDivider id={"permission-editor"} defaultValue={25} direction={"horizontal"}>
<div className={cssStyle.contextContainer + " " + cssStyle.left}>
<ActiveTabInfo events={this.modalEvents}/>
<SideBar modalEvents={this.modalEvents} editorEvents={this.editorEvents}
connection={this.connection}/>
</div>
<div className={cssStyle.contextContainer + " " + cssStyle.right}>
<TabSelector events={this.modalEvents}/>
<PermissionEditor events={this.editorEvents} connection={this.connection}/>
</div>
</ContextDivider>
<div className={cssStyle.contextContainer + " " + cssStyle.left}>
<ActiveTabInfo events={this.modalEvents}/>
<SideBar modalEvents={this.modalEvents} editorEvents={this.editorEvents}
connection={this.connection}/>
</div>
<ContextDivider id={"permission-editor"} defaultValue={25} direction={"horizontal"} />
<div className={cssStyle.contextContainer + " " + cssStyle.right}>
<TabSelector events={this.modalEvents}/>
<PermissionEditor events={this.editorEvents} connection={this.connection}/>
</div>
</div>
);
}

View File

@ -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<VideoSource> {
const refSource: VideoSourceRef = {
source: undefined
};
export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, selectMode: "quick" | "default" | "none", defaultDeviceId?: string) : Promise<VideoSource> {
const controller = new VideoSourceController(type);
const events = new Registry<ModalVideoSourceEvents>();
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<ModalVideoSourceEvents>;
private readonly type: VideoBroadcastType;
function initializeController(events: Registry<ModalVideoSourceEvents>, 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<ModalVideoSourceEvents>();
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<boolean> {
const driver = getVideoDriver();
let streamPromise: Promise<VideoSource>;
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<ModalVideoSourceEvents>, 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<ModalVideoSourceEvents>, 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<ModalVideoSourceEvents>, 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);
});
}

View File

@ -12,7 +12,7 @@ export const IconRenderer = (props: {
if(!props.icon) {
return <div className={cssStyle.container + " icon-container icon-empty " + props.className} title={props.title} />;
} else if(typeof props.icon === "string") {
return <div className={cssStyle.container + " icon " + props.icon + " " + props.className} title={props.title} />;
return <div className={cssStyle.container + " icon_em " + props.icon + " " + props.className} title={props.title} />;
} else {
throw "JQuery icons are not longer supported";
}