Implemented support for the native client
parent
1759fb1756
commit
33422db12e
|
@ -1,10 +1,14 @@
|
||||||
# Changelog:
|
# Changelog:
|
||||||
** 22.11.20**
|
* **29.11.20**
|
||||||
|
- Added support for the native client to show and broadcast video
|
||||||
|
- By default using a quick select method when sharing the screen
|
||||||
|
|
||||||
|
* **22.11.20**
|
||||||
- Added a ton of video settings
|
- Added a ton of video settings
|
||||||
- Added screen sharing (Currently via the camera channel)
|
- Added screen sharing (Currently via the camera channel)
|
||||||
- Using codec H264 instead of VP8
|
- Using codec H264 instead of VP8
|
||||||
|
|
||||||
** 14.11.20**
|
* **14.11.20**
|
||||||
- Fixed bug where the microphone has been requested when muting it.
|
- Fixed bug where the microphone has been requested when muting it.
|
||||||
|
|
||||||
* **07.11.20**
|
* **07.11.20**
|
||||||
|
|
|
@ -3,20 +3,6 @@ import * as template_loader from "./template_loader";
|
||||||
import * as Animation from "../animation";
|
import * as Animation from "../animation";
|
||||||
import {getUrlParameter} from "./utils";
|
import {getUrlParameter} from "./utils";
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
tr(message: string) : string;
|
|
||||||
tra(message: string, ...args: (string | number | boolean)[]) : string;
|
|
||||||
tra(message: string, ...args: any[]) : JQuery[];
|
|
||||||
|
|
||||||
log: any;
|
|
||||||
StaticSettings: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
//const tr: typeof window.tr;
|
|
||||||
//const tra: typeof window.tra;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApplicationLoader {
|
export interface ApplicationLoader {
|
||||||
execute();
|
execute();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
tr(message: string) : string;
|
|
||||||
tra(message: string, ...args: (string | number | boolean)[]) : string;
|
|
||||||
tra(message: string, ...args: any[]) : JQuery[];
|
|
||||||
|
|
||||||
log: any;
|
|
||||||
StaticSettings: any;
|
|
||||||
|
|
||||||
detectedBrowser: any;
|
|
||||||
__native_client_init_shared: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tr: typeof window.tr;
|
|
||||||
const tra: typeof window.tra;
|
|
||||||
|
|
||||||
/* webpack compiler variable */
|
|
||||||
const __build;
|
|
||||||
const __webpack_require__;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {};
|
|
|
@ -187,11 +187,16 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
|
||||||
|
|
||||||
event_registry.on("action_toggle_video_broadcasting", event => {
|
event_registry.on("action_toggle_video_broadcasting", event => {
|
||||||
if(event.enabled) {
|
if(event.enabled) {
|
||||||
spawnVideoSourceSelectModal(event.broadcastType, true).then(async source => {
|
const connection = event.connection;
|
||||||
|
if(!connection.connected) {
|
||||||
|
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 => {
|
||||||
if(!source) { return; }
|
if(!source) { return; }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
event.connection.getServerConnection().getVideoConnection().startBroadcasting(event.broadcastType, source)
|
connection.getServerConnection().getVideoConnection().startBroadcasting(event.broadcastType, source)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
logError(LogCategory.VIDEO, tr("Failed to start %s broadcasting: %o"), event.broadcastType, error);
|
logError(LogCategory.VIDEO, tr("Failed to start %s broadcasting: %o"), event.broadcastType, error);
|
||||||
if(typeof error !== "string") {
|
if(typeof error !== "string") {
|
||||||
|
|
|
@ -29,7 +29,7 @@ export interface ClientGlobalControlEvents {
|
||||||
videoUrl: string,
|
videoUrl: string,
|
||||||
handlerId: string
|
handlerId: string
|
||||||
},
|
},
|
||||||
action_toggle_video_broadcasting: { connection: ConnectionHandler, enabled: boolean, broadcastType: VideoBroadcastType }
|
action_toggle_video_broadcasting: { connection: ConnectionHandler, enabled: boolean, broadcastType: VideoBroadcastType, quickSelect?: boolean }
|
||||||
|
|
||||||
/* some more specific window openings */
|
/* some more specific window openings */
|
||||||
action_open_window_connect: {
|
action_open_window_connect: {
|
||||||
|
|
|
@ -326,5 +326,19 @@ export async function initialize() {
|
||||||
// await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/test.json");
|
// await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/test.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
tr(message: string) : string;
|
||||||
|
tra(message: string, ...args: (string | number | boolean)[]) : string;
|
||||||
|
tra(message: string, ...args: any[]) : JQuery[];
|
||||||
|
|
||||||
|
log: any;
|
||||||
|
StaticSettings: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tr: typeof window.tr;
|
||||||
|
const tra: typeof window.tra;
|
||||||
|
}
|
||||||
|
|
||||||
window.tr = tr;
|
window.tr = tr;
|
||||||
window.tra = tra;
|
window.tra = tra;
|
|
@ -19,7 +19,7 @@ export interface MediaStreamEvents {
|
||||||
export const mediaStreamEvents = new Registry<MediaStreamEvents>();
|
export const mediaStreamEvents = new Registry<MediaStreamEvents>();
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function requestMediaStream0(constraints: MediaTrackConstraints, type: MediaStreamType, updateDeviceList: boolean) : Promise<MediaStreamRequestResult | MediaStream> {
|
export async function requestMediaStreamWithConstraints(constraints: MediaTrackConstraints, type: MediaStreamType) : Promise<MediaStreamRequestResult | MediaStream> {
|
||||||
const beginTimestamp = Date.now();
|
const beginTimestamp = Date.now();
|
||||||
try {
|
try {
|
||||||
log.info(LogCategory.AUDIO, tr("Requesting a %s stream for device %s in group %s"), type, constraints.deviceId, constraints.groupId);
|
log.info(LogCategory.AUDIO, tr("Requesting a %s stream for device %s in group %s"), type, constraints.deviceId, constraints.groupId);
|
||||||
|
@ -74,7 +74,7 @@ export async function requestMediaStream(deviceId: string | undefined, groupId:
|
||||||
constrains.autoGainControl = true;
|
constrains.autoGainControl = true;
|
||||||
constrains.noiseSuppression = true;
|
constrains.noiseSuppression = true;
|
||||||
|
|
||||||
const promise = (currentMediaStreamRequest = requestMediaStream0(constrains, type, true));
|
const promise = (currentMediaStreamRequest = requestMediaStreamWithConstraints(constrains, type));
|
||||||
try {
|
try {
|
||||||
return await currentMediaStreamRequest;
|
return await currentMediaStreamRequest;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
ScreenCaptureDevice,
|
||||||
VideoDevice,
|
VideoDevice,
|
||||||
VideoDriver,
|
VideoDriver,
|
||||||
VideoDriverEvents,
|
VideoDriverEvents,
|
||||||
|
@ -212,7 +213,15 @@ export class WebVideoDriver implements VideoDriver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createScreenSource(): Promise<VideoSource> {
|
screenQueryAvailable(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async queryScreenCaptureDevices(): Promise<ScreenCaptureDevice[]> {
|
||||||
|
throw tr("screen capture device query not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
async createScreenSource(_id: string | undefined, _allowFocusLoss: boolean): Promise<VideoSource> {
|
||||||
try {
|
try {
|
||||||
const source = await navigator.mediaDevices.getDisplayMedia({ audio: false, video: true });
|
const source = await navigator.mediaDevices.getDisplayMedia({ audio: false, video: true });
|
||||||
const videoTrack = source.getVideoTracks()[0];
|
const videoTrack = source.getVideoTracks()[0];
|
||||||
|
@ -233,7 +242,7 @@ export class WebVideoDriver implements VideoDriver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebVideoSource implements VideoSource {
|
export class WebVideoSource implements VideoSource {
|
||||||
private readonly deviceId: string;
|
private readonly deviceId: string;
|
||||||
private readonly displayName: string;
|
private readonly displayName: string;
|
||||||
private readonly stream: MediaStream;
|
private readonly stream: MediaStream;
|
||||||
|
|
|
@ -58,6 +58,7 @@ export interface ChannelVideoEvents {
|
||||||
action_toggle_expended: { expended: boolean },
|
action_toggle_expended: { expended: boolean },
|
||||||
action_video_scroll: { direction: "left" | "right" },
|
action_video_scroll: { direction: "left" | "right" },
|
||||||
action_set_spotlight: { videoId: string | undefined, expend: boolean },
|
action_set_spotlight: { videoId: string | undefined, expend: boolean },
|
||||||
|
action_focus_spotlight: {},
|
||||||
action_set_fullscreen: { videoId: string | undefined },
|
action_set_fullscreen: { videoId: string | undefined },
|
||||||
action_toggle_mute: { videoId: string, broadcastType: VideoBroadcastType, muted: boolean },
|
action_toggle_mute: { videoId: string, broadcastType: VideoBroadcastType, muted: boolean },
|
||||||
|
|
||||||
|
|
|
@ -240,6 +240,7 @@ const VideoContainer = React.memo((props: { videoId: string, isSpotlight: boolea
|
||||||
events.fire("action_set_fullscreen", { videoId: props.videoId });
|
events.fire("action_set_fullscreen", { videoId: props.videoId });
|
||||||
} else {
|
} else {
|
||||||
events.fire("action_set_spotlight", { videoId: props.videoId, expend: true });
|
events.fire("action_set_spotlight", { videoId: props.videoId, expend: true });
|
||||||
|
events.fire("action_focus_spotlight", { });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onContextMenu={event => {
|
onContextMenu={event => {
|
||||||
|
@ -262,6 +263,7 @@ const VideoContainer = React.memo((props: { videoId: string, isSpotlight: boolea
|
||||||
icon: ClientIcon.Fullscreen,
|
icon: ClientIcon.Fullscreen,
|
||||||
click: () => {
|
click: () => {
|
||||||
events.fire("action_set_spotlight", { videoId: props.isSpotlight ? undefined : props.videoId, expend: true });
|
events.fire("action_set_spotlight", { videoId: props.isSpotlight ? undefined : props.videoId, expend: true });
|
||||||
|
events.fire("action_focus_spotlight", { });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
@ -277,6 +279,7 @@ const VideoContainer = React.memo((props: { videoId: string, isSpotlight: boolea
|
||||||
events.fire("action_set_fullscreen", { videoId: isFullscreen ? undefined : props.videoId });
|
events.fire("action_set_fullscreen", { videoId: isFullscreen ? undefined : props.videoId });
|
||||||
} else {
|
} else {
|
||||||
events.fire("action_set_spotlight", { videoId: props.videoId, expend: true });
|
events.fire("action_set_spotlight", { videoId: props.videoId, expend: true });
|
||||||
|
events.fire("action_focus_spotlight", { });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title={props.isSpotlight ? tr("Toggle fullscreen") : tr("Toggle spotlight")}
|
title={props.isSpotlight ? tr("Toggle fullscreen") : tr("Toggle spotlight")}
|
||||||
|
@ -394,11 +397,14 @@ const VideoBar = () => {
|
||||||
|
|
||||||
const Spotlight = () => {
|
const Spotlight = () => {
|
||||||
const events = useContext(EventContext);
|
const events = useContext(EventContext);
|
||||||
|
const refContainer = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
const [ videoId, setVideoId ] = useState<string>(() => {
|
const [ videoId, setVideoId ] = useState<string>(() => {
|
||||||
events.fire("query_spotlight");
|
events.fire("query_spotlight");
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
events.reactUse("notify_spotlight", event => setVideoId(event.videoId));
|
events.reactUse("notify_spotlight", event => setVideoId(event.videoId), undefined, []);
|
||||||
|
events.reactUse("action_focus_spotlight", () => refContainer.current?.focus(), undefined, []);
|
||||||
|
|
||||||
let body;
|
let body;
|
||||||
if(videoId) {
|
if(videoId) {
|
||||||
|
@ -412,7 +418,16 @@ const Spotlight = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.spotlight}>
|
<div
|
||||||
|
className={cssStyle.spotlight}
|
||||||
|
onKeyDown={event => {
|
||||||
|
if(event.key === "Escape") {
|
||||||
|
events.fire("action_set_spotlight", { videoId: undefined, expend: false });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
ref={refContainer}
|
||||||
|
>
|
||||||
{body}
|
{body}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,14 +7,21 @@ import {LogCategory, logError} from "tc-shared/log";
|
||||||
import {VideoBroadcastType} from "tc-shared/connection/VideoConnection";
|
import {VideoBroadcastType} from "tc-shared/connection/VideoConnection";
|
||||||
|
|
||||||
type VideoSourceRef = { source: VideoSource };
|
type VideoSourceRef = { source: VideoSource };
|
||||||
export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, selectDefault: boolean) : Promise<VideoSource> {
|
|
||||||
|
/**
|
||||||
|
* @param type The video type which should be prompted
|
||||||
|
* @param selectDefault If we're trying to select a source on default
|
||||||
|
* @param quickSelect If we want to quickly select a source and instantly use it.
|
||||||
|
* This option is only useable for screen sharing.
|
||||||
|
*/
|
||||||
|
export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, selectDefault: boolean, quickSelect: boolean) : Promise<VideoSource> {
|
||||||
const refSource: VideoSourceRef = {
|
const refSource: VideoSourceRef = {
|
||||||
source: undefined
|
source: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
const events = new Registry<ModalVideoSourceEvents>();
|
const events = new Registry<ModalVideoSourceEvents>();
|
||||||
events.enableDebug("video-source-select");
|
events.enableDebug("video-source-select");
|
||||||
initializeController(events, refSource, type);
|
initializeController(events, refSource, type, quickSelect);
|
||||||
|
|
||||||
const modal = spawnReactModal(ModalVideoSource, events, type);
|
const modal = spawnReactModal(ModalVideoSource, events, type);
|
||||||
modal.events.on("destroy", () => {
|
modal.events.on("destroy", () => {
|
||||||
|
@ -25,12 +32,23 @@ export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, sele
|
||||||
modal.destroy();
|
modal.destroy();
|
||||||
});
|
});
|
||||||
modal.show().then(() => {
|
modal.show().then(() => {
|
||||||
if(selectDefault) {
|
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 });
|
events.fire("action_select_source", { id: undefined });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
|
if(type === "screen" && quickSelect) {
|
||||||
|
events.on("notify_video_preview", event => {
|
||||||
|
if(event.status.status === "preview") {
|
||||||
|
/* we've successfully selected something */
|
||||||
|
modal.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
modal.events.one(["destroy", "close"], resolve);
|
modal.events.one(["destroy", "close"], resolve);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -39,7 +57,7 @@ export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, sele
|
||||||
|
|
||||||
type SourceConstraints = { width?: number, height?: number, frameRate?: number };
|
type SourceConstraints = { width?: number, height?: number, frameRate?: number };
|
||||||
|
|
||||||
function initializeController(events: Registry<ModalVideoSourceEvents>, currentSourceRef: VideoSourceRef, type: VideoBroadcastType) {
|
function initializeController(events: Registry<ModalVideoSourceEvents>, currentSourceRef: VideoSourceRef, type: VideoBroadcastType, quickSelect: boolean) {
|
||||||
let currentSource: VideoSource | string;
|
let currentSource: VideoSource | string;
|
||||||
let currentConstraints: SourceConstraints;
|
let currentConstraints: SourceConstraints;
|
||||||
|
|
||||||
|
@ -74,6 +92,20 @@ function initializeController(events: Registry<ModalVideoSourceEvents>, currentS
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notifyScreenCaptureDevices = () => {
|
||||||
|
const driver = getVideoDriver();
|
||||||
|
driver.queryScreenCaptureDevices().then(devices => {
|
||||||
|
events.fire_react("notify_screen_capture_devices", { devices: { status: "success", devices: devices }});
|
||||||
|
}).catch(error => {
|
||||||
|
if(typeof error !== "string") {
|
||||||
|
logError(LogCategory.VIDEO, tr("Failed to query screen capture devices: %o"), error);
|
||||||
|
error = tr("lookup the console");
|
||||||
|
}
|
||||||
|
|
||||||
|
events.fire_react("notify_screen_capture_devices", { devices: { status: "error", reason: error }});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const notifyVideoPreview = () => {
|
const notifyVideoPreview = () => {
|
||||||
const driver = getVideoDriver();
|
const driver = getVideoDriver();
|
||||||
switch (driver.getPermissionStatus()) {
|
switch (driver.getPermissionStatus()) {
|
||||||
|
@ -188,6 +220,7 @@ function initializeController(events: Registry<ModalVideoSourceEvents>, currentS
|
||||||
|
|
||||||
events.on("query_source", () => notifyCurrentSource());
|
events.on("query_source", () => notifyCurrentSource());
|
||||||
events.on("query_device_list", () => notifyDeviceList());
|
events.on("query_device_list", () => notifyDeviceList());
|
||||||
|
events.on("query_screen_capture_devices", () => notifyScreenCaptureDevices());
|
||||||
events.on("query_video_preview", () => notifyVideoPreview());
|
events.on("query_video_preview", () => notifyVideoPreview());
|
||||||
events.on("query_start_button", () => notifyStartButton());
|
events.on("query_start_button", () => notifyStartButton());
|
||||||
events.on("query_setting_dimension", () => notifySettingDimension());
|
events.on("query_setting_dimension", () => notifySettingDimension());
|
||||||
|
@ -227,10 +260,12 @@ function initializeController(events: Registry<ModalVideoSourceEvents>, currentS
|
||||||
setCurrentSource(tr("Failed to open video device (Lookup the console)"));
|
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 {
|
} else {
|
||||||
currentSourceId = undefined;
|
currentSourceId = undefined;
|
||||||
fallbackCurrentSourceName = tr("loading...");
|
fallbackCurrentSourceName = tr("loading...");
|
||||||
driver.createScreenSource().then(stream => {
|
driver.createScreenSource(event.id, quickSelect).then(stream => {
|
||||||
setCurrentSource(stream);
|
setCurrentSource(stream);
|
||||||
fallbackCurrentSourceName = stream?.getName() || tr("No stream");
|
fallbackCurrentSourceName = stream?.getName() || tr("No stream");
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import {ScreenCaptureDevice} from "tc-shared/video/VideoSource";
|
||||||
|
|
||||||
export type DeviceListResult = {
|
export type DeviceListResult = {
|
||||||
status: "success",
|
status: "success",
|
||||||
devices: { id: string, displayName: string }[],
|
devices: { id: string, displayName: string }[],
|
||||||
|
@ -30,6 +32,18 @@ export type VideoSourceState = {
|
||||||
error: string
|
error: string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ScreenCaptureDeviceList = {
|
||||||
|
status: "success",
|
||||||
|
devices: ScreenCaptureDevice[],
|
||||||
|
} | {
|
||||||
|
status: "error",
|
||||||
|
reason: string
|
||||||
|
} | {
|
||||||
|
status: "not-supported"
|
||||||
|
} | {
|
||||||
|
status: "loading"
|
||||||
|
}
|
||||||
|
|
||||||
export type SettingFrameRate = {
|
export type SettingFrameRate = {
|
||||||
min: number,
|
min: number,
|
||||||
max: number,
|
max: number,
|
||||||
|
@ -43,6 +57,8 @@ export interface ModalVideoSourceEvents {
|
||||||
action_select_source: { id: string | undefined },
|
action_select_source: { id: string | undefined },
|
||||||
action_setting_dimension: { width: number, height: number },
|
action_setting_dimension: { width: number, height: number },
|
||||||
action_setting_framerate: { frameRate: number },
|
action_setting_framerate: { frameRate: number },
|
||||||
|
action_toggle_screen_capture_device_select: { shown: boolean },
|
||||||
|
action_preselect_screen_capture_device: { deviceId: string },
|
||||||
|
|
||||||
query_source: {},
|
query_source: {},
|
||||||
query_device_list: {},
|
query_device_list: {},
|
||||||
|
@ -50,6 +66,7 @@ export interface ModalVideoSourceEvents {
|
||||||
query_start_button: {},
|
query_start_button: {},
|
||||||
query_setting_dimension: {},
|
query_setting_dimension: {},
|
||||||
query_setting_framerate: {},
|
query_setting_framerate: {},
|
||||||
|
query_screen_capture_devices: { }
|
||||||
|
|
||||||
notify_source: { state: VideoSourceState }
|
notify_source: { state: VideoSourceState }
|
||||||
notify_device_list: { status: DeviceListResult },
|
notify_device_list: { status: DeviceListResult },
|
||||||
|
@ -70,6 +87,9 @@ export interface ModalVideoSourceEvents {
|
||||||
},
|
},
|
||||||
notify_settings_framerate: {
|
notify_settings_framerate: {
|
||||||
frameRate: SettingFrameRate | undefined
|
frameRate: SettingFrameRate | undefined
|
||||||
|
},
|
||||||
|
notify_screen_capture_devices: {
|
||||||
|
devices: ScreenCaptureDeviceList
|
||||||
}
|
}
|
||||||
|
|
||||||
notify_destroy: {}
|
notify_destroy: {}
|
||||||
|
|
|
@ -54,148 +54,148 @@
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.head {
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHead {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
align-self: flex-end;
|
||||||
|
|
||||||
|
&.title, .title {
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #557edc;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
@include text-dotdotdot();
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced {
|
||||||
|
margin-left: auto;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: revert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionBody {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
.selectError {
|
||||||
|
color: #a10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoContainer {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
width: 37.5em; /* 600px for 16px/em */
|
||||||
|
height: 25em; /* 400px for 16px/em */
|
||||||
|
|
||||||
|
border-radius: .2em;
|
||||||
|
border: 1px solid var(--boxed-input-field-border);
|
||||||
|
background-color: var(--boxed-input-field-background);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
video {
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
min-height: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
z-index: 10;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
background-color: var(--boxed-input-field-background);
|
||||||
|
|
||||||
|
&.shown {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
}
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
flex-grow: 1;
|
&.permissions {
|
||||||
flex-shrink: 1;
|
.text {
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
align-self: flex-end;
|
|
||||||
|
|
||||||
&.title, .title {
|
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
color: #557edc;
|
padding-bottom: 1em;
|
||||||
text-transform: uppercase;
|
|
||||||
|
|
||||||
align-self: center;
|
|
||||||
|
|
||||||
@include text-dotdotdot();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.advanced {
|
.button {
|
||||||
margin-left: auto;
|
width: min-content;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
||||||
label {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: revert;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.error {
|
||||||
display: flex;
|
font-size: 1.2em;
|
||||||
flex-direction: column;
|
color: #a10000;
|
||||||
justify-content: flex-start;
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.selectError {
|
.info {
|
||||||
|
font-size: 1.8em;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4d4d4d;
|
||||||
|
|
||||||
|
&.selected {}
|
||||||
|
&.none {}
|
||||||
|
&.error {
|
||||||
color: #a10000;
|
color: #a10000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.videoContainer {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
width: 37.5em; /* 600px for 16px/em */
|
|
||||||
height: 25em; /* 400px for 16px/em */
|
|
||||||
|
|
||||||
border-radius: .2em;
|
|
||||||
border: 1px solid var(--boxed-input-field-border);
|
|
||||||
background-color: var(--boxed-input-field-background);
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
video {
|
|
||||||
max-height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
min-height: 100%;
|
|
||||||
min-width: 100%;
|
|
||||||
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
z-index: 10;
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
background-color: var(--boxed-input-field-background);
|
|
||||||
|
|
||||||
&.shown {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.permissions {
|
|
||||||
.text {
|
|
||||||
font-size: 1.2em;
|
|
||||||
padding-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
width: min-content;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
font-size: 1.2em;
|
|
||||||
color: #a10000;
|
|
||||||
padding-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
font-size: 1.8em;
|
|
||||||
padding-bottom: 1em;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #4d4d4d;
|
|
||||||
|
|
||||||
&.selected {}
|
|
||||||
&.none {}
|
|
||||||
&.error {
|
|
||||||
color: #a10000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sourcePrompt {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin-right: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
> * {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
.sourcePrompt {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-right: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,11 +208,11 @@
|
||||||
margin-bottom: .5em;
|
margin-bottom: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.head {
|
.sectionHead {
|
||||||
flex-grow: 0!important;
|
flex-grow: 0!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.sectionBody {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,4 +249,126 @@
|
||||||
font-size: .8em;
|
font-size: .8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayScreenDeviceList {
|
||||||
|
position: absolute;
|
||||||
|
@include user-select(none);
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
background-color: inherit;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
.sectionBody {
|
||||||
|
margin-top: .5em;
|
||||||
|
margin-bottom: .5em;
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
font-size: 1.2em;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
color: #a32929;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.listContainer {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
@include chat-scrollbar-vertical();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenDeviceEntry {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
margin: .25em .5em;
|
||||||
|
padding: .5em;
|
||||||
|
|
||||||
|
border-radius: .2em;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
width: 16em;
|
||||||
|
height: 9.5em;
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: #2f3137;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #393c43;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
/* 16:9 format */
|
||||||
|
width: 15em;
|
||||||
|
height: 8.4375em;
|
||||||
|
|
||||||
|
border-radius: .2em;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
line-height: 1.2em;
|
||||||
|
margin-top: .25em;
|
||||||
|
|
||||||
|
@include text-dotdotdot();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@ import {Registry} from "tc-shared/events";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
DeviceListResult,
|
DeviceListResult,
|
||||||
ModalVideoSourceEvents, SettingFrameRate,
|
ModalVideoSourceEvents, ScreenCaptureDeviceList, SettingFrameRate,
|
||||||
VideoPreviewStatus, VideoSourceState
|
VideoPreviewStatus, VideoSourceState
|
||||||
} from "tc-shared/ui/modal/video-source/Definitions";
|
} from "tc-shared/ui/modal/video-source/Definitions";
|
||||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||||
|
@ -13,6 +13,9 @@ import {useContext, useEffect, useRef, useState} from "react";
|
||||||
import {VideoBroadcastType} from "tc-shared/connection/VideoConnection";
|
import {VideoBroadcastType} from "tc-shared/connection/VideoConnection";
|
||||||
import {Slider} from "tc-shared/ui/react-elements/Slider";
|
import {Slider} from "tc-shared/ui/react-elements/Slider";
|
||||||
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
||||||
|
import {Tab, TabEntry} from "tc-shared/ui/react-elements/Tab";
|
||||||
|
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||||
|
import {ScreenCaptureDevice} from "tc-shared/video/VideoSource";
|
||||||
|
|
||||||
const cssStyle = require("./Renderer.scss");
|
const cssStyle = require("./Renderer.scss");
|
||||||
const ModalEvents = React.createContext<Registry<ModalVideoSourceEvents>>(undefined);
|
const ModalEvents = React.createContext<Registry<ModalVideoSourceEvents>>(undefined);
|
||||||
|
@ -30,7 +33,7 @@ const VideoSourceSelector = () => {
|
||||||
|
|
||||||
if(deviceList === "loading") {
|
if(deviceList === "loading") {
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.body} key={"loading"}>
|
<div className={cssStyle.sectionBody} key={"loading"}>
|
||||||
<Select type={"boxed"} disabled={true}>
|
<Select type={"boxed"} disabled={true}>
|
||||||
<option>{tr("loading ...")}</option>
|
<option>{tr("loading ...")}</option>
|
||||||
</Select>
|
</Select>
|
||||||
|
@ -52,7 +55,7 @@ const VideoSourceSelector = () => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.body} key={"error"}>
|
<div className={cssStyle.sectionBody} key={"error"}>
|
||||||
<Select type={"boxed"} disabled={true} className={cssStyle.selectError}>
|
<Select type={"boxed"} disabled={true} className={cssStyle.selectError}>
|
||||||
<option>{message}</option>
|
<option>{message}</option>
|
||||||
</Select>
|
</Select>
|
||||||
|
@ -60,7 +63,7 @@ const VideoSourceSelector = () => {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.body} key={"normal"}>
|
<div className={cssStyle.sectionBody} key={"normal"}>
|
||||||
<Select
|
<Select
|
||||||
type={"boxed"}
|
type={"boxed"}
|
||||||
value={deviceList.selectedDeviceId || kNoDeviceId}
|
value={deviceList.selectedDeviceId || kNoDeviceId}
|
||||||
|
@ -117,7 +120,7 @@ const VideoSourceRequester = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.body} key={"normal"}>
|
<div className={cssStyle.sectionBody} key={"normal"}>
|
||||||
<div className={cssStyle.sourcePrompt}>
|
<div className={cssStyle.sourcePrompt}>
|
||||||
<Button type={"small"} onClick={() => events.fire("action_select_source", { id: undefined })}>
|
<Button type={"small"} onClick={() => events.fire("action_select_source", { id: undefined })}>
|
||||||
<Translatable>Select source</Translatable>
|
<Translatable>Select source</Translatable>
|
||||||
|
@ -539,7 +542,7 @@ const calculateBps = (width: number, height: number, frameRate: number) => {
|
||||||
return estimatedBitsPerPixed * width * height * (frameRate / 30);
|
return estimatedBitsPerPixed * width * height * (frameRate / 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
const BpsInfo = () => {
|
const BpsInfo = React.memo(() => {
|
||||||
const events = useContext(ModalEvents);
|
const events = useContext(ModalEvents);
|
||||||
|
|
||||||
const [ dimensions, setDimensions ] = useState<{ width: number, height: number } | undefined>(undefined);
|
const [ dimensions, setDimensions ] = useState<{ width: number, height: number } | undefined>(undefined);
|
||||||
|
@ -580,9 +583,9 @@ const BpsInfo = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
const Settings = () => {
|
const Settings = React.memo(() => {
|
||||||
if(window.detectedBrowser.name === "firefox") {
|
if(window.detectedBrowser.name === "firefox") {
|
||||||
/* Firefox does not seem to give a fuck about any of our settings */
|
/* Firefox does not seem to give a fuck about any of our settings */
|
||||||
return null;
|
return null;
|
||||||
|
@ -593,13 +596,13 @@ const Settings = () => {
|
||||||
return (
|
return (
|
||||||
<AdvancedSettings.Provider value={advanced}>
|
<AdvancedSettings.Provider value={advanced}>
|
||||||
<div className={cssStyle.section + " " + cssStyle.columnSettings}>
|
<div className={cssStyle.section + " " + cssStyle.columnSettings}>
|
||||||
<div className={cssStyle.head}>
|
<div className={cssStyle.sectionHead}>
|
||||||
<div className={cssStyle.title}><Translatable>Settings</Translatable></div>
|
<div className={cssStyle.title}><Translatable>Settings</Translatable></div>
|
||||||
<div className={cssStyle.advanced}>
|
<div className={cssStyle.advanced}>
|
||||||
<Checkbox label={<Translatable>Advanced</Translatable>} onChange={value => setAdvanced(value)} />
|
<Checkbox label={<Translatable>Advanced</Translatable>} onChange={value => setAdvanced(value)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={cssStyle.body}>
|
<div className={cssStyle.sectionBody}>
|
||||||
<SettingDimension />
|
<SettingDimension />
|
||||||
<SettingFramerate />
|
<SettingFramerate />
|
||||||
<BpsInfo />
|
<BpsInfo />
|
||||||
|
@ -607,8 +610,151 @@ const Settings = () => {
|
||||||
</div>
|
</div>
|
||||||
</AdvancedSettings.Provider>
|
</AdvancedSettings.Provider>
|
||||||
);
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
const ScreenCaptureDeviceRenderer = React.memo((props: { device: ScreenCaptureDevice, selected: boolean }) => {
|
||||||
|
const events = useContext(ModalEvents);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cssStyle.screenDeviceEntry + " " + (props.selected ? cssStyle.selected : undefined)}
|
||||||
|
onClick={() => events.fire_react("action_preselect_screen_capture_device", { deviceId: props.device.id })}
|
||||||
|
onDoubleClick={() => {
|
||||||
|
events.fire("action_toggle_screen_capture_device_select", { shown: false });
|
||||||
|
events.fire("action_select_source", { id: props.device.id })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={cssStyle.preview}>
|
||||||
|
<img src={props.device.appPreview} alt={tr("Preview image")} />
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.name} title={props.device.name || props.device.id}>{props.device.name || props.device.id}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
const ScreenCaptureDeviceSelectTag = React.memo((props: { data: ScreenCaptureDeviceList, type: "full-screen" | "window", selectedDevice: string }) => {
|
||||||
|
let body;
|
||||||
|
switch (props.data.status) {
|
||||||
|
case "loading":
|
||||||
|
body = (
|
||||||
|
<div className={cssStyle.overlay} key={"loading"}>
|
||||||
|
<a><Translatable>loading</Translatable> <LoadingDots /></a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "error":
|
||||||
|
case "not-supported":
|
||||||
|
let message = props.data.status === "error" ? props.data.reason : tr("Not supported");
|
||||||
|
body = (
|
||||||
|
<div className={cssStyle.overlay + " " + cssStyle.error} key={"error"}><a>{message}</a></div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "success":
|
||||||
|
let devices = props.data.devices
|
||||||
|
.filter(e => e.type === props.type);
|
||||||
|
if(devices.length === 0) {
|
||||||
|
body = (
|
||||||
|
<div className={cssStyle.overlay} key={"no-devices"}>
|
||||||
|
<a><Translatable>No sources</Translatable></a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
body = devices
|
||||||
|
.map(e => <ScreenCaptureDeviceRenderer device={e} key={e.id} selected={e.id === props.selectedDevice} />);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.listContainer}>{body}</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
const ScreenCaptureDeviceSelectList = React.memo(() => {
|
||||||
|
const events = useContext(ModalEvents);
|
||||||
|
const refUpdateTimer = useRef<number>(undefined);
|
||||||
|
|
||||||
|
const [ list, setList ] = useState<ScreenCaptureDeviceList>(() => {
|
||||||
|
events.fire("query_screen_capture_devices");
|
||||||
|
return { status: "loading" };
|
||||||
|
});
|
||||||
|
events.reactUse("notify_screen_capture_devices", event => {
|
||||||
|
setList(event.devices);
|
||||||
|
if(!refUpdateTimer.current) {
|
||||||
|
refUpdateTimer.current = setTimeout(() => {
|
||||||
|
refUpdateTimer.current = undefined;
|
||||||
|
events.fire("query_screen_capture_devices");
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, undefined, []);
|
||||||
|
useEffect(() => () => clearTimeout(refUpdateTimer.current), []);
|
||||||
|
|
||||||
|
const [ selectedDevice, setSelectedDevice ] = useState(undefined);
|
||||||
|
events.reactUse("action_preselect_screen_capture_device", event => setSelectedDevice(event.deviceId), undefined, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tab defaultTab={"screen"} className={cssStyle.tab}>
|
||||||
|
<TabEntry id={"screen"}>
|
||||||
|
<Translatable>Full Screen</Translatable>
|
||||||
|
<ScreenCaptureDeviceSelectTag type={"full-screen"} data={list} selectedDevice={selectedDevice} />
|
||||||
|
</TabEntry>
|
||||||
|
<TabEntry id={"window"}>
|
||||||
|
<Translatable>Window</Translatable>
|
||||||
|
<ScreenCaptureDeviceSelectTag type={"window"} data={list} selectedDevice={selectedDevice} />
|
||||||
|
</TabEntry>
|
||||||
|
</Tab>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const SelectSourceButton = () => {
|
||||||
|
const events = useContext(ModalEvents);
|
||||||
|
const [ selectedDevice, setSelectedDevice ] = useState(undefined);
|
||||||
|
events.reactUse("action_preselect_screen_capture_device", event => setSelectedDevice(event.deviceId), undefined, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button type={"small"} color={"green"} disabled={!selectedDevice} onClick={() => {
|
||||||
|
events.fire("action_toggle_screen_capture_device_select", { shown: false });
|
||||||
|
events.fire("action_select_source", { id: selectedDevice })
|
||||||
|
}}>
|
||||||
|
<Translatable>Select Source</Translatable>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ScreenCaptureDeviceSelect = React.memo(() => {
|
||||||
|
const events = useContext(ModalEvents);
|
||||||
|
const [ shown, setShown ] = useState(() => {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
events.reactUse("action_toggle_screen_capture_device_select", event => {
|
||||||
|
setShown(event.shown);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!shown) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.overlayScreenDeviceList} key={"shown"}>
|
||||||
|
<div className={cssStyle.sectionHead + " " + cssStyle.title}>
|
||||||
|
<Translatable>Select your source</Translatable>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.sectionBody}>
|
||||||
|
<ScreenCaptureDeviceSelectList />
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.buttons}>
|
||||||
|
<Button type={"small"} color={"red"} onClick={() => events.fire("action_toggle_screen_capture_device_select", { shown: false })}>
|
||||||
|
<Translatable>Cancel</Translatable>
|
||||||
|
</Button>
|
||||||
|
<SelectSourceButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
export class ModalVideoSource extends InternalModal {
|
export class ModalVideoSource extends InternalModal {
|
||||||
protected readonly events: Registry<ModalVideoSourceEvents>;
|
protected readonly events: Registry<ModalVideoSourceEvents>;
|
||||||
private readonly sourceType: VideoBroadcastType;
|
private readonly sourceType: VideoBroadcastType;
|
||||||
|
@ -627,16 +773,16 @@ export class ModalVideoSource extends InternalModal {
|
||||||
<div className={cssStyle.content}>
|
<div className={cssStyle.content}>
|
||||||
<div className={cssStyle.columnSource}>
|
<div className={cssStyle.columnSource}>
|
||||||
<div className={cssStyle.section}>
|
<div className={cssStyle.section}>
|
||||||
<div className={cssStyle.head + " " + cssStyle.title}>
|
<div className={cssStyle.sectionHead + " " + cssStyle.title}>
|
||||||
<Translatable>Select your source</Translatable>
|
<Translatable>Select your source</Translatable>
|
||||||
</div>
|
</div>
|
||||||
{this.sourceType === "camera" ? <VideoSourceSelector key={"source-selector"} /> : <VideoSourceRequester key={"source-requester"} />}
|
{this.sourceType === "camera" ? <VideoSourceSelector key={"source-selector"} /> : <VideoSourceRequester key={"source-requester"} />}
|
||||||
</div>
|
</div>
|
||||||
<div className={cssStyle.section}>
|
<div className={cssStyle.section}>
|
||||||
<div className={cssStyle.head + " " + cssStyle.title}>
|
<div className={cssStyle.sectionHead + " " + cssStyle.title}>
|
||||||
<Translatable>Video preview</Translatable>
|
<Translatable>Video preview</Translatable>
|
||||||
</div>
|
</div>
|
||||||
<div className={cssStyle.body}>
|
<div className={cssStyle.sectionBody}>
|
||||||
<VideoPreview />
|
<VideoPreview />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -650,6 +796,7 @@ export class ModalVideoSource extends InternalModal {
|
||||||
<ButtonStart />
|
<ButtonStart />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ScreenCaptureDeviceSelect />
|
||||||
</ModalEvents.Provider>
|
</ModalEvents.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
@import "../../../css/static/properties";
|
||||||
|
@import "../../../css/static/mixin";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
border-radius: .2em;
|
||||||
|
border: 1px solid #111112;
|
||||||
|
|
||||||
|
background-color: #17171a;
|
||||||
|
|
||||||
|
.categories {
|
||||||
|
height: 2.5em;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
border-bottom: 1px solid #1d1d1d;
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
padding: .5em;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #b6c4d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-bottom: 3px solid #245184;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
|
||||||
|
color: #245184;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include transition(color $button_hover_animation_time, border-bottom-color $button_hover_animation_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodies {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
min-height: 12em;
|
||||||
|
height: 20em;
|
||||||
|
|
||||||
|
.body {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
padding: .5em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
overflow: auto;
|
||||||
|
@include chat-scrollbar-vertical();
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const cssStyle = require("./Tab.scss");
|
||||||
|
|
||||||
|
export class TabEntry extends React.Component<{
|
||||||
|
children: [React.ReactNode, React.ReactNode],
|
||||||
|
|
||||||
|
id: string,
|
||||||
|
}, {}> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() : React.ReactNode {
|
||||||
|
throw tr("the TabEntry isn't for render purposes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Tab extends React.PureComponent<{
|
||||||
|
children: React.ReactElement[],
|
||||||
|
|
||||||
|
defaultTab: string;
|
||||||
|
selectedTab?: string;
|
||||||
|
|
||||||
|
/* permanent render all body parts (defaults to true) */
|
||||||
|
permanentRender?: boolean,
|
||||||
|
|
||||||
|
className?: string
|
||||||
|
}, {
|
||||||
|
selectedTab: string
|
||||||
|
}> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = { selectedTab: this.props.defaultTab };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.props.children?.forEach((child, index) => {
|
||||||
|
if(!(child.type === TabEntry)) {
|
||||||
|
throw tra("Child {} isn't of type TabEntry", index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedTab = this.props.selectedTab || this.state.selectedTab;
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.container + " " + this.props.className}>
|
||||||
|
<div className={cssStyle.categories}>
|
||||||
|
{this.props.children?.map(child => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cssStyle.entry + " " + (child.props.id === selectedTab ? cssStyle.selected : "")}
|
||||||
|
key={child.props.id}
|
||||||
|
onClick={() => !this.props.selectedTab && this.setState({ selectedTab: child.props.id })}
|
||||||
|
>
|
||||||
|
{child.props.children[0]}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.bodies}>
|
||||||
|
{this.props.children?.filter(child => typeof this.props.permanentRender !== "boolean" || this.props.permanentRender || child.props.id === selectedTab).map(child => {
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.body + " " + (child.props.id === selectedTab ? "" : cssStyle.hidden)} key={child.props.id}>
|
||||||
|
{child.props.children[1]}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -197,6 +197,9 @@ html:root {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
/* explicitly set the background color so the next element could use background-color: inherited; */
|
||||||
|
background: var(--modal-content-background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,15 @@ export interface VideoDriverEvents {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VideoDevice = { id: string, name: string }
|
export type VideoDevice = { id: string, name: string }
|
||||||
|
export type ScreenCaptureDevice = {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
|
||||||
|
type: "full-screen" | "window",
|
||||||
|
|
||||||
|
appIcon?: string,
|
||||||
|
appPreview?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface VideoDriver {
|
export interface VideoDriver {
|
||||||
getEvents() : Registry<VideoDriverEvents>;
|
getEvents() : Registry<VideoDriverEvents>;
|
||||||
|
@ -46,10 +55,14 @@ export interface VideoDriver {
|
||||||
*/
|
*/
|
||||||
createVideoSource(id: string | undefined) : Promise<VideoSource>;
|
createVideoSource(id: string | undefined) : Promise<VideoSource>;
|
||||||
|
|
||||||
|
screenQueryAvailable() : boolean;
|
||||||
|
|
||||||
|
queryScreenCaptureDevices() : Promise<ScreenCaptureDevice[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a source from the screen
|
* Create a source from the screen
|
||||||
*/
|
*/
|
||||||
createScreenSource() : Promise<VideoSource>;
|
createScreenSource(id: string | undefined, allowFocusLoss: boolean) : Promise<VideoSource>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let driverInstance: VideoDriver;
|
let driverInstance: VideoDriver;
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
"../js/workers"
|
"../js/workers"
|
||||||
],
|
],
|
||||||
"include": [
|
"include": [
|
||||||
"../declaration_fix.d.ts",
|
"../js/proto.ts",
|
||||||
"../backend.d",
|
"../backend.d",
|
||||||
"../js/**/*.ts"
|
"../js/**/*.ts"
|
||||||
]
|
]
|
||||||
|
|
|
@ -24,6 +24,8 @@
|
||||||
"file.ts"
|
"file.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"declarations"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -30,6 +30,8 @@
|
||||||
"web/environment/",
|
"web/environment/",
|
||||||
"loader/app/targets/certaccept.ts",
|
"loader/app/targets/certaccept.ts",
|
||||||
"tools/",
|
"tools/",
|
||||||
"vendor"
|
"vendor",
|
||||||
|
"dist",
|
||||||
|
"declarations"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -118,4 +118,8 @@ export function initializeFromGesture() {
|
||||||
} else {
|
} else {
|
||||||
createNewContext();
|
createNewContext();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function globalAudioContext() : AudioContext {
|
||||||
|
return context();
|
||||||
}
|
}
|
|
@ -3,7 +3,6 @@ import {
|
||||||
VoiceConnectionStatus,
|
VoiceConnectionStatus,
|
||||||
WhisperSessionInitializer
|
WhisperSessionInitializer
|
||||||
} from "tc-shared/connection/VoiceConnection";
|
} from "tc-shared/connection/VoiceConnection";
|
||||||
import {RtpVoiceConnection} from "tc-backend/web/rtc/voice/Connection";
|
|
||||||
import {VoiceConnection} from "tc-backend/web/legacy/voice/VoiceHandler";
|
import {VoiceConnection} from "tc-backend/web/legacy/voice/VoiceHandler";
|
||||||
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
||||||
import {VoiceClient} from "tc-shared/voice/VoiceClient";
|
import {VoiceClient} from "tc-shared/voice/VoiceClient";
|
||||||
|
@ -12,6 +11,7 @@ import {AbstractServerConnection, ConnectionStatistics} from "tc-shared/connecti
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {VoicePlayerEvents, VoicePlayerLatencySettings, VoicePlayerState} from "tc-shared/voice/VoicePlayer";
|
import {VoicePlayerEvents, VoicePlayerLatencySettings, VoicePlayerState} from "tc-shared/voice/VoicePlayer";
|
||||||
import { tr } from "tc-shared/i18n/localize";
|
import { tr } from "tc-shared/i18n/localize";
|
||||||
|
import {RtpVoiceConnection} from "tc-backend/web/voice/Connection";
|
||||||
|
|
||||||
class ProxiedVoiceClient implements VoiceClient {
|
class ProxiedVoiceClient implements VoiceClient {
|
||||||
readonly clientId: number;
|
readonly clientId: number;
|
||||||
|
|
Loading…
Reference in New Issue