From 4d2e8e98e022b4dacdccf377f2490735fc11cf95 Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Fri, 12 Jun 2020 18:27:33 +0200 Subject: [PATCH 01/11] Improved audio de/encoding --- ChangeLog.md | 1 + web/js/codec/CodecWorkerMessages.ts | 92 ++++++++++ web/js/codec/CodecWrapperWorker.ts | 237 +++++++++++++++++--------- web/js/workers/codec/CodecWorker.ts | 249 +++++++++++++++++----------- web/js/workers/codec/OpusCodec.ts | 31 ++-- web/native-codec/src/ilarvecon.cpp | 68 ++++++++ web/native-codec/src/opus.cpp | 15 +- 7 files changed, 496 insertions(+), 197 deletions(-) create mode 100644 web/js/codec/CodecWorkerMessages.ts create mode 100644 web/native-codec/src/ilarvecon.cpp diff --git a/ChangeLog.md b/ChangeLog.md index 519ed197..c64e2daf 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,7 @@ # Changelog: * **12.06.20** - Added a copy/paste menu for all HTML input elements + - Heavily improved web client audio de/encoding * **11.06.20** - Fixed channel tree deletions diff --git a/web/js/codec/CodecWorkerMessages.ts b/web/js/codec/CodecWorkerMessages.ts new file mode 100644 index 00000000..f74e2aaa --- /dev/null +++ b/web/js/codec/CodecWorkerMessages.ts @@ -0,0 +1,92 @@ +import {CodecType} from "tc-backend/web/codec/Codec"; + +export type CWMessageResponse = { + type: "success"; + token: string; + + response: any; + + timestampReceived: number; + timestampSend: number; +}; + +export type CWMessageErrorResponse = { + type: "error"; + token: string; + + error: string; + + timestampReceived: number; + timestampSend: number; +} + +export type CWMessageCommand = { + type: "command"; + token: string; + + command: keyof T; + payload: any; +} + +export type CWMessageNotify = { + type: "notify"; +} + +export type CWMessage = CWMessageCommand | CWMessageErrorResponse | CWMessageResponse | CWMessageNotify; + +/* from handle to worker */ +export interface CWCommand { + "global-initialize": {}, + + + "initialise": { + type: CodecType, + channelCount: number + }, + "reset": {} + "finalize": {}, + + "decode-payload": { + buffer: ArrayBuffer; + byteLength: number; + byteOffset: number; + maxByteLength: number; + }, + + "encode-payload": { + buffer: ArrayBuffer; + byteLength: number; + byteOffset: number; + maxByteLength: number; + }, +} + +/* from worker to handle */ +export interface CWCommandResponse { + "decode-payload-result": { + buffer: ArrayBuffer; + byteLength: number; + byteOffset: number; + }, + + "encode-payload-result": { + buffer: ArrayBuffer; + byteLength: number; + byteOffset: number; + } +} + +export interface CWMessageRelations { + "decode-payload": "decode-payload-result", + "decode-payload-result": never, + + "encode-payload": "encode-payload-result", + "encode-payload-result": never, + + "global-initialize": void, + "initialise": void, + "reset": void, + "finalize": void +} + +export type CWCommandResponseType = CWMessageRelations[T] extends string ? CWCommandResponse[CWMessageRelations[T]] : CWMessageRelations[T]; \ No newline at end of file diff --git a/web/js/codec/CodecWrapperWorker.ts b/web/js/codec/CodecWrapperWorker.ts index 91526a66..f75fc05a 100644 --- a/web/js/codec/CodecWrapperWorker.ts +++ b/web/js/codec/CodecWrapperWorker.ts @@ -2,18 +2,52 @@ import {BasicCodec} from "./BasicCodec"; import {CodecType} from "./Codec"; import * as log from "tc-shared/log"; import {LogCategory} from "tc-shared/log"; +import { + CWCommand, + CWCommandResponseType, + CWMessage, CWMessageCommand, + CWMessageErrorResponse, + CWMessageResponse +} from "tc-backend/web/codec/CodecWorkerMessages"; -interface ExecuteResult { - result?: any; - error?: string; +type MessageTimings = { + upstream: number; + downstream: number; + handle: number; +}; +interface ExecuteResultBase { success: boolean; - timings: { - upstream: number; - downstream: number; - handle: number; + timings: MessageTimings +} + +interface SuccessExecuteResult extends ExecuteResultBase { + success: true; + result: T; +} + +interface ErrorExecuteResult extends ExecuteResultBase { + success: false; + error: string; +} +type ExecuteResult = SuccessExecuteResult | ErrorExecuteResult; + +const cachedBufferSize = 1024 * 8; +let cachedBuffers: ArrayBuffer[] = []; +function nextCachedBuffer() : ArrayBuffer { + if(cachedBuffers.length === 0) { + return new ArrayBuffer(cachedBufferSize); } + return cachedBuffers.pop(); +} + +function freeCachedBuffer(buffer: ArrayBuffer) { + if(cachedBuffers.length > 32) + return; + else if(buffer.byteLength < cachedBufferSize) + return; + cachedBuffers.push(buffer); } export class CodecWrapperWorker extends BasicCodec { @@ -27,10 +61,8 @@ export class CodecWrapperWorker extends BasicCodec { private pending_executes: {[key: string]: { timeout?: any; - timestamp_send: number, - + timestampSend: number, resolve: (_: ExecuteResult) => void; - reject: (_: any) => void; }} = {}; constructor(type: CodecType) { @@ -61,7 +93,7 @@ export class CodecWrapperWorker extends BasicCodec { type: this.type, channelCount: this.channelCount, })).then(result => { - if(result.success) { + if(result.success === true) { this._initialized = true; return Promise.resolve(true); } @@ -78,7 +110,7 @@ export class CodecWrapperWorker extends BasicCodec { } deinitialise() { - this.execute("deinitialise", {}); + this.execute("finalize", {}); this._initialized = false; this._initialize_promise = undefined; } @@ -86,44 +118,52 @@ export class CodecWrapperWorker extends BasicCodec { async decode(data: Uint8Array): Promise { if(!this.initialized()) throw "codec not initialized/initialize failed"; - const result = await this.execute("decodeSamples", { data: data, length: data.length }); + const cachedBuffer = nextCachedBuffer(); + new Uint8Array(cachedBuffer).set(data); + + const result = await this.execute("decode-payload", { + byteLength: data.byteLength, + buffer: cachedBuffer, + byteOffset: 0, + maxByteLength: cachedBuffer.byteLength + }, 5000, [ cachedBuffer ]); if(result.timings.downstream > 5 || result.timings.upstream > 5 || result.timings.handle > 5) log.warn(LogCategory.VOICE, tr("Worker message stock time: {downstream: %dms, handle: %dms, upstream: %dms}"), result.timings.downstream, result.timings.handle, result.timings.upstream); - if(!result.success) throw result.error || tr("unknown decode error"); + if(result.success === false) + throw result.error; - let array = new Float32Array(result.result.length); - for(let index = 0; index < array.length; index++) - array[index] = result.result.data[index]; + const chunkLength = result.result.byteLength / this.channelCount; + const audioBuffer = this._audioContext.createBuffer(this.channelCount, chunkLength / 4, this._codecSampleRate); - let audioBuf = this._audioContext.createBuffer(this.channelCount, array.length / this.channelCount, this._codecSampleRate); - for (let channel = 0; channel < this.channelCount; channel++) { - for (let offset = 0; offset < audioBuf.length; offset++) { - audioBuf.getChannelData(channel)[offset] = array[channel + offset * this.channelCount]; - } + for(let channel = 0; channel < this.channelCount; channel++) { + const buffer = new Float32Array(result.result.buffer, result.result.byteOffset + chunkLength * channel, chunkLength / 4); + audioBuffer.copyToChannel(buffer, channel, 0); } - return audioBuf; + freeCachedBuffer(result.result.buffer); + return audioBuffer; } async encode(data: AudioBuffer) : Promise { if(!this.initialized()) throw "codec not initialized/initialize failed"; - let buffer = new Float32Array(this.channelCount * data.length); - for (let offset = 0; offset < data.length; offset++) { - for (let channel = 0; channel < this.channelCount; channel++) - buffer[offset * this.channelCount + channel] = data.getChannelData(channel)[offset]; - } + const buffer = nextCachedBuffer(); + const f32Buffer = new Float32Array(buffer); + for(let channel = 0; channel < this.channelCount; channel++) + data.copyFromChannel(f32Buffer, channel, data.length * channel); + + const result = await this.execute("encode-payload", { byteLength: data.length * this.channelCount * 4, buffer: buffer, byteOffset: 0, maxByteLength: buffer.byteLength }); - const result = await this.execute("encodeSamples", { data: buffer, length: buffer.length }); if(result.timings.downstream > 5 || result.timings.upstream > 5) log.warn(LogCategory.VOICE, tr("Worker message stock time: {downstream: %dms, handle: %dms, upstream: %dms}"), result.timings.downstream, result.timings.handle, result.timings.upstream); - if(!result.success) throw result.error || tr("unknown encode error"); - let array = new Uint8Array(result.result.length); - for(let index = 0; index < array.length; index++) - array[index] = result.result.data[index]; - return array; + if(result.success === false) + throw result.error; + + const encodedResult = new Uint8Array(result.result.buffer, result.result.byteOffset, result.result.byteLength).slice(0); + freeCachedBuffer(result.result.buffer); + return encodedResult; } reset() : boolean { @@ -132,85 +172,118 @@ export class CodecWrapperWorker extends BasicCodec { return true; } - private handle_worker_message(message: any) { - if(!message["token"]) { - log.error(LogCategory.VOICE, tr("Invalid worker token!")); + private handleWorkerMessage(message: CWMessage) { + if(message.type === "notify") { + log.warn(LogCategory.VOICE, tr("Received unknown notify from worker.")); return; - } - - if(message["token"] === "notify") { - /* currently not really used */ - if(message["type"] == "chatmessage_server") { - //FIXME? + } else if(message.type === "error") { + const request = this.pending_executes[message.token]; + if(typeof request !== "object") { + log.warn(LogCategory.VOICE, tr("Received worker execute error for unknown token (%s)"), message.token); return; } - log.debug(LogCategory.VOICE, tr("Costume callback! (%o)"), message); - return; - } + delete this.pending_executes[message.token]; + clearTimeout(request.timeout); - const request = this.pending_executes[message["token"]]; - if(typeof request !== "object") { - log.error(LogCategory.VOICE, tr("Received worker execute result for unknown token (%s)"), message["token"]); - return; - } - delete this.pending_executes[message["token"]]; - - const result: ExecuteResult = { - success: message["success"], - error: message["error"], - result: message["result"], - timings: { - downstream: message["timestamp_received"] - request.timestamp_send, - handle: message["timestamp_send"] - message["timestamp_received"], - upstream: Date.now() - message["timestamp_send"] + const eresponse = message as CWMessageErrorResponse; + request.resolve({ + success: false, + timings: { + downstream: eresponse.timestampReceived - request.timestampSend, + handle: eresponse.timestampSend - eresponse.timestampReceived, + upstream: Date.now() - eresponse.timestampSend + }, + error: eresponse.error + }); + } else if(message.type === "success") { + const request = this.pending_executes[message.token]; + if(typeof request !== "object") { + log.warn(LogCategory.VOICE, tr("Received worker execute result for unknown token (%s)"), message.token); + return; } - }; - clearTimeout(request.timeout); - request.resolve(result); + delete this.pending_executes[message.token]; + clearTimeout(request.timeout); + + const response = message as CWMessageResponse; + request.resolve({ + success: true, + timings: { + downstream: response.timestampReceived - request.timestampSend, + handle: response.timestampSend - response.timestampReceived, + upstream: Date.now() - response.timestampSend + }, + result: response.response + }); + } else if(message.type === "command") { + log.warn(LogCategory.VOICE, tr("Received command %s from voice worker. This should never happen!"), (message as CWMessageCommand).command); + return; + } else { + log.warn(LogCategory.VOICE, tr("Received unknown message of type %s from voice worker. This should never happen!"), (message as any).type); + return; + } } - private handle_worker_error(error: any) { + private handleWorkerError() { log.error(LogCategory.VOICE, tr("Received error from codec worker. Closing worker.")); for(const token of Object.keys(this.pending_executes)) { - this.pending_executes[token].reject(error); + this.pending_executes[token].resolve({ + success: false, + error: tr("worker terminated with an error"), + timings: { downstream: 0, handle: 0, upstream: 0} + }); delete this.pending_executes[token]; } this._worker = undefined; } - private execute(command: string, data: any, timeout?: number) : Promise { - return new Promise((resolve, reject) => { + private execute(command: T, data: CWCommand[T], timeout?: number, transfer?: Transferable[]) : Promise>> { + return new Promise(resolve => { if(!this._worker) { - reject(tr("worker does not exists")); + resolve({ + success: false, + error: tr("worker does not exists"), + timings: { + downstream: 0, + handle: 0, + upstream: 0 + } + }); return; } const token = this._token_index++ + "_token"; - const payload = { - token: token, - command: command, - data: data, - }; - this.pending_executes[token] = { - timeout: typeof timeout === "number" ? setTimeout(() => reject(tr("timeout for command ") + command), timeout) : undefined, + timeout: typeof timeout === "number" ? setTimeout(() => { + delete this.pending_executes[token]; + resolve({ + success: false, + error: tr("command timed out"), + timings: { upstream: 0, handle: 0, downstream: 0 } + }) + }, timeout) : undefined, resolve: resolve, - reject: reject, - timestamp_send: Date.now() + timestampSend: Date.now() }; - this._worker.postMessage(payload); + this._worker.postMessage({ + command: command, + type: "command", + + payload: data, + token: token + } as CWMessageCommand, transfer); }); } private async spawn_worker() : Promise { this._worker = new Worker("tc-backend/web/workers/codec", { type: "module" }); - this._worker.onmessage = event => this.handle_worker_message(event.data); - this._worker.onerror = event => this.handle_worker_error(event.error); + this._worker.onmessage = event => this.handleWorkerMessage(event.data); + this._worker.onerror = () => this.handleWorkerError(); const result = await this.execute("global-initialize", {}, 15000); - if(!result.success) throw result.error; + if(result.success === false) + throw result.error; } } \ No newline at end of file diff --git a/web/js/workers/codec/CodecWorker.ts b/web/js/workers/codec/CodecWorker.ts index e27ddad3..5ea2ba7d 100644 --- a/web/js/workers/codec/CodecWorker.ts +++ b/web/js/workers/codec/CodecWorker.ts @@ -1,4 +1,11 @@ import {CodecType} from "tc-backend/web/codec/Codec"; +import { + CWMessageCommand, + CWCommand, + CWMessage, + CWMessageResponse, + CWMessageErrorResponse, CWCommandResponseType +} from "tc-backend/web/codec/CodecWorkerMessages"; const prefix = "[CodecWorker] "; @@ -6,8 +13,8 @@ export interface CodecWorker { name(); initialise?() : string; deinitialise(); - decode(data: Uint8Array); - encode(data: Float32Array) : Uint8Array | string; + decode(buffer: Uint8Array, responseBuffer: (length: number) => Uint8Array) : number | string; + encode(buffer: Uint8Array, responseBuffer: (length: number) => Uint8Array) : number | string; reset(); } @@ -25,110 +32,152 @@ export function set_initialize_callback(callback: () => Promise) export let codec_instance: CodecWorker; let globally_initialized = false; let global_initialize_result; +let commandTransferableResponse: Transferable[]; -/** - * @param command - * @param data - * @return string on error or object on success - */ -async function handle_message(command: string, data: any) : Promise { - switch (command) { - case "global-initialize": - try { - const init_result = globally_initialized ? global_initialize_result : await initialize_callback(); - globally_initialized = true; +let messageHandlers: { [T in keyof CWCommand]: (message: CWCommand[T]) => Promise> } = {} as any; - if(typeof init_result === "string") - throw init_result; - } catch (e) { - if(typeof e === "string") - return e; - throw e; - } - - return {}; - case "initialise": - console.log(prefix + "Initialize for codec %s", CodecType[data.type as CodecType]); - if(!supported_types[data.type]) - return "type unsupported"; - - try { - codec_instance = await supported_types[data.type](data.options); - } catch(ex) { - console.error(prefix + "Failed to allocate codec: %o", ex); - return typeof ex === "string" ? ex : "failed to allocate codec"; - } - - const error = codec_instance.initialise(); - if(error) return error; - - return {}; - case "encodeSamples": - if(!codec_instance) - return "codec not initialized/initialize failed"; - - let encodeArray = new Float32Array(data.length); - for(let index = 0; index < encodeArray.length; index++) - encodeArray[index] = data.data[index]; - - let encodeResult = codec_instance.encode(encodeArray); - if(typeof encodeResult === "string") - return encodeResult; - else - return { data: encodeResult, length: encodeResult.length }; - case "decodeSamples": - if(!codec_instance) - return "codec not initialized/initialize failed"; - - let decodeArray = new Uint8Array(data.length); - for(let index = 0; index < decodeArray.length; index++) - decodeArray[index] = data.data[index]; - - let decodeResult = codec_instance.decode(decodeArray); - if(typeof decodeResult === "string") - return decodeResult; - else - return { data: decodeResult, length: decodeResult.length }; - case "reset": - codec_instance.reset(); - break; - default: - return "unknown command"; - } +function registerCommandHandler(command: T, callback: (message: CWCommand[T]) => Promise>) { + messageHandlers[command as any] = callback; } +const handleOwnerMessage = (e: MessageEvent) => { + const timestampReceived = Date.now(); + const message = e.data as CWMessage; -const handle_message_event = (e: MessageEvent) => { - const token = e.data.token; - const received = Date.now(); + if(message.type === "error" || message.type === "success") { + console.warn("%sReceived a command response within the worker. We're not sending any commands so this should not happen!", prefix); + return; + } else if(message.type === "notify") { + console.warn("%sReceived a notify within the worker. This should not happen!", prefix); + return; + } else if(message.type === "command") { + const command = message as CWMessageCommand; - const send_result = result => { - const data = {}; - if(typeof result === "object") { - data["result"] = result; - data["success"] = true; - } else if(typeof result === "string") { - data["error"] = result; - data["success"] = false; - } else { - data["error"] = "invalid result"; - data["success"] = false; - } - data["token"] = token; - data["timestamp_received"] = received; - data["timestamp_send"] = Date.now(); + const sendExecuteError = error => { + let errorMessage; + if(typeof error === "string") { + errorMessage = error; + } else if(error instanceof Error) { + console.error("%sMessage handle error: %o", prefix, error); + errorMessage = error.message; + } else { + console.error("%sMessage handle error: %o", prefix, error); + errorMessage = "lookup the console"; + } - postMessage(data, undefined); - }; - handle_message(e.data.command, e.data.data).then(res => { - if(token) { - send_result(res); + postMessage({ + type: "error", + error: errorMessage, + + timestampReceived: timestampReceived, + timestampSend: Date.now(), + + token: command.token + } as CWMessageErrorResponse, undefined, commandTransferableResponse); + }; + + const sendExecuteResult = result => { + postMessage({ + type: "success", + response: result, + + timestampReceived: timestampReceived, + timestampSend: Date.now(), + + token: command.token + } as CWMessageResponse, undefined); + }; + + const handler = messageHandlers[message.command as any]; + if(!handler) { + sendExecuteError("unknown command"); + return; } - }).catch(error => { - console.warn("An error has been thrown while handing command %s: %o", e.data.command, error); - if(token) { - send_result(typeof error === "string" ? error : "unexpected exception has been thrown"); - } - }); + + handler(command.payload).then(sendExecuteResult).catch(sendExecuteError); + } }; -addEventListener("message", handle_message_event); \ No newline at end of file +addEventListener("message", handleOwnerMessage); + + +/* command handlers */ +registerCommandHandler("global-initialize", async () => { + const init_result = globally_initialized ? global_initialize_result : await initialize_callback(); + globally_initialized = true; + + if(typeof init_result === "string") + throw init_result; +}); + +registerCommandHandler("initialise", async data => { + console.log(prefix + "Initialize for codec %s", CodecType[data.type as CodecType]); + if(!supported_types[data.type]) + throw "type unsupported"; + + try { + codec_instance = await supported_types[data.type](data); + } catch(ex) { + console.error("%sFailed to allocate codec: %o", prefix, ex); + throw typeof ex === "string" ? ex : "failed to allocate codec"; + } + + const error = codec_instance.initialise(); + if(error) + throw error; +}); + +registerCommandHandler("reset", async () => { + codec_instance.reset(); +}); + +registerCommandHandler("finalize", async () => { + /* memory will be cleaned up by its own */ +}); + +let responseBuffer: Uint8Array; +const popResponseBuffer = () => { const temp = responseBuffer; responseBuffer = undefined; return temp; } +registerCommandHandler("decode-payload", async data => { + if(!codec_instance) + throw "codec not initialized/initialize failed"; + + const byteLength = codec_instance.decode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength), length => { + if(length > data.maxByteLength) + throw "source buffer too small to hold the result"; + //return responseBuffer = new Uint8Array(length); + return responseBuffer = new Uint8Array(data.buffer, 0, data.maxByteLength); + }); + const buffer = popResponseBuffer(); + if(typeof byteLength === "string") { + throw byteLength; + } + + commandTransferableResponse = [buffer.buffer]; + return { + buffer: buffer.buffer, + byteLength: byteLength, + byteOffset: 0, + }; +}); + +registerCommandHandler("encode-payload", async data => { + if(!codec_instance) + throw "codec not initialized/initialize failed"; + + const byteLength = codec_instance.encode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength), length => { + if(length > data.maxByteLength) + throw "source buffer too small to hold the result"; + //return responseBuffer = new Uint8Array(length); + return responseBuffer = new Uint8Array(data.buffer, 0, data.maxByteLength); + }); + const buffer = popResponseBuffer(); + if(typeof byteLength === "string") { + throw byteLength; + } + + commandTransferableResponse = [buffer.buffer]; + return { + buffer: buffer.buffer, + byteLength: byteLength, + byteOffset: 0 + }; +}); \ No newline at end of file diff --git a/web/js/workers/codec/OpusCodec.ts b/web/js/workers/codec/OpusCodec.ts index 0af18483..f892ac3f 100644 --- a/web/js/workers/codec/OpusCodec.ts +++ b/web/js/workers/codec/OpusCodec.ts @@ -1,7 +1,6 @@ import * as cworker from "./CodecWorker"; import {CodecType} from "tc-backend/web/codec/Codec"; import {CodecWorker} from "./CodecWorker"; -import {type} from "os"; declare global { interface Window { @@ -113,7 +112,7 @@ enum OpusType { const OPUS_ERROR_CODES = [ "One or more invalid/out of range arguments", //-1 (OPUS_BAD_ARG) - "Not enough bytes allocated in the buffer", //-2 (OPUS_BUFFER_TOO_SMALL) + "Not enough bytes allocated in the target buffer", //-2 (OPUS_BUFFER_TOO_SMALL) "An internal error was detected", //-3 (OPUS_INTERNAL_ERROR) "The compressed data passed is corrupted", //-4 (OPUS_INVALID_PACKET) "Invalid/unsupported request number", //-5 (OPUS_UNIMPLEMENTED) @@ -162,23 +161,33 @@ class OpusWorker implements CodecWorker { deinitialise() { } //TODO - decode(data: Uint8Array): Float32Array | string { - if (data.byteLength > this.decode_buffer.byteLength) return "supplied data exceeds internal buffer"; - this.decode_buffer.set(data); + decode(buffer: Uint8Array, responseBuffer: (length: number) => Uint8Array): number | string { + if (buffer.byteLength > this.decode_buffer.byteLength) + return "supplied data exceeds internal buffer"; - let result = this.fn_decode(this.nativeHandle, this.decode_buffer.byteOffset, data.byteLength, this.decode_buffer.byteLength); + this.decode_buffer.set(buffer); + + let result = this.fn_decode(this.nativeHandle, this.decode_buffer.byteOffset, buffer.byteLength, this.decode_buffer.byteLength); if (result < 0) return OPUS_ERROR_CODES[-result] || "unknown decode error " + result; - return Module.HEAPF32.slice(this.decode_buffer.byteOffset / 4, (this.decode_buffer.byteOffset / 4) + (result * this.channelCount)); + const resultByteLength = result * this.channelCount * 4; + const resultBuffer = responseBuffer(resultByteLength); + resultBuffer.set(this.decode_buffer.subarray(0, resultByteLength), 0); + return resultByteLength; } - encode(data: Float32Array): Uint8Array | string { - this.encode_buffer.set(data); + encode(buffer: Uint8Array, responseBuffer: (length: number) => Uint8Array): number | string { + if (buffer.byteLength > this.decode_buffer.byteLength) + return "supplied data exceeds internal buffer"; - let result = this.fn_encode(this.nativeHandle, this.encode_buffer.byteOffset, data.length, this.encode_buffer.byteLength); + this.encode_buffer.set(buffer); + + let result = this.fn_encode(this.nativeHandle, this.encode_buffer.byteOffset, buffer.byteLength, this.encode_buffer.byteLength); if (result < 0) return OPUS_ERROR_CODES[-result] || "unknown encode error " + result; - return Module.HEAP8.slice(this.encode_buffer.byteOffset, this.encode_buffer.byteOffset + result); + const resultBuffer = responseBuffer(result); + resultBuffer.set(Module.HEAP8.subarray(this.encode_buffer.byteOffset, this.encode_buffer.byteOffset + result)); + return result; } reset() { diff --git a/web/native-codec/src/ilarvecon.cpp b/web/native-codec/src/ilarvecon.cpp new file mode 100644 index 00000000..0261504b --- /dev/null +++ b/web/native-codec/src/ilarvecon.cpp @@ -0,0 +1,68 @@ +// +// Created by WolverinDEV on 12/06/2020. +// + +/* source and target should not be intersecting! */ +template +void sequenced2interleaved(float* source, float* target, size_t sample_count) { + #pragma unroll + for(size_t channel = 0; channel < kChannelCount; channel++) { + auto src_ptr = source + channel * sample_count; + auto dest_ptr = target + channel; + + auto samples_left = sample_count; + while(samples_left--) { + *dest_ptr = *src_ptr; + + src_ptr++; + dest_ptr += kChannelCount; + } + } +} + +/* source and target should not be intersecting! */ +template +void interleaved2sequenced(float* source, float* target, size_t sample_count) { + #pragma unroll + for(size_t channel = 0; channel < kChannelCount; channel++) { + auto src_ptr = source + channel; + auto dest_ptr = target + channel * sample_count; + + auto samples_left = sample_count; + while(samples_left--) { + *dest_ptr = *src_ptr; + + src_ptr += kChannelCount; + dest_ptr++; + } + } +} + +#define kTempBufferSize (1024 * 8) + +/* since js is single threaded we need no lock here */ +float temp_buffer[kTempBufferSize]; + +template +void interleaved2sequenced_intersecting(float* buffer, size_t sample_count) { + auto temp = temp_buffer; + if(sample_count * kChannelCount > kTempBufferSize) + temp = (float*) malloc(sample_count * sizeof(float) * kChannelCount); + + memcpy(temp, buffer, sample_count * sizeof(float) * kChannelCount); + interleaved2sequenced(temp, buffer, sample_count); + if(temp != temp_buffer) + free(temp); +} + +template +void sequenced2interleaved_intersecting(float* buffer, size_t sample_count) { + auto temp = temp_buffer; + if(sample_count * kChannelCount > kTempBufferSize) + temp = (float*) malloc(sample_count * sizeof(float) * kChannelCount); + + memcpy(temp, buffer, sample_count * sizeof(float) * kChannelCount); + sequenced2interleaved(temp, buffer, sample_count); + if(temp != temp_buffer) + free(temp); +} \ No newline at end of file diff --git a/web/native-codec/src/opus.cpp b/web/native-codec/src/opus.cpp index bdc1030a..a3384f30 100644 --- a/web/native-codec/src/opus.cpp +++ b/web/native-codec/src/opus.cpp @@ -3,6 +3,7 @@ #include #include #include +#include "./ilarvecon.cpp" typedef std::unique_ptr opus_encoder_t; typedef std::unique_ptr opus_decoder_t; @@ -82,16 +83,22 @@ void codec_opus_deleteNativeHandle(OpusHandle *codec) { } EMSCRIPTEN_KEEPALIVE -int codec_opus_encode(OpusHandle *handle, uint8_t *buffer, size_t length, size_t maxLength) { - auto result = opus_encode_float(&*handle->encoder, (float *) buffer, length / handle->channelCount, buffer, maxLength); +int codec_opus_encode(OpusHandle *handle, uint8_t *buffer, size_t byte_length, size_t maxLength) { + if(handle->channelCount == 2) + sequenced2interleaved_intersecting<2>((float *) buffer, byte_length / (sizeof(float) * 2)); + + auto result = opus_encode_float(&*handle->encoder, (float *) buffer, byte_length / handle->channelCount, buffer, maxLength); if (result < 0) return result; return result; } EMSCRIPTEN_KEEPALIVE -int codec_opus_decode(OpusHandle *handle, uint8_t *buffer, size_t length, size_t maxLength) { - auto result = opus_decode_float(&*handle->decoder, buffer, length, (float *) buffer, maxLength / sizeof(float) / handle->channelCount, false); +int codec_opus_decode(OpusHandle *handle, uint8_t *buffer, size_t byte_length, size_t buffer_max_byte_length) { + auto result = opus_decode_float(&*handle->decoder, buffer, byte_length, (float *) buffer, buffer_max_byte_length / sizeof(float) / handle->channelCount, false); if (result < 0) return result; + + if(handle->channelCount == 2) + interleaved2sequenced_intersecting<2>((float *) buffer, result); return result; } From a335e823acc1c65f44b8b3e6281dc79a5de2b74c Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Fri, 12 Jun 2020 19:33:05 +0200 Subject: [PATCH 02/11] Fixed script node decoding --- ChangeLog.md | 1 + shared/js/ConnectionHandler.ts | 11 ++- shared/js/connection/CommandHandler.ts | 6 +- shared/js/ui/frames/chat_frame.ts | 2 +- web/js/audio/recorder.ts | 1 + web/js/codec/BasicCodec.ts | 7 +- web/js/codec/Codec.ts | 1 - web/js/voice/AudioResampler.ts | 4 +- web/js/voice/VoiceHandler.ts | 96 +++++++++++++++++--------- web/js/workers/codec/OpusCodec.ts | 30 ++++---- web/native-codec/src/opus.cpp | 2 +- 11 files changed, 95 insertions(+), 66 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index c64e2daf..d581173a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -2,6 +2,7 @@ * **12.06.20** - Added a copy/paste menu for all HTML input elements - Heavily improved web client audio de/encoding + - Fixed script node voice encoding * **11.06.20** - Fixed channel tree deletions diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 0126f262..6d938033 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -337,13 +337,12 @@ export class ConnectionHandler { getClient() : LocalClientEntry { return this._local_client; } getClientId() { return this._clientId; } - set clientId(id: number) { - this._clientId = id; - this._local_client["_clientId"] = id; - } + initializeLocalClient(clientId: number, acceptedName: string) { + this._clientId = clientId; + this._local_client["_clientId"] = clientId; - get clientId() { - return this._clientId; + this.channelTree.registerClient(this._local_client); + this._local_client.updateVariables( { key: "client_nickname", value: acceptedName }); } getServerConnection() : AbstractServerConnection { return this.serverConnection; } diff --git a/shared/js/connection/CommandHandler.ts b/shared/js/connection/CommandHandler.ts index c8dc004c..417055d4 100644 --- a/shared/js/connection/CommandHandler.ts +++ b/shared/js/connection/CommandHandler.ts @@ -191,10 +191,8 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { json = json[0]; //Only one bulk - this.connection_handler.channelTree.registerClient(this.connection_handler.getClient()); this.connection.client.side_bar.channel_conversations().reset(); - this.connection.client.clientId = parseInt(json["aclid"]); - this.connection.client.getClient().updateVariables( {key: "client_nickname", value: json["acn"]}); + this.connection.client.initializeLocalClient(parseInt(json["aclid"]), json["acn"]); let updates: { key: string, @@ -825,7 +823,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { const channel_id = typeof(json["cid"]) !== "undefined" ? parseInt(json["cid"]) : own_channel_id; const channel = this.connection_handler.channelTree.findChannel(channel_id) || this.connection_handler.getClient().currentChannel(); - if(json["invokerid"] == this.connection.client.clientId) + if(json["invokerid"] == this.connection.client.getClientId()) this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); else if(channel_id == own_channel_id) { this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5}); diff --git a/shared/js/ui/frames/chat_frame.ts b/shared/js/ui/frames/chat_frame.ts index a584719a..7ba3fc39 100644 --- a/shared/js/ui/frames/chat_frame.ts +++ b/shared/js/ui/frames/chat_frame.ts @@ -251,7 +251,7 @@ export class InfoFrame { client_id: selected_client.clientId() }, { create: false, attach: false }) : undefined; - const visibility = (selected_client && selected_client.clientId() !== this.handle.handle.clientId) ? "visible" : "hidden"; + const visibility = (selected_client && selected_client.clientId() !== this.handle.handle.getClientId()) ? "visible" : "hidden"; if(this._button_conversation.style.visibility !== visibility) this._button_conversation.style.visibility = visibility; if(conversation) { diff --git a/web/js/audio/recorder.ts b/web/js/audio/recorder.ts index 909dabfe..66375a9a 100644 --- a/web/js/audio/recorder.ts +++ b/web/js/audio/recorder.ts @@ -407,6 +407,7 @@ class JavascriptInput implements AbstractInput { const callback = this._current_consumer as CallbackInputConsumer; if(callback.callback_audio) callback.callback_audio(event.inputBuffer); + if(callback.callback_buffer) { log.warn(LogCategory.AUDIO, tr("AudioInput has callback buffer, but this isn't supported yet!")); } diff --git a/web/js/codec/BasicCodec.ts b/web/js/codec/BasicCodec.ts index 6c772a0c..cc04d330 100644 --- a/web/js/codec/BasicCodec.ts +++ b/web/js/codec/BasicCodec.ts @@ -53,9 +53,10 @@ export abstract class BasicCodec implements Codec { encodeSamples(cache: CodecClientCache, pcm: AudioBuffer) { - this._encodeResampler.resample(pcm).catch(error => log.error(LogCategory.VOICE, tr("Could not resample PCM data for codec. Error: %o"), error)) - .then(buffer => this.encodeSamples0(cache, buffer as any)).catch(error => console.error(tr("Could not encode PCM data for codec. Error: %o"), error)) - + this._encodeResampler.resample(pcm) + .catch(error => log.error(LogCategory.VOICE, tr("Could not resample PCM data for codec. Error: %o"), error)) + .then(buffer => this.encodeSamples0(cache, buffer as any)) + .catch(error => console.error(tr("Could not encode PCM data for codec. Error: %o"), error)) } private encodeSamples0(cache: CodecClientCache, buffer: AudioBuffer) { diff --git a/web/js/codec/Codec.ts b/web/js/codec/Codec.ts index 9b74ae52..81ef7c86 100644 --- a/web/js/codec/Codec.ts +++ b/web/js/codec/Codec.ts @@ -35,7 +35,6 @@ export class BufferChunk { } export class CodecClientCache { - _last_access: number; _chunks: BufferChunk[] = []; bufferedSamples(max: number = 0) : number { diff --git a/web/js/voice/AudioResampler.ts b/web/js/voice/AudioResampler.ts index 5d833300..207d0203 100644 --- a/web/js/voice/AudioResampler.ts +++ b/web/js/voice/AudioResampler.ts @@ -2,7 +2,7 @@ import {LogCategory} from "tc-shared/log"; import * as log from "tc-shared/log"; export class AudioResampler { - targetSampleRate: number; + readonly targetSampleRate: number; private _use_promise: boolean; constructor(targetSampleRate: number){ @@ -15,7 +15,7 @@ export class AudioResampler { log.warn(LogCategory.AUDIO, tr("Received empty buffer as input! Returning empty output!")); return Promise.resolve(buffer); } - //console.log("Encode from %i to %i", buffer.sampleRate, this.targetSampleRate); + if(buffer.sampleRate == this.targetSampleRate) return Promise.resolve(buffer); diff --git a/web/js/voice/VoiceHandler.ts b/web/js/voice/VoiceHandler.ts index 0eef79c4..e06c9d67 100644 --- a/web/js/voice/VoiceHandler.ts +++ b/web/js/voice/VoiceHandler.ts @@ -137,6 +137,7 @@ export class VoiceConnection extends AbstractVoiceConnection { private _type: VoiceEncodeType = VoiceEncodeType.NATIVE_ENCODE; + private localAudioStarted = false; /* * To ensure we're not sending any audio because the settings activates the input, * we self mute the audio stream @@ -245,15 +246,15 @@ export class VoiceConnection extends AbstractVoiceConnection { if(this._audio_source) await this._audio_source.unmount(); - this.handle_local_voice_ended(); + this.handleLocalVoiceEnded(); this._audio_source = recorder; if(recorder) { recorder.current_handler = this.connection.client; recorder.callback_unmount = this.on_recorder_yield.bind(this); - recorder.callback_start = this.handle_local_voice_started.bind(this); - recorder.callback_stop = this.handle_local_voice_ended.bind(this); + recorder.callback_start = this.handleLocalVoiceStarted.bind(this); + recorder.callback_stop = this.handleLocalVoiceEnded.bind(this); recorder.callback_input_change = async (old_input, new_input) => { if(old_input) { @@ -289,11 +290,16 @@ export class VoiceConnection extends AbstractVoiceConnection { log.warn(LogCategory.VOICE, tr("Failed to set consumer to the new recorder input: %o"), e); } } else { - //TODO: Error handling? - await recorder.input.set_consumer({ - type: InputConsumerType.CALLBACK, - callback_audio: buffer => this.handle_local_voice(buffer, false) - } as CallbackInputConsumer); + try { + await recorder.input.set_consumer({ + type: InputConsumerType.CALLBACK, + callback_audio: buffer => this.handleLocalVoiceBuffer(buffer, false) + } as CallbackInputConsumer); + + log.debug(LogCategory.VOICE, tr("Successfully set/updated to the new input for the recorder")); + } catch (e) { + log.warn(LogCategory.VOICE, tr("Failed to set consumer to the new recorder input: %o"), e); + } } } }; @@ -333,22 +339,27 @@ export class VoiceConnection extends AbstractVoiceConnection { const buffer = this.voice_send_queue.pop_front(); if(!buffer) return; - this.send_voice_packet(buffer.data, buffer.codec); + this.sendVoicePacket(buffer.data, buffer.codec); } - send_voice_packet(encoded_data: Uint8Array, codec: number) { + private fillVoicePacketHeader(packet: Uint8Array, codec: number) { + packet[0] = this.chunkVPacketId++ < 5 ? 1 : 0; //Flag header + packet[1] = 0; //Flag fragmented + packet[2] = (this.voice_packet_id >> 8) & 0xFF; //HIGHT (voiceID) + packet[3] = (this.voice_packet_id >> 0) & 0xFF; //LOW (voiceID) + packet[4] = codec; //Codec + } + + sendVoicePacket(encoded_data: Uint8Array, codec: number) { if(this.dataChannel) { this.voice_packet_id++; if(this.voice_packet_id > 65535) this.voice_packet_id = 0; let packet = new Uint8Array(encoded_data.byteLength + 5); - packet[0] = this.chunkVPacketId++ < 5 ? 1 : 0; //Flag header - packet[1] = 0; //Flag fragmented - packet[2] = (this.voice_packet_id >> 8) & 0xFF; //HIGHT (voiceID) - packet[3] = (this.voice_packet_id >> 0) & 0xFF; //LOW (voiceID) - packet[4] = codec; //Codec + this.fillVoicePacketHeader(packet, codec); packet.set(encoded_data, 5); + try { this.dataChannel.send(packet); } catch (error) { @@ -359,6 +370,20 @@ export class VoiceConnection extends AbstractVoiceConnection { } } + sendVoiceStopPacket(codec: number) { + if(!this.dataChannel) + return; + + const packet = new Uint8Array(5); + this.fillVoicePacketHeader(packet, codec); + + try { + this.dataChannel.send(packet); + } catch (error) { + log.warn(LogCategory.VOICE, tr("Failed to send voice packet. Error: %o"), error); + } + } + private _audio_player_waiting = false; start_rtc_session() { if(!aplayer.initialized()) { @@ -390,8 +415,8 @@ export class VoiceConnection extends AbstractVoiceConnection { const dataChannelConfig = { ordered: false, maxRetransmits: 0 }; this.dataChannel = this.rtcPeerConnection.createDataChannel('main', dataChannelConfig); - this.dataChannel.onmessage = this.on_data_channel_message.bind(this); - this.dataChannel.onopen = this.on_data_channel.bind(this); + this.dataChannel.onmessage = this.onMainDataChannelMessage.bind(this); + this.dataChannel.onopen = this.onMainDataChannelOpen.bind(this); this.dataChannel.binaryType = "arraybuffer"; let sdpConstraints : RTCOfferOptions = {}; @@ -524,15 +549,15 @@ export class VoiceConnection extends AbstractVoiceConnection { }); } - private on_data_channel(channel) { + private onMainDataChannelOpen(channel) { log.info(LogCategory.VOICE, tr("Got new data channel! (%s)"), this.dataChannel.readyState); this.connection.client.update_voice_status(); } - private on_data_channel_message(message: MessageEvent) { + private onMainDataChannelMessage(message: MessageEvent) { const chandler = this.connection.client; - if(chandler.client_status.output_muted) /* we dont need to do anything with sound playback when we're not listening to it */ + if(chandler.isSpeakerMuted() || chandler.isSpeakerDisabled()) /* we dont need to do anything with sound playback when we're not listening to it */ return; let bin = new Uint8Array(message.data); @@ -571,18 +596,18 @@ export class VoiceConnection extends AbstractVoiceConnection { } } - private handle_local_voice(data: AudioBuffer, head: boolean) { + private handleLocalVoiceBuffer(data: AudioBuffer, head: boolean) { const chandler = this.connection.client; - if(!chandler.connected) + if(!this.localAudioStarted || !chandler.connected) return false; - if(chandler.client_status.input_muted) + if(chandler.isMicrophoneMuted()) return false; if(head) this.chunkVPacketId = 0; - let client = this.find_client(chandler.clientId); + let client = this.find_client(chandler.getClientId()); if(!client) { log.error(LogCategory.VOICE, tr("Tried to send voice data, but local client hasn't a voice client handle")); return; @@ -594,25 +619,31 @@ export class VoiceConnection extends AbstractVoiceConnection { .then(encoder => encoder.encodeSamples(client.get_codec_cache(codec), data)); } - private handle_local_voice_ended() { + private handleLocalVoiceEnded() { const chandler = this.connection.client; const ch = chandler.getClient(); if(ch) ch.speaking = false; if(!chandler.connected) return false; - if(chandler.client_status.input_muted) + if(chandler.isMicrophoneMuted()) return false; log.info(LogCategory.VOICE, tr("Local voice ended")); + this.localAudioStarted = false; - if(this.dataChannel && this._encoder_codec >= 0) - this.send_voice_packet(new Uint8Array(0), this._encoder_codec); + if(this._type === VoiceEncodeType.NATIVE_ENCODE) { + setTimeout(() => { + /* first send all data, than send the stop signal */ + this.sendVoiceStopPacket(this._encoder_codec); + }, 150); + } else { + this.sendVoiceStopPacket(this._encoder_codec); + } } - private handle_local_voice_started() { + private handleLocalVoiceStarted() { const chandler = this.connection.client; - if(chandler.client_status.input_muted) { - /* evail hack due to the settings :D */ + if(chandler.isMicrophoneMuted()) { log.warn(LogCategory.VOICE, tr("Received local voice started event, even thou we're muted! Do not send any voice.")); if(this.local_audio_mute) this.local_audio_mute.gain.value = 0; @@ -620,6 +651,8 @@ export class VoiceConnection extends AbstractVoiceConnection { } if(this.local_audio_mute) this.local_audio_mute.gain.value = 1; + + this.localAudioStarted = true; log.info(LogCategory.VOICE, tr("Local voice started")); const ch = chandler.getClient(); @@ -683,7 +716,6 @@ export class VoiceConnection extends AbstractVoiceConnection { } - /* funny fact that typescript dosn't find this */ declare global { interface RTCPeerConnection { diff --git a/web/js/workers/codec/OpusCodec.ts b/web/js/workers/codec/OpusCodec.ts index f892ac3f..48c36cf6 100644 --- a/web/js/workers/codec/OpusCodec.ts +++ b/web/js/workers/codec/OpusCodec.ts @@ -121,6 +121,8 @@ const OPUS_ERROR_CODES = [ ]; class OpusWorker implements CodecWorker { + private static readonly kProcessBufferSize = 4096 * 2; + private readonly channelCount: number; private readonly type: OpusType; private nativeHandle: any; @@ -130,11 +132,8 @@ class OpusWorker implements CodecWorker { private fn_encode: any; private fn_reset: any; - private buffer_size = 4096 * 2; - private buffer: any; - - private encode_buffer: Float32Array; - private decode_buffer: Uint8Array; + private nativeBufferPtr: number; + private processBuffer: Uint8Array; constructor(channelCount: number, type: OpusType) { this.channelCount = channelCount; @@ -153,40 +152,39 @@ class OpusWorker implements CodecWorker { this.nativeHandle = this.fn_newHandle(this.channelCount, this.type); - this.buffer = Module._malloc(this.buffer_size); - this.encode_buffer = new Float32Array(Module.HEAPF32.buffer, this.buffer, Math.floor(this.buffer_size / 4)); - this.decode_buffer = new Uint8Array(Module.HEAPU8.buffer, this.buffer, this.buffer_size); + this.nativeBufferPtr = Module._malloc(OpusWorker.kProcessBufferSize); + this.processBuffer = new Uint8Array(Module.HEAPU8.buffer, this.nativeBufferPtr, OpusWorker.kProcessBufferSize); return undefined; } deinitialise() { } //TODO decode(buffer: Uint8Array, responseBuffer: (length: number) => Uint8Array): number | string { - if (buffer.byteLength > this.decode_buffer.byteLength) + if (buffer.byteLength > this.processBuffer.byteLength) return "supplied data exceeds internal buffer"; - this.decode_buffer.set(buffer); + this.processBuffer.set(buffer); - let result = this.fn_decode(this.nativeHandle, this.decode_buffer.byteOffset, buffer.byteLength, this.decode_buffer.byteLength); + let result = this.fn_decode(this.nativeHandle, this.processBuffer.byteOffset, buffer.byteLength, this.processBuffer.byteLength); if (result < 0) return OPUS_ERROR_CODES[-result] || "unknown decode error " + result; const resultByteLength = result * this.channelCount * 4; const resultBuffer = responseBuffer(resultByteLength); - resultBuffer.set(this.decode_buffer.subarray(0, resultByteLength), 0); + resultBuffer.set(this.processBuffer.subarray(0, resultByteLength), 0); return resultByteLength; } encode(buffer: Uint8Array, responseBuffer: (length: number) => Uint8Array): number | string { - if (buffer.byteLength > this.decode_buffer.byteLength) + if (buffer.byteLength > this.processBuffer.byteLength) return "supplied data exceeds internal buffer"; - this.encode_buffer.set(buffer); + this.processBuffer.set(buffer); - let result = this.fn_encode(this.nativeHandle, this.encode_buffer.byteOffset, buffer.byteLength, this.encode_buffer.byteLength); + let result = this.fn_encode(this.nativeHandle, this.processBuffer.byteOffset, buffer.byteLength, this.processBuffer.byteLength); if (result < 0) return OPUS_ERROR_CODES[-result] || "unknown encode error " + result; const resultBuffer = responseBuffer(result); - resultBuffer.set(Module.HEAP8.subarray(this.encode_buffer.byteOffset, this.encode_buffer.byteOffset + result)); + resultBuffer.set(this.processBuffer.subarray(0, result), 0); return result; } diff --git a/web/native-codec/src/opus.cpp b/web/native-codec/src/opus.cpp index a3384f30..bd89f882 100644 --- a/web/native-codec/src/opus.cpp +++ b/web/native-codec/src/opus.cpp @@ -87,7 +87,7 @@ int codec_opus_encode(OpusHandle *handle, uint8_t *buffer, size_t byte_length, s if(handle->channelCount == 2) sequenced2interleaved_intersecting<2>((float *) buffer, byte_length / (sizeof(float) * 2)); - auto result = opus_encode_float(&*handle->encoder, (float *) buffer, byte_length / handle->channelCount, buffer, maxLength); + auto result = opus_encode_float(&*handle->encoder, (float *) buffer, byte_length / (handle->channelCount * sizeof(float)), buffer, maxLength); if (result < 0) return result; return result; } From ea05ca6f0333622f10b65f12404cf65a90e96b2e Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Sat, 13 Jun 2020 18:47:05 +0200 Subject: [PATCH 03/11] Some updates --- ChangeLog.md | 4 + loader/app/targets/app.ts | 4 - shared/css/generate_packed.sh | 4 - shared/css/static/connection_handlers.scss | 4 + shared/css/static/frame/SelectInfo.scss | 119 ---- shared/css/static/general.scss | 4 + shared/css/static/helptag.scss | 134 ---- shared/css/static/hostbanner.scss | 6 +- shared/css/static/htmltags.scss | 2 +- shared/css/static/main-layout.scss | 39 +- shared/css/static/modal-newcomer.scss | 26 +- shared/css/static/modal-permissions.scss | 138 ++-- shared/css/static/modal-playlist.scss | 440 ------------- shared/css/static/modal-poke.scss | 24 +- shared/css/static/modal-query.scss | 41 +- shared/css/static/modal-serverinfo.scss | 24 +- .../css/static/modal-serverinfobandwidth.scss | 22 +- shared/css/static/modal-volume.scss | 2 +- shared/css/static/modals.scss | 174 ----- shared/css/static/music/info_plate.scss | 315 --------- shared/css/static/server-log.scss | 16 +- shared/html/templates.html | 239 +------ shared/js/permission/GroupManager.ts | 231 ++++--- shared/js/ui/channel.ts | 8 +- shared/js/ui/frames/side/client_info.ts | 4 +- shared/js/ui/modal/ModalClientInfo.ts | 2 +- shared/js/ui/modal/ModalGroupAssignment.ts | 2 +- shared/js/ui/modal/ModalMusicManage.ts | 2 +- .../ui/modal/transfer/ModalFileTransfer.scss | 59 +- shared/js/ui/modal/transfer/TransferInfo.scss | 28 +- shared/js/ui/react-elements/Button.scss | 35 +- shared/js/ui/react-elements/Checkbox.scss | 15 +- shared/js/ui/react-elements/InputField.scss | 41 +- shared/js/ui/react-elements/Modal.scss | 12 +- shared/js/ui/react-elements/ProgressBar.scss | 16 +- shared/js/ui/react-elements/Slider.scss | 16 +- shared/js/ui/react-elements/Tooltip.scss | 9 +- shared/js/ui/tree/Client.scss | 13 +- shared/js/ui/tree/Client.tsx | 8 +- shared/js/voice/RecorderProfile.ts | 2 +- shared/loader/app.ts | 617 ------------------ shared/loader/loader.ts | 0 42 files changed, 562 insertions(+), 2339 deletions(-) delete mode 100644 shared/css/static/frame/SelectInfo.scss delete mode 100644 shared/css/static/helptag.scss delete mode 100644 shared/css/static/modal-playlist.scss delete mode 100644 shared/css/static/music/info_plate.scss delete mode 100644 shared/loader/app.ts delete mode 100644 shared/loader/loader.ts diff --git a/ChangeLog.md b/ChangeLog.md index d581173a..ba638103 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,8 @@ # Changelog: +* **13.06.20** + - Started to extract all color values and put them into css variables + - Fixed a minor issue related to server/channel groups + * **12.06.20** - Added a copy/paste menu for all HTML input elements - Heavily improved web client audio de/encoding diff --git a/loader/app/targets/app.ts b/loader/app/targets/app.ts index 9b8f06e0..76c1c7a2 100644 --- a/loader/app/targets/app.ts +++ b/loader/app/targets/app.ts @@ -152,7 +152,6 @@ const loader_style = { await loader.style.load_multiple([ "css/static/main.css", "css/static/main-layout.css", - "css/static/helptag.css", "css/static/scroll.css", "css/static/channel-tree.css", "css/static/ts/tab.css", @@ -173,7 +172,6 @@ const loader_style = { "css/static/modal-volume.css", "css/static/modal-latency.css", "css/static/modal-invite.css", - "css/static/modal-playlist.css", "css/static/modal-banlist.css", "css/static/modal-banclient.css", "css/static/modal-channelinfo.css", @@ -190,8 +188,6 @@ const loader_style = { "css/static/modal-permissions.css", "css/static/modal-group-assignment.css", "css/static/overlay-image-preview.css", - "css/static/music/info_plate.css", - "css/static/frame/SelectInfo.css", "css/static/context_menu.css", "css/static/frame-chat.css", "css/static/connection_handlers.css", diff --git a/shared/css/generate_packed.sh b/shared/css/generate_packed.sh index 35e31b98..db4078a0 100644 --- a/shared/css/generate_packed.sh +++ b/shared/css/generate_packed.sh @@ -15,7 +15,6 @@ files=( "css/static/frame-chat.css" "css/static/server-log.css" "css/static/scroll.css" - "css/static/helptag.css" "css/static/hostbanner.css" "css/static/htmltags.css" "css/static/menu-bar.css" @@ -38,7 +37,6 @@ files=( "css/static/modal-invite.css" "css/static/modal-keyselect.css" "css/static/modal-permissions.css" - "css/static/modal-playlist.css" "css/static/modal-poke.css" "css/static/modal-query.css" "css/static/modal-server.css" @@ -54,8 +52,6 @@ files=( "css/static/ts/icons.css" "css/static/ts/icons_em.css" "css/static/ts/country.css" - "css/static/music/info_plate.css" - "css/static/frame/SelectInfo.css" ) target_file=`pwd`/../generated/static/base.css diff --git a/shared/css/static/connection_handlers.scss b/shared/css/static/connection_handlers.scss index 4bec40a9..62ebc96b 100644 --- a/shared/css/static/connection_handlers.scss +++ b/shared/css/static/connection_handlers.scss @@ -1,5 +1,9 @@ @import "mixin"; +html:root { + +} + .container-connection-handlers { $animation_length: .25s; diff --git a/shared/css/static/frame/SelectInfo.scss b/shared/css/static/frame/SelectInfo.scss deleted file mode 100644 index 3c0f068d..00000000 --- a/shared/css/static/frame/SelectInfo.scss +++ /dev/null @@ -1,119 +0,0 @@ -.select_info_table { - tr { - td { - &:nth-child(1) { - font-weight: bold; - padding-right: 5px; - //min-width: max(35%, 20px); - } - - &:nth-child(2) { - //min-width: max(75%, 40px); - word-break: break-word; - } - } - } - - .reserved-slots { - display: inline; - color: red; - } -} - -.select_server { - height: 100%; - display: inline-flex; - flex-direction: column; - justify-content: space-between; - flex-grow: 1; - - .button-update { - width: 100%; - - &:disabled { - pointer-events: none; - } - } - - .container { - max-height: 100%; - display: flex; - flex-direction: column; - padding-right: 0; - padding-left: 0; - } -} - -.select_info { - display: flex; - flex-direction: column; - justify-content: stretch; - width: 100%; - - > .close { - z-index: 500; - display: none; - position: absolute; - right: 5px; - top: 5px; - } - - > div { - width: 100%; - } - - .container-banner { - position: relative; - - flex-grow: 1; - flex-shrink: 2; - - max-height: 300px; - min-height: 0; - - display: flex; - justify-content: stretch; - overflow: hidden; - - - &.disabled { - display: none; - margin-bottom: 5px; - } - } - - .container-select-info { - padding: 2px; - flex-grow: 1; - flex-shrink: 1; - display: flex; - flex-direction: column; - justify-content: stretch; - - .select_server { - > div { - flex-grow: 1; - } - } - } - - .client-avatar { - > div { - flex-grow: 1; - flex-shrink: 1; - > img { - max-width: 100%; - max-height: 100%; - } - } - } - - .button-browser-info { - vertical-align: bottom; - cursor: pointer; - - &:hover { - background-color: gray; - } - } -} \ No newline at end of file diff --git a/shared/css/static/general.scss b/shared/css/static/general.scss index c5eb861d..167a6071 100644 --- a/shared/css/static/general.scss +++ b/shared/css/static/general.scss @@ -67,6 +67,10 @@ --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } +html:root { + --text: #999; +} + *, :before, :after { box-sizing: border-box; outline: none; diff --git a/shared/css/static/helptag.scss b/shared/css/static/helptag.scss deleted file mode 100644 index 9bfaaae5..00000000 --- a/shared/css/static/helptag.scss +++ /dev/null @@ -1,134 +0,0 @@ -.help-tip-container { - /* position: relative; */ - display: inline; - - .help-tip { - position: absolute; - } -} - -.help-tip { - z-index: 100; - - display: inline-block; - position: relative; - text-align: center; - background-color: #BCDBEA; - border-radius: 50%; - - width: 24px; - height: 24px; - - font-size: 14px; - line-height: 26px; - - cursor: default; - - &:before { - content:'?'; - font-weight: bold; - color:#fff; - } - - &:hover, &.show { - p { - display:block; - transform-origin: 100% 0%; - - -webkit-animation: fadeIn 0.3s ease-in-out; - animation: fadeIn 0.3s ease-in-out; - } - } - - p { - display: none; - text-align: left; - background-color: #1E2021; - padding: 20px; - - width: 400px; /* fallback */ - width: max-content; - - position: absolute; - border-radius: 3px; - box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2); - color: #FFF; - font-size: 13px; - line-height: 1.4; - - - &:before { - position: absolute; - content: ''; - width:0; - height: 0; - border:6px solid transparent; - border-bottom-color:#1E2021; - top:-12px; - } - - &:after { - width:100%; - height:40px; - content:''; - position: absolute; - top:-40px; - left:0; - } - } - - &.tip-left { - p { - right: -4px; - - &:before { - right: 10px; - } - } - } - &.tip-right { - p { - left: -4px; - - &:before { - left: 10px; - } - } - } - - &.tip-center { - p { - left: 50%; - transform: translate(-50%, 0); - - &:before { - right: calc(50% - 5px); - } - } - } - - &.tip-small { - width: 16px; - height: 16px; - - font-size: 12px; - line-height: 18px; - } -} - -@-webkit-keyframes fadeIn { - 0% { - opacity:0; - transform: scale(0.6); - } - - 100% { - opacity:100%; - transform: scale(1); - } -} - -@keyframes fadeIn { - 0% { opacity:0; } - 100% { opacity:100%; } -} \ No newline at end of file diff --git a/shared/css/static/hostbanner.scss b/shared/css/static/hostbanner.scss index 36823db5..079c6cb6 100644 --- a/shared/css/static/hostbanner.scss +++ b/shared/css/static/hostbanner.scss @@ -1,5 +1,9 @@ @import "mixin"; +html:root { + --hostbanner-background: #2e2e2e; +} + .hostbanner { .container-hostbanner { position: relative; @@ -14,7 +18,7 @@ cursor: pointer; &:not(.no-background) { - background-color: #2e2e2e; + background-color: var(--hostbanner-background); border-top-left-radius: 5px; border-top-right-radius: 5px; -moz-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.25); diff --git a/shared/css/static/htmltags.scss b/shared/css/static/htmltags.scss index 090a84d6..afe59076 100644 --- a/shared/css/static/htmltags.scss +++ b/shared/css/static/htmltags.scss @@ -1,5 +1,5 @@ .htmltag-client, .htmltag-channel { - color: blue; + color: var(--text); font-weight: bold; cursor: pointer; } \ No newline at end of file diff --git a/shared/css/static/main-layout.scss b/shared/css/static/main-layout.scss index 7b5261b7..78cc39b6 100644 --- a/shared/css/static/main-layout.scss +++ b/shared/css/static/main-layout.scss @@ -4,6 +4,21 @@ $separator_thickness: 5px; $small_device: 650px; $animation_length: .5s; +html:root { + --app-background: #1e1e1e; + + --control-bar-background: #454545; + + --chat-background: #353535; + --channel-tree-background: #353535; + --server-log-background: #353535; + + --footer-background: #252525; + --footer-text: #666666; + + --channel-chat-seperator: #707070; +} + .app { min-width: 600px; min-height: 330px; @@ -45,7 +60,7 @@ $animation_length: .5s; width: 50%; /* "default" settings */ height: 100%; - background: #353535; + background: var(--channel-tree-background); min-width: 200px; display: flex; @@ -89,7 +104,7 @@ $animation_length: .5s; width: 50%; /* "default" settings */ height: 100%; - background: #353535; + background: var(--chat-background); min-width: 350px; display: flex; @@ -125,7 +140,7 @@ $animation_length: .5s; padding-right: 5px; padding-left: 5px; - background: #353535; + background: var(--server-log-background); } > .container-footer { @@ -134,17 +149,17 @@ $animation_length: .5s; height: 1.5em; - background: #252525; - color: #666666; + background: var(--footer-background); + color: var(--footer-text); border-radius: 0 0 5px 5px; padding-right: 5px; padding-left: 5px; padding-top: 2px; - -webkit-box-shadow: inset 0px 2px 5px 0px rgba(0,0,0,0.125); - -moz-box-shadow: inset 0px 2px 5px 0px rgba(0,0,0,0.125); - box-shadow: inset 0px 2px 5px 0px rgba(0,0,0,0.125); + -webkit-box-shadow: inset 0 2px 5px 0 rgba(0,0,0,0.125); + -moz-box-shadow: inset 0 2px 5px 0 rgba(0,0,0,0.125); + box-shadow: inset 0 2px 5px 0 rgba(0,0,0,0.125); display: flex; flex-direction: row; @@ -165,7 +180,7 @@ $animation_length: .5s; } a[href], a[href]:visited { - color: #666666!important; + color: var(--footer-text)!important; } } } @@ -181,7 +196,7 @@ $animation_length: .5s; height: 2em; width: 100%; - background-color: #454545; + background-color: var(--control-bar-background); display: flex; flex-direction: column; @@ -250,7 +265,7 @@ $animation_seperator_length: .1s; &.seperator-selected { @include transition(all $animation_seperator_length ease-in-out); - background-color: #707070; + background-color: var(--channel-chat-seperator); } } @@ -270,5 +285,5 @@ html, body { } body { - background: #1e1e1e !important; + background: var(--app-background)!important; } \ No newline at end of file diff --git a/shared/css/static/modal-newcomer.scss b/shared/css/static/modal-newcomer.scss index 8e3f5479..03632a1b 100644 --- a/shared/css/static/modal-newcomer.scss +++ b/shared/css/static/modal-newcomer.scss @@ -1,6 +1,11 @@ @import "mixin"; @import "properties"; +html:root { + --modal-newcomer-header-color: hsla(222, 5%, 39%, 1); + --modal-newcomer-divider: #313135; +} + .modal-body.modal-newcomer { display: flex!important; flex-direction: column!important; @@ -17,7 +22,7 @@ flex-shrink: 0; flex-grow: 0; - color: #565656; + color: var(--modal-newcommer-header-color); padding: .5em .5em .25em; position: relative; @@ -40,7 +45,7 @@ height: 1.25px; //background: linear-gradient(90deg, rgba(49,49,53,1) 80%, rgba(49,49,53,0) 100%); - background: rgba(49,49,53,1); + background: var(--modal-newcomer-divider); } &.hidden { @@ -118,23 +123,20 @@ padding: .5em; .left .body { - // background-color: #19191b; - background-color: hsla(220, 4%, 13%, 1); + background-color: #202122; .overlay { - background-color: hsla(220, 4%, 13%, 1); - + background-color: #202122; } + .profile.selected { - background-color: hsla(240, 2%, 8%, 1); + background-color: #141415; } } } - &.step-identity { - } + &.step-identity { } - &.step-microphone { - } + &.step-microphone { } &.hidden { display: none; @@ -151,7 +153,7 @@ flex-direction: row; justify-content: space-between; - border-top: 1.25px solid rgba(49,49,53,1); + border-top: 1.25px solid var(--modal-newcomer-divider); padding: .5em; } } \ No newline at end of file diff --git a/shared/css/static/modal-permissions.scss b/shared/css/static/modal-permissions.scss index 43336af4..43ebf757 100644 --- a/shared/css/static/modal-permissions.scss +++ b/shared/css/static/modal-permissions.scss @@ -1,6 +1,47 @@ @import "mixin"; @import "properties"; +html:root { + --modal-permissions-header-text: #e1e1e1; + --modal-permissions-header-background: #19191b; + --modal-permissions-header-hover: #4e4e4e; + --modal-permissions-header-selected: #0073d4; + + --modal-permission-right: #303036; + --modal-permission-left: #222226; + + --modal-permissions-entry-hover: #28282c; + --modal-permissions-entry-selected: #111111; + --modal-permissions-current-group: #101012; + + --modal-permissions-buttons-background: #1b1b1b; + --modal-permissions-buttons-hover: #262626; + --modal-permissions-buttons-disabled: hsla(0, 0%, 9%, 1); + + --modal-permissions-seperator: #1e1e1e; /* the seperator for the "enter a unique id" and "client info" part */ + --modal-permissions-container-seperator: #222224; /* the seperator between left and right */ + + --modal-permissions-icon-select: #121213; + --modal-permissions-icon-select-border: #0d0d0d; + --modal-permissions-icon-select-hover: #17171a; + --modal-permissions-icon-select-hover-border: #333333; + + --modal-permissions-table-border: #1e2025; + + --modal-permissions-table-header: #303036; + --modal-permissions-table-entry-odd: #303036; + --modal-permissions-table-entry-even: #25252a; + --modal-permissions-table-entry-hover: #343a47; + + --modal-permissions-table-header-text: #e1e1e1; + --modal-permissions-table-entry-text: #535455; + --modal-permissions-table-entry-active-text: #e1e1e1; + --modal-permissions-table-entry-group-text: #e1e1e1; + + --modal-permissions-table-input: #e1e1e1; + --modal-permissions-table-input-focus: #3f7dbf; +} + .modal-body.modal-permission-editor { padding: 0!important; @@ -28,8 +69,8 @@ .header { height: 4em; - background-color: #19191b; - color: #e1e1e1; + background-color: var(--modal-permissions-header-text); + color: var(--modal-permissions-header-background); display: flex; flex-direction: row; @@ -73,7 +114,7 @@ width: 75%; min-width: 30em; - background-color: #303036; + background-color: var(--modal-permission-right); .header { > .entry { @@ -91,7 +132,7 @@ &:hover { border: none; - border-bottom: 2px solid #4e4e4e; + border-bottom: 2px solid var(--modal-permissions-header-hover); padding-bottom: 0; @@ -107,13 +148,13 @@ height: 100%; width: calc(100% + 20em); - box-shadow: inset 0px -1.2em 3em -20px #424242; + box-shadow: inset 0px -1.2em 3em -20px var(--modal-permissions-header-hover); } } &.selected { border: none; - border-bottom: 2px solid #0073d4; + border-bottom: 2px solid var(--modal-permissions-header-selected); padding-bottom: 0; @@ -129,7 +170,7 @@ height: 100%; width: calc(100% + 20em); - box-shadow: inset 0px -1.2em 3em -20px #0073d4; + box-shadow: inset 0px -1.2em 3em -20px var(--modal-permissions-header-selected); } } } @@ -168,7 +209,7 @@ min-height: 10em; overflow: hidden; - background-color: #222226; + background-color: var(--modal-permission-left); .header { font-weight: bold; @@ -212,7 +253,7 @@ width: 100%; .list-groups, .list-channel, .list-clients { - color: #999999; + color: var(--text); display: flex; flex-direction: column; @@ -252,15 +293,11 @@ width: 100%; &:hover { - background-color: #28282c; + background-color: var(--modal-permissions-entry-hover); } &.selected { - background-color: #111111; - - &.client { - background-color: #1a1b1e; - } + background-color: var(--modal-permissions-entry-selected); } @include transition(background-color .25s ease-in-out); @@ -296,15 +333,16 @@ cursor: pointer; - background-color: #1b1b1b; + background-color: var(--modal-permissions-buttons-background); &:hover { - background-color: #262626; + background-color: var(--modal-permissions-buttons-hover); } &:disabled { - background-color: hsla(0, 0%, 9%, 1); + background-color: var(--modal-permissions-buttons-disabled); } + @include transition(background-color .25s ease-in-out); img { @@ -365,8 +403,8 @@ flex-direction: row; justify-content: stretch; - background-color: #101012; - color: #999999; + background-color: var(--modal-permissions-current-group); + color: var(--text); padding-left: .25em; @@ -405,7 +443,7 @@ hr { border: none; - border-top: 2px solid #1e1e1e; + border-top: 2px solid var(--modal-permissions-seperator); } } @@ -420,7 +458,7 @@ .container-seperator { width: 3px; height: unset!important; - background-color: #222224!important; + background-color: var(--modal-permissions-container-seperator)!important; } } @@ -531,7 +569,7 @@ max-width: 10em; min-width: 5em; - color: #999999; + color: var(--text); > label { font-size: .75em; @@ -571,15 +609,15 @@ justify-content: flex-end; cursor: pointer; - background-color: #121213; - border: 1px solid #0d0d0d; + background-color: var(--modal-permissions-icon-select); + border: 1px solid var(--modal-permissions-icon-select-border); .icon-preview { height: 100%; width: 3em; border: none; - border-right: 1px solid #0d0d0d; + border-right: 1px solid var(--modal-permissions-icon-select-border); display: flex; flex-direction: column; @@ -614,7 +652,7 @@ text-align: center; .arrow { - border-color: #999999; + border-color: var(--text); } } @@ -623,13 +661,13 @@ position: absolute; width: max-content; - top: calc(2.5em - 1px); + top: calc(2.5em - 2px); flex-direction: column; justify-content: flex-start; - background-color: #121213; - border: 1px solid #0d0d0d; + background-color: var(--modal-permissions-icon-select); + border: 1px solid var(--modal-permissions-icon-select-border); border-radius: .2em 0 .2em .2em; right: -1px; @@ -639,11 +677,11 @@ &:not(:last-of-type) { border: none; - border-bottom: 1px solid #0d0d0d; + border-bottom: 1px solid var(--modal-permissions-icon-select-border); } &:hover { - background-color: #17171a; + background-color: var(--modal-permissions-icon-select-hover); } } } @@ -657,11 +695,11 @@ } &:hover { - background-color: #17171a; - border-color: hsla(0, 0%, 20%, 1); + background-color: var(--modal-permissions-icon-select-hover); + border-color: var(--modal-permissions-icon-select-hover-border); .icon-preview { - border-color: hsla(0, 0%, 20%, 1); + border-color: var(--modal-permissions-icon-select-hover-border); } } @@ -677,7 +715,7 @@ &.container-mode-permissions { .container-permission-list { width: 100%; - color: #999999; + color: var(--text); display: flex; flex-direction: column; @@ -697,9 +735,10 @@ height: 2em; border: none; - border-bottom: 1px solid #1e2025; + border-bottom: 1px solid var(--modal-permissions-table-border); + background-color: var(--modal-permissions-table-entry-odd); - color: #535455; + color: var(--modal-permissions-table-entry-text); @mixin fixed-column($name, $width) { .column-#{$name} { @@ -717,7 +756,7 @@ padding-left: 1em; border: none; - border-right: 1px solid #1e2025; + border-right: 1px solid var(--modal-permissions-table-border); overflow: hidden; @@ -743,7 +782,7 @@ .arrow { cursor: pointer; - border-color: #e1e1e1; + border-color: var(--modal-permissions-table-entry-active-text); } .group-name { @@ -757,16 +796,16 @@ &.active { - color: #e1e1e1; + color: var(--modal-permissions-table-entry-active-text); } &.group { - color: #e1e1e1; + color: var(--modal-permissions-table-entry-group-text); font-weight: bold; } input { - color: #e1e1e1; + color: var(--modal-permissions-table-input); outline: none; background: transparent; @@ -782,7 +821,7 @@ border-bottom: 2px solid transparent; &:focus { - border-bottom-color: #3f7dbf; + border-bottom-color: var(--modal-permissions-table-input-focus); } @include transition(border-bottom-color $button_hover_animation_time ease-in-out); } @@ -806,10 +845,11 @@ .entry { &.even { - background-color: #25252a; + background-color: var(--modal-permissions-table-entry-even); } + &:hover { - background-color: #343a47; + background-color: var(--modal-permissions-table-entry-hover); } /* We cant use this effect here because the odd/even effect would be a bit crazy then */ //@include transition(background-color $button_hover_animation_time ease-in-out); @@ -817,9 +857,9 @@ } .header { - background-color: unset; + background-color: var(--modal-permissions-table-header); + color: var(--modal-permissions-table-header-text); - color: #e1e1e1; font-weight: bold; .column-granted { @@ -838,7 +878,7 @@ text-align: center; font-size: 2em; - color: #222226; + color: var(--modal-permission-left); } } diff --git a/shared/css/static/modal-playlist.scss b/shared/css/static/modal-playlist.scss deleted file mode 100644 index ca9ed038..00000000 --- a/shared/css/static/modal-playlist.scss +++ /dev/null @@ -1,440 +0,0 @@ - -.playlist-management { - height: 100%; - display: flex!important;; - flex-direction: column!important;; - - .header, .footer { - flex-grow: 0; - flex-shrink: 0; - } - - .header { - display: flex; - flex-direction: row; - justify-content: stretch; - - .buttons { - flex-grow: 0; - } - - .search { - margin-left: 5px; - flex-grow: 1; - - input { - width: 100%; - } - } - } - - .playlist-list { - margin-top: 5px; - - display: flex; - flex-grow: 1; - flex-direction: column; - justify-content: stretch; - - $width_id: 80px; - $width_type: 150px; - $width_used: 40px; - .column { - &.column-id { - width: 80px; - text-align: center; - } - - &.column-title { - width: calc(50% - 95px - 40px); - } - - &.column-creator { - width: calc(50% - 95px - 40px); - text-align: center; - } - - &.column-type { - width: 150px; - flex-grow: 0; - text-align: center; - } - - &.column-used { - width: 40px; - flex-grow: 0; - text-align: center; - - - display: flex; - flex-direction: row; - justify-content: center; - align-self: center; - } - } - - .playlist-list-header { - flex-grow: 0; - flex-shrink: 0; - display: flex; - flex-direction: row; - - .column { - border: 1px solid lightgray; - text-align: center; - } - } - - .playlist-list-entries-container { - flex-grow: 1; - display: flex; - flex-direction: column; - justify-content: start; - overflow-y: auto; - min-height: 250px; - - .entry { - display: flex; - flex-direction: row; - - .column { - margin-left: 2px; - } - - cursor: pointer; - - &.selected { - background-color: blue; - } - - &.highlighted { - font-weight: bold; - } - } - - &.scrollbar { - .column-title { - width: calc(50% - 95px - 40px + 30px) - } - - .column-creator { - width: calc(50% - 95px - 40px + 30px) - } - } - } - } - - .footer { - margin-top: 5px; - display: flex; - flex-direction: row; - justify-content: space-between; - - .info { - align-self: center; - } - - .buttons { - display: flex; - flex-direction: row; - justify-content: stretch; - - .highlight-own { - display: flex; - flex-direction: row; - justify-content: stretch; - - margin-right: 10px; - align-self: center; - } - } - } -} - -.playlist-edit { - display: flex; - flex-direction: column; - justify-content: stretch; - min-height: 0; - - .tab-content { - padding: 0; /* override tab-content setting */ - } - - .general-properties, .playback-properties { - padding: 5px; - width: 100%; - display: flex; - - flex-direction: column; - - .property { - display: flex; - flex-direction: row; - margin-bottom: 5px; - - .key { - width: 150px; - flex-grow: 0; - } - - .value { - flex-grow: 1; - flex-shrink: 1; - } - - .checkbox-container { - input { - margin-left: 0; - } - } - - &.property-description { - textarea { - resize: vertical; - max-height: 400px; - } - } - } - - flex-shrink: 0; - flex-grow: 0; - } - - .playback-properties { - .property .key { - width: 175px; - } - } - - .container-permissions { - padding: 5px; - display: flex; - flex-direction: row; - justify-content: space-around; - - .group_box { - min-width: 30%; - } - - .permissions-list { - display: flex; - flex-direction: column; - } - } - - .container-no-permissions { - background: lightgray; - padding: 50px; - text-align: center; - } - - x-content { - overflow-y: hidden; - display: flex; - flex-direction: column; - } - - .container-songs { - width: 100%; - display: flex; - flex-direction: column; - padding: 5px; - - .song-list { - min-height: 300px; - - margin-top: 5px; - - display: flex; - flex-grow: 1; - flex-direction: column; - justify-content: stretch; - - .column { - &.column-id { - width: 50px; - } - - &.column-url { - width: calc(100% - 140px) - } - - &.column-loaded { - width: 50px; - flex-grow: 0; - - display: flex; - justify-content: center; - flex-direction: row; - } - - &.column-buttons { - width: 40px; - flex-grow: 0; - - display: flex; - justify-content: center; - flex-direction: row; - - .button { - display: flex; - flex-direction: column; - justify-content: center; - - &:hover { - background: rgba(0, 0, 0, 0.2); - } - } - } - } - - .song-list-header { - flex-grow: 0; - flex-shrink: 0; - display: flex; - flex-direction: row; - justify-content: center; - - - .column { - border: 1px solid lightgray; - text-align: center; - } - } - - .song-list-entries-container { - flex-grow: 1; - display: flex; - flex-direction: column; - justify-content: start; - overflow-y: auto; - min-height: 250px; - - .entry { - display: flex; - flex-direction: row; - - .column { - margin-left: 2px; - } - - cursor: pointer; - - &.selected { - background-color: blue; - } - - &.playing { - background-color: lightgreen; - } - } - - &.scrollbar { - &.column-url { - width: calc(100% - 140px + 30px) - } - } - } - } - - .footer { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - - flex-grow: 0; - flex-shrink: 0; - margin-top: 5px; - } - } - - > .buttons { - margin-top: 5px; - align-self: flex-end; - - button { - width: 100px; - } - } -} - -.container-song-info { - display: flex; - flex-shrink: 1; - flex-direction: column; - - .properties { - display: flex; - flex-direction: column; - padding-bottom: 5px; - - .property { - display: flex; - flex-direction: row; - justify-content: stretch; - flex-shrink: 0; - - .key { - width: 150px; - flex-grow: 0; - } - - .value { - flex-grow: 1; - } - - &.property-metadata-raw { - flex-direction: column; - flex-shrink: 1; - margin-top: 5px; - - .line { - width: 100%; - display: flex; - flex-direction: row; - justify-content: stretch; - flex-shrink: 0; - } - - textarea { - margin-top: 5px; - - width: 100%; - max-height: 100%; - resize: vertical; - } - } - } - } -} - -.container-song-add { - display: flex; - flex-shrink: 1; - flex-direction: column; - - .properties { - display: flex; - flex-direction: column; - padding-bottom: 5px; - - .property { - margin-bottom: 5px; - display: flex; - flex-direction: row; - justify-content: stretch; - flex-shrink: 0; - - .key { - width: 150px; - flex-grow: 0; - } - - .value { - flex-grow: 1; - } - } - } -} diff --git a/shared/css/static/modal-poke.scss b/shared/css/static/modal-poke.scss index 7dc8c778..89df1be4 100644 --- a/shared/css/static/modal-poke.scss +++ b/shared/css/static/modal-poke.scss @@ -1,3 +1,8 @@ +html:root { + --modal-poke-date: cornflowerblue; + --modal-poke-text: #004d00; +} + .container-poke { display: flex!important;; flex-direction: column!important;; @@ -47,10 +52,10 @@ } .date { - color: cornflowerblue; + color: var(--modal-poke-date); } .text { - color: #004d00; + color: var(--modal-poke-text); } } } @@ -80,17 +85,4 @@ float: right; } } -} - -/* - - */ \ No newline at end of file +} \ No newline at end of file diff --git a/shared/css/static/modal-query.scss b/shared/css/static/modal-query.scss index b3345206..3e7759d6 100644 --- a/shared/css/static/modal-query.scss +++ b/shared/css/static/modal-query.scss @@ -54,6 +54,22 @@ } } +html:root { + --modal-query-title: #e0e0e0; + + --modal-query-list: #28292b; + --modal-query-list-border: #1f2122; + + --modal-query-empty: #4d4d4d; + --modal-query-error: #732626; + + --modal-query-entry-hover: #2c2d2f; + --modal-query-entry-selected: #1a1a1b; + + --modal-query-key: #557edc; + --modal-query-copy-hover: #28292b; +} + .modal-body.modal-query-manage { display: flex!important; flex-direction: row!important; @@ -100,7 +116,7 @@ a { font-weight: bold; - color: #e0e0e0; + color: var(--modal-query-title); flex-grow: 1; flex-shrink: 1; @@ -145,8 +161,8 @@ justify-content: stretch; border-radius: 0.2em; - border: 1px solid #1f2122; - background-color: #28292b; + border: 1px solid var(--modal-query-list-border); + background-color: var(--modal-query-list); .container-entries { flex-shrink: 1; @@ -175,12 +191,15 @@ text-align: center; font-size: 2em; - background-color: #28292b; - color: hsla(0, 0%, 30%, 1); + background-color: var(--modal-query-list); + } + + .container-empty { + color: var(--modal-query-empty); } .container-error { - color: #732626; + color: var(--modal-query-error); } .entry { @@ -201,11 +220,11 @@ cursor: pointer; &:hover { - background-color: #2c2d2f; + background-color: var(--modal-query-entry-hover); } &.selected { - background-color: #1a1a1b; + background-color: var(--modal-query-entry-selected); } } } @@ -216,7 +235,7 @@ padding: 0 .5em; - border-top: 1px solid #1f2122; + border-top: 1px solid var(--modal-query-list-border); display: flex; flex-direction: row; @@ -271,7 +290,7 @@ .title, .title a { text-transform: uppercase; - color: #557edc; + color: var(--modal-query-key); white-space: nowrap; overflow: hidden; @@ -307,7 +326,7 @@ border-radius: .2em; &:hover { - background: #28292b; + background: var(--modal-query-copy-hover); } margin-bottom: .2em; /* "text sub" */ diff --git a/shared/css/static/modal-serverinfo.scss b/shared/css/static/modal-serverinfo.scss index a24e73fe..577c6a67 100644 --- a/shared/css/static/modal-serverinfo.scss +++ b/shared/css/static/modal-serverinfo.scss @@ -1,5 +1,16 @@ @import "mixin"; +html:root { + --serverinfo-background: #2f2f35; + --serverinfo-hostbanner-background: #26222a; + + --serverinfo-group-border: #1f2122; + --serverinfo-group-background: #28292b; + + --serverinfo-key: #557edc; + --serverinfo-value: #d6d6d7; +} + .modal-body.modal-server-info { padding: 0!important; width: 55em; @@ -8,7 +19,7 @@ flex-direction: column!important; justify-content: flex-start!important; - background-color: #2f2f35; + background-color: var(--serverinfo-background); .container-tooltip { flex-shrink: 0; @@ -51,8 +62,7 @@ .container-hostbanner { border: none; border-radius: 0; - //background-color: #261f30; - background-color: hsla(265, 10%, 15%, 1); + background-color: var(--serverinfo-hostbanner-background); } &.hidden { @@ -77,9 +87,9 @@ padding: .5em; border-radius: .2em; - border: 1px solid #1f2122; + border: 1px solid var(--serverinfo-group-border); - background-color: #28292b; + background-color: var(--serverinfo-group-background); display: flex; flex-direction: row; @@ -135,7 +145,7 @@ flex-shrink: 0; flex-grow: 0; - color: #557edc; + color: var(--serverinfo-key); text-transform: uppercase; align-self: center; @@ -147,7 +157,7 @@ } .value { - color: #d6d6d7; + color: var(--serverinfo-value); align-self: center; white-space: nowrap; overflow: hidden; diff --git a/shared/css/static/modal-serverinfobandwidth.scss b/shared/css/static/modal-serverinfobandwidth.scss index d7bac716..a8b8505e 100644 --- a/shared/css/static/modal-serverinfobandwidth.scss +++ b/shared/css/static/modal-serverinfobandwidth.scss @@ -1,7 +1,13 @@ @import "mixin"; -$color_upload: #0a5eaa; -$color_download: #9f2712; +html:root { + --serverinfo-bandwidth-upload: #0a5eaa; + --serverinfo-bandwidth-download: #9f2712; + + --serverinfo-title: #e3e3e4; + --serverinfo-statistics-title: #244c78; +} + .modal-body.modal-server-info-bandwidth { padding: 0!important; width: 55em; @@ -90,7 +96,7 @@ $color_download: #9f2712; > a { font-size: 1.25em; - color: #e3e3e4; + color: var(--serverinfo-title); line-height: normal; text-transform: uppercase; @@ -107,11 +113,11 @@ $color_download: #9f2712; } .upload { - color: $color_upload; + color: var(--serverinfo-bandwidth-upload); } .download { - color: $color_download; + color: var(--serverinfo-bandwidth-download); } } @@ -150,7 +156,7 @@ $color_download: #9f2712; flex-grow: 0; flex-shrink: 0; - color: #244c78; + color: var(--serverinfo-statistics-title); font-size: 1.25em; text-transform: uppercase; @@ -186,11 +192,11 @@ $color_download: #9f2712; text-align: right; .upload { - color: $color_upload; + color: var(--serverinfo-bandwidth-upload); } .download { - color: $color_download; + color: var(--serverinfo-bandwidth-download); } } } diff --git a/shared/css/static/modal-volume.scss b/shared/css/static/modal-volume.scss index 73663f0d..e45a75f6 100644 --- a/shared/css/static/modal-volume.scss +++ b/shared/css/static/modal-volume.scss @@ -9,7 +9,7 @@ justify-content: stretch; .htmltag-client { - color: #999!important; + color: var(--text)!important; margin-left: .25em; } diff --git a/shared/css/static/modals.scss b/shared/css/static/modals.scss index 6d6366b1..bffef800 100644 --- a/shared/css/static/modals.scss +++ b/shared/css/static/modals.scss @@ -1,90 +1,3 @@ -.channel_perm_tbl .value { - width: 60px; -} - - -.group_box { - display: flex; - flex-direction: column; - justify-content: stretch; - - .header { - flex-grow: 0; - flex-shrink: 0; - float: left; - margin-bottom: 2px; - } - - .content { - flex-grow: 1; - flex-shrink: 1; - - background: rgba(0, 0, 0, .035); - border: lightgray solid 1px; - border-radius: 0 2px; - padding: 6px; - } -} - -/* Channel edit/create modal */ -.settings_audio { - display: grid; - grid-template-columns: 40% 60%; - grid-gap: 10px; - - .custom { - display: grid; - grid-template-columns: min-content auto; - grid-template-rows: repeat(auto-fill, min-content); - grid-column-gap: 5px; - - select { - height: fit-content; - } - - .quality { - display: inline-grid; - grid-template-columns: auto min-content; - grid-column-gap: 5px; - } - } -} - -.settings_advanced { - display: grid; - grid-template-columns: repeat(auto-fill, max-content); - grid-template-rows: repeat(auto-fill, max-content); - grid-gap: 5px; - - > div:first-of-type { - grid-column: auto / span 2; - } - - .max_limited { - width: 100%; - display: inline-flex; - input[type="number"] { - width: 75px; - } - } - - .group_box { - fieldset, fieldset > div { - width: 100%; - } - } -} - -.horizontal-stretch { - display: flex; - flex-grow: 1; - flex-direction: column; -} - -.container-ban-type { - margin: 5px; -} - .arrow { display: inline-block; border: solid black; @@ -115,91 +28,4 @@ transform: rotate(45deg); -webkit-transform: rotate(45deg); } -} - -.layout-group-server, .layout-group-channel, .layout-channel, .layout-client, .layout-client-channel { - width: 100%; - height: 100%; - display: flex; - flex-direction: row; - justify-content: stretch; - - & > div { - margin: 5px; - } - - .list-group-server, .list-group-channel, .list-group-server-clients, .list-channel { - border: grey solid 1px; - position: relative; - width: 175px; - flex-grow: 0; - min-width: 125px; - - .entries { - display: table; - position: absolute; - top: 0; bottom: 0; - left: 0; right: 0; - min-width: 100%; - } - } - - .list-group-server, .list-group-channel { - border: grey solid 1px; - user-select: none; - overflow: auto; - position: relative; - - .group { - display: block; - white-space: nowrap; - cursor: pointer; - - .icon, .icon_empty, .icon-container { - margin-right: 3px; - margin-left: 3px; - } - - .name.savedb { - color: blue; - } - .name.default { - color: black; - font-weight: bold; - } - - &.selected { - background-color: blue; - - .name.savedb { - color: white; - } - } - } - } -} - -.layout-channel, .layout-client-channel { - .list-channel { - display: flex; - flex-direction: column; - - overflow: auto; - - .channel { - cursor: pointer; - display: block; - width: 100%; - height: max-content; - white-space: nowrap; - - .icon, .icon_empty { - margin-right: 3px; - } - - &.selected { - background-color: blue; - } - } - } } \ No newline at end of file diff --git a/shared/css/static/music/info_plate.scss b/shared/css/static/music/info_plate.scss deleted file mode 100644 index 78986776..00000000 --- a/shared/css/static/music/info_plate.scss +++ /dev/null @@ -1,315 +0,0 @@ -$animtime: .5s; -$ease: cubic-bezier(.45, 0, .55, 1); - -.music-wrapper { - display: flex; - position: relative; - width: 400px; - height: 400px; - user-select: none; - - .left, .right { - position: absolute; - width: 50%; - height: 100%; - perspective-origin: 50% 50%; - perspective: 1200px; - - label { - margin-bottom: 0!important; /* bootstrap fix */ - } - - .flip-card, - .static-card { - background: white; - position: absolute; - width: 100%; - height: 100%; - overflow: hidden; - border: 7px solid #dedede; - - img { - width: calc(100% * 2); - //height: 100%; - } - } - - .static-card { - border-right: none; - } - - .flip-card { - border-left: none; - transform-origin: 0% 50%; - transition: transform $animtime $ease; - transform: rotateY(0); - - &:before { - position: absolute; - content: ''; - width: 100%; - height: 100%; - top: 0; - right: -20px; - box-shadow: 29px 0px 52px 6px rgba(186, 186, 186, 1); - } - - img { - position: absolute; - background-color: #fff; - right: 0; - } - } - } - - .left { - left: 0; - } - .right { - right: 0; - } - .right:hover { - .flip-card { - transform: rotateY(-60deg); - } - //z-index: 120; - } - - .controls { - position: absolute; - right: 0; - width: 50%; - height: 100%; - overflow: hidden; - display: flex; - flex-direction: column; - cursor: pointer; - - &:after { - position: absolute; - content: ''; - right: 0; - top: 0; - width: 100%; - height: 100%; - box-shadow: inset 20px 0px 37px -10px rgba(0, 0, 0, 0.75); - pointer-events: none; - transition: width $animtime $ease; - } - - input[type="radio"] { - position: absolute; - left: -1000px; - } - - label { - flex-grow: 1; - display: block; - width: 100%; - border-top: 1px #e6e6e6 solid; - border-bottom: 1px #9c9c9c solid; - box-sizing: border-box; - cursor: pointer; - background-color: #dcdcdc; - - span { - background: no-repeat 16px 42px; - width: 80px; - height: 125px; - display: block; - pointer-events: none; - } - } - - input:checked + label, - label:active { - background-color: #BCBCBC; - box-shadow: inset 0px 0px 10px 5px rgba(120, 120, 120, 0.2); - border: 1px solid #fff; - } - - //https://insidemartialartsmagazine.com.au/images/glyphicons/glyphicons/svg/individual_svg/ - .btn-forward span { - background-size: calc(42px * 2) calc(42px * 2); - margin-left: 10px; - background: url("%%base_path%%/img/music/forward.svg") no-repeat center; - background: url("/img/music/forward.svg") no-repeat center; - background: url("img/music/forward.svg") no-repeat center; - } - .btn-rewind span { - background-size: calc(42px * 2) calc(42px * 2); - margin-left: 10px; - background: url("%%base_path%%/img/music/rewind.svg") no-repeat center; - background: url("/img/music/rewind.svg") no-repeat center; - background: url("img/music/rewind.svg") no-repeat center; - } - .btn-settings span { - background-size: calc(42px * 2) calc(42px * 2); - margin-left: 10px; - background: url("%%base_path%%/img/music/playlist.svg") no-repeat center; - background: url("/img/music/playlist.svg") no-repeat center; - background: url("img/music/playlist.svg") no-repeat center; - } - } - - .controls-overlay { - position: absolute; - display: block; - top: calc(100% - 60px); - width: 100%; - height: 60px; - z-index: 100; - overflow-x: hidden; - transition: width $animtime $ease; - - .song { - margin-top: 5px; - margin-left: 20px; - height: 15px; - width: 360px; - - font-family: "DejaVu Serif", serif; - } - - .timer { - margin-left: 20px; - height: 15px; - z-index: 200; - width: 360px; - display: inline-flex; - justify-content: space-between; - vertical-align: center; - - .button-container{ - display: inline-block; - - > div { - display: inline-block; - } - } - - .button { - width: 10px; - height: 12px; - margin-left: 2px; - - svg { - - fill: none; - stroke: #4c4c4c;; - stroke-width: 0.5; - stroke-miterlimit: 10; - cursor: pointer; - - color: white; - mix-blend-mode: difference; - //box-shadow: 20px 20px 20px 20px rgb(186, 0, 12); - } - } - - .button.active { - svg { - animation: bounce 500ms alternate; - transform: scale(1.3); - transition: transform 150ms; - } - } - - .button:hover { - svg { - animation: bounce 500ms alternate; - transform: scale(1.1); - transition: transform 150ms; - } - } - - .button.active:hover { - svg { - animation: bounce 500ms alternate; - transform: scale(1.5); - transition: transform 150ms; - } - } - - .timeline * { - border: gray 0; - border-radius: 8px; - } - - //TODO box SHADOW - .timeline { - width: calc(100% - 100px); - height: 4px; - float: left; - background: #DBE3E3; - position: relative; - align-self: center; - border: gray 0; - border-radius: 8px; - - .buffered { - position: absolute; - width: 80%; - height: 100%; - background: #a0a0a0; - } - - .played { - position: absolute; - width: 60%; - height: 100%; - background: #1fe2e3; - } - - .slider { - position: absolute; - width: 4px; - height: 12px; - top: -4px; - background: #303030; - cursor: pointer; - } - } - - .time { - min-width: 38px; - margin-left: 5px; - position: relative; - align-self: center; - font-family: 'fantasy' - } - } - } - - .controls-overlay.flipped { - width: calc(50% + 7px); - } -} - -.music-wrapper.empty { - border: 7px solid #dedede; - display: flex; - flex-direction: column; - background: white; -} - -.music-wrapper.empty img { - margin: 5px; - -webkit-animation: rotation 5s infinite linear; -} -@-webkit-keyframes rotation { - from { - -webkit-transform: rotate(0deg); - } - to { - -webkit-transform: rotate(359deg); - } -} - -.music-wrapper.empty a { - text-align: center; - margin: 5px; - margin-top: 20px; - font-size: 20px; - font-family: Arial; -} \ No newline at end of file diff --git a/shared/css/static/server-log.scss b/shared/css/static/server-log.scss index 1f97ac77..173d179c 100644 --- a/shared/css/static/server-log.scss +++ b/shared/css/static/server-log.scss @@ -1,5 +1,11 @@ @import "mixin"; +html:root { + --server-log-text: #6e6e6e; + --server-log-error: #e62222; + --server-log-tree-entry: #d8d8d8; +} + .container-log { display: block; overflow-y: auto; @@ -19,7 +25,7 @@ flex-shrink: 0; flex-grow: 0; - color: #6e6e6e; + color: var(--server-log-text); overflow-x: hidden; overflow-y: hidden; @@ -47,15 +53,11 @@ .log-error { - color: rgba(230, 34, 34, 1); - - &:hover { - color: rgba(230, 34, 34, 1); - } + color: var(--server-log-error); } .htmltag-client, .htmltag-channel { - color: #d8d8d8; + color: var(--server-log-tree-entry); } } } \ No newline at end of file diff --git a/shared/html/templates.html b/shared/html/templates.html index b5bc3ec6..0c28911e 100644 --- a/shared/html/templates.html +++ b/shared/html/templates.html @@ -3447,81 +3447,6 @@ - - - - -