diff --git a/ChangeLog.md b/ChangeLog.md index 5772de8f..c29085d3 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,7 @@ # Changelog: +* **18.03.21** + - Finally got fully rid of the initial backend glue and changes all systems to provider + * **17.03.21** - Updated from webpack 4 to webpack 5 - Reworked the client build process diff --git a/documentation/file-structure.md b/documentation/file-structure.md deleted file mode 100644 index aeecbcaf..00000000 --- a/documentation/file-structure.md +++ /dev/null @@ -1,42 +0,0 @@ -# File structure -The TeaSpeak web client is separated into 2 different parts. - -## I) Application files -Application files are all files which directly belong to the app itself. -Like the javascript files who handle the UI stuff or even translation templates. -Theses files are separated into two type of files. -1. [Shared application files](#1-shared-application-files) -2. [Web application files](#2-web-application-files) - -### 1. Shared application files -Containing all files used by the TeaSpeak client and the Web client. -All of these files will be found within the folder `shared`. -This folder follows the general application file structure. -More information could be found [here](#application-file-structure) - -### 2. Web application files -All files which only belong to a browser only instance. -All of these files will be found within the folder `web`. -This folder follows the general application file structure. -More information could be found [here](#application-file-structure) - -### application file structure -Every application root contains several subfolders. -In the following list will be listed which files belong to which folder - -| Folder | Description | -| --- | --- | -| `audio` | This folder contains all audio files used by the application. More information could be found [here](). | -| `css` | This folder contains all style sheets used by the application. More information could be found [here](). | -| `js` | This folder contains all javascript files used by the application. More information could be found [here](). | -| `html` | This folder contains all HTML and PHP files used by the application. More information could be found [here](). | -| `i18n` | This folder contains all default translations. Information about the translation system could be found [here](). | -| `img` | This folder contains all image files. | - -## I) Additional tools - -## Environment builder -The environment builder is one of the most important tools of the entire project. -This tool, basically implemented in the file `files.php`, will be your helper while live developing. -What this tool does is, it creates a final environment where you could navigate to with your browser. -It merges all the type separated files, which had been listed above ([here](#application-file-structure)). \ No newline at end of file diff --git a/shared/backend.d/audio/player.d.ts b/shared/backend.d/audio/player.d.ts deleted file mode 100644 index cb66d161..00000000 --- a/shared/backend.d/audio/player.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {Device} from "tc-shared/audio/player"; - -export function initialize() : boolean; -export function initialized() : boolean; - -export function get_master_volume() : number; -export function set_master_volume(volume: number); - -export function on_ready(cb: () => any); - -export function available_devices() : Promise; -export function set_device(device_id: string) : Promise; -export function current_device() : Device; - -export function initializeFromGesture(); - -export function globalAudioContext() : AudioContext; \ No newline at end of file diff --git a/shared/backend.d/audio/sounds.d.ts b/shared/backend.d/audio/sounds.d.ts deleted file mode 100644 index 1140ee9e..00000000 --- a/shared/backend.d/audio/sounds.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {SoundFile} from "tc-shared/sound/Sounds"; - -export function play_sound(file: SoundFile) : Promise; \ No newline at end of file diff --git a/shared/backend.d/ppt.d.ts b/shared/backend.d/ppt.d.ts deleted file mode 100644 index 06e7a2ef..00000000 --- a/shared/backend.d/ppt.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {KeyEvent, KeyHook, SpecialKey} from "tc-shared/PPTListener"; - -export function initialize() : Promise; -export function finalize(); // most the times not really required - -export function register_key_listener(listener: (_: KeyEvent) => any); -export function unregister_key_listener(listener: (_: KeyEvent) => any); - -export function register_key_hook(hook: KeyHook); -export function unregister_key_hook(hook: KeyHook); - -export function key_pressed(code: string | SpecialKey) : boolean; \ No newline at end of file diff --git a/shared/backend.d/readme.md b/shared/backend.d/readme.md deleted file mode 100644 index 771585b6..00000000 --- a/shared/backend.d/readme.md +++ /dev/null @@ -1,2 +0,0 @@ -This folder contains declarations files which are required to be implemented -Else the UI shared pack wound work \ No newline at end of file diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 93a14c98..d3f604ae 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -2,7 +2,7 @@ import {AbstractServerConnection} from "./connection/ConnectionBase"; import {PermissionManager} from "./permission/PermissionManager"; import {GroupManager} from "./permission/GroupManager"; import {ServerSettings, Settings, settings, StaticSettings} from "./settings"; -import {Sound, SoundManager} from "./sound/Sounds"; +import {Sound, SoundManager} from "./audio/Sounds"; import {ConnectionProfile} from "./profiles/ConnectionProfile"; import {LogCategory, logError, logInfo, logTrace, logWarn} from "./log"; import {createErrorModal, createInfoModal, createInputModal, Modal} from "./ui/elements/Modal"; diff --git a/shared/js/KeyControl.ts b/shared/js/KeyControl.ts index 41d672a2..e3e43eaa 100644 --- a/shared/js/KeyControl.ts +++ b/shared/js/KeyControl.ts @@ -1,7 +1,5 @@ -import * as ppt from "tc-backend/ppt"; -import * as log from "./log"; import {LogCategory, logError, logWarn} from "./log"; -import {KeyDescriptor, KeyHook} from "./PPTListener"; +import {getKeyBoard, KeyDescriptor, KeyHook} from "./PPTListener"; import {Settings, settings} from "./settings"; import {server_connections} from "tc-shared/ConnectionManager"; import {tr} from "./i18n/localize"; @@ -181,18 +179,18 @@ function bindKey(action: string, key: KeyDescriptor) { keyBindings[action] = { hook: Object.assign({ - callback_press: () => control.handler(), - callback_release: () => {}, + callbackPress: () => control.handler(), + callbackRelease: () => {}, cancel: false }, key), binding: key }; - ppt.register_key_hook(keyBindings[action].hook); + getKeyBoard().registerHook(keyBindings[action].hook); } export function setKey(action: string, key?: KeyDescriptor) { if(typeof keyBindings[action] !== "undefined") { - ppt.unregister_key_hook(keyBindings[action].hook); + getKeyBoard().unregisterHook(keyBindings[action].hook); delete keyBindings[action]; } diff --git a/shared/js/PPTListener.ts b/shared/js/PPTListener.ts index 706af184..2c8306c2 100644 --- a/shared/js/PPTListener.ts +++ b/shared/js/PPTListener.ts @@ -1,4 +1,5 @@ import { tr } from "./i18n/localize"; +import {LogCategory, logTrace} from "tc-shared/log"; export enum KeyCode { KEY_CANCEL = 3, @@ -134,57 +135,199 @@ export enum SpecialKey { } export interface KeyDescriptor { - key_code: string; + keyCode: string; - key_ctrl: boolean; - key_windows: boolean; - key_shift: boolean; - key_alt: boolean; + keyCtrl: boolean; + keyWindows: boolean; + keyShift: boolean; + keyAlt: boolean; } export interface KeyEvent extends KeyDescriptor { readonly type: EventType; - readonly key: string; } export interface KeyHook extends KeyDescriptor { - cancel: boolean; - - callback_press: () => any; - callback_release: () => any; + callbackPress: () => any; + callbackRelease: () => any; } -export function key_description(key: KeyDescriptor) { +export interface KeyBoardBackend { + registerListener(listener: (event: KeyEvent) => void); + unregisterListener(listener: (event: KeyEvent) => void); + + registerHook(hook: KeyHook); + unregisterHook(hook: KeyHook); + + isKeyPressed(key: string | SpecialKey) : boolean; +} + +export class AbstractKeyBoard implements KeyBoardBackend { + protected readonly registeredListener: ((event: KeyEvent) => void)[]; + protected readonly activeSpecialKeys: { [key: number] : boolean }; + protected readonly activeKeys; + + protected registeredKeyHooks: KeyHook[] = []; + protected activeKeyHooks: KeyHook[] = []; + + constructor() { + this.activeSpecialKeys = {}; + this.activeKeys = {}; + this.registeredListener = []; + } + + protected destroy() {} + + isKeyPressed(key: string | SpecialKey): boolean { + if(typeof(key) === 'string') { + return typeof this.activeKeys[key] !== "undefined"; + } + + return this.activeSpecialKeys[key]; + } + + registerHook(hook: KeyHook) { + this.registeredKeyHooks.push(hook); + } + + unregisterHook(hook: KeyHook) { + this.registeredKeyHooks.remove(hook); + this.activeKeyHooks.remove(hook); + } + + registerListener(listener: (event: KeyEvent) => void) { + this.registeredListener.push(listener); + } + + unregisterListener(listener: (event: KeyEvent) => void) { + this.registeredListener.remove(listener); + } + + protected fireKeyEvent(event: KeyEvent) { + //console.debug("Trigger key event %o", key_event); + for(const listener of this.registeredListener) { + listener(event); + } + + if(event.type == EventType.KEY_TYPED) { + return; + } + + let oldHooks = [...this.activeKeyHooks]; + let newHooks = []; + + this.activeSpecialKeys[SpecialKey.ALT] = event.keyAlt; + this.activeSpecialKeys[SpecialKey.CTRL] = event.keyCtrl; + this.activeSpecialKeys[SpecialKey.SHIFT] = event.keyShift; + this.activeSpecialKeys[SpecialKey.WINDOWS] = event.keyWindows; + + delete this.activeKeys[event.keyCode]; + if(event.type == EventType.KEY_PRESS) { + this.activeKeys[event.keyCode] = event; + + for(const hook of this.registeredKeyHooks) { + if(hook.keyCode !== event.keyCode) { + continue; + } + + if(hook.keyAlt != event.keyAlt) { + continue; + } + + if(hook.keyCtrl != event.keyCtrl) { + continue; + } + + if(hook.keyShift != event.keyShift) { + continue; + } + + if(hook.keyWindows != event.keyWindows) { + continue; + } + + newHooks.push(hook); + if(!oldHooks.remove(hook) && hook.callbackPress) { + hook.callbackPress(); + logTrace(LogCategory.GENERAL, tr("Trigger key press for %o!"), hook); + } + } + } + + //We have a new situation + for(const hook of oldHooks) { + //Do not test for meta key states because they could differ in a key release event + if(hook.keyCode === event.keyCode) { + if(hook.callbackRelease) { + hook.callbackRelease(); + logTrace(LogCategory.GENERAL, tr("Trigger key release for %o!"), hook); + } + } else { + newHooks.push(hook); + } + } + + this.activeKeyHooks = newHooks; + } + + protected resetKeyboardState() { + this.activeSpecialKeys[SpecialKey.ALT] = false; + this.activeSpecialKeys[SpecialKey.CTRL] = false; + this.activeSpecialKeys[SpecialKey.SHIFT] = false; + this.activeSpecialKeys[SpecialKey.WINDOWS] = false; + + for(const code of Object.keys(this.activeKeys)) { + delete this.activeKeys[code]; + } + + for(const hook of this.activeKeyHooks) { + hook.callbackRelease(); + } + + this.activeKeyHooks = []; + } +} + +let keyBoardBackend: KeyBoardBackend; +export function getKeyBoard() : KeyBoardBackend { + return keyBoardBackend; +} + +export function setKeyBoardBackend(newBackend: KeyBoardBackend) { + keyBoardBackend = newBackend; +} + +export function getKeyDescription(key: KeyDescriptor) { let result = ""; - if(key.key_shift) { + if(key.keyShift) { result += " + " + tr("Shift"); } - if(key.key_alt) { + if(key.keyAlt) { result += " + " + tr("Alt"); } - if(key.key_ctrl) { + if(key.keyCtrl) { result += " + " + tr("CTRL"); } - if(key.key_windows) { + if(key.keyWindows) { result += " + " + tr("Win"); } - if(key.key_code) { - let key_name; - if(key.key_code.startsWith("Key")) { - key_name = key.key_code.substr(3); - } else if(key.key_code.startsWith("Digit")) { - key_name = key.key_code.substr(5); - } else if(key.key_code.startsWith("Numpad")) { - key_name = "Numpad " + key.key_code.substr(6); + if(key.keyCode) { + let keyName; + if(key.keyCode.startsWith("Key")) { + keyName = key.keyCode.substr(3); + } else if(key.keyCode.startsWith("Digit")) { + keyName = key.keyCode.substr(5); + } else if(key.keyCode.startsWith("Numpad")) { + keyName = "Numpad " + key.keyCode.substr(6); } else { - key_name = key.key_code; + keyName = key.keyCode; } - result += " + " + key_name; + result += " + " + keyName; } return result ? result.substr(3) : tr("unset"); } \ No newline at end of file diff --git a/shared/js/audio/Player.ts b/shared/js/audio/Player.ts new file mode 100644 index 00000000..9dee3f4b --- /dev/null +++ b/shared/js/audio/Player.ts @@ -0,0 +1,39 @@ +export interface OutputDevice { + device_id: string; + + driver: string; + name: string; +} + +export interface AudioBackendEvents { + notify_initialized: {}, + notify_volume_changed: { oldVolume: number, newVolume: number } +} + +export interface AudioBackend { + isInitialized() : boolean; + getAudioContext() : AudioContext | undefined; + + isDeviceRefreshAvailable() : boolean; + refreshDevices() : Promise; + + getAvailableDevices() : Promise; + getDefaultDeviceId() : string; + + getCurrentDevice() : OutputDevice; + setCurrentDevice(targetId: string | undefined) : Promise; + + getMasterVolume() : number; + setMasterVolume(volume: number); + + executeWhenInitialized(callback: () => void); +} + +let backend: AudioBackend; +export function getAudioBackend(): AudioBackend { + return backend; +} + +export function setAudioBackend(newBackend: AudioBackend) { + backend = newBackend; +} \ No newline at end of file diff --git a/shared/js/audio/recorder.ts b/shared/js/audio/Recorder.ts similarity index 95% rename from shared/js/audio/recorder.ts rename to shared/js/audio/Recorder.ts index 8f5b3147..28e4085e 100644 --- a/shared/js/audio/recorder.ts +++ b/shared/js/audio/Recorder.ts @@ -4,11 +4,9 @@ import {AbstractInput, LevelMeter} from "../voice/RecorderBase"; import {Registry} from "../events"; import {Settings, settings} from "tc-shared/settings"; -export type DeviceQueryResult = {} - export interface AudioRecorderBacked { createInput() : AbstractInput; - createLevelMeter(device: IDevice) : Promise; + createLevelMeter(device: InputDevice) : Promise; getDeviceList() : DeviceList; @@ -35,14 +33,14 @@ export interface DeviceListEvents { export type DeviceListState = "healthy" | "uninitialized" | "no-permissions" | "error"; -export interface IDevice { +export interface InputDevice { deviceId: string; driver: string; name: string; } -export namespace IDevice { +export namespace InputDevice { export const NoDeviceId = "none"; export const DefaultDeviceId = "default"; } @@ -60,7 +58,7 @@ export interface DeviceList { getPermissionState() : PermissionState; getStatus() : DeviceListState; - getDevices() : IDevice[]; + getDevices() : InputDevice[]; getDefaultDeviceId() : string; @@ -144,7 +142,7 @@ export abstract class AbstractDeviceList implements DeviceList { } abstract getDefaultDeviceId(): string; - abstract getDevices(): IDevice[]; + abstract getDevices(): InputDevice[]; abstract getEvents(): Registry; abstract isRefreshAvailable(): boolean; abstract refresh(): Promise; diff --git a/shared/js/sound/Sounds.ts b/shared/js/audio/Sounds.ts similarity index 96% rename from shared/js/sound/Sounds.ts rename to shared/js/audio/Sounds.ts index 4e020750..fe49e04b 100644 --- a/shared/js/sound/Sounds.ts +++ b/shared/js/audio/Sounds.ts @@ -1,8 +1,6 @@ -import * as log from "../log"; import {LogCategory, logError, logInfo, logWarn} from "../log"; import {Settings, settings} from "../settings"; import {ConnectionHandler} from "../ConnectionHandler"; -import * as sbackend from "tc-backend/audio/sounds"; import { tr } from "tc-shared/i18n/localize"; export enum Sound { @@ -226,7 +224,6 @@ export async function resolve_sound(sound: Sound) : Promise { } export let manager: SoundManager; - export class SoundManager { private readonly _handle: ConnectionHandler; private _playing_sounds: {[key: string]:number} = {}; @@ -256,7 +253,7 @@ export class SoundManager { } this._playing_sounds[handle.filename] = (this._playing_sounds[handle.filename] || 0) + 1; - sbackend.play_sound({ + getSoundBackend().playSound({ path: "audio/" + handle.filename, volume: volume * master_volume }).then(() => { @@ -275,4 +272,17 @@ export class SoundManager { options.callback(false); }); } +} + +export interface SoundBackend { + playSound(sound: SoundFile) : Promise; +} +let soundBackend: SoundBackend; + +export function getSoundBackend() { + return soundBackend; +} + +export function setSoundBackend(newSoundBackend: SoundBackend) { + soundBackend = newSoundBackend; } \ No newline at end of file diff --git a/shared/js/audio/player.ts b/shared/js/audio/player.ts deleted file mode 100644 index 0afb5f7c..00000000 --- a/shared/js/audio/player.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface Device { - device_id: string; - - driver: string; - name: string; -} \ No newline at end of file diff --git a/shared/js/connection/CommandHandler.ts b/shared/js/connection/CommandHandler.ts index afa5a6fd..042e8c46 100644 --- a/shared/js/connection/CommandHandler.ts +++ b/shared/js/connection/CommandHandler.ts @@ -1,6 +1,6 @@ import {LogCategory, logError, logInfo, logWarn} from "../log"; import {AbstractServerConnection, CommandOptions, ServerCommand} from "../connection/ConnectionBase"; -import {Sound} from "../sound/Sounds"; +import {Sound} from "../audio/Sounds"; import {CommandResult} from "../connection/ServerConnectionDeclaration"; import {createErrorModal, createInfoModal, createInputModal, createModal} from "../ui/elements/Modal"; import { diff --git a/shared/js/connection/rtc/Connection.ts b/shared/js/connection/rtc/Connection.ts index 21d9b42f..42680307 100644 --- a/shared/js/connection/rtc/Connection.ts +++ b/shared/js/connection/rtc/Connection.ts @@ -9,9 +9,9 @@ import {RemoteRTPAudioTrack, RemoteRTPTrackState, RemoteRTPVideoTrack, TrackClie import {SdpCompressor, SdpProcessor} from "./SdpUtils"; import {ErrorCode} from "tc-shared/connection/ErrorCode"; import {WhisperTarget} from "tc-shared/voice/VoiceWhisper"; -import {globalAudioContext} from "tc-backend/audio/player"; import {VideoBroadcastConfig, VideoBroadcastType} from "tc-shared/connection/VideoConnection"; import {Settings, settings} from "tc-shared/settings"; +import {getAudioBackend} from "tc-shared/audio/Player"; const kSdpCompressionMode = 1; @@ -144,7 +144,7 @@ function getIdleTrack(kind: "video" | "audio") : MediaStreamTrack | null { return dummyVideoTrack; } else if(kind === "audio") { if(!dummyAudioTrack) { - const dest = globalAudioContext().createMediaStreamDestination(); + const dest = getAudioBackend().getAudioContext().createMediaStreamDestination(); dummyAudioTrack = dest.stream.getAudioTracks()[0]; } diff --git a/shared/js/connection/rtc/RemoteTrack.ts b/shared/js/connection/rtc/RemoteTrack.ts index 009844b6..643a93fd 100644 --- a/shared/js/connection/rtc/RemoteTrack.ts +++ b/shared/js/connection/rtc/RemoteTrack.ts @@ -1,7 +1,7 @@ import {Registry} from "tc-shared/events"; import {LogCategory, logTrace, logWarn} from "tc-shared/log"; import {tr} from "tc-shared/i18n/localize"; -import {globalAudioContext, on_ready} from "tc-backend/audio/player"; +import {getAudioBackend} from "tc-shared/audio/Player"; export interface TrackClientInfo { media?: number, @@ -170,13 +170,13 @@ export class RemoteRTPAudioTrack extends RemoteRTPTrack { } */ - on_ready(() => { + getAudioBackend().executeWhenInitialized(() => { if(!this.mediaStream) { /* we've already been destroyed */ return; } - const audioContext = globalAudioContext(); + const audioContext = getAudioBackend().getAudioContext(); this.audioNode = audioContext.createMediaStreamSource(this.mediaStream); this.gainNode = audioContext.createGain(); this.updateGainNode(); diff --git a/shared/js/events/ClientGlobalControlHandler.ts b/shared/js/events/ClientGlobalControlHandler.ts index 872a7faf..3281df62 100644 --- a/shared/js/events/ClientGlobalControlHandler.ts +++ b/shared/js/events/ClientGlobalControlHandler.ts @@ -1,6 +1,5 @@ import {Registry} from "../events"; import {ClientGlobalControlEvents} from "../events/GlobalEvents"; -import {Sound} from "../sound/Sounds"; import {ConnectionHandler} from "../ConnectionHandler"; import {createErrorModal, createInfoModal, createInputModal} from "../ui/elements/Modal"; import PermissionType from "../permission/PermissionType"; @@ -20,6 +19,7 @@ import {LogCategory, logError, logWarn} from "tc-shared/log"; import {spawnEchoTestModal} from "tc-shared/ui/modal/echo-test/Controller"; import {spawnConnectModalNew} from "tc-shared/ui/modal/connect/Controller"; import {spawnBookmarkModal} from "tc-shared/ui/modal/bookmarks/Controller"; +import {Sound} from "tc-shared/audio/Sounds"; /* function initialize_sounds(event_registry: Registry) { diff --git a/shared/js/main.tsx b/shared/js/main.tsx index 2abacd47..0fd27e80 100644 --- a/shared/js/main.tsx +++ b/shared/js/main.tsx @@ -1,12 +1,9 @@ import * as loader from "tc-loader"; -import {Stage} from "tc-loader"; import * as bipc from "./ipc/BrowserIPC"; -import * as sound from "./sound/Sounds"; +import * as sound from "./audio/Sounds"; import * as i18n from "./i18n/localize"; import {tra} from "./i18n/localize"; import * as fidentity from "./profiles/identities/TeaForumIdentity"; -import * as aplayer from "tc-backend/audio/player"; -import * as ppt from "tc-backend/ppt"; import * as global_ev_handler from "./events/ClientGlobalControlHandler"; import {AppParameters, settings, Settings, UrlParameterBuilder, UrlParameterParser} from "tc-shared/settings"; import {LogCategory, logDebug, logError, logInfo, logWarn} from "tc-shared/log"; @@ -55,6 +52,7 @@ import "./ui/elements/Tab"; import "./clientservice"; import "./text/bbcode/InviteController"; import "./text/bbcode/YoutubeController"; +import {getAudioBackend} from "tc-shared/audio/Player"; assertMainApplication(); @@ -73,12 +71,7 @@ async function initialize() { async function initializeApp() { global_ev_handler.initialize(global_client_actions); - - if(!aplayer.initialize()) { - console.warn(tr("Failed to initialize audio controller!")); - } - - aplayer.on_ready(() => aplayer.set_master_volume(settings.getValue(Settings.KEY_SOUND_MASTER) / 100)); + getAudioBackend().setMasterVolume(settings.getValue(Settings.KEY_SOUND_MASTER) / 100); const recorder = new RecorderProfile("default"); try { @@ -93,14 +86,6 @@ async function initializeApp() { logInfo(LogCategory.AUDIO, tr("Sounds initialized")); }); sound.set_master_volume(settings.getValue(Settings.KEY_SOUND_MASTER_SOUNDS) / 100); - - try { - await ppt.initialize(); - } catch(error) { - logError(LogCategory.GENERAL, tr("Failed to initialize ppt!\nError: %o"), error); - loader.critical_error(tr("Failed to initialize ppt!")); - return; - } } /* The native client has received a connect request. */ @@ -200,7 +185,7 @@ async function doHandleConnectRequest(serverAddress: string, serverUniqueId: str return { status: "profile-invalid" }; } - if(!aplayer.initialized()) { + if(!getAudioBackend().isInitialized()) { /* Trick the client into clicking somewhere on the site to initialize audio */ const resultPromise = new Promise(resolve => { spawnYesNo(tra("Connect to {}", serverAddress), tra("Would you like to connect to {}?", serverAddress), resolve).open(); @@ -211,7 +196,7 @@ async function doHandleConnectRequest(serverAddress: string, serverUniqueId: str return { status: "client-aborted" }; } - await new Promise(resolve => aplayer.on_ready(resolve)); + await new Promise(resolve => getAudioBackend().executeWhenInitialized(resolve)); } const clientNickname = parameters.getValue(AppParameters.KEY_CONNECT_NICKNAME, undefined); @@ -403,15 +388,6 @@ const task_teaweb_starter: loader.Task = { try { await initializeApp(); main(); - if(!aplayer.initialized()) { - logInfo(LogCategory.VOICE, tr("Initialize audio controller later!")); - if(!aplayer.initializeFromGesture) { - console.error(tr("Missing aplayer.initializeFromGesture")); - } else { - $(document).one('click', () => aplayer.initializeFromGesture()); - } - } - loader.config.abortAnimationOnFinish = settings.getValue(Settings.KEY_LOADER_ANIMATION_ABORT); } catch (ex) { console.error(ex.stack); diff --git a/shared/js/tree/Channel.ts b/shared/js/tree/Channel.ts index 581b8b07..0a6a0105 100644 --- a/shared/js/tree/Channel.ts +++ b/shared/js/tree/Channel.ts @@ -6,10 +6,9 @@ import {PermissionType} from "../permission/PermissionType"; import {settings, Settings} from "../settings"; import * as contextmenu from "../ui/elements/ContextMenu"; import {MenuEntryType} from "../ui/elements/ContextMenu"; -import {Sound} from "../sound/Sounds"; +import {Sound} from "../audio/Sounds"; import {createErrorModal, createInfoModal, createInputModal} from "../ui/elements/Modal"; import {CommandResult} from "../connection/ServerConnectionDeclaration"; -import * as htmltags from "../ui/htmltags"; import {hashPassword} from "../utils/helpers"; import {openChannelInfo} from "../ui/modal/ModalChannelInfo"; import {formatMessage} from "../ui/frames/chat"; diff --git a/shared/js/tree/ChannelTree.tsx b/shared/js/tree/ChannelTree.tsx index 96cf50ed..45e35ca4 100644 --- a/shared/js/tree/ChannelTree.tsx +++ b/shared/js/tree/ChannelTree.tsx @@ -2,7 +2,7 @@ import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; import {MenuEntryType} from "tc-shared/ui/elements/ContextMenu"; import {LogCategory, logDebug, logError, logWarn} from "tc-shared/log"; import {PermissionType} from "tc-shared/permission/PermissionType"; -import {Sound} from "tc-shared/sound/Sounds"; +import {Sound} from "tc-shared/audio/Sounds"; import {Group} from "tc-shared/permission/GroupManager"; import {ServerAddress, ServerEntry} from "./Server"; import {ChannelEntry, ChannelProperties, ChannelSubscribeMode} from "./Channel"; diff --git a/shared/js/tree/Client.ts b/shared/js/tree/Client.ts index 36e72e4d..80c4b29f 100644 --- a/shared/js/tree/Client.ts +++ b/shared/js/tree/Client.ts @@ -4,7 +4,7 @@ import {ChannelTree} from "./ChannelTree"; import * as log from "../log"; import {LogCategory, logDebug, logError, logInfo, LogType} from "../log"; import {Settings, settings} from "../settings"; -import {Sound} from "../sound/Sounds"; +import {Sound} from "../audio/Sounds"; import {Group, GroupManager, GroupTarget, GroupType} from "../permission/GroupManager"; import PermissionType from "../permission/PermissionType"; import {createErrorModal, createInputModal} from "../ui/elements/Modal"; diff --git a/shared/js/tree/Server.ts b/shared/js/tree/Server.ts index 9d3159ea..95ffa989 100644 --- a/shared/js/tree/Server.ts +++ b/shared/js/tree/Server.ts @@ -3,7 +3,7 @@ import {Settings, settings} from "../settings"; import * as contextmenu from "../ui/elements/ContextMenu"; import * as log from "../log"; import {LogCategory, logInfo, LogType} from "../log"; -import {Sound} from "../sound/Sounds"; +import {Sound} from "../audio/Sounds"; import {openServerInfo} from "../ui/modal/ModalServerInfo"; import {createServerModal} from "../ui/modal/ModalServerEdit"; import {spawnIconSelect} from "../ui/modal/ModalIconSelect"; diff --git a/shared/js/ui/frames/control-bar/Controller.ts b/shared/js/ui/frames/control-bar/Controller.ts index 775ec407..ab753d53 100644 --- a/shared/js/ui/frames/control-bar/Controller.ts +++ b/shared/js/ui/frames/control-bar/Controller.ts @@ -16,7 +16,7 @@ import {VideoBroadcastType, VideoConnectionStatus} from "tc-shared/connection/Vi import {tr} from "tc-shared/i18n/localize"; import {getVideoDriver} from "tc-shared/video/VideoSource"; import {kLocalBroadcastChannels} from "tc-shared/ui/frames/video/Definitions"; -import {getRecorderBackend, IDevice} from "tc-shared/audio/recorder"; +import {getRecorderBackend, InputDevice} from "tc-shared/audio/Recorder"; import {defaultRecorder, defaultRecorderEvents} from "tc-shared/voice/RecorderProfile"; import {bookmarks} from "tc-shared/Bookmarks"; import {connectionHistory} from "tc-shared/connectionlog/History"; @@ -276,7 +276,7 @@ class InfoController { this.events.fire_react("notify_microphone_list", { devices: devices.map(device => { let selected = false; - if(selectedDevice === IDevice.DefaultDeviceId && device.deviceId === defaultDevice) { + if(selectedDevice === InputDevice.DefaultDeviceId && device.deviceId === defaultDevice) { selected = true; } else if(selectedDevice === device.deviceId) { selected = true; diff --git a/shared/js/ui/modal/ModalKeySelect.ts b/shared/js/ui/modal/ModalKeySelect.ts index c9775ded..7eee2d69 100644 --- a/shared/js/ui/modal/ModalKeySelect.ts +++ b/shared/js/ui/modal/ModalKeySelect.ts @@ -1,6 +1,5 @@ import {createModal} from "../../ui/elements/Modal"; -import {EventType, key_description, KeyEvent} from "../../PPTListener"; -import * as ppt from "tc-backend/ppt"; +import {EventType, getKeyBoard, getKeyDescription, KeyEvent} from "../../PPTListener"; import {tr} from "tc-shared/i18n/localize"; export function spawnKeySelect(callback: (key?: KeyEvent) => void) { @@ -27,7 +26,7 @@ export function spawnKeySelect(callback: (key?: KeyEvent) => void) { current_key = event; current_key_age = Date.now(); - container_key.text(key_description(event)); + container_key.text(getKeyDescription(event)); button_save.prop("disabled", false); } }; @@ -36,7 +35,7 @@ export function spawnKeySelect(callback: (key?: KeyEvent) => void) { button_save.on('click', () => { if (__build.version !== "web") { /* Because pressing the close button is also a mouse action */ - if (current_key_age + 1000 > Date.now() && current_key.key_code == "MOUSE1") + if (current_key_age + 1000 > Date.now() && current_key.keyCode == "MOUSE1") current_key = last_key; } @@ -45,8 +44,9 @@ export function spawnKeySelect(callback: (key?: KeyEvent) => void) { }).prop("disabled", true); button_cancel.on('click', () => modal.close()); - ppt.register_key_listener(listener); - modal.close_listener.push(() => ppt.unregister_key_listener(listener)); + const keyboard = getKeyBoard(); + keyboard.registerListener(listener); + modal.close_listener.push(() => keyboard.unregisterListener(listener)); modal.htmlTag.find(".modal-body").addClass("modal-keyselect modal-green"); modal.open(); diff --git a/shared/js/ui/modal/ModalSettings.tsx b/shared/js/ui/modal/ModalSettings.tsx index 1037621d..ea5c4f42 100644 --- a/shared/js/ui/modal/ModalSettings.tsx +++ b/shared/js/ui/modal/ModalSettings.tsx @@ -1,8 +1,8 @@ import {createErrorModal, createInfoModal, createInputModal, createModal, Modal} from "tc-shared/ui/elements/Modal"; import {sliderfy} from "tc-shared/ui/elements/Slider"; import {settings, Settings} from "tc-shared/settings"; -import * as sound from "tc-shared/sound/Sounds"; -import {manager, set_master_volume, Sound} from "tc-shared/sound/Sounds"; +import * as sound from "tc-shared/audio/Sounds"; +import {manager, set_master_volume, Sound} from "tc-shared/audio/Sounds"; import * as profiles from "tc-shared/profiles/ConnectionProfile"; import {ConnectionProfile} from "tc-shared/profiles/ConnectionProfile"; import {IdentitifyType} from "tc-shared/profiles/Identity"; @@ -18,8 +18,7 @@ import * as i18nc from "tc-shared/i18n/country"; import * as forum from "tc-shared/profiles/identities/teaspeak-forum"; import {formatMessage, set_icon_size} from "tc-shared/ui/frames/chat"; import {spawnTeamSpeakIdentityImport, spawnTeamSpeakIdentityImprove} from "tc-shared/ui/modal/ModalIdentity"; -import {Device} from "tc-shared/audio/player"; -import * as aplayer from "tc-backend/audio/player"; +import {getAudioBackend, OutputDevice} from "tc-shared/audio/Player"; import {KeyMapSettings} from "tc-shared/ui/modal/settings/Keymap"; import * as React from "react"; import * as ReactDOM from "react-dom"; @@ -628,8 +627,8 @@ function settings_audio_speaker(container: JQuery, modal: Modal) { const update_devices = () => { container_devices.children().remove(); - const current_selected = aplayer.current_device(); - const generate_device = (device: Device | undefined) => { + const current_selected = getAudioBackend().getCurrentDevice(); + const generate_device = (device: OutputDevice | undefined) => { const selected = device === current_selected || (typeof (current_selected) !== "undefined" && typeof (device) !== "undefined" && current_selected.device_id == device.device_id); const tag = $.spawn("div").addClass("device").toggleClass("selected", selected).append( @@ -654,7 +653,7 @@ function settings_audio_speaker(container: JQuery, modal: Modal) { _old.removeClass("selected"); tag.addClass("selected"); - aplayer.set_device(device ? device.device_id : null).then(() => { + getAudioBackend().setCurrentDevice(device?.device_id).then(() => { logDebug(LogCategory.AUDIO, tr("Changed default speaker device")); }).catch((error) => { _old.addClass("selected"); @@ -669,7 +668,7 @@ function settings_audio_speaker(container: JQuery, modal: Modal) { }; generate_device(undefined).appendTo(container_devices); - aplayer.available_devices().then(result => { + getAudioBackend().getAvailableDevices().then(result => { contianer_error.text("").hide(); result.forEach(e => generate_device(e).appendTo(container_devices)); }).catch(error => { @@ -710,8 +709,7 @@ function settings_audio_speaker(container: JQuery, modal: Modal) { slider.on('change', event => { const volume = parseInt(slider.attr('value')); - if (aplayer.set_master_volume) - aplayer.set_master_volume(volume / 100); + getAudioBackend().setMasterVolume(volume / 100); settings.setValue(Settings.KEY_SOUND_MASTER, volume); }); } diff --git a/shared/js/ui/modal/settings/Keymap.tsx b/shared/js/ui/modal/settings/Keymap.tsx index 161e12a6..8f696d35 100644 --- a/shared/js/ui/modal/settings/Keymap.tsx +++ b/shared/js/ui/modal/settings/Keymap.tsx @@ -86,7 +86,7 @@ class KeyActionEntry extends ReactComponentBase{ppt.key_description(this.state.assignedKey)}; + rightItem =
{ppt.getKeyDescription(this.state.assignedKey)}
; } else { rightItem =
{ let deviceId = defaultRecorder.getDeviceId(); - if(deviceId === IDevice.DefaultDeviceId) { + if(deviceId === InputDevice.DefaultDeviceId) { return { type: "default" }; - } else if(deviceId === IDevice.NoDeviceId) { + } else if(deviceId === InputDevice.NoDeviceId) { return { type: "none" }; } else { return { type: "device", deviceId: deviceId }; @@ -233,7 +233,7 @@ export function initialize_audio_microphone_controller(events: Registry { - if (!aplayer.initialized()) { + if (!getAudioBackend().isInitialized()) { events.fire_react("notify_devices", { status: "audio-not-initialized" }); @@ -452,10 +452,8 @@ export function initialize_audio_microphone_controller(events: Registry { - events.fire_react("query_devices"); - }); + if(!getAudioBackend().isInitialized()) { + getAudioBackend().executeWhenInitialized(() => events.fire_react("query_devices")); } } diff --git a/shared/js/ui/modal/settings/MicrophoneRenderer.tsx b/shared/js/ui/modal/settings/MicrophoneRenderer.tsx index 6837513e..de07fce2 100644 --- a/shared/js/ui/modal/settings/MicrophoneRenderer.tsx +++ b/shared/js/ui/modal/settings/MicrophoneRenderer.tsx @@ -11,12 +11,12 @@ import {createErrorModal} from "tc-shared/ui/elements/Modal"; import {Slider} from "tc-shared/ui/react-elements/Slider"; import {RadioButton} from "tc-shared/ui/react-elements/RadioButton"; import {VadType} from "tc-shared/voice/RecorderProfile"; -import {key_description, KeyDescriptor} from "tc-shared/PPTListener"; +import {getKeyDescription, KeyDescriptor} from "tc-shared/PPTListener"; import {spawnKeySelect} from "tc-shared/ui/modal/ModalKeySelect"; import {Checkbox} from "tc-shared/ui/react-elements/Checkbox"; import {BoxedInputField} from "tc-shared/ui/react-elements/InputField"; -import {IDevice} from "tc-shared/audio/recorder"; import {HighlightContainer, HighlightRegion, HighlightText} from "./Heighlight"; +import {InputDevice} from "tc-shared/audio/Recorder"; const cssStyle = require("./Microphone.scss"); @@ -128,7 +128,7 @@ const Microphone = (props: { events: Registry, device:
{props.device.name}
- {props.device.id === IDevice.NoDeviceId ? undefined : + {props.device.id === InputDevice.NoDeviceId ? undefined : }
@@ -401,7 +401,7 @@ const PPTKeyButton = React.memo((props: { events: Registry{key_description(key)}; + >{getKeyDescription(key)}; } }); diff --git a/shared/js/ui/modal/transfer/FileBrowserControllerRemote.ts b/shared/js/ui/modal/transfer/FileBrowserControllerRemote.ts index c6bb8e0c..9638c44c 100644 --- a/shared/js/ui/modal/transfer/FileBrowserControllerRemote.ts +++ b/shared/js/ui/modal/transfer/FileBrowserControllerRemote.ts @@ -5,8 +5,7 @@ import {CommandResult} from "../../../connection/ServerConnectionDeclaration"; import PermissionType from "../../../permission/PermissionType"; import {LogCategory, logError, logTrace} from "../../../log"; import {Entry, MenuEntry, MenuEntryType, spawn_context_menu} from "../../../ui/elements/ContextMenu"; -import * as ppt from "tc-backend/ppt"; -import {SpecialKey} from "../../../PPTListener"; +import {getKeyBoard, SpecialKey} from "../../../PPTListener"; import {spawnYesNo} from "../../../ui/modal/ModalYesNo"; import {tr, tra, traj} from "../../../i18n/localize"; import { @@ -427,7 +426,7 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand icon_class: "client-file_refresh" }); } else { - const forceDelete = ppt.key_pressed(SpecialKey.SHIFT); + const forceDelete = getKeyBoard().isKeyPressed(SpecialKey.SHIFT); if (selection.length === 0) { entries.push({ type: MenuEntryType.ENTRY, diff --git a/shared/js/ui/modal/transfer/FileBrowserRenderer.tsx b/shared/js/ui/modal/transfer/FileBrowserRenderer.tsx index 98b5797f..44dcd227 100644 --- a/shared/js/ui/modal/transfer/FileBrowserRenderer.tsx +++ b/shared/js/ui/modal/transfer/FileBrowserRenderer.tsx @@ -1,8 +1,7 @@ import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events"; import {useContext, useEffect, useRef, useState} from "react"; import {FileType} from "tc-shared/file/FileManager"; -import * as ppt from "tc-backend/ppt"; -import {SpecialKey} from "tc-shared/PPTListener"; +import {getKeyBoard, SpecialKey} from "tc-shared/PPTListener"; import {createErrorModal} from "tc-shared/ui/elements/Modal"; import {tra} from "tc-shared/i18n/localize"; import {network} from "tc-shared/ui/frames/chat"; @@ -422,7 +421,7 @@ const FileName = (props: { path: string, file: ListedFileInfo }) => { if (props.file.virtual || props.file.mode === "creating" || props.file.mode === "uploading") return; - if (!ppt.key_pressed(SpecialKey.SHIFT)) + if (!getKeyBoard().isKeyPressed(SpecialKey.SHIFT)) return; event.stopPropagation(); @@ -656,7 +655,7 @@ const FileListEntry = (props: { row: TableRow, columns: TableCol onClick={() => props.events.fire("action_select_files", { files: [{name: file.name, type: file.type}], - mode: ppt.key_pressed(SpecialKey.SHIFT) ? "toggle" : "exclusive" + mode: getKeyBoard().isKeyPressed(SpecialKey.SHIFT) ? "toggle" : "exclusive" })} onContextMenu={e => { if (!selected) { @@ -664,7 +663,7 @@ const FileListEntry = (props: { row: TableRow, columns: TableCol /* explicitly clicked on one file */ props.events.fire("action_select_files", { files: [{name: file.name, type: file.type}], - mode: ppt.key_pressed(SpecialKey.SHIFT) ? "toggle" : "exclusive" + mode: getKeyBoard().isKeyPressed(SpecialKey.SHIFT) ? "toggle" : "exclusive" }); } else { props.events.fire("action_select_files", {files: [], mode: "exclusive"}); diff --git a/shared/js/voice/RecorderBase.ts b/shared/js/voice/RecorderBase.ts index 09c53cda..25e3d6b0 100644 --- a/shared/js/voice/RecorderBase.ts +++ b/shared/js/voice/RecorderBase.ts @@ -1,4 +1,4 @@ -import {IDevice} from "../audio/recorder"; +import {InputDevice} from "../audio/Recorder"; import {Registry} from "../events"; import {Filter, FilterType, FilterTypeClass} from "../voice/Filter"; @@ -119,7 +119,7 @@ export interface AbstractInput { } export interface LevelMeter { - getDevice() : IDevice; + getDevice() : InputDevice; setObserver(callback: (value: number) => any); diff --git a/shared/js/voice/RecorderProfile.ts b/shared/js/voice/RecorderProfile.ts index 70edabe7..9d657bd5 100644 --- a/shared/js/voice/RecorderProfile.ts +++ b/shared/js/voice/RecorderProfile.ts @@ -1,15 +1,13 @@ -import * as log from "../log"; import {LogCategory, logDebug, logError, logWarn} from "../log"; import {AbstractInput, FilterMode} from "../voice/RecorderBase"; -import {KeyDescriptor, KeyHook} from "../PPTListener"; +import {getKeyBoard, KeyDescriptor, KeyHook} from "../PPTListener"; import {Settings, settings} from "../settings"; import {ConnectionHandler} from "../ConnectionHandler"; -import * as aplayer from "tc-backend/audio/player"; -import * as ppt from "tc-backend/ppt"; -import {getRecorderBackend, IDevice} from "../audio/recorder"; +import {getRecorderBackend, InputDevice} from "../audio/Recorder"; import {FilterType, StateFilter, ThresholdFilter} from "../voice/Filter"; import { tr } from "tc-shared/i18n/localize"; import {Registry} from "tc-shared/events"; +import {getAudioBackend} from "tc-shared/audio/Player"; export type VadType = "threshold" | "push_to_talk" | "active"; export interface RecorderProfileConfig { @@ -85,7 +83,7 @@ export class RecorderProfile { this.volatile = typeof(volatile) === "boolean" ? volatile : false; this.pptHook = { - callback_release: () => { + callbackRelease: () => { if(this.pptTimeout) clearTimeout(this.pptTimeout); @@ -94,14 +92,12 @@ export class RecorderProfile { }, Math.max(this.config.vad_push_to_talk.delay, 0)); }, - callback_press: () => { + callbackPress: () => { if(this.pptTimeout) clearTimeout(this.pptTimeout); this.registeredFilter["ppt-gate"]?.setState(false); }, - - cancel: false } as KeyHook; this.pptHookRegistered = false; } @@ -125,7 +121,7 @@ export class RecorderProfile { /* default values */ this.config = { version: 1, - device_id: IDevice.DefaultDeviceId, + device_id: InputDevice.DefaultDeviceId, volume: 100, vad_threshold: { @@ -145,7 +141,7 @@ export class RecorderProfile { Object.assign(this.config, config || {}); } - aplayer.on_ready(async () => { + getAudioBackend().executeWhenInitialized(async () => { await getRecorderBackend().getDeviceList().awaitInitialized(); await this.initializeInput(); @@ -185,7 +181,7 @@ export class RecorderProfile { if(this.config.device_id) { await this.input.setDeviceId(this.config.device_id); } else { - await this.input.setDeviceId(IDevice.DefaultDeviceId); + await this.input.setDeviceId(InputDevice.DefaultDeviceId); } } @@ -201,15 +197,12 @@ export class RecorderProfile { } if(this.pptHookRegistered) { - ppt.unregister_key_hook(this.pptHook); + getKeyBoard().unregisterHook(this.pptHook); this.pptHookRegistered = false; } - for(const key of ["key_alt", "key_ctrl", "key_shift", "key_windows", "key_code"]) { - this.pptHook[key] = this.config.vad_push_to_talk[key]; - } - - ppt.register_key_hook(this.pptHook); + Object.assign(this.pptHook, this.getPushToTalkKey()); + getKeyBoard().registerHook(this.pptHook); this.pptHookRegistered = true; this.registeredFilter["ppt-gate"]?.setState(true); @@ -227,7 +220,7 @@ export class RecorderProfile { this.registeredFilter["ppt-gate"].setEnabled(false); if(this.pptHookRegistered) { - ppt.unregister_key_hook(this.pptHook); + getKeyBoard().unregisterHook(this.pptHook); this.pptHookRegistered = false; } @@ -251,10 +244,8 @@ export class RecorderProfile { filter.setEnabled(true); filter.setState(true); /* by default set filtered */ - for(const key of ["key_alt", "key_ctrl", "key_shift", "key_windows", "key_code"]) - this.pptHook[key] = this.config.vad_push_to_talk[key]; - - ppt.register_key_hook(this.pptHook); + Object.assign(this.pptHook, this.getPushToTalkKey()); + getKeyBoard().registerHook(this.pptHook); this.pptHookRegistered = true; } else if(this.config.vad_type === "active") { /* we don't have to initialize any filters */ @@ -311,10 +302,27 @@ export class RecorderProfile { this.save(); } - getPushToTalkKey() : KeyDescriptor { return this.config.vad_push_to_talk; } + getPushToTalkKey() : KeyDescriptor { + return { + keyCode: this.config.vad_push_to_talk.key_code, + + keyAlt: this.config.vad_push_to_talk.key_alt, + keyCtrl: this.config.vad_push_to_talk.key_ctrl, + keyShift: this.config.vad_push_to_talk.key_shift, + keyWindows: this.config.vad_push_to_talk.key_windows, + } + } + setPushToTalkKey(key: KeyDescriptor) { - for(const _key of ["key_alt", "key_ctrl", "key_shift", "key_windows", "key_code"]) - this.config.vad_push_to_talk[_key] = key[_key]; + this.config.vad_push_to_talk = { + delay: this.config.vad_push_to_talk.delay, + key_code: key.keyCode, + + key_alt: key.keyAlt, + key_ctrl: key.keyCtrl, + key_shift: key.keyShift, + key_windows: key.keyWindows + }; this.reinitializePPTHook(); this.save(); @@ -329,8 +337,8 @@ export class RecorderProfile { this.save(); } - getDeviceId() : string | typeof IDevice.DefaultDeviceId | typeof IDevice.NoDeviceId { return this.config.device_id; } - setDevice(device: IDevice | typeof IDevice.DefaultDeviceId | typeof IDevice.NoDeviceId) : Promise { + getDeviceId() : string | typeof InputDevice.DefaultDeviceId | typeof InputDevice.NoDeviceId { return this.config.device_id; } + setDevice(device: InputDevice | typeof InputDevice.DefaultDeviceId | typeof InputDevice.NoDeviceId) : Promise { let deviceId; if(typeof device === "object") { deviceId = device.deviceId; diff --git a/shared/tsconfig/tsconfig.declarations.json b/shared/tsconfig/tsconfig.declarations.json index 5a5392ce..299688e8 100644 --- a/shared/tsconfig/tsconfig.declarations.json +++ b/shared/tsconfig/tsconfig.declarations.json @@ -11,7 +11,6 @@ "baseUrl": "../../", "paths": { "tc-shared/*": ["shared/js/*"], - "tc-backend/*": ["shared/backend.d/*"], "tc-loader": ["loader/exports/loader.d.ts"], "svg-sprites/*": ["shared/svg-sprites/*"], "vendor/xbbcode/*": ["vendor/xbbcode/src/*"], diff --git a/tsconfig.json b/tsconfig.json index ee853b27..46826bdd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,9 +12,6 @@ "baseUrl": ".", "paths": { "tc-shared/*": ["shared/js/*"], - "tc-backend/audio-lib/*": ["web/audio-lib/pkg/*"], /* specific web part */ - "tc-backend/web/*": ["web/app/*"], /* specific web part */ - "tc-backend/*": ["shared/backend.d/*"], "tc-loader": ["loader/exports/loader.d.ts"], "tc-events": ["vendor/TeaEventBus/src/index.ts"], "tc-services": ["vendor/TeaClientServices/src/index.ts"], diff --git a/web/app/FileTransfer.ts b/web/app/FileTransfer.ts index 988779cb..bf835016 100644 --- a/web/app/FileTransfer.ts +++ b/web/app/FileTransfer.ts @@ -12,7 +12,6 @@ import { TransferSourceType, TransferTargetType } from "tc-shared/file/Transfer"; -import * as log from "tc-shared/log"; import {LogCategory, logError} from "tc-shared/log"; import { tr } from "tc-shared/i18n/localize"; diff --git a/web/app/KeyBoard.ts b/web/app/KeyBoard.ts new file mode 100644 index 00000000..7f37c496 --- /dev/null +++ b/web/app/KeyBoard.ts @@ -0,0 +1,49 @@ +import {AbstractKeyBoard, EventType, KeyEvent} from "tc-shared/PPTListener"; + +export class WebKeyBoard extends AbstractKeyBoard { + private readonly listenerBlur; + private readonly listenerKeyPress; + private readonly listenerKeyDown; + private readonly listenerKeyUp; + + constructor() { + super(); + + this.listenerBlur = () => this.handleBlurEvent(); + this.listenerKeyPress = event => this.handleNativeKeyEvent(EventType.KEY_TYPED, event); + this.listenerKeyDown = event => this.handleNativeKeyEvent(EventType.KEY_PRESS, event); + this.listenerKeyUp = event => this.handleNativeKeyEvent(EventType.KEY_RELEASE, event); + + window.addEventListener("blur", () => this.handleBlurEvent()); + document.addEventListener('keypress', this.listenerKeyPress); + document.addEventListener('keydown', this.listenerKeyDown); + document.addEventListener('keyup', this.listenerKeyUp); + } + + destroy() { + window.removeEventListener("blur", () => this.handleBlurEvent()); + document.removeEventListener('keypress', this.listenerKeyPress); + document.removeEventListener('keydown', this.listenerKeyDown); + document.removeEventListener('keyup', this.listenerKeyUp); + } + + private handleNativeKeyEvent(type: EventType, nativeEvent: KeyboardEvent) { + const event: KeyEvent = { + type: type, + + key: nativeEvent.key, + keyCode: nativeEvent.code, + + keyCtrl: nativeEvent.ctrlKey, + keyShift: nativeEvent.shiftKey, + keyAlt: nativeEvent.altKey, + keyWindows: nativeEvent.metaKey, + }; + + this.fireKeyEvent(event); + } + + private handleBlurEvent() { + this.resetKeyboardState(); + } +} \ No newline at end of file diff --git a/web/app/audio/Player.ts b/web/app/audio/Player.ts new file mode 100644 index 00000000..45d0c35f --- /dev/null +++ b/web/app/audio/Player.ts @@ -0,0 +1,148 @@ +import {AudioBackend, AudioBackendEvents, OutputDevice} from "tc-shared/audio/Player"; +import {LogCategory, logDebug, logError, logInfo, logWarn} from "tc-shared/log"; +import {tr} from "tc-shared/i18n/localize"; +import {Registry} from "tc-events"; + +const kWebDevice: OutputDevice = { + device_id: "default", + name: "default playback", + driver: 'Web Audio' +}; + +export class WebAudioBackend implements AudioBackend { + private readonly events: Registry; + private readonly audioContext: AudioContext; + private state: "initializing" | "running" | "closed"; + private masterVolume: number; + + private gestureListener: () => void; + + constructor() { + this.events = new Registry(); + this.state = "initializing"; + this.masterVolume = 1; + + this.audioContext = new (window.webkitAudioContext || window.AudioContext)(); + this.audioContext.onstatechange = () => this.handleAudioContextStateChanged(false); + this.handleAudioContextStateChanged(true); + } + + destroy() { + this.state = "closed"; + + document.removeEventListener("click", this.gestureListener); + this.gestureListener = undefined; + + this.audioContext.close().catch(error => { + logWarn(LogCategory.AUDIO, tr("Failed to close AudioContext: %o"), error); + }); + } + + executeWhenInitialized(callback: () => void) { + if(this.state === "running") { + callback(); + } else { + this.events.one("notify_initialized", callback); + } + } + + isInitialized(): boolean { + return this.state === "running"; + } + + getAudioContext(): AudioContext | undefined { + return this.audioContext; + } + + async getAvailableDevices(): Promise { + return [ kWebDevice ]; + } + + getDefaultDeviceId(): string { + return kWebDevice.device_id; + } + + getMasterVolume(): number { + return this.masterVolume; + } + + setMasterVolume(volume: number) { + if(this.masterVolume === volume) { + return; + } + + const oldVolume = this.masterVolume; + this.masterVolume = volume; + this.events.fire("notify_volume_changed", { + oldVolume: oldVolume, + newVolume: volume + }); + } + + isDeviceRefreshAvailable(): boolean { + return false; + } + + refreshDevices(): Promise { + return Promise.resolve(undefined); + } + + private handleAudioContextStateChanged(initialState: boolean) { + switch (this.audioContext.state) { + case "suspended": + if(initialState) { + logDebug(LogCategory.AUDIO, tr("Created new AudioContext but user hasn't yet allowed audio playback. Awaiting his gesture.")); + this.awaitGesture(); + return; + } else { + logWarn(LogCategory.AUDIO, tr("AudioContext state changed to 'suspended'. Trying to resume it.")); + this.tryResume(); + } + break; + + case "closed": + if(this.state === "closed") { + return; + } + + logError(LogCategory.AUDIO, tr("AudioContext state changed to 'closed'. No audio will be payed.")); + return; + + case "running": + logDebug(LogCategory.AUDIO, tr("Successfully initialized the AudioContext.")); + this.state = "running"; + this.events.fire("notify_initialized"); + return; + } + } + + private tryResume() { + this.audioContext.resume().then(() => { + logInfo(LogCategory.AUDIO, tr("Successfully resumed AudioContext.")); + }).catch(error => { + logError(LogCategory.AUDIO, tr("Failed to resume AudioContext: %o"), error); + }); + } + + private awaitGesture() { + if(this.gestureListener) { + return; + } + + this.gestureListener = () => { + document.removeEventListener("click", this.gestureListener); + this.gestureListener = undefined; + this.tryResume(); + }; + + document.addEventListener("click", this.gestureListener); + } + + async setCurrentDevice(targetId: string | undefined): Promise { + /* TODO: Mute on "no device"? */ + } + + getCurrentDevice(): OutputDevice { + return kWebDevice; + } +} \ No newline at end of file diff --git a/web/app/audio/Recorder.ts b/web/app/audio/Recorder.ts index 71b2752a..2884c50b 100644 --- a/web/app/audio/Recorder.ts +++ b/web/app/audio/Recorder.ts @@ -1,4 +1,4 @@ -import {AudioRecorderBacked, DeviceList, IDevice,} from "tc-shared/audio/recorder"; +import {AudioRecorderBacked, DeviceList, InputDevice,} from "tc-shared/audio/Recorder"; import {Registry} from "tc-shared/events"; import { AbstractInput, @@ -12,12 +12,12 @@ import { NodeInputConsumer } from "tc-shared/voice/RecorderBase"; import {LogCategory, logDebug, logWarn} from "tc-shared/log"; -import * as aplayer from "./player"; import {JAbstractFilter, JStateFilter, JThresholdFilter} from "./RecorderFilter"; import {Filter, FilterType, FilterTypeClass} from "tc-shared/voice/Filter"; -import {inputDeviceList} from "tc-backend/web/audio/RecorderDeviceList"; +import {inputDeviceList} from "./RecorderDeviceList"; import {requestMediaStream, stopMediaStream} from "tc-shared/media/Stream"; import {tr} from "tc-shared/i18n/localize"; +import {getAudioBackend} from "tc-shared/audio/Player"; declare global { interface MediaStream { @@ -25,7 +25,7 @@ declare global { } } -export interface WebIDevice extends IDevice { +export interface WebIDevice extends InputDevice { groupId: string; } @@ -34,7 +34,7 @@ export class WebAudioRecorder implements AudioRecorderBacked { return new JavascriptInput(); } - async createLevelMeter(device: IDevice): Promise { + async createLevelMeter(device: InputDevice): Promise { const meter = new JavascriptLevelMeter(device as any); await meter.initialize(); return meter; @@ -81,14 +81,14 @@ class JavascriptInput implements AbstractInput { constructor() { this.events = new Registry(); - aplayer.on_ready(() => this.handleAudioInitialized()); + getAudioBackend().executeWhenInitialized(() => this.handleAudioInitialized()); this.audioScriptProcessorCallback = this.handleAudio.bind(this); } destroy() { } private handleAudioInitialized() { - this.audioContext = aplayer.context(); + this.audioContext = getAudioBackend().getAudioContext(); this.audioNodeMute = this.audioContext.createGain(); this.audioNodeMute.gain.value = 0; this.audioNodeMute.connect(this.audioContext.destination); @@ -179,7 +179,7 @@ class JavascriptInput implements AbstractInput { private async doStart() : Promise { try { - if(!aplayer.initialized() || !this.audioContext) { + if(!getAudioBackend().isInitialized() || !this.audioContext) { return InputStartError.ESYSTEMUNINITIALIZED; } @@ -189,9 +189,9 @@ class JavascriptInput implements AbstractInput { this.setState(InputState.INITIALIZING); let deviceId; - if(this.deviceId === IDevice.NoDeviceId) { + if(this.deviceId === InputDevice.NoDeviceId) { throw tr("no device selected"); - } else if(this.deviceId === IDevice.DefaultDeviceId) { + } else if(this.deviceId === InputDevice.DefaultDeviceId) { deviceId = undefined; } else { deviceId = this.deviceId; @@ -511,7 +511,7 @@ class JavascriptLevelMeter implements LevelMeter { try { await new Promise((resolve, reject) => { const timeout = setTimeout(reject, 5000); - aplayer.on_ready(() => { + getAudioBackend().executeWhenInitialized(() => { clearTimeout(timeout); resolve(); }); @@ -519,7 +519,7 @@ class JavascriptLevelMeter implements LevelMeter { } catch(error) { throw tr("audio context timeout"); } - this._context = aplayer.context(); + this._context = getAudioBackend().getAudioContext(); if(!this._context) throw tr("invalid context"); this._gain_node = this._context.createGain(); @@ -591,7 +591,7 @@ class JavascriptLevelMeter implements LevelMeter { } } - getDevice(): IDevice { + getDevice(): InputDevice { return this._device; } diff --git a/web/app/audio/RecorderDeviceList.ts b/web/app/audio/RecorderDeviceList.ts index 13a9b2fc..a308a04e 100644 --- a/web/app/audio/RecorderDeviceList.ts +++ b/web/app/audio/RecorderDeviceList.ts @@ -2,13 +2,12 @@ import { AbstractDeviceList, DeviceListEvents, DeviceListState, - IDevice, + InputDevice, PermissionState -} from "tc-shared/audio/recorder"; -import * as log from "tc-shared/log"; +} from "tc-shared/audio/Recorder"; import {LogCategory, logDebug, logError} from "tc-shared/log"; import {Registry} from "tc-shared/events"; -import {WebIDevice} from "tc-backend/web/audio/Recorder"; +import {WebIDevice} from "./Recorder"; import * as loader from "tc-loader"; import {queryMediaPermissions} from "tc-shared/media/Stream"; import { tr } from "tc-shared/i18n/localize"; @@ -49,7 +48,7 @@ class WebInputDeviceList extends AbstractDeviceList { return "default"; } - getDevices(): IDevice[] { + getDevices(): InputDevice[] { return this.devices; } diff --git a/web/app/audio/sounds.ts b/web/app/audio/Sounds.ts similarity index 87% rename from web/app/audio/sounds.ts rename to web/app/audio/Sounds.ts index 656c6ccb..9a98cc52 100644 --- a/web/app/audio/sounds.ts +++ b/web/app/audio/Sounds.ts @@ -1,7 +1,7 @@ import {LogCategory, logError, logWarn} from "tc-shared/log"; -import {SoundFile} from "tc-shared/sound/Sounds"; -import * as aplayer from "./player"; import { tr } from "tc-shared/i18n/localize"; +import {getAudioBackend} from "tc-shared/audio/Player"; +import {SoundBackend, SoundFile} from "tc-shared/audio/Sounds"; interface SoundEntry { cached?: AudioBuffer; @@ -13,7 +13,7 @@ const error_already_handled = "---- error handled ---"; const file_cache: {[key: string]: Promise & { timestamp: number }} = {}; let warned = false; -function get_song_entry(file: SoundFile) : Promise { +function getSongEntry(file: SoundFile) : Promise { if(typeof file_cache[file.path] === "object") { return new Promise((resolve, reject) => { if(file_cache[file.path].timestamp + 60 * 1000 > Date.now()) { @@ -26,12 +26,12 @@ function get_song_entry(file: SoundFile) : Promise { if(file_cache[file.path].timestamp + 60 * 1000 > original_timestamp) return Promise.reject(error); delete file_cache[file.path]; - return get_song_entry(file); + return getSongEntry(file); }); }); } - const context = aplayer.context(); + const context = getAudioBackend().getAudioContext(); if(!context) throw tr("audio context not initialized"); return (file_cache[file.path] = Object.assign((async () => { @@ -78,8 +78,8 @@ function get_song_entry(file: SoundFile) : Promise { })(), { timestamp: Date.now() })); } -export async function play_sound(file: SoundFile) : Promise { - const entry = get_song_entry(file); +async function replaySound(file: SoundFile) : Promise { + const entry = getSongEntry(file); if(!entry) { logWarn(LogCategory.AUDIO, tr("Failed to replay sound %s because it could not be resolved."), file.path); return; @@ -89,7 +89,7 @@ export async function play_sound(file: SoundFile) : Promise { const sound = await entry; if(sound.cached) { - const context = aplayer.context(); + const context = getAudioBackend().getAudioContext(); if(!context) throw tr("audio context not initialized (this error should never show up!)"); const player = context.createBufferSource(); @@ -126,4 +126,11 @@ export async function play_sound(file: SoundFile) : Promise { logWarn(LogCategory.AUDIO, tr("Failed to replay sound %s: %o"), file.path, error); return; } +} + +export class WebSoundBackend implements SoundBackend { + playSound(sound: SoundFile): Promise { + return replaySound(sound); + } + } \ No newline at end of file diff --git a/web/app/audio/player.ts b/web/app/audio/player.ts deleted file mode 100644 index 84d4aa91..00000000 --- a/web/app/audio/player.ts +++ /dev/null @@ -1,125 +0,0 @@ -import {Device} from "tc-shared/audio/player"; -import * as log from "tc-shared/log"; -import {LogCategory, logError, logInfo} from "tc-shared/log"; -import { tr } from "tc-shared/i18n/localize"; - -/* lets try without any gestures, maybe the user already clicked the page */ -const kAvoidAudioContextWarning = false; - -let audioContextRequiredGesture = false; -let audioContextInstance: AudioContext; -let globalAudioGainInstance: GainNode; - -let audioContextInitializeCallbacks: (() => any)[] = []; -let _master_volume: number = 1; -let _no_device = false; - -export function initialize() : boolean { - context(); - return true; -} - -export function initialized() : boolean { - return !!audioContextInstance && audioContextInstance.state === 'running'; -} - -function fire_initialized() { - logInfo(LogCategory.AUDIO, tr("Fire audio player initialized for %d listeners"), audioContextInitializeCallbacks.length); - while(audioContextInitializeCallbacks.length > 0) - audioContextInitializeCallbacks.pop_front()(); -} - -function createNewContext() { - audioContextInstance = new (window.webkitAudioContext || window.AudioContext)(); - audioContextInstance.onstatechange = () => { - if(audioContextInstance.state === "running") - fire_initialized(); - }; - - audioContextInitializeCallbacks.unshift(() => { - globalAudioGainInstance = audioContextInstance.createGain(); - globalAudioGainInstance.gain.value = _no_device ? 0 : _master_volume; - globalAudioGainInstance.connect(audioContextInstance.destination); - }); - - if(audioContextInstance.state === "suspended") { - audioContextRequiredGesture = true; - return audioContextInstance; - } else if(audioContextInstance.state === "running") { - fire_initialized(); - } else if(audioContextInstance.state === "closed") { - throw tr("Audio context has been closed"); - } else { - throw tr("invalid audio context state"); - } -} - -export function context() : AudioContext { - if(audioContextInstance || kAvoidAudioContextWarning) - return audioContextInstance; - - if(!audioContextInstance) - createNewContext(); - - return audioContextInstance; -} - -export function get_master_volume() : number { - return _master_volume; -} -export function set_master_volume(volume: number) { - _master_volume = volume; - if(globalAudioGainInstance) - globalAudioGainInstance.gain.value = _no_device ? 0 : _master_volume; -} - -export function destination() : AudioNode { - const ctx = context(); - if(!ctx) throw tr("Audio player isn't initialized yet!"); - - return globalAudioGainInstance; -} - -export function on_ready(cb: () => any) { - if(initialized()) - cb(); - else - audioContextInitializeCallbacks.push(cb); -} - -export const WEB_DEVICE: Device = { - device_id: "default", - name: "default playback", - driver: 'Web Audio' -}; - -export function available_devices() : Promise { - return Promise.resolve([WEB_DEVICE]) -} - -export function set_device(device_id: string) : Promise { - _no_device = !device_id; - globalAudioGainInstance.gain.value = _no_device ? 0 : _master_volume; - - return Promise.resolve(); -} - -export function current_device() : Device { - return WEB_DEVICE; -} - -export function initializeFromGesture() { - if(audioContextInstance) { - if(audioContextInstance.state !== "running") { - audioContextInstance.resume().catch(error => { - logError(LogCategory.AUDIO, tr("Failed to initialize audio context instance from gesture: %o"), error); - }); - } - } else { - createNewContext(); - } -} - -export function globalAudioContext() : AudioContext { - return context(); -} \ No newline at end of file diff --git a/web/app/connection/CommandParser.ts b/web/app/connection/CommandParser.ts index 97a4ba1a..0e8e6bfe 100644 --- a/web/app/connection/CommandParser.ts +++ b/web/app/connection/CommandParser.ts @@ -10,8 +10,9 @@ function unescapeCommandValue(value: string) : string { while (true) { index = value.indexOf('\\', lastIndex); - if(index === -1 || index >= value.length + 1) + if(index === -1 || index >= value.length + 1) { break; + } let replace; switch (value.charAt(index + 1)) { @@ -58,8 +59,9 @@ export function parseCommand(command: string): ParsedCommand { const parts = command.split("|").map(element => element.split(" ").map(e => e.trim()).filter(e => !!e)); let cmd; - if(parts[0][0].indexOf("=") === -1) + if(parts[0][0].indexOf("=") === -1) { cmd = parts[0].pop_front(); + } let switches = []; let payloads = []; @@ -72,10 +74,11 @@ export function parseCommand(command: string): ParsedCommand { } const separator = keyValue.indexOf('='); - if(separator === -1) + if(separator === -1) { payload[keyValue] = ""; - else + } else { payload[keyValue.substring(0, separator)] = unescapeCommandValue(keyValue.substring(separator + 1)); + } } payloads.push(payload) @@ -95,13 +98,15 @@ export function buildCommand(data: any | any[], switches?: string[], command?: s result += " |"; for(const key of Object.keys(payload)) { result += " " + key; - if(payload[key] !== undefined && payload[key] !== null) + if(payload[key] !== undefined && payload[key] !== null) { result += " " + key + "=" + escapeCommandValue(payload[key].toString()); + } } } - if(switches?.length) + if(switches?.length) { result += " " + switches.map(e => "-" + e).join(" "); + } return command ? command + result.substring(2) : result.substring(3); } \ No newline at end of file diff --git a/web/app/connection/ServerConnection.ts b/web/app/connection/ServerConnection.ts index b7f20eb3..e5068a68 100644 --- a/web/app/connection/ServerConnection.ts +++ b/web/app/connection/ServerConnection.ts @@ -13,11 +13,11 @@ import * as log from "tc-shared/log"; import {LogCategory, logDebug, logError, logInfo, logTrace, logWarn} from "tc-shared/log"; import {Regex} from "tc-shared/ui/modal/ModalConnect"; import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler"; -import {WrappedWebSocket} from "tc-backend/web/connection/WrappedWebSocket"; +import {WrappedWebSocket} from "./WrappedWebSocket"; import {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection"; -import {parseCommand} from "tc-backend/web/connection/CommandParser"; +import {parseCommand} from "./CommandParser"; import {ServerAddress} from "tc-shared/tree/Server"; -import {RtpVoiceConnection} from "tc-backend/web/voice/Connection"; +import {RtpVoiceConnection} from "../voice/Connection"; import {VideoConnection} from "tc-shared/connection/VideoConnection"; import {ServerFeature} from "tc-shared/connection/ServerFeatures"; import {RTCConnection} from "tc-shared/connection/rtc/Connection"; diff --git a/web/app/dns/api.ts b/web/app/dns/api.ts index 121171a8..5f2e95b4 100644 --- a/web/app/dns/api.ts +++ b/web/app/dns/api.ts @@ -1,5 +1,4 @@ import {tr} from "tc-shared/i18n/localize"; -import * as log from "tc-shared/log"; import {LogCategory, logError, logTrace} from "tc-shared/log"; export enum RRType { diff --git a/web/app/hooks/AudioBackend.ts b/web/app/hooks/AudioBackend.ts new file mode 100644 index 00000000..0dd9ae81 --- /dev/null +++ b/web/app/hooks/AudioBackend.ts @@ -0,0 +1,12 @@ +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; +import {setAudioBackend} from "tc-shared/audio/Player"; +import {WebAudioBackend} from "../audio/Player"; + +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + name: "audio backend init", + function: async () => { + setAudioBackend(new WebAudioBackend()); + }, + priority: 100 +}); \ No newline at end of file diff --git a/web/app/hooks/AudioRecorder.ts b/web/app/hooks/AudioRecorder.ts index 592d56c1..c3ed4069 100644 --- a/web/app/hooks/AudioRecorder.ts +++ b/web/app/hooks/AudioRecorder.ts @@ -1,4 +1,4 @@ -import {setRecorderBackend} from "tc-shared/audio/recorder"; +import {setRecorderBackend} from "tc-shared/audio/Recorder"; import {WebAudioRecorder} from "../audio/Recorder"; setRecorderBackend(new WebAudioRecorder()); \ No newline at end of file diff --git a/web/app/hooks/Dns.ts b/web/app/hooks/Dns.ts index f545d0cf..4c092a6a 100644 --- a/web/app/hooks/Dns.ts +++ b/web/app/hooks/Dns.ts @@ -1,7 +1,7 @@ import {DNSAddress, DNSProvider, DNSResolveOptions, DNSResolveResult, setDNSProvider} from "tc-shared/dns"; -import {resolveAddressIpV4, resolveTeaSpeakServerAddress} from "tc-backend/web/dns/resolver"; import {LogCategory, logError} from "tc-shared/log"; import {tr} from "tc-shared/i18n/localize"; +import {resolveAddressIpV4, resolveTeaSpeakServerAddress} from "../dns/resolver"; setDNSProvider(new class implements DNSProvider { resolveAddress(address: DNSAddress, options: DNSResolveOptions): Promise { diff --git a/web/app/hooks/ExternalModal.ts b/web/app/hooks/ExternalModal.ts index 2da3f6e5..cdaba7eb 100644 --- a/web/app/hooks/ExternalModal.ts +++ b/web/app/hooks/ExternalModal.ts @@ -1,7 +1,7 @@ import * as loader from "tc-loader"; import {Stage} from "tc-loader"; import {setExternalModalControllerFactory} from "tc-shared/ui/react-elements/external-modal"; -import {ExternalModalController} from "tc-backend/web/ExternalModalFactory"; +import {ExternalModalController} from "../ExternalModalFactory"; loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { priority: 50, diff --git a/web/app/hooks/KeyBoard.ts b/web/app/hooks/KeyBoard.ts new file mode 100644 index 00000000..9cce889d --- /dev/null +++ b/web/app/hooks/KeyBoard.ts @@ -0,0 +1,10 @@ +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; +import {setKeyBoardBackend} from "tc-shared/PPTListener"; +import {WebKeyBoard} from "../KeyBoard"; + +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + name: "audio backend init", + function: async () => setKeyBoardBackend(new WebKeyBoard()), + priority: 100 +}); \ No newline at end of file diff --git a/web/app/hooks/MenuBar.ts b/web/app/hooks/MenuBar.ts index ca93c483..b4aba445 100644 --- a/web/app/hooks/MenuBar.ts +++ b/web/app/hooks/MenuBar.ts @@ -1,4 +1,4 @@ import {setMenuBarDriver} from "tc-shared/ui/frames/menu-bar"; -import {WebMenuBarDriver} from "tc-backend/web/ui/menu-bar/Controller"; +import {WebMenuBarDriver} from "../ui/menu-bar/Controller"; setMenuBarDriver(new WebMenuBarDriver()); \ No newline at end of file diff --git a/web/app/hooks/ServerConnection.ts b/web/app/hooks/ServerConnection.ts index 30e8fe06..cb9decb4 100644 --- a/web/app/hooks/ServerConnection.ts +++ b/web/app/hooks/ServerConnection.ts @@ -1,9 +1,9 @@ import {ServerConnectionFactory, setServerConnectionFactory} from "tc-shared/connection/ConnectionFactory"; import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; -import {ServerConnection} from "tc-backend/web/connection/ServerConnection"; import * as loader from "tc-loader"; import {Stage} from "tc-loader"; +import {ServerConnection} from "../connection/ServerConnection"; loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { priority: 50, diff --git a/web/app/hooks/Sounds.ts b/web/app/hooks/Sounds.ts new file mode 100644 index 00000000..39e644c5 --- /dev/null +++ b/web/app/hooks/Sounds.ts @@ -0,0 +1,10 @@ +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; +import {setSoundBackend} from "tc-shared/audio/Sounds"; +import {WebSoundBackend} from "../audio/Sounds"; + +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + name: "audio sound init", + function: async () => setSoundBackend(new WebSoundBackend()), + priority: 100 +}); \ No newline at end of file diff --git a/web/app/hooks/index.ts b/web/app/hooks/index.ts new file mode 100644 index 00000000..16cfd413 --- /dev/null +++ b/web/app/hooks/index.ts @@ -0,0 +1,9 @@ +import "./AudioBackend"; +import "./AudioRecorder"; +import "./Backend"; +import "./Dns"; +import "./MenuBar"; +import "./ServerConnection"; +import "./Sounds"; +import "./Video"; +import "./KeyBoard"; \ No newline at end of file diff --git a/web/app/index.ts b/web/app/index.ts index 5c3749ce..73205f60 100644 --- a/web/app/index.ts +++ b/web/app/index.ts @@ -4,12 +4,7 @@ import "webcrypto-liner"; import "./index.scss"; import "./FileTransfer"; -import "./hooks/ServerConnection"; -import "./hooks/ExternalModal"; -import "./hooks/AudioRecorder"; -import "./hooks/MenuBar"; -import "./hooks/Video"; -import "./hooks/Dns"; +import "./hooks" import "./UnloadHandler"; diff --git a/web/app/ppt.ts b/web/app/ppt.ts deleted file mode 100644 index 45534f1f..00000000 --- a/web/app/ppt.ts +++ /dev/null @@ -1,151 +0,0 @@ -import {EventType, KeyEvent, KeyHook, SpecialKey} from "tc-shared/PPTListener"; -import {LogCategory, logTrace} from "tc-shared/log"; -import { tr } from "tc-shared/i18n/localize"; - -interface WebKeyEvent extends KeyEvent { - canceled: boolean; -} - -let key_listener: ((_: KeyEvent) => any)[] = []; - -function listener_key(type: EventType, event: KeyboardEvent) { - const key_event = { - type: type, - - key: event.key, - key_code: event.code, - - key_ctrl: event.ctrlKey, - key_shift: event.shiftKey, - key_alt: event.altKey, - key_windows: event.metaKey, - - canceled: event.defaultPrevented - } as WebKeyEvent; - //console.debug("Trigger key event %o", key_event); - - for(const listener of key_listener) - listener(key_event); - - if(key_event.canceled) - event.preventDefault(); -} - -const proxy_key_press = event => listener_key(EventType.KEY_PRESS, event); -const proxy_key_release = event => listener_key(EventType.KEY_RELEASE, event); -const proxy_key_typed = event => listener_key(EventType.KEY_TYPED, event); - -export function initialize() : Promise { - document.addEventListener('keypress', proxy_key_typed); - document.addEventListener('keydown', proxy_key_press); - document.addEventListener('keyup', proxy_key_release); - window.addEventListener('blur', listener_blur); - - register_key_listener(listener_hook); - return Promise.resolve(); -} - -export function finalize() { - document.removeEventListener("keypress", proxy_key_typed); - document.removeEventListener("keydown", proxy_key_press); - document.removeEventListener("keyup", proxy_key_release); - window.removeEventListener('blur', listener_blur); - - unregister_key_listener(listener_hook); -} - -export function register_key_listener(listener: (_: KeyEvent) => any) { - key_listener.push(listener); -} - -export function unregister_key_listener(listener: (_: KeyEvent) => any) { - key_listener.remove(listener); -} - - -let key_hooks: KeyHook[] = []; - -interface CurrentState { - keys: {[code: string]:KeyEvent}; - special: { [key:number]:boolean }; -} -let current_state: CurrentState = { - special: [] -} as any; - -let key_hooks_active: KeyHook[] = []; - -function listener_blur() { - current_state.special[SpecialKey.ALT] = false; - current_state.special[SpecialKey.CTRL] = false; - current_state.special[SpecialKey.SHIFT] = false; - current_state.special[SpecialKey.WINDOWS] = false; - - for(const code of Object.keys(current_state)) - if(code !== "special") - delete current_state[code]; - - for(const hook of key_hooks_active) - hook.callback_release(); - key_hooks_active = []; -} - -function listener_hook(event: KeyEvent) { - if(event.type == EventType.KEY_TYPED) - return; - - let old_hooks = [...key_hooks_active]; - let new_hooks = []; - - current_state.special[SpecialKey.ALT] = event.key_alt; - current_state.special[SpecialKey.CTRL] = event.key_ctrl; - current_state.special[SpecialKey.SHIFT] = event.key_shift; - current_state.special[SpecialKey.WINDOWS] = event.key_windows; - - current_state[event.key_code] = undefined; - if(event.type == EventType.KEY_PRESS) { - current_state[event.key_code] = event; - - for(const hook of key_hooks) { - if(hook.key_code !== event.key_code) continue; - if(hook.key_alt != event.key_alt) continue; - if(hook.key_ctrl != event.key_ctrl) continue; - if(hook.key_shift != event.key_shift) continue; - if(hook.key_windows != event.key_windows) continue; - - new_hooks.push(hook); - if(!old_hooks.remove(hook) && hook.callback_press) { - hook.callback_press(); - logTrace(LogCategory.GENERAL, tr("Trigger key press for %o!"), hook); - } - } - } - - //We have a new situation - for(const hook of old_hooks) { - //Do not test for meta key states because they could differ in a key release event - if(hook.key_code === event.key_code) { - if(hook.callback_release) { - hook.callback_release(); - logTrace(LogCategory.GENERAL, tr("Trigger key release for %o!"), hook); - } - } else { - new_hooks.push(hook); - } - } - key_hooks_active = new_hooks; -} - -export function register_key_hook(hook: KeyHook) { - key_hooks.push(hook); -} - -export function unregister_key_hook(hook: KeyHook) { - key_hooks.remove(hook); -} - -export function key_pressed(code: string | SpecialKey) : boolean { - if(typeof(code) === 'string') - return typeof current_state[code] !== "undefined"; - return current_state.special[code]; -} \ No newline at end of file diff --git a/web/app/ui/menu-bar/Controller.ts b/web/app/ui/menu-bar/Controller.ts index fefb1bcd..f4e1d507 100644 --- a/web/app/ui/menu-bar/Controller.ts +++ b/web/app/ui/menu-bar/Controller.ts @@ -1,7 +1,7 @@ import {MenuBarDriver, MenuBarEntry} from "tc-shared/ui/frames/menu-bar"; import * as React from "react"; import * as ReactDOM from "react-dom"; -import {MenuBarRenderer} from "tc-backend/web/ui/menu-bar/Renderer"; +import {MenuBarRenderer} from "./Renderer"; const cssStyle = require("./Renderer.scss"); diff --git a/web/app/voice/Connection.ts b/web/app/voice/Connection.ts index d5441137..8bf31ea6 100644 --- a/web/app/voice/Connection.ts +++ b/web/app/voice/Connection.ts @@ -1,4 +1,3 @@ -import * as aplayer from "../audio/player"; import { AbstractVoiceConnection, VoiceConnectionStatus, @@ -17,9 +16,10 @@ import {AbstractServerConnection, ConnectionStatistics} from "tc-shared/connecti import {VoicePlayerState} from "tc-shared/voice/VoicePlayer"; import {LogCategory, logDebug, logError, logInfo, logTrace, logWarn} from "tc-shared/log"; import {tr} from "tc-shared/i18n/localize"; -import {RtpVoiceClient} from "tc-backend/web/voice/VoiceClient"; import {InputConsumerType} from "tc-shared/voice/RecorderBase"; -import {RtpWhisperSession} from "tc-backend/web/voice/WhisperClient"; +import {getAudioBackend} from "tc-shared/audio/Player"; +import {RtpVoiceClient} from "./VoiceClient"; +import {RtpWhisperSession} from "./WhisperClient"; type CancelableWhisperTarget = WhisperTarget & { canceled: boolean }; export class RtpVoiceConnection extends AbstractVoiceConnection { @@ -86,8 +86,8 @@ export class RtpVoiceConnection extends AbstractVoiceConnection { this.speakerMuted = connection.client.isSpeakerMuted() || connection.client.isSpeakerDisabled(); this.setConnectionState(VoiceConnectionStatus.Disconnected); - aplayer.on_ready(() => { - this.localAudioDestination = aplayer.context().createMediaStreamDestination(); + getAudioBackend().executeWhenInitialized(() => { + this.localAudioDestination = getAudioBackend().getAudioContext().createMediaStreamDestination(); if(this.currentAudioSourceNode) { this.currentAudioSourceNode.connect(this.localAudioDestination); } diff --git a/web/app/voice/VoiceClient.ts b/web/app/voice/VoiceClient.ts index 86328f8e..45a27fa1 100644 --- a/web/app/voice/VoiceClient.ts +++ b/web/app/voice/VoiceClient.ts @@ -1,8 +1,5 @@ import {VoiceClient} from "tc-shared/voice/VoiceClient"; import {VoicePlayer} from "./VoicePlayer"; -import {LogCategory, logTrace} from "tc-shared/log"; -import {tr} from "tc-shared/i18n/localize"; -import {RemoteRTPAudioTrack} from "tc-shared/connection/rtc/RemoteTrack"; export class RtpVoiceClient extends VoicePlayer implements VoiceClient { private readonly clientId: number; diff --git a/web/app/voice/VoicePlayer.ts b/web/app/voice/VoicePlayer.ts index 2f233f73..316a8896 100644 --- a/web/app/voice/VoicePlayer.ts +++ b/web/app/voice/VoicePlayer.ts @@ -1,6 +1,6 @@ import {VoicePlayerEvents, VoicePlayerLatencySettings, VoicePlayerState} from "tc-shared/voice/VoicePlayer"; import {Registry} from "tc-shared/events"; -import {LogCategory, logTrace, logWarn} from "tc-shared/log"; +import {LogCategory, logWarn} from "tc-shared/log"; import {RemoteRTPAudioTrack, RemoteRTPTrackState} from "tc-shared/connection/rtc/RemoteTrack"; import {tr} from "tc-shared/i18n/localize"; diff --git a/webpack-client.config.ts b/webpack-client.config.ts index 6569b4a7..7b118347 100644 --- a/webpack-client.config.ts +++ b/webpack-client.config.ts @@ -9,8 +9,6 @@ export = env => config_base.config(env, "client").then(config => { Object.assign(config.resolve.alias, { "tc-shared": path.resolve(__dirname, "shared/js"), - /* backend hasn't declared but its available via "require()" */ - "tc-backend": path.resolve(__dirname, "shared/backend.d"), }); if(!Array.isArray(config.externals)) diff --git a/webpack-web.config.ts b/webpack-web.config.ts index e9f46390..a2c04df7 100644 --- a/webpack-web.config.ts +++ b/webpack-web.config.ts @@ -9,8 +9,6 @@ export = env => config_base.config(env, "web").then(config => { Object.assign(config.resolve.alias, { "tc-shared": path.resolve(__dirname, "shared/js"), - "tc-backend/web": path.resolve(__dirname, "web/app"), - "tc-backend": path.resolve(__dirname, "web/app"), }); return Promise.resolve(config);