Implemented support for the native client

canary
WolverinDEV 2020-11-29 21:00:59 +01:00
parent 1759fb1756
commit 33422db12e
23 changed files with 711 additions and 195 deletions

View File

@ -1,10 +1,14 @@
# 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 screen sharing (Currently via the camera channel)
- Using codec H264 instead of VP8
** 14.11.20**
* **14.11.20**
- Fixed bug where the microphone has been requested when muting it.
* **07.11.20**

View File

@ -3,20 +3,6 @@ import * as template_loader from "./template_loader";
import * as Animation from "../animation";
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 {
execute();
}

View File

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

View File

@ -187,11 +187,16 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
event_registry.on("action_toggle_video_broadcasting", event => {
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; }
try {
event.connection.getServerConnection().getVideoConnection().startBroadcasting(event.broadcastType, source)
connection.getServerConnection().getVideoConnection().startBroadcasting(event.broadcastType, source)
.catch(error => {
logError(LogCategory.VIDEO, tr("Failed to start %s broadcasting: %o"), event.broadcastType, error);
if(typeof error !== "string") {

View File

@ -29,7 +29,7 @@ export interface ClientGlobalControlEvents {
videoUrl: 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 */
action_open_window_connect: {

View File

@ -326,5 +326,19 @@ export async function initialize() {
// 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.tra = tra;

View File

@ -19,7 +19,7 @@ export interface 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();
try {
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.noiseSuppression = true;
const promise = (currentMediaStreamRequest = requestMediaStream0(constrains, type, true));
const promise = (currentMediaStreamRequest = requestMediaStreamWithConstraints(constrains, type));
try {
return await currentMediaStreamRequest;
} finally {

View File

@ -1,4 +1,5 @@
import {
ScreenCaptureDevice,
VideoDevice,
VideoDriver,
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 {
const source = await navigator.mediaDevices.getDisplayMedia({ audio: false, video: true });
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 displayName: string;
private readonly stream: MediaStream;

View File

@ -58,6 +58,7 @@ export interface ChannelVideoEvents {
action_toggle_expended: { expended: boolean },
action_video_scroll: { direction: "left" | "right" },
action_set_spotlight: { videoId: string | undefined, expend: boolean },
action_focus_spotlight: {},
action_set_fullscreen: { videoId: string | undefined },
action_toggle_mute: { videoId: string, broadcastType: VideoBroadcastType, muted: boolean },

View File

@ -240,6 +240,7 @@ const VideoContainer = React.memo((props: { videoId: string, isSpotlight: boolea
events.fire("action_set_fullscreen", { videoId: props.videoId });
} else {
events.fire("action_set_spotlight", { videoId: props.videoId, expend: true });
events.fire("action_focus_spotlight", { });
}
}}
onContextMenu={event => {
@ -262,6 +263,7 @@ const VideoContainer = React.memo((props: { videoId: string, isSpotlight: boolea
icon: ClientIcon.Fullscreen,
click: () => {
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 });
} else {
events.fire("action_set_spotlight", { videoId: props.videoId, expend: true });
events.fire("action_focus_spotlight", { });
}
}}
title={props.isSpotlight ? tr("Toggle fullscreen") : tr("Toggle spotlight")}
@ -394,11 +397,14 @@ const VideoBar = () => {
const Spotlight = () => {
const events = useContext(EventContext);
const refContainer = useRef<HTMLDivElement>();
const [ videoId, setVideoId ] = useState<string>(() => {
events.fire("query_spotlight");
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;
if(videoId) {
@ -412,7 +418,16 @@ const Spotlight = () => {
}
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}
</div>
)

View File

@ -7,14 +7,21 @@ import {LogCategory, logError} from "tc-shared/log";
import {VideoBroadcastType} from "tc-shared/connection/VideoConnection";
type VideoSourceRef = { source: VideoSource };
export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, selectDefault: boolean) : Promise<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 = {
source: undefined
};
const events = new Registry<ModalVideoSourceEvents>();
events.enableDebug("video-source-select");
initializeController(events, refSource, type);
initializeController(events, refSource, type, quickSelect);
const modal = spawnReactModal(ModalVideoSource, events, type);
modal.events.on("destroy", () => {
@ -25,12 +32,23 @@ export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, sele
modal.destroy();
});
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 });
}
});
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);
});
@ -39,7 +57,7 @@ export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, sele
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 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 driver = getVideoDriver();
switch (driver.getPermissionStatus()) {
@ -188,6 +220,7 @@ function initializeController(events: Registry<ModalVideoSourceEvents>, currentS
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());
@ -227,10 +260,12 @@ function initializeController(events: Registry<ModalVideoSourceEvents>, currentS
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().then(stream => {
driver.createScreenSource(event.id, quickSelect).then(stream => {
setCurrentSource(stream);
fallbackCurrentSourceName = stream?.getName() || tr("No stream");
}).catch(error => {

View File

@ -1,3 +1,5 @@
import {ScreenCaptureDevice} from "tc-shared/video/VideoSource";
export type DeviceListResult = {
status: "success",
devices: { id: string, displayName: string }[],
@ -30,6 +32,18 @@ export type VideoSourceState = {
error: string
};
export type ScreenCaptureDeviceList = {
status: "success",
devices: ScreenCaptureDevice[],
} | {
status: "error",
reason: string
} | {
status: "not-supported"
} | {
status: "loading"
}
export type SettingFrameRate = {
min: number,
max: number,
@ -43,6 +57,8 @@ export interface ModalVideoSourceEvents {
action_select_source: { id: string | undefined },
action_setting_dimension: { width: number, height: number },
action_setting_framerate: { frameRate: number },
action_toggle_screen_capture_device_select: { shown: boolean },
action_preselect_screen_capture_device: { deviceId: string },
query_source: {},
query_device_list: {},
@ -50,6 +66,7 @@ export interface ModalVideoSourceEvents {
query_start_button: {},
query_setting_dimension: {},
query_setting_framerate: {},
query_screen_capture_devices: { }
notify_source: { state: VideoSourceState }
notify_device_list: { status: DeviceListResult },
@ -70,6 +87,9 @@ export interface ModalVideoSourceEvents {
},
notify_settings_framerate: {
frameRate: SettingFrameRate | undefined
},
notify_screen_capture_devices: {
devices: ScreenCaptureDeviceList
}
notify_destroy: {}

View File

@ -54,148 +54,148 @@
.section {
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;
flex-direction: row;
justify-content: flex-start;
}
flex-grow: 1;
flex-shrink: 1;
width: 100%;
align-self: flex-end;
&.title, .title {
&.permissions {
.text {
font-size: 1.2em;
color: #557edc;
text-transform: uppercase;
align-self: center;
@include text-dotdotdot();
padding-bottom: 1em;
}
.advanced {
margin-left: auto;
.button {
width: min-content;
align-self: center;
label {
display: flex;
flex-direction: revert;
}
}
}
.body {
display: flex;
flex-direction: column;
justify-content: flex-start;
.error {
font-size: 1.2em;
color: #a10000;
padding-bottom: 1em;
}
.selectError {
.info {
font-size: 1.8em;
padding-bottom: 1em;
font-weight: 600;
color: #4d4d4d;
&.selected {}
&.none {}
&.error {
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;
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;
}
.head {
.sectionHead {
flex-grow: 0!important;
}
.body {
.sectionBody {
flex-grow: 1;
}
@ -249,4 +249,126 @@
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();
}
}

View File

@ -2,7 +2,7 @@ import {Registry} from "tc-shared/events";
import * as React from "react";
import {
DeviceListResult,
ModalVideoSourceEvents, SettingFrameRate,
ModalVideoSourceEvents, ScreenCaptureDeviceList, SettingFrameRate,
VideoPreviewStatus, VideoSourceState
} from "tc-shared/ui/modal/video-source/Definitions";
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 {Slider} from "tc-shared/ui/react-elements/Slider";
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 ModalEvents = React.createContext<Registry<ModalVideoSourceEvents>>(undefined);
@ -30,7 +33,7 @@ const VideoSourceSelector = () => {
if(deviceList === "loading") {
return (
<div className={cssStyle.body} key={"loading"}>
<div className={cssStyle.sectionBody} key={"loading"}>
<Select type={"boxed"} disabled={true}>
<option>{tr("loading ...")}</option>
</Select>
@ -52,7 +55,7 @@ const VideoSourceSelector = () => {
break;
}
return (
<div className={cssStyle.body} key={"error"}>
<div className={cssStyle.sectionBody} key={"error"}>
<Select type={"boxed"} disabled={true} className={cssStyle.selectError}>
<option>{message}</option>
</Select>
@ -60,7 +63,7 @@ const VideoSourceSelector = () => {
);
} else {
return (
<div className={cssStyle.body} key={"normal"}>
<div className={cssStyle.sectionBody} key={"normal"}>
<Select
type={"boxed"}
value={deviceList.selectedDeviceId || kNoDeviceId}
@ -117,7 +120,7 @@ const VideoSourceRequester = () => {
}
return (
<div className={cssStyle.body} key={"normal"}>
<div className={cssStyle.sectionBody} key={"normal"}>
<div className={cssStyle.sourcePrompt}>
<Button type={"small"} onClick={() => events.fire("action_select_source", { id: undefined })}>
<Translatable>Select source</Translatable>
@ -539,7 +542,7 @@ const calculateBps = (width: number, height: number, frameRate: number) => {
return estimatedBitsPerPixed * width * height * (frameRate / 30);
}
const BpsInfo = () => {
const BpsInfo = React.memo(() => {
const events = useContext(ModalEvents);
const [ dimensions, setDimensions ] = useState<{ width: number, height: number } | undefined>(undefined);
@ -580,9 +583,9 @@ const BpsInfo = () => {
</div>
</div>
);
}
});
const Settings = () => {
const Settings = React.memo(() => {
if(window.detectedBrowser.name === "firefox") {
/* Firefox does not seem to give a fuck about any of our settings */
return null;
@ -593,13 +596,13 @@ const Settings = () => {
return (
<AdvancedSettings.Provider value={advanced}>
<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.advanced}>
<Checkbox label={<Translatable>Advanced</Translatable>} onChange={value => setAdvanced(value)} />
</div>
</div>
<div className={cssStyle.body}>
<div className={cssStyle.sectionBody}>
<SettingDimension />
<SettingFramerate />
<BpsInfo />
@ -607,8 +610,151 @@ const Settings = () => {
</div>
</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 {
protected readonly events: Registry<ModalVideoSourceEvents>;
private readonly sourceType: VideoBroadcastType;
@ -627,16 +773,16 @@ export class ModalVideoSource extends InternalModal {
<div className={cssStyle.content}>
<div className={cssStyle.columnSource}>
<div className={cssStyle.section}>
<div className={cssStyle.head + " " + cssStyle.title}>
<div className={cssStyle.sectionHead + " " + cssStyle.title}>
<Translatable>Select your source</Translatable>
</div>
{this.sourceType === "camera" ? <VideoSourceSelector key={"source-selector"} /> : <VideoSourceRequester key={"source-requester"} />}
</div>
<div className={cssStyle.section}>
<div className={cssStyle.head + " " + cssStyle.title}>
<div className={cssStyle.sectionHead + " " + cssStyle.title}>
<Translatable>Video preview</Translatable>
</div>
<div className={cssStyle.body}>
<div className={cssStyle.sectionBody}>
<VideoPreview />
</div>
</div>
@ -650,6 +796,7 @@ export class ModalVideoSource extends InternalModal {
<ButtonStart />
</div>
</div>
<ScreenCaptureDeviceSelect />
</ModalEvents.Provider>
);
}

View File

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

View File

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

View File

@ -197,6 +197,9 @@ html:root {
display: flex;
flex-direction: column;
position: relative;
/* explicitly set the background color so the next element could use background-color: inherited; */
background: var(--modal-content-background);
}
}

View File

@ -26,6 +26,15 @@ export interface VideoDriverEvents {
}
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 {
getEvents() : Registry<VideoDriverEvents>;
@ -46,10 +55,14 @@ export interface VideoDriver {
*/
createVideoSource(id: string | undefined) : Promise<VideoSource>;
screenQueryAvailable() : boolean;
queryScreenCaptureDevices() : Promise<ScreenCaptureDevice[]>;
/**
* Create a source from the screen
*/
createScreenSource() : Promise<VideoSource>;
createScreenSource(id: string | undefined, allowFocusLoss: boolean) : Promise<VideoSource>;
}
let driverInstance: VideoDriver;

View File

@ -21,7 +21,7 @@
"../js/workers"
],
"include": [
"../declaration_fix.d.ts",
"../js/proto.ts",
"../backend.d",
"../js/**/*.ts"
]

View File

@ -24,6 +24,8 @@
"file.ts"
],
"exclude": [
"node_modules"
"node_modules",
"dist",
"declarations"
]
}

View File

@ -30,6 +30,8 @@
"web/environment/",
"loader/app/targets/certaccept.ts",
"tools/",
"vendor"
"vendor",
"dist",
"declarations"
]
}

View File

@ -118,4 +118,8 @@ export function initializeFromGesture() {
} else {
createNewContext();
}
}
export function globalAudioContext() : AudioContext {
return context();
}

View File

@ -3,7 +3,6 @@ import {
VoiceConnectionStatus,
WhisperSessionInitializer
} from "tc-shared/connection/VoiceConnection";
import {RtpVoiceConnection} from "tc-backend/web/rtc/voice/Connection";
import {VoiceConnection} from "tc-backend/web/legacy/voice/VoiceHandler";
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
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 {VoicePlayerEvents, VoicePlayerLatencySettings, VoicePlayerState} from "tc-shared/voice/VoicePlayer";
import { tr } from "tc-shared/i18n/localize";
import {RtpVoiceConnection} from "tc-backend/web/voice/Connection";
class ProxiedVoiceClient implements VoiceClient {
readonly clientId: number;