Some video related changes and bugfixes (requires nightly 18)
parent
de527a750d
commit
0457c68a04
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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"
|
||||
];
|
||||
|
|
|
@ -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"}>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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: {}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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"} />);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue