Made the opus replay working again

canary
WolverinDEV 2020-03-31 01:27:59 +02:00
parent c2fa1badcb
commit 956f778c01
26 changed files with 2434 additions and 841 deletions

View File

@ -8,7 +8,7 @@ set(CMAKE_C_COMPILER "emcc")
set(CMAKE_C_LINK_EXECUTABLE "emcc")
set(CMAKE_CXX_FLAGS "-O3 --llvm-lto 1 --memory-init-file 0 -s WASM=1 -s ASSERTIONS=1") # -s ALLOW_MEMORY_GROWTH=1 -O3
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_EXE_LINKER_FLAGS "-s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\", \"Pointer_stringify\"]' -s ENVIRONMENT='worker'") #
set(CMAKE_EXE_LINKER_FLAGS "-s EXPORTED_FUNCTIONS='[\"_malloc\", \"_free\"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]' -s ENVIRONMENT='worker' --pre-js ${CMAKE_SOURCE_DIR}/init.js") #
#add_definitions(-D_GLIBCXX_USE_CXX11_ABI=0)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/generated/")

2
asm/init.js Normal file
View File

@ -0,0 +1,2 @@
for(const callback of Array.isArray(self.__init_em_module) ? self.__init_em_module : [])
callback(Module);

View File

@ -23,6 +23,7 @@ extern "C" {
"Memory allocation has failed" //-7 (OPUS_ALLOC_FAIL)
};
EMSCRIPTEN_KEEPALIVE
inline const char* opus_error_message(int error) {
error = abs(error);
if(error > 0 && error <= 7) return opus_errors[error - 1];
@ -59,11 +60,13 @@ extern "C" {
INVOKE_OPUS(error, opus_encoder_ctl, handle->encoder, OPUS_SET_COMPLEXITY(1));
//INVOKE_OPUS(error, opus_encoder_ctl, handle->encoder, OPUS_SET_BITRATE(4740));
/* //This method is obsolete!
EM_ASM(
printMessageToServerTab('Encoder initialized!');
printMessageToServerTab(' Comprexity: 1');
printMessageToServerTab(' Bitrate: 4740');
);
*/
return true;
}
@ -98,9 +101,11 @@ extern "C" {
auto result = opus_encode_float(handle->encoder, (float*) buffer, length / handle->channelCount, buffer, maxLength);
if(result < 0) return result;
auto end = currentMillies();
/* //This message is obsolete
EM_ASM({
printMessageToServerTab("codec_opus_encode(...) tooks " + $0 + "ms to execute!");
}, end - begin);
printMessageToServerTab("codec_opus_encode(...) tooks " + $0 + "ms to execute!");
}, end - begin);
*/
return result;
}

192
file.ts
View File

@ -463,7 +463,7 @@ const WEB_APP_FILE_LIST = [
"path": "./",
"local-path": "./shared/html/"
},
{ /* javascript loader for releases */
{ /* javascript files as manifest.json */
"type": "js",
"search-pattern": /.*$/,
"build-target": "dev|rel",
@ -471,7 +471,14 @@ const WEB_APP_FILE_LIST = [
"path": "js/",
"local-path": "./dist/"
},
{ /* loader javascript file */
"type": "js",
"search-pattern": /.*$/,
"build-target": "dev|rel",
"path": "js/",
"local-path": "./loader/dist/"
},
{ /* shared javascript files (WebRTC adapter) */
"type": "js",
"search-pattern": /.*\.js$/,
@ -661,41 +668,49 @@ namespace generator {
return result.digest("hex");
}
const rreaddir = async p => {
const result = [];
try {
const files = await fs.readdir(p);
for(const file of files) {
const file_path = path.join(p, file);
const info = await fs.stat(file_path);
if(info.isDirectory()) {
result.push(...await rreaddir(file_path));
} else {
result.push(file_path);
}
}
} catch(error) {
if(error.code === "ENOENT")
return [];
throw error;
}
return result;
};
function file_matches_options(file: ProjectResource, options: SearchOptions) {
if(typeof file["web-only"] === "boolean" && file["web-only"] && options.target !== "web")
return false;
if(typeof file["client-only"] === "boolean" && file["client-only"] && options.target !== "client")
return false;
if(typeof file["serve-only"] === "boolean" && file["serve-only"] && !options.serving)
return false;
if(!file["build-target"].split("|").find(e => e === options.mode))
return false;
return !(Array.isArray(file["req-parm"]) && file["req-parm"].find(e => !options.parameter.find(p => p.toLowerCase() === e.toLowerCase())));
}
export async function search_files(files: ProjectResource[], options: SearchOptions) : Promise<Entry[]> {
const result: Entry[] = [];
const rreaddir = async p => {
const result = [];
try {
const files = await fs.readdir(p);
for(const file of files) {
const file_path = path.join(p, file);
const info = await fs.stat(file_path);
if(info.isDirectory()) {
result.push(...await rreaddir(file_path));
} else {
result.push(file_path);
}
}
} catch(error) {
if(error.code === "ENOENT")
return [];
throw error;
}
return result;
};
for(const file of files) {
if(typeof file["web-only"] === "boolean" && file["web-only"] && options.target !== "web")
continue;
if(typeof file["client-only"] === "boolean" && file["client-only"] && options.target !== "client")
continue;
if(typeof file["serve-only"] === "boolean" && file["serve-only"] && !options.serving)
continue;
if(!file["build-target"].split("|").find(e => e === options.mode))
continue;
if(Array.isArray(file["req-parm"]) && file["req-parm"].find(e => !options.parameter.find(p => p.toLowerCase() === e.toLowerCase())))
if(!file_matches_options(file, options))
continue;
const normal_local = path.normalize(path.join(options.source_path, file["local-path"]));
@ -723,23 +738,52 @@ namespace generator {
return result;
}
export async function search_http_file(files: ProjectResource[], target_file: string, options: SearchOptions) : Promise<string> {
for(const file of files) {
if(!file_matches_options(file, options))
continue;
if(file.path !== "./" && !target_file.startsWith("/" + file.path.replace(/\\/g, "/")))
continue;
const normal_local = path.normalize(path.join(options.source_path, file["local-path"]));
const files: string[] = await rreaddir(normal_local);
for(const f of files) {
const local_name = f.substr(normal_local.length);
if(!local_name.match(file["search-pattern"]) && !local_name.replace("\\\\", "/").match(file["search-pattern"]))
continue;
if(typeof(file["search-exclude"]) !== "undefined" && f.match(file["search-exclude"]))
continue;
if("/" + path.join(file.path, local_name).replace(/\\/g, "/") === target_file)
return f;
}
}
return undefined;
}
}
namespace server {
import SearchOptions = generator.SearchOptions;
export type Options = {
port: number;
php: string;
search_options: SearchOptions;
}
const exec: (command: string) => Promise<{ stdout: string, stderr: string }> = util.promisify(cp.exec);
let files: (generator.Entry & { http_path: string; })[] = [];
let files: ProjectResource[] = [];
let server: http.Server;
let php: string;
export async function launch(_files: generator.Entry[], options: Options) {
//Don't use this check anymore, because we're searching within the PATH variable
//if(!await fs.exists(options.php) || !(await fs.stat(options.php)).isFile())
// throw "invalid php interpreter (not found)";
let options: Options;
export async function launch(_files: ProjectResource[], options_: Options) {
options = options_;
files = _files;
try {
const info = await exec(options.php + " --version");
@ -763,17 +807,6 @@ namespace server {
resolve();
});
});
files = _files.map(e =>{
return {
type: e.type,
name: e.name,
hash: e.hash,
local_path: e.local_path,
target_path: e.target_path,
http_path: "/" + e.target_path.replace(/\\/g, "/")
}
});
}
export async function shutdown() {
@ -817,8 +850,8 @@ namespace server {
});
}
function serve_file(pathname: string, query: any, response: http.ServerResponse) {
const file = files.find(e => e.http_path === pathname);
async function serve_file(pathname: string, query: any, response: http.ServerResponse) {
const file = await generator.search_http_file(files, pathname, options.search_options);
if(!file) {
console.log("[SERVER] Client requested unknown file %s", pathname);
response.writeHead(404);
@ -827,13 +860,13 @@ namespace server {
return;
}
let type = mt.lookup(path.extname(file.local_path)) || "text/html";
console.log("[SERVER] Serving file %s (%s) (%s)", file.target_path, type, file.local_path);
if(path.extname(file.local_path) === ".php") {
serve_php(file.local_path, query, response);
let type = mt.lookup(path.extname(file)) || "text/html";
console.log("[SERVER] Serving file %s", file, type);
if(path.extname(file) === ".php") {
serve_php(file, query, response);
return;
}
const fis = fs.createReadStream(file.local_path);
const fis = fs.createReadStream(file);
response.writeHead(200, "success", {
"Content-Type": type + "; charset=utf-8"
@ -846,23 +879,22 @@ namespace server {
fis.on("data", data => response.write(data));
}
function handle_api_request(request: http.IncomingMessage, response: http.ServerResponse, url: url_utils.UrlWithParsedQuery) {
async function handle_api_request(request: http.IncomingMessage, response: http.ServerResponse, url: url_utils.UrlWithParsedQuery) {
if(url.query["type"] === "files") {
response.writeHead(200, { "info-version": 1 });
response.write("type\thash\tpath\tname\n");
for(const file of files)
if(file.http_path.endsWith(".php"))
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.http_path) + "\t" + path.basename(file.http_path, ".php") + ".html" + "\n");
for(const file of await generator.search_files(files, options.search_options))
if(file.name.endsWith(".php"))
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.target_path) + "\t" + path.basename(file.name, ".php") + ".html" + "\n");
else
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.http_path) + "\t" + path.basename(file.http_path) + "\n");
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.target_path) + "\t" + file.name + "\n");
response.end();
return;
} else if(url.query["type"] === "file") {
let p = path.join(url.query["path"] as string, url.query["name"] as string).replace(/\\/g, "/");
if(p.endsWith(".html")) {
const np = p.substr(0, p.length - 5) + ".php";
if(files.find(e => e.http_path == np) && !files.find(e => e.http_path == p))
p = np;
const np = await generator.search_http_file(files, p, options.search_options);
if(np) p = np;
}
serve_file(p, url.query, response);
return;
@ -1041,17 +1073,16 @@ function php_exe() : string {
}
async function main_serve(target: "client" | "web", mode: "rel" | "dev", port: number) {
const files = await generator.search_files(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
source_path: __dirname,
parameter: [],
target: target,
mode: mode,
serving: true
});
await server.launch(files, {
await server.launch(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
port: port,
php: php_exe(),
search_options: {
source_path: __dirname,
parameter: [],
target: target,
mode: mode,
serving: true
}
});
console.log("Server started on %d", port);
@ -1060,14 +1091,6 @@ async function main_serve(target: "client" | "web", mode: "rel" | "dev", port: n
}
async function main_develop(node: boolean, target: "client" | "web", port: number, flags: string[]) {
const files = await generator.search_files(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
source_path: __dirname,
parameter: [],
target: target,
mode: "dev",
serving: true
});
const tscwatcher = new watcher.TSCWatcher();
try {
if(flags.indexOf("--no-tsc") == -1)
@ -1079,9 +1102,16 @@ async function main_develop(node: boolean, target: "client" | "web", port: numbe
await sasswatcher.start();
try {
await server.launch(files, {
await server.launch(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
port: port,
php: php_exe(),
search_options: {
source_path: __dirname,
parameter: [],
target: target,
mode: "dev",
serving: true
}
});
} catch(error) {
console.error("Failed to start server: %o", error instanceof Error ? error.message : error);

View File

@ -1,4 +1,4 @@
import * as loader from "./loader";
import * as loader from "./loader/loader";
declare global {
interface Window {
@ -8,6 +8,11 @@ declare global {
const node_require: typeof require = window.require;
function cache_tag() {
const ui = ui_version();
return "?_ts=" + (!!ui && ui !== "unknown" ? ui : Date.now());
}
let _ui_version;
export function ui_version() {
if(typeof(_ui_version) !== "string") {
@ -22,6 +27,15 @@ export function ui_version() {
return _ui_version;
}
interface Manifest {
version: number;
chunks: {[key: string]: {
hash: string,
file: string
}[]};
}
/* all javascript loaders */
const loader_javascript = {
detect_type: async () => {
@ -72,7 +86,7 @@ const loader_javascript = {
},
load_scripts: async () => {
if(!window.require) {
await loader.load_script(["vendor/jquery/jquery.min.js"]);
await loader.scripts.load(["vendor/jquery/jquery.min.js"], { cache_tag: cache_tag() });
} else {
/*
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
@ -84,45 +98,41 @@ const loader_javascript = {
});
*/
}
await loader.load_script(["vendor/DOMPurify/purify.min.js"]);
await loader.scripts.load(["vendor/DOMPurify/purify.min.js"], { cache_tag: cache_tag() });
await loader.load_script("vendor/jsrender/jsrender.min.js");
await loader.load_scripts([
await loader.scripts.load("vendor/jsrender/jsrender.min.js", { cache_tag: cache_tag() });
await loader.scripts.load_multiple([
["vendor/xbbcode/src/parser.js"],
["vendor/moment/moment.js"],
["vendor/twemoji/twemoji.min.js", ""], /* empty string means not required */
["vendor/highlight/highlight.pack.js", ""], /* empty string means not required */
["vendor/remarkable/remarkable.min.js", ""], /* empty string means not required */
["adapter/adapter-latest.js", "https://webrtc.github.io/adapter/adapter-latest.js"]
]);
await loader.load_scripts([
["vendor/emoji-picker/src/jquery.lsxemojipicker.js"]
]);
], {
cache_tag: cache_tag(),
max_parallel_requests: -1
});
if(!loader.version().debug_mode) {
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "scripts release",
priority: 20,
function: loader_javascript.load_release
});
} else {
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "scripts debug",
priority: 20,
function: loader_javascript.load_scripts_debug
});
await loader.scripts.load("vendor/emoji-picker/src/jquery.lsxemojipicker.js", { cache_tag: cache_tag() });
let manifest: Manifest;
try {
const response = await fetch("js/manifest.json");
if(!response.ok) throw response.status + " " + response.statusText;
manifest = await response.json();
} catch(error) {
console.error("Failed to load javascript manifest: %o", error);
loader.critical_error("Failed to load manifest.json", error);
throw "failed to load manifest.json";
}
},
load_scripts_debug: async () => {
await loader.load_scripts(["js/shared-app.js"])
},
load_release: async () => {
console.log("Load for release!");
if(manifest.version !== 1)
throw "invalid manifest version";
await loader.load_scripts([
//Load general API's
["js/client.min.js", "js/client.js"]
]);
await loader.scripts.load_multiple(manifest.chunks["shared-app"].map(e => "js/" + e.file), {
cache_tag: undefined,
max_parallel_requests: -1
});
}
};
@ -149,15 +159,20 @@ const loader_webassembly = {
const loader_style = {
load_style: async () => {
await loader.load_styles([
const options = {
cache_tag: cache_tag(),
max_parallel_requests: -1
};
await loader.style.load_multiple([
"vendor/xbbcode/src/xbbcode.css"
]);
await loader.load_styles([
], options);
await loader.style.load_multiple([
"vendor/emoji-picker/src/jquery.lsxemojipicker.css"
]);
await loader.load_styles([
], options);
await loader.style.load_multiple([
["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */
]);
], options);
if(loader.version().debug_mode) {
await loader_style.load_style_debug();
@ -167,7 +182,7 @@ const loader_style = {
},
load_style_debug: async () => {
await loader.load_styles([
await loader.style.load_multiple([
"css/static/main.css",
"css/static/main-layout.css",
"css/static/helptag.css",
@ -218,14 +233,20 @@ const loader_style = {
"css/static/htmltags.css",
"css/static/hostbanner.css",
"css/static/menu-bar.css"
]);
], {
cache_tag: cache_tag(),
max_parallel_requests: -1
});
},
load_style_release: async () => {
await loader.load_styles([
await loader.style.load_multiple([
"css/static/base.css",
"css/static/main.css",
]);
], {
cache_tag: cache_tag(),
max_parallel_requests: -1
});
}
};
@ -313,11 +334,14 @@ loader.register_task(loader.Stage.STYLE, {
loader.register_task(loader.Stage.TEMPLATES, {
name: "templates",
function: async () => {
await loader.load_templates([
await loader.templates.load_multiple([
"templates.html",
"templates/modal/musicmanage.html",
"templates/modal/newcomer.html",
]);
], {
cache_tag: cache_tag(),
max_parallel_requests: -1
});
},
priority: 10
});

View File

@ -1,4 +1,4 @@
import * as loader from "./loader";
import * as loader from "./loader/loader";
let is_debug = false;

View File

@ -1,5 +1,5 @@
import * as loader from "./app";
import * as loader_base from "./loader";
import * as loader_base from "./loader/loader";
export = loader_base;
loader.run();

View File

@ -1,5 +1,8 @@
import {AppVersion} from "../exports/loader";
import {type} from "os";
import {AppVersion} from "tc-loader";
import {LoadSyntaxError, script_name} from "./utils";
import * as script_loader from "./script_loader";
import * as style_loader from "./style_loader";
import * as template_loader from "./template_loader";
declare global {
interface Window {
@ -205,10 +208,6 @@ export async function execute() {
}
}
/* cleanup */
{
_script_promises = {};
}
if(config.verbose) console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin);
}
export function execute_managed() {
@ -233,347 +232,32 @@ export function execute_managed() {
});
}
export type DependSource = {
url: string;
depends: string[];
}
export type SourcePath = string | DependSource | string[];
function script_name(path: SourcePath, html: boolean) {
if(Array.isArray(path)) {
let buffer = "";
let _or = " or ";
for(let entry of path)
buffer += _or + script_name(entry, html);
return buffer.slice(_or.length);
} else if(typeof(path) === "string")
return html ? "<code>" + path + "</code>" : path;
else
return html ? "<code>" + path.url + "</code>" : path.url;
}
class SyntaxError {
source: any;
constructor(source: any) {
this.source = source;
}
}
let _script_promises: {[key: string]: Promise<void>} = {};
export async function load_script(path: SourcePath) : Promise<void> {
if(Array.isArray(path)) { //We have some fallback
return load_script(path[0]).catch(error => {
console.log(typeof error + " - " + (error instanceof SyntaxError));
if(error instanceof SyntaxError)
return Promise.reject(error);
if(path.length > 1)
return load_script(path.slice(1));
return Promise.reject(error);
});
} else {
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
if(source.url.length == 0) return Promise.resolve();
return _script_promises[source.url] = (async () => {
/* await depends */
for(const depend of source.depends) {
if(!_script_promises[depend])
throw "Missing dependency " + depend;
await _script_promises[depend];
}
const tag: HTMLScriptElement = document.createElement("script");
await new Promise((resolve, reject) => {
let error = false;
const error_handler = (event: ErrorEvent) => {
if(event.filename == tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
window.removeEventListener('error', error_handler as any);
reject(new SyntaxError(event.error));
event.preventDefault();
error = true;
}
};
window.addEventListener('error', error_handler as any);
const cleanup = () => {
tag.onerror = undefined;
tag.onload = undefined;
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
};
const timeout_handle = setTimeout(() => {
cleanup();
reject("timeout");
}, 5000);
tag.type = "application/javascript";
tag.async = true;
tag.defer = true;
tag.onerror = error => {
cleanup();
tag.remove();
reject(error);
};
tag.onload = () => {
cleanup();
if(config.verbose) console.debug("Script %o loaded", path);
setTimeout(resolve, 100);
};
document.getElementById("scripts").appendChild(tag);
tag.src = source.url + (cache_tag || "");
});
})();
}
}
export async function load_scripts(paths: SourcePath[]) : Promise<void> {
const promises: Promise<void>[] = [];
const errors: {
script: SourcePath,
error: any
}[] = [];
for(const script of paths)
promises.push(load_script(script).catch(error => {
errors.push({
script: script,
error: error
});
return Promise.resolve();
}));
await Promise.all([...promises]);
if(errors.length > 0) {
if(config.error) {
console.error("Failed to load the following scripts:");
for(const script of errors) {
const sname = script_name(script.script, false);
if(script.error instanceof SyntaxError) {
const source = script.error.source as Error;
if(source.name === "TypeError") {
let prefix = "";
while(prefix.length < sname.length + 7) prefix += " ";
console.log(" - %s: %s:\n%s", sname, source.message, source.stack.split("\n").map(e => prefix + e.trim()).slice(1).join("\n"));
} else {
console.log(" - %s: %o", sname, source);
}
} else {
console.log(" - %s: %o", sname, script.error);
}
}
}
critical_error("Failed to load script " + script_name(errors[0].script, true) + " <br>" + "View the browser console for more information!");
throw "failed to load script " + script_name(errors[0].script, false);
}
}
export async function load_style(path: SourcePath) : Promise<void> {
if(Array.isArray(path)) { //We have some fallback
return load_style(path[0]).catch(error => {
if(error instanceof SyntaxError)
return Promise.reject(error.source);
if(path.length > 1)
return load_script(path.slice(1));
return Promise.reject(error);
});
} else {
if(!path) {
return Promise.resolve();
}
return new Promise<void>((resolve, reject) => {
const tag: HTMLLinkElement = document.createElement("link");
let error = false;
const error_handler = (event: ErrorEvent) => {
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
if(event.filename == tag.href) { //FIXME!
window.removeEventListener('error', error_handler as any);
reject(new SyntaxError(event.error));
event.preventDefault();
error = true;
}
};
window.addEventListener('error', error_handler as any);
tag.type = "text/css";
tag.rel = "stylesheet";
const cleanup = () => {
tag.onerror = undefined;
tag.onload = undefined;
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
};
const timeout_handle = setTimeout(() => {
cleanup();
reject("timeout");
}, 5000);
tag.onerror = error => {
cleanup();
tag.remove();
if(config.error)
console.error("File load error for file %s: %o", path, error);
reject("failed to load file " + path);
};
tag.onload = () => {
cleanup();
{
const css: CSSStyleSheet = tag.sheet as CSSStyleSheet;
const rules = css.cssRules;
const rules_remove: number[] = [];
const rules_add: string[] = [];
for(let index = 0; index < rules.length; index++) {
const rule = rules.item(index);
let rule_text = rule.cssText;
if(rule.cssText.indexOf("%%base_path%%") != -1) {
rules_remove.push(index);
rules_add.push(rule_text.replace("%%base_path%%", document.location.origin + document.location.pathname));
}
}
for(const index of rules_remove.sort((a, b) => b > a ? 1 : 0)) {
if(css.removeRule)
css.removeRule(index);
else
css.deleteRule(index);
}
for(const rule of rules_add)
css.insertRule(rule, rules_remove[0]);
}
if(config.verbose) console.debug("Style sheet %o loaded", path);
setTimeout(resolve, 100);
};
document.getElementById("style").appendChild(tag);
tag.href = path + (cache_tag || "");
});
}
}
export async function load_styles(paths: SourcePath[]) : Promise<void> {
const promises: Promise<void>[] = [];
const errors: {
sheet: SourcePath,
error: any
}[] = [];
for(const sheet of paths)
promises.push(load_style(sheet).catch(error => {
errors.push({
sheet: sheet,
error: error
});
return Promise.resolve();
}));
await Promise.all([...promises]);
if(errors.length > 0) {
if(config.error) {
console.error("Failed to load the following style sheet:");
for(const sheet of errors)
console.log(" - %o: %o", sheet.sheet, sheet.error);
}
critical_error("Failed to load style sheet " + script_name(errors[0].sheet, true) + " <br>" + "View the browser console for more information!");
throw "failed to load style sheet " + script_name(errors[0].sheet, false);
}
}
export async function load_template(path: SourcePath) : Promise<void> {
try {
const response = await $.ajax(path + (cache_tag || ""));
let node = document.createElement("html");
node.innerHTML = response;
let tags: HTMLCollection;
if(node.getElementsByTagName("body").length > 0)
tags = node.getElementsByTagName("body")[0].children;
else
tags = node.children;
let root = document.getElementById("templates");
if(!root) {
critical_error("Failed to find template tag!");
throw "Failed to find template tag";
}
while(tags.length > 0){
let tag = tags.item(0);
root.appendChild(tag);
}
} catch(error) {
let msg;
if('responseText' in error)
msg = error.responseText;
else if(error instanceof Error)
msg = error.message;
critical_error("failed to load template " + script_name(path, true), msg);
throw "template error";
}
}
export async function load_templates(paths: SourcePath[]) : Promise<void> {
const promises: Promise<void>[] = [];
const errors: {
template: SourcePath,
error: any
}[] = [];
for(const template of paths)
promises.push(load_template(template).catch(error => {
errors.push({
template: template,
error: error
});
return Promise.resolve();
}));
await Promise.all([...promises]);
if(errors.length > 0) {
if (config.error) {
console.error("Failed to load the following templates:");
for (const sheet of errors)
console.log(" - %s: %o", script_name(sheet.template, false), sheet.error);
}
critical_error("Failed to load template " + script_name(errors[0].template, true) + " <br>" + "View the browser console for more information!");
throw "failed to load template " + script_name(errors[0].template, false);
let _fadeout_warned;
export function hide_overlay() {
if(typeof($) === "undefined") {
if(!_fadeout_warned)
console.warn("Could not fadeout loader screen. Missing jquery functions.");
_fadeout_warned = true;
return;
}
const animation_duration = 750;
$(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, animation_duration);
$(".loader .half").animate({width: 0}, animation_duration, () => {
$(".loader").detach();
});
}
/* versions management */
let version_: AppVersion;
export function version() : AppVersion { return version_; }
export function set_version(version: AppVersion) { version_ = version; }
export type ErrorHandler = (message: string, detail: string) => void;
/* critical error handler */
export type ErrorHandler = (message: string, detail: string) => void;
let _callback_critical_error: ErrorHandler;
let _callback_critical_called: boolean = false;
export function critical_error(message: string, detail?: string) {
if(_callback_critical_called) {
console.warn("[CRITICAL] %s", message);
@ -603,29 +287,24 @@ export function critical_error(message: string, detail?: string) {
tag.classList.add("shown");
}
export function critical_error_handler(handler?: ErrorHandler, override?: boolean) : ErrorHandler {
if((typeof(handler) === "object" && handler !== _callback_critical_error) || override)
_callback_critical_error = handler;
return _callback_critical_error;
}
let _fadeout_warned;
export function hide_overlay() {
if(typeof($) === "undefined") {
if(!_fadeout_warned)
console.warn("Could not fadeout loader screen. Missing jquery functions.");
_fadeout_warned = true;
return;
}
const animation_duration = 750;
$(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, animation_duration);
$(".loader .half").animate({width: 0}, animation_duration, () => {
$(".loader").detach();
});
/* loaders */
export type DependSource = {
url: string;
depends: string[];
}
export type SourcePath = string | DependSource | string[];
export const scripts = script_loader;
export const style = style_loader;
export const templates = template_loader;
/* Hello World message */
{
const hello_world = () => {

View File

@ -0,0 +1,121 @@
import {config, critical_error, SourcePath} from "./loader";
import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
let _script_promises: {[key: string]: Promise<void>} = {};
function load_script_url(url: string) : Promise<void> {
if(typeof _script_promises[url] === "object")
return _script_promises[url];
return (_script_promises[url] = new Promise((resolve, reject) => {
const script_tag: HTMLScriptElement = document.createElement("script");
let error = false;
const error_handler = (event: ErrorEvent) => {
if(event.filename == script_tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
window.removeEventListener('error', error_handler as any);
reject(new LoadSyntaxError(event.error));
event.preventDefault();
error = true;
}
};
window.addEventListener('error', error_handler as any);
const cleanup = () => {
script_tag.onerror = undefined;
script_tag.onload = undefined;
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
};
const timeout_handle = setTimeout(() => {
cleanup();
reject("timeout");
}, 5000);
script_tag.type = "application/javascript";
script_tag.async = true;
script_tag.defer = true;
script_tag.onerror = error => {
cleanup();
script_tag.remove();
reject(error);
};
script_tag.onload = () => {
cleanup();
if(config.verbose) console.debug("Script %o loaded", url);
setTimeout(resolve, 100);
};
document.getElementById("scripts").appendChild(script_tag);
script_tag.src = url;
})).then(result => {
/* cleanup memory */
_script_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
return _script_promises[url];
}).catch(error => {
/* cleanup memory */
_script_promises[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
return _script_promises[url];
});
}
export interface Options {
cache_tag?: string;
}
export async function load(path: SourcePath, options: Options) : Promise<void> {
if(Array.isArray(path)) { //We have fallback scripts
return load(path[0], options).catch(error => {
if(error instanceof LoadSyntaxError)
return Promise.reject(error);
if(path.length > 1)
return load(path.slice(1), options);
return Promise.reject(error);
});
} else {
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
if(source.url.length == 0) return Promise.resolve();
/* await depends */
for(const depend of source.depends) {
if(!_script_promises[depend])
throw "Missing dependency " + depend;
await _script_promises[depend];
}
await load_script_url(source.url + (options.cache_tag || ""));
}
}
type MultipleOptions = Options | ParallelOptions;
export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise<void> {
const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options);
if(result.failed.length > 0) {
if(config.error) {
console.error("Failed to load the following scripts:");
for(const script of result.failed) {
const sname = script_name(script.request, false);
if(script.error instanceof LoadSyntaxError) {
const source = script.error.source as Error;
if(source.name === "TypeError") {
let prefix = "";
while(prefix.length < sname.length + 7) prefix += " ";
console.log(" - %s: %s:\n%s", sname, source.message, source.stack.split("\n").map(e => prefix + e.trim()).slice(1).join("\n"));
} else {
console.log(" - %s: %o", sname, source);
}
} else {
console.log(" - %s: %o", sname, script.error);
}
}
}
critical_error("Failed to load script " + script_name(result.failed[0].request, true) + " <br>" + "View the browser console for more information!");
throw "failed to load script " + script_name(result.failed[0].request, false);
}
}

View File

@ -0,0 +1,142 @@
import {config, critical_error, SourcePath} from "./loader";
import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
let _style_promises: {[key: string]: Promise<void>} = {};
function load_style_url(url: string) : Promise<void> {
if(typeof _style_promises[url] === "object")
return _style_promises[url];
return (_style_promises[url] = new Promise((resolve, reject) => {
const tag: HTMLLinkElement = document.createElement("link");
let error = false;
const error_handler = (event: ErrorEvent) => {
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
if(event.filename == tag.href) { //FIXME!
window.removeEventListener('error', error_handler as any);
reject(new SyntaxError(event.error));
event.preventDefault();
error = true;
}
};
window.addEventListener('error', error_handler as any);
tag.type = "text/css";
tag.rel = "stylesheet";
const cleanup = () => {
tag.onerror = undefined;
tag.onload = undefined;
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
};
const timeout_handle = setTimeout(() => {
cleanup();
reject("timeout");
}, 5000);
tag.onerror = error => {
cleanup();
tag.remove();
if(config.error)
console.error("File load error for file %s: %o", url, error);
reject("failed to load file " + url);
};
tag.onload = () => {
cleanup();
{
const css: CSSStyleSheet = tag.sheet as CSSStyleSheet;
const rules = css.cssRules;
const rules_remove: number[] = [];
const rules_add: string[] = [];
for(let index = 0; index < rules.length; index++) {
const rule = rules.item(index);
let rule_text = rule.cssText;
if(rule.cssText.indexOf("%%base_path%%") != -1) {
rules_remove.push(index);
rules_add.push(rule_text.replace("%%base_path%%", document.location.origin + document.location.pathname));
}
}
for(const index of rules_remove.sort((a, b) => b > a ? 1 : 0)) {
if(css.removeRule)
css.removeRule(index);
else
css.deleteRule(index);
}
for(const rule of rules_add)
css.insertRule(rule, rules_remove[0]);
}
if(config.verbose) console.debug("Style sheet %o loaded", url);
setTimeout(resolve, 100);
};
document.getElementById("style").appendChild(tag);
tag.href = url;
})).then(result => {
/* cleanup memory */
_style_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
return _style_promises[url];
}).catch(error => {
/* cleanup memory */
_style_promises[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
return _style_promises[url];
});
}
export interface Options {
cache_tag?: string;
}
export async function load(path: SourcePath, options: Options) : Promise<void> {
if(Array.isArray(path)) { //We have fallback scripts
return load(path[0], options).catch(error => {
if(error instanceof LoadSyntaxError)
return Promise.reject(error);
if(path.length > 1)
return load(path.slice(1), options);
return Promise.reject(error);
});
} else {
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
if(source.url.length == 0) return Promise.resolve();
/* await depends */
for(const depend of source.depends) {
if(!_style_promises[depend])
throw "Missing dependency " + depend;
await _style_promises[depend];
}
await load_style_url(source.url + (options.cache_tag || ""));
}
}
export type MultipleOptions = Options | ParallelOptions;
export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise<void> {
const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options);
if(result.failed.length > 0) {
if(config.error) {
console.error("Failed to load the following style sheets:");
for(const style of result.failed) {
const sname = script_name(style.request, false);
if(style.error instanceof LoadSyntaxError) {
console.log(" - %s: %o", sname, style.error.source);
} else {
console.log(" - %s: %o", sname, style.error);
}
}
}
critical_error("Failed to load style " + script_name(result.failed[0].request, true) + " <br>" + "View the browser console for more information!");
throw "failed to load style " + script_name(result.failed[0].request, false);
}
}

View File

@ -0,0 +1,90 @@
import {config, critical_error, SourcePath} from "./loader";
import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
let _template_promises: {[key: string]: Promise<void>} = {};
function load_template_url(url: string) : Promise<void> {
if(typeof _template_promises[url] === "object")
return _template_promises[url];
return (_template_promises[url] = (async () => {
const response = await $.ajax(url);
let node = document.createElement("html");
node.innerHTML = response;
let tags: HTMLCollection;
if(node.getElementsByTagName("body").length > 0)
tags = node.getElementsByTagName("body")[0].children;
else
tags = node.children;
let root = document.getElementById("templates");
if(!root) {
critical_error("Failed to find template tag!");
throw "Failed to find template tag";
}
while(tags.length > 0){
let tag = tags.item(0);
root.appendChild(tag);
}
})()).then(result => {
/* cleanup memory */
_template_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
return _template_promises[url];
}).catch(error => {
/* cleanup memory */
_template_promises[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
return _template_promises[url];
});
}
export interface Options {
cache_tag?: string;
}
export async function load(path: SourcePath, options: Options) : Promise<void> {
if(Array.isArray(path)) { //We have fallback scripts
return load(path[0], options).catch(error => {
if(error instanceof LoadSyntaxError)
return Promise.reject(error);
if(path.length > 1)
return load(path.slice(1), options);
return Promise.reject(error);
});
} else {
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
if(source.url.length == 0) return Promise.resolve();
/* await depends */
for(const depend of source.depends) {
if(!_template_promises[depend])
throw "Missing dependency " + depend;
await _template_promises[depend];
}
await load_template_url(source.url + (options.cache_tag || ""));
}
}
export type MultipleOptions = Options | ParallelOptions;
export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise<void> {
const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options);
if(result.failed.length > 0) {
if(config.error) {
console.error("Failed to load the following template files:");
for(const style of result.failed) {
const sname = script_name(style.request, false);
if(style.error instanceof LoadSyntaxError) {
console.log(" - %s: %o", sname, style.error.source);
} else {
console.log(" - %s: %o", sname, style.error);
}
}
}
critical_error("Failed to load template file " + script_name(result.failed[0].request, true) + " <br>" + "View the browser console for more information!");
throw "failed to load template file " + script_name(result.failed[0].request, false);
}
}

View File

@ -0,0 +1,65 @@
import {SourcePath} from "./loader";
import {Options} from "./script_loader";
export class LoadSyntaxError {
readonly source: any;
constructor(source: any) {
this.source = source;
}
}
export function script_name(path: SourcePath, html: boolean) {
if(Array.isArray(path)) {
let buffer = "";
let _or = " or ";
for(let entry of path)
buffer += _or + script_name(entry, html);
return buffer.slice(_or.length);
} else if(typeof(path) === "string")
return html ? "<code>" + path + "</code>" : path;
else
return html ? "<code>" + path.url + "</code>" : path.url;
}
export interface ParallelOptions extends Options {
max_parallel_requests?: number
}
export interface ParallelResult<T> {
succeeded: T[];
failed: {
request: T,
error: T
}[],
skipped: T[];
}
export async function load_parallel<T>(requests: T[], executor: (_: T) => Promise<void>, stringify: (_: T) => string, options: ParallelOptions) : Promise<ParallelResult<T>> {
const result: ParallelResult<T> = { failed: [], succeeded: [], skipped: [] };
const pending_requests = requests.slice(0).reverse(); /* we're only able to pop from the back */
const current_requests = {};
while (pending_requests.length > 0) {
while(typeof options.max_parallel_requests !== "number" || options.max_parallel_requests <= 0 || Object.keys(current_requests).length < options.max_parallel_requests) {
const script = pending_requests.pop();
const name = stringify(script);
current_requests[name] = executor(script).catch(e => result.failed.push({ request: script, error: e })).then(() => {
delete current_requests[name];
});
if(pending_requests.length == 0) break;
}
/*
* Wait 'till a new "slot" for downloading is free.
* This should also not throw because any errors will be caught before.
*/
await Promise.race(Object.keys(current_requests).map(e => current_requests[e]));
if(result.failed.length > 0)
break; /* finish loading the other requests and than show the error */
}
await Promise.all(Object.keys(current_requests).map(e => current_requests[e]));
result.skipped.push(...pending_requests);
return result;
}

View File

@ -73,12 +73,6 @@ export type DependSource = {
depends: string[];
}
export type SourcePath = string | DependSource | string[];
export function load_script(path: SourcePath) : Promise<void>;
export function load_scripts(paths: SourcePath[]) : Promise<void>;
export function load_style(path: SourcePath) : Promise<void>;
export function load_styles(paths: SourcePath[]) : Promise<void>;
export function load_template(path: SourcePath) : Promise<void>;
export function load_templates(paths: SourcePath[]) : Promise<void>;
export type ErrorHandler = (message: string, detail: string) => void;
export function critical_error(message: string, detail?: string);
export function critical_error_handler(handler?: ErrorHandler, override?: boolean);

View File

@ -54,9 +54,9 @@ module.exports = {
},
output: {
filename: 'loader.js',
path: path.resolve(__dirname, '../dist'),
path: path.resolve(__dirname, 'dist'),
library: "loader",
//libraryTarget: "umd" //"var" | "assign" | "this" | "window" | "self" | "global" | "commonjs" | "commonjs2" | "commonjs-module" | "amd" | "amd-require" | "umd" | "umd2" | "jsonp" | "system"
libraryTarget: "window" //"var" | "assign" | "this" | "window" | "self" | "global" | "commonjs" | "commonjs2" | "commonjs-module" | "amd" | "amd-require" | "umd" | "umd2" | "jsonp" | "system"
},
optimization: { }
};

1343
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,11 +34,17 @@
"@types/react-dom": "^16.9.5",
"@types/sha256": "^0.2.0",
"@types/websocket": "0.0.40",
"chunk-manifest-webpack-plugin": "^1.1.2",
"clean-css": "^4.2.1",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.4.2",
"csso-cli": "^2.0.2",
"exports-loader": "^0.7.0",
"file-loader": "^6.0.0",
"fs-extra": "latest",
"gulp": "^4.0.2",
"html-loader": "^1.0.0",
"html-webpack-plugin": "^4.0.3",
"mime-types": "^2.1.24",
"mini-css-extract-plugin": "^0.9.0",
"mkdirp": "^0.5.1",
@ -53,7 +59,10 @@
"typescript": "3.6.5",
"wat2wasm": "^1.0.2",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11"
"webpack-bundle-analyzer": "^3.6.1",
"webpack-cli": "^3.3.11",
"webpack-manifest-plugin": "^2.2.0",
"worker-plugin": "^4.0.2"
},
"repository": {
"type": "git",

View File

@ -1,5 +1,6 @@
import {settings, Settings} from "tc-shared/settings";
import * as loader from "tc-loader";
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import * as bipc from "tc-shared/BrowserIPC";

View File

@ -11,6 +11,7 @@
"paths": {
"*": ["shared/declarations/*"],
"tc-shared/*": ["shared/js/*"],
"tc-backend/web/*": ["web/js/*"], /* specific web part */
"tc-backend/*": ["shared/backend.d/*"],
"tc-loader": ["loader/exports/loader.d.ts"]
}

View File

@ -1,6 +1,6 @@
import {BasicCodec} from "./BasicCodec";
export class CodecWrapperRaw extends BasicCodec {
export class CodecRaw extends BasicCodec {
converterRaw: any;
converter: Uint8Array;
bufferSize: number = 4096 * 4;

View File

@ -1,135 +1,37 @@
import {BasicCodec} from "./BasicCodec";
import {CodecType} from "./Codec";
import {LogCategory} from "tc-shared/log";
import * as log from "tc-shared/log";
import {settings} from "tc-shared/settings";
import {LogCategory} from "tc-shared/log";
interface ExecuteResult {
result?: any;
error?: string;
success: boolean;
timings: {
upstream: number;
downstream: number;
handle: number;
}
}
export class CodecWrapperWorker extends BasicCodec {
private _worker: Worker;
private _workerListener: {token: string, resolve: (data: any) => void}[] = [];
private _workerCallbackToken = "callback_token";
private _workerTokeIndex: number = 0;
type: CodecType;
private _initialized: boolean = false;
private _workerCallbackResolve: () => any;
private _workerCallbackReject: ($: any) => any;
private _initialize_promise: Promise<Boolean>;
private _initializePromise: Promise<Boolean>;
name(): string {
return "Worker for " + CodecType[this.type] + " Channels " + this.channelCount;
}
private _token_index: number = 0;
readonly type: CodecType;
initialise() : Promise<Boolean> {
if(this._initializePromise) return this._initializePromise;
return this._initializePromise = this.spawnWorker().then(() => new Promise<Boolean>((resolve, reject) => {
const token = this.generateToken();
this.sendWorkerMessage({
command: "initialise",
type: this.type,
channelCount: this.channelCount,
token: token
});
private pending_executes: {[key: string]: {
timeout?: any;
this._workerListener.push({
token: token,
resolve: data => {
this._initialized = data["success"] == true;
if(data["success"] == true)
resolve();
else
reject(data.message);
}
})
}));
}
timestamp_send: number,
initialized() : boolean {
return this._initialized;
}
deinitialise() {
this.sendWorkerMessage({
command: "deinitialise"
});
}
decode(data: Uint8Array): Promise<AudioBuffer> {
let token = this.generateToken();
let result = new Promise<AudioBuffer>((resolve, reject) => {
this._workerListener.push(
{
token: token,
resolve: (data) => {
if(data.success) {
let array = new Float32Array(data.dataLength);
for(let index = 0; index < array.length; index++)
array[index] = data.data[index];
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];
}
}
resolve(audioBuf);
} else {
reject(data.message);
}
}
}
);
});
this.sendWorkerMessage({
command: "decodeSamples",
token: token,
data: data,
dataLength: data.length
});
return result;
}
encode(data: AudioBuffer) : Promise<Uint8Array> {
let token = this.generateToken();
let result = new Promise<Uint8Array>((resolve, reject) => {
this._workerListener.push(
{
token: token,
resolve: (data) => {
if(data.success) {
let array = new Uint8Array(data.dataLength);
for(let index = 0; index < array.length; index++)
array[index] = data.data[index];
resolve(array);
} else {
reject(data.message);
}
}
}
);
});
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];
}
this.sendWorkerMessage({
command: "encodeSamples",
token: token,
data: buffer,
dataLength: buffer.length
});
return result;
}
reset() : boolean {
this.sendWorkerMessage({
command: "reset"
});
return true;
}
resolve: (_: ExecuteResult) => void;
reject: (_: any) => void;
}} = {};
constructor(type: CodecType) {
super(48000);
@ -146,35 +48,92 @@ export class CodecWrapperWorker extends BasicCodec {
}
}
private generateToken() {
return this._workerTokeIndex++ + "_token";
name(): string {
return "Worker for " + CodecType[this.type] + " Channels " + this.channelCount;
}
private sendWorkerMessage(message: any, transfare?: any[]) {
message["timestamp"] = Date.now();
this._worker.postMessage(message, transfare as any);
async initialise() : Promise<Boolean> {
if(this._initialized) return;
this._initialize_promise = this.spawn_worker().then(() => this.execute("initialise", {
type: this.type,
channelCount: this.channelCount,
})).then(result => {
if(result.success)
return Promise.resolve(true);
log.error(LogCategory.VOICE, tr("Failed to initialize codec %s: %s"), CodecType[this.type], result.error);
return Promise.reject(result.error);
});
this._initialized = true;
await this._initialize_promise;
}
private onWorkerMessage(message: any) {
initialized() : boolean {
return this._initialized;
}
deinitialise() {
this.execute("deinitialise", {});
this._initialized = false;
this._initialize_promise = undefined;
}
async decode(data: Uint8Array): Promise<AudioBuffer> {
const result = await this.execute("decodeSamples", { data: data, length: data.length });
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");
let array = new Float32Array(result.result.length);
for(let index = 0; index < array.length; index++)
array[index] = result.result.data[index];
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];
}
}
return audioBuf;
}
async encode(data: AudioBuffer) : Promise<Uint8Array> {
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 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;
}
reset() : boolean {
//TODO: Await result!
this.execute("reset", {});
return true;
}
private handle_worker_message(message: any) {
if(!message["token"]) {
log.error(LogCategory.VOICE, tr("Invalid worker token!"));
return;
}
if(message["token"] == this._workerCallbackToken) {
if(message["type"] == "loaded") {
log.info(LogCategory.VOICE, tr("[Codec] Got worker init response: Success: %o Message: %o"), message["success"], message["message"]);
if(message["success"]) {
if(this._workerCallbackResolve)
this._workerCallbackResolve();
} else {
if(this._workerCallbackReject)
this._workerCallbackReject(message["message"]);
}
this._workerCallbackReject = undefined;
this._workerCallbackResolve = undefined;
return;
} else if(message["type"] == "chatmessage_server") {
if(message["token"] === "notify") {
/* currently not really used */
if(message["type"] == "chatmessage_server") {
//FIXME?
return;
}
@ -182,29 +141,69 @@ export class CodecWrapperWorker extends BasicCodec {
return;
}
/* lets warn on general packets. Control packets are allowed to "stuck" a bit longer */
if(Date.now() - message["timestamp"] > 5)
log.warn(LogCategory.VOICE, tr("Worker message stock time: %d"), Date.now() - message["timestamp"]);
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"]];
for(let entry of this._workerListener) {
if(entry.token == message["token"]) {
entry.resolve(message);
this._workerListener.remove(entry);
return;
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"]
}
};
clearTimeout(request.timeout);
request.resolve(result);
}
private handle_worker_error(error: any) {
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);
delete this.pending_executes[token];
}
log.error(LogCategory.VOICE, tr("Could not find worker token entry! (%o)"), message["token"]);
this._worker = undefined;
}
private spawnWorker() : Promise<Boolean> {
return new Promise<Boolean>((resolve, reject) => {
this._workerCallbackReject = reject;
this._workerCallbackResolve = resolve;
private execute(command: string, data: any, timeout?: number) : Promise<ExecuteResult> {
return new Promise<any>((resolve, reject) => {
if(!this._worker) {
reject(tr("worker does not exists"));
return;
}
this._worker = new Worker(settings.static("worker_directory", "js/workers/") + "WorkerCodec.js");
this._worker.onmessage = event => this.onWorkerMessage(event.data);
this._worker.onerror = (error: ErrorEvent) => reject("Failed to load worker (" + error.message + ")"); //TODO tr
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,
resolve: resolve,
reject: reject,
timestamp_send: Date.now()
};
this._worker.postMessage(payload);
});
}
private async spawn_worker() : Promise<void> {
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);
const result = await this.execute("global-initialize", {}, 15000);
if(!result.success) throw result.error;
}
}

View File

@ -1,7 +1,8 @@
const prefix = "[CodecWorker] ";
const workerCallbackToken = "callback_token";
import {CodecType} from "tc-backend/web/codec/Codec";
interface CodecWorker {
const prefix = "[CodecWorker] ";
export interface CodecWorker {
name();
initialise?() : string;
deinitialise();
@ -11,78 +12,17 @@ interface CodecWorker {
reset();
}
let codecInstance: CodecWorker;
let supported_types = {};
export function register_codec(type: CodecType, allocator: (options?: any) => Promise<CodecWorker>) {
supported_types[type] = allocator;
}
onmessage = function(e: MessageEvent) {
let data = e.data;
let initialize_callback: () => Promise<true | string>;
export function set_initialize_callback(callback: () => Promise<true | string>) {
initialize_callback = callback;
}
let res: any = {};
res.token = data.token;
res.success = false;
//console.log(prefix + " Got from main: %o", data);
switch (data.command) {
case "initialise":
let error;
console.log(prefix + "Got initialize for type " + CodecType[data.type as CodecType]);
switch (data.type as CodecType) {
case CodecType.OPUS_MUSIC:
codecInstance = new OpusWorker(2, OpusType.AUDIO);
break;
case CodecType.OPUS_VOICE:
codecInstance = new OpusWorker(1, OpusType.VOIP);
break;
default:
error = "Could not find worker type!";
console.error("Could not resolve opus type!");
break;
}
error = error || codecInstance.initialise();
if(error)
res["message"] = error;
else
res["success"] = true;
break;
case "encodeSamples":
let encodeArray = new Float32Array(data.dataLength);
for(let index = 0; index < encodeArray.length; index++)
encodeArray[index] = data.data[index];
let encodeResult = codecInstance.encode(encodeArray);
if(typeof encodeResult === "string") {
res.message = encodeResult;
} else {
res.success = true;
res.data = encodeResult;
res.dataLength = encodeResult.length;
}
break;
case "decodeSamples":
let decodeArray = new Uint8Array(data.dataLength);
for(let index = 0; index < decodeArray.length; index++)
decodeArray[index] = data.data[index];
let decodeResult = codecInstance.decode(decodeArray);
if(typeof decodeResult === "string") {
res.message = decodeResult;
} else {
res.success = true;
res.data = decodeResult;
res.dataLength = decodeResult.length;
}
break;
case "reset":
codecInstance.reset();
break;
default:
console.error(prefix + "Unknown type " + data.command);
}
if(res.token && res.token.length > 0) sendMessage(res, e.origin);
};
export let codecInstance: CodecWorker;
function printMessageToServerTab(message: string) {
/*
@ -95,7 +35,105 @@ function printMessageToServerTab(message: string) {
}
declare function postMessage(message: any): void;
function sendMessage(message: any, origin?: string){
function sendMessage(message: any, origin?: string) {
message["timestamp"] = Date.now();
postMessage(message);
}
}
let globally_initialized = false;
let global_initialize_result;
/**
* @param command
* @param data
* @return string on error or object on success
*/
async function handle_message(command: string, data: any) : Promise<string | object> {
switch (command) {
case "global-initialize":
const init_result = globally_initialized ? global_initialize_result : await initialize_callback();
globally_initialized = true;
if(typeof init_result === "string")
return init_result;
return {};
case "initialise":
console.log(prefix + "Got initialize for type " + CodecType[data.type as CodecType]);
if(!supported_types[data.type])
return "type unsupported";
try {
codecInstance = 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 = codecInstance.initialise();
if(error) return error;
return {};
case "encodeSamples":
let encodeArray = new Float32Array(data.length);
for(let index = 0; index < encodeArray.length; index++)
encodeArray[index] = data.data[index];
let encodeResult = codecInstance.encode(encodeArray);
if(typeof encodeResult === "string")
return encodeResult;
else
return { data: encodeResult, length: encodeResult.length };
case "decodeSamples":
let decodeArray = new Uint8Array(data.length);
for(let index = 0; index < decodeArray.length; index++)
decodeArray[index] = data.data[index];
let decodeResult = codecInstance.decode(decodeArray);
if(typeof decodeResult === "string")
return decodeResult;
else
return { data: decodeResult, length: decodeResult.length };
case "reset":
codecInstance.reset();
break;
default:
return "unknown command";
}
}
const handle_message_event = (e: MessageEvent) => {
const token = e.data.token;
const received = Date.now();
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();
sendMessage(data, e.origin);
};
handle_message(e.data.command, e.data.data).then(res => {
if(token) {
send_result(res);
}
}).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");
}
});
};
addEventListener("message", handle_message_event);

View File

@ -1,78 +1,82 @@
/// <reference path="CodecWorker.ts" />
import * as cworker from "./CodecWorker";
import {CodecType} from "tc-backend/web/codec/Codec";
import {CodecWorker} from "./CodecWorker";
const prefix = "OpusWorker";
declare global {
interface Window {
__init_em_module: ((Module: any) => void)[];
}
}
self.__init_em_module = self.__init_em_module || [];
const WASM_ERROR_MESSAGES = [
'no native wasm support detected'
];
this["Module"] = this["Module"] || ({} as any); /* its required to cast {} to any!*/
let Module;
self.__init_em_module.push(m => Module = m);
const runtime_initialize_promise = new Promise((resolve, reject) => {
self.__init_em_module.push(Module => {
const cleanup = () => {
Module['onRuntimeInitialized'] = undefined;
Module['onAbort'] = undefined;
};
let initialized = false;
Module['onRuntimeInitialized'] = function() {
initialized = true;
console.log(prefix + "Initialized!");
Module['onRuntimeInitialized'] = () => {
cleanup();
resolve();
};
sendMessage({
token: workerCallbackToken,
type: "loaded",
success: true
})
};
Module['onAbort'] = error => {
cleanup();
let message;
if(error instanceof DOMException)
message = "DOMException (" + error.name + "): " + error.code + " => " + error.message;
else {
abort_message = error;
message = error;
if(error.indexOf("no binaryen method succeeded") != -1) {
for(const error of WASM_ERROR_MESSAGES) {
if(last_error_message.indexOf(error) != -1) {
message = "no native wasm support detected, but its required";
break;
}
}
}
}
reject(message);
}
});
});
let abort_message: string = undefined;
let last_error_message: string;
self.__init_em_module.push(Module => {
Module['print'] = function() {
if(arguments.length == 1 && arguments[0] == abort_message)
return; /* we don't need to reprint the abort message! */
Module['print'] = function() {
if(arguments.length == 1 && arguments[0] == abort_message)
return; /* we don't need to reprint the abort message! */
console.log(...arguments);
};
console.log("Print: ", ...arguments);
};
Module['printErr'] = function() {
if(arguments.length == 1 && arguments[0] == abort_message)
return; /* we don't need to reprint the abort message! */
Module['printErr'] = function() {
if(arguments.length == 1 && arguments[0] == abort_message)
return; /* we don't need to reprint the abort message! */
last_error_message = arguments[0];
for(const suppress of WASM_ERROR_MESSAGES)
if((arguments[0] as string).indexOf(suppress) != -1)
return;
last_error_message = arguments[0];
for(const suppress of WASM_ERROR_MESSAGES)
if((arguments[0] as string).indexOf(suppress) != -1)
return;
console.error(...arguments);
};
console.error("Error: ",...arguments);
};
Module['onAbort'] = (message: string | DOMException) => {
/* no native wasm support detected */
Module['onAbort'] = undefined;
if(message instanceof DOMException)
message = "DOMException (" + message.name + "): " + message.code + " => " + message.message;
else {
abort_message = message;
if(message.indexOf("no binaryen method succeeded") != -1)
for(const error of WASM_ERROR_MESSAGES)
if(last_error_message.indexOf(error) != -1) {
message = "no native wasm support detected, but its required";
break;
}
}
sendMessage({
token: workerCallbackToken,
type: "loaded",
success: false,
message: message
});
};
try {
console.log("Node init!");
Module['locateFile'] = file => "../../wasm/" + file;
importScripts("../../wasm/TeaWeb-Worker-Codec-Opus.js");
} catch (e) {
if(typeof(Module['onAbort']) === "function") {
console.log(e);
Module['onAbort']("Failed to load native scripts");
} /* else the error had been already handled because its a WASM error */
}
});
enum OpusType {
VOIP = 2048,
@ -89,6 +93,7 @@ class OpusWorker implements CodecWorker {
private fn_decode: any;
private fn_encode: any;
private fn_reset: any;
private fn_error_message: any;
private bufferSize = 4096 * 2;
private encodeBufferRaw: any;
@ -106,11 +111,12 @@ class OpusWorker implements CodecWorker {
}
initialise?() : string {
this.fn_newHandle = cwrap("codec_opus_createNativeHandle", "number", ["number", "number"]);
this.fn_decode = cwrap("codec_opus_decode", "number", ["number", "number", "number", "number"]);
this.fn_newHandle = Module.cwrap("codec_opus_createNativeHandle", "number", ["number", "number"]);
this.fn_decode = Module.cwrap("codec_opus_decode", "number", ["number", "number", "number", "number"]);
/* codec_opus_decode(handle, buffer, length, maxlength) */
this.fn_encode = cwrap("codec_opus_encode", "number", ["number", "number", "number", "number"]);
this.fn_reset = cwrap("codec_opus_reset", "number", ["number"]);
this.fn_encode = Module.cwrap("codec_opus_encode", "number", ["number", "number", "number", "number"]);
this.fn_reset = Module.cwrap("codec_opus_reset", "number", ["number"]);
this.fn_error_message = Module.cwrap("opus_error_message", "string", ["number"]);
this.nativeHandle = this.fn_newHandle(this.channelCount, this.type);
@ -127,12 +133,8 @@ class OpusWorker implements CodecWorker {
decode(data: Uint8Array): Float32Array | string {
if (data.byteLength > this.decodeBuffer.byteLength) return "Data to long!";
this.decodeBuffer.set(data);
//console.log("decode(" + data.length + ")");
//console.log(data);
let result = this.fn_decode(this.nativeHandle, this.decodeBuffer.byteOffset, data.byteLength, this.decodeBuffer.byteLength);
if (result < 0) {
return "invalid result on decode (" + result + ")";
}
if (result < 0) return this.fn_error_message(result);
return Module.HEAPF32.slice(this.decodeBuffer.byteOffset / 4, (this.decodeBuffer.byteOffset / 4) + (result * this.channelCount));
}
@ -140,9 +142,7 @@ class OpusWorker implements CodecWorker {
this.encodeBuffer.set(data);
let result = this.fn_encode(this.nativeHandle, this.encodeBuffer.byteOffset, data.length, this.encodeBuffer.byteLength);
if (result < 0) {
return "invalid result on encode (" + result + ")";
}
if (result < 0) return this.fn_error_message(result);
let buf = Module.HEAP8.slice(this.encodeBuffer.byteOffset, this.encodeBuffer.byteOffset + result);
return Uint8Array.from(buf);
}
@ -151,4 +151,25 @@ class OpusWorker implements CodecWorker {
console.log(prefix + " Reseting opus codec!");
this.fn_reset(this.nativeHandle);
}
}
}
cworker.register_codec(CodecType.OPUS_MUSIC, async () => new OpusWorker(2, OpusType.AUDIO));
cworker.register_codec(CodecType.OPUS_VOICE, async () => new OpusWorker(1, OpusType.VOIP));
cworker.set_initialize_callback(async () => {
try {
require("tc-generated/codec/opus");
} catch (e) {
if(Module) {
if(typeof(Module['onAbort']) === "function") {
Module['onAbort']("Failed to load native scripts");
} /* else the error had been already handled because its a WASM error */
} else {
throw e;
}
}
if(!Module)
throw "Missing module handle";
await runtime_initialize_promise;
return true;
});

View File

@ -0,0 +1 @@
require("./OpusCodec");

View File

@ -1,16 +0,0 @@
{
"compilerOptions": {
"module": "none",
"target": "es6",
"sourceMap": true,
"outFile": "WorkerCodec.js"
},
"include": [
"../../types/"
],
"files": [
"codec/CodecWorker.ts",
"codec/OpusCodec.ts",
"../codec/Codec.ts"
]
}

View File

@ -1,6 +1,10 @@
const path = require('path');
const webpack = require("webpack");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CircularDependencyPlugin = require('circular-dependency-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const ManifestGenerator = require("./webpack/ManifestPlugin");
const WorkerPlugin = require('worker-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const isDevelopment = process.env.NODE_ENV === 'development';
module.exports = {
@ -11,10 +15,16 @@ module.exports = {
devtool: 'inline-source-map',
mode: "development",
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: isDevelopment ? '[name].css' : '[name].[hash].css',
chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css'
}),
new ManifestGenerator({
file: path.join(__dirname, "dist/manifest.json")
}),
new WorkerPlugin(),
//new BundleAnalyzerPlugin()
/*
new CircularDependencyPlugin({
//exclude: /a\.js|node_modules/,
@ -23,6 +33,12 @@ module.exports = {
cwd: process.cwd(),
})
*/
/*
new webpack.optimize.AggressiveSplittingPlugin({
minSize: 1024 * 128,
maxSize: 1024 * 1024
})
*/
],
module: {
rules: [
@ -58,49 +74,28 @@ module.exports = {
}
}
]
},
}
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', ".scss"],
alias: {
"tc-shared": path.resolve(__dirname, "shared/js"),
"tc-backend": path.resolve(__dirname, "web/js")
"tc-backend/web": path.resolve(__dirname, "web/js"),
"tc-backend": path.resolve(__dirname, "web/js"),
"tc-generated/codec/opus": path.resolve(__dirname, "asm/generated/TeaWeb-Worker-Codec-Opus.js"),
//"tc-backend": path.resolve(__dirname, "shared/backend.d"),
},
},
externals: {
"tc-loader": "umd loader"
"tc-loader": "window loader"
},
output: {
filename: 'shared-app.js',
filename: '[contenthash].js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: "umd",
library: "shared"
publicPath: "js/"
},
optimization: {
/*
splitChunks: {
chunks: 'async',
minSize: 1,
maxSize: 500000,
minChunks: 1,
maxAsyncRequests: 6,
maxInitialRequests: 4,
automaticNameDelimiter: '~',
automaticNameMaxLength: 30,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
*/
splitChunks: { }
}
};

49
webpack/ManifestPlugin.ts Normal file
View File

@ -0,0 +1,49 @@
import * as webpack from "webpack";
import * as fs from "fs";
interface Options {
file?: string;
}
class ManifestGenerator {
private manifest_content;
readonly options: Options;
constructor(options: Options) {
this.options = options || {};
}
apply(compiler: webpack.Compiler) {
compiler.hooks.afterCompile.tap(this.constructor.name, compilation => {
const chunks_data = {};
for(const chunk_group of compilation.chunkGroups) {
console.log(chunk_group.options.name);
const js_files = [];
for(const chunk of chunk_group.chunks) {
if(chunk.files.length !== 1) throw "expected only one file per chunk";
const file = chunk.files[0];
console.log("Chunk: %s - %s - %s", chunk.id, chunk.hash, file);
//console.log(chunk);
//console.log(" - %s - %o", chunk.id, chunk);
js_files.push({
hash: chunk.hash,
file: file
})
}
chunks_data[chunk_group.options.name] = js_files;
}
this.manifest_content = {
version: 1,
chunks: chunks_data
};
});
compiler.hooks.done.tap(this.constructor.name, () => {
fs.writeFileSync(this.options.file || "manifest.json", JSON.stringify(this.manifest_content));
});
}
}
export = ManifestGenerator;