Implemented support for the native client
parent
1759fb1756
commit
33422db12e
|
@ -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**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
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") {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 },
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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: {}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
flex-direction: column;
|
||||
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 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;
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"../js/workers"
|
||||
],
|
||||
"include": [
|
||||
"../declaration_fix.d.ts",
|
||||
"../js/proto.ts",
|
||||
"../backend.d",
|
||||
"../js/**/*.ts"
|
||||
]
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
"file.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
"node_modules",
|
||||
"dist",
|
||||
"declarations"
|
||||
]
|
||||
}
|
|
@ -30,6 +30,8 @@
|
|||
"web/environment/",
|
||||
"loader/app/targets/certaccept.ts",
|
||||
"tools/",
|
||||
"vendor"
|
||||
"vendor",
|
||||
"dist",
|
||||
"declarations"
|
||||
]
|
||||
}
|
|
@ -118,4 +118,8 @@ export function initializeFromGesture() {
|
|||
} else {
|
||||
createNewContext();
|
||||
}
|
||||
}
|
||||
|
||||
export function globalAudioContext() : AudioContext {
|
||||
return context();
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue