Initial commit
This commit is contained in:
commit
6b70b8d425
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
.idea/
|
||||
.sass-cache/
|
||||
tmp/
|
||||
node_modules/
|
||||
/build/
|
||||
updater/postzip/test
|
||||
updater/postzip/cmake-build-*/
|
||||
updater/postzip/.idea
|
||||
updater/postzip/TeaClient-linux.tar.gz
|
||||
|
||||
#For the main JS file
|
||||
*.js.map
|
||||
*.js
|
||||
|
||||
.deploy_secret
|
||||
|
||||
**/*.d.ts
|
||||
|
||||
package-lock.json
|
14
.gitmodules
vendored
Normal file
14
.gitmodules
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
[submodule "native/codec/libraries/opus"]
|
||||
path = native/codec/libraries/opus
|
||||
url = https://github.com/xiph/opus.git
|
||||
[submodule "native/codec/libraries/speex"]
|
||||
path = native/codec/libraries/speex
|
||||
url = https://github.com/xiph/speex.git
|
||||
[submodule "native/codec/libraries/celt"]
|
||||
path = native/codec/libraries/celt
|
||||
url = https://github.com/WolverinDEV/celt-0.11.0.git
|
||||
[submodule "DbConnector"]
|
||||
branch = stable
|
||||
[submodule "github"]
|
||||
path = github
|
||||
url = https://github.com/TeaSpeak/TeaClient
|
17
bugs
Normal file
17
bugs
Normal file
@ -0,0 +1,17 @@
|
||||
Linux:
|
||||
Updater:
|
||||
After updater has extracted the file set the executable flag again for the TeaClient binary
|
||||
|
||||
libomp.so.5: Kann die Shared-Object-Datei nicht öffnen: Datei oder Verzeichnis nicht gefunden
|
||||
remove OMP from PortableAudio
|
||||
|
||||
Windows:
|
||||
Updater:
|
||||
Updater popups console which says that there are invalid arguments! Fix this!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
62
build_declarations.sh
Executable file
62
build_declarations.sh
Executable file
@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
cd "${BASEDIR}"
|
||||
|
||||
file_paths=(
|
||||
"$(pwd ~)/../../Web-Client/shared/declarations"
|
||||
"$(pwd ~)/TeaSpeak/Web-Client/shared/declarations"
|
||||
"$(pwd ~)/../../TeaSpeak/Web-Client/shared/declarations"
|
||||
"app/dummy-declarations"
|
||||
#TODO Windows path
|
||||
)
|
||||
files=(
|
||||
"exports.d.ts;imports_shared.d.ts"
|
||||
# "exports_loader.d.ts;imports_shared_loader.d.ts"
|
||||
)
|
||||
|
||||
path_target="./modules/renderer/imports"
|
||||
path_found=0
|
||||
{
|
||||
mkdir -p "${path_target}"
|
||||
|
||||
for path in "${file_paths[@]}"
|
||||
do
|
||||
path_found=1
|
||||
for file_mapping in "${files[@]}"
|
||||
do
|
||||
file_mapping=($(echo ${file_mapping} | tr ";" " "))
|
||||
file=${file_mapping[0]}
|
||||
|
||||
if [[ ! -f "${path}/${file}" ]]; then
|
||||
path_found=0
|
||||
echo "path test ${path} failed to file ${file}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
[[ path_found -eq 1 ]] || continue
|
||||
|
||||
for file in "${files[@]}"
|
||||
do
|
||||
file_mapping=$(echo ${file_mapping} | tr ";" "\n")
|
||||
src_file=${file_mapping[0]}
|
||||
dst_file=${file_mapping[1]}
|
||||
|
||||
if [[ -e "${path_target}/${dst_file}" ]] || [[ -L "${path_target}/${dst_file}" ]]; then
|
||||
rm "${path_target}/${dst_file}"
|
||||
fi
|
||||
|
||||
ln -rs "${path}/${src_file}" "${path_target}/${dst_file}"
|
||||
echo "Linking \"${path_target}/${dst_file}\" to \"${path}/${src_file}\""
|
||||
done
|
||||
break
|
||||
done
|
||||
}
|
||||
|
||||
if [[ path_found -eq 0 ]]; then
|
||||
echo "Failed to find UI imports"
|
||||
echo "Add your path to 'file_paths' and build the declarations first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 1
|
1
github
Submodule
1
github
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 0866874725f393c2804f2926687ea3d858d88653
|
46
installer/WinInstall.ejs
Normal file
46
installer/WinInstall.ejs
Normal file
@ -0,0 +1,46 @@
|
||||
; Script generated by the Inno Setup Script Wizard.
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application.
|
||||
; Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
AppId={{0F43B730-DF59-4A23-82AD-E895E72BE4AF}
|
||||
AppName=TeaSpeak Client
|
||||
AppVersion=<%= version %>
|
||||
;AppVerName=TeaSpeak 1.0.0
|
||||
AppPublisher=TeaSpeak
|
||||
AppPublisherURL=http://www.teaspeak.com/
|
||||
AppSupportURL=http://www.teaspeak.com/
|
||||
AppUpdatesURL=http://www.teaspeak.com/
|
||||
DefaultDirName={pf}\TeaSpeak
|
||||
DisableProgramGroupPage=yes
|
||||
OutputBaseFilename=<%= executable_name %>
|
||||
OutputDir=<%= dest_dir %>
|
||||
SetupIconFile=<%= icon_file %>
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
DisableDirPage=no
|
||||
DisableWelcomePage=no
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1
|
||||
|
||||
[Files]
|
||||
Source: "<%= source_dir %>"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{commonprograms}\TeaSpeak"; Filename: "{app}\TeaClient.exe"
|
||||
Name: "{commondesktop}\TeaSpeak"; Filename: "{app}\TeaClient.exe"; Tasks: desktopicon
|
||||
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\TeaSpeak"; Filename: "{app}\TeaClient.exe"; Tasks: quicklaunchicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\TeaClient.exe"; Description: "{cm:LaunchProgram,TeaSpeak}"; Flags: nowait postinstall skipifsilent
|
||||
|
344
installer/build.ts
Normal file
344
installer/build.ts
Normal file
@ -0,0 +1,344 @@
|
||||
import {Options} from "electron-packager";
|
||||
import * as packager from "electron-packager"
|
||||
const pkg = require('../package.json');
|
||||
const dev_dependencies = Object.keys(pkg.devDependencies);
|
||||
|
||||
import * as fs from "fs-extra";
|
||||
import * as path_helper from "path";
|
||||
import {parse_version} from "../modules/shared/version";
|
||||
import * as util from "util";
|
||||
import * as child_process from "child_process";
|
||||
import * as os from "os";
|
||||
import * as asar from "asar";
|
||||
import * as querystring from "querystring";
|
||||
import request = require("request");
|
||||
|
||||
let options: Options = {} as any;
|
||||
let version = parse_version(pkg.version);
|
||||
version.timestamp = Date.now();
|
||||
|
||||
options.dir = '.';
|
||||
options.name = "TeaClient";
|
||||
options.executableName = "TeaClient";
|
||||
options.appVersion = pkg.version;
|
||||
options.appCopyright = "© 2018-2019 Markus Hadenfeldt All Rights Reserved";
|
||||
options.out = "build/";
|
||||
|
||||
if(!pkg.dependencies['electron']) {
|
||||
console.error("Missing electron version");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
options.electronVersion = pkg.dependencies['electron'];
|
||||
options.protocols = [{name: "TeaSpeak - Connect", schemes: ["teaserver"]}];
|
||||
options.overwrite = true;
|
||||
options.derefSymlinks = true;
|
||||
options.buildVersion = version.toString(true);
|
||||
|
||||
options.asar = {
|
||||
unpackDir: "teaclient-unpacked"
|
||||
};
|
||||
|
||||
|
||||
interface ProjectEntry {
|
||||
type: ProjectEntryType;
|
||||
}
|
||||
|
||||
interface ProjectFile extends ProjectEntry {
|
||||
path: string;
|
||||
name: string | RegExp;
|
||||
|
||||
//target_name?: string | ((file: string) => string);
|
||||
}
|
||||
|
||||
interface ProjectDirectory extends ProjectEntry {
|
||||
|
||||
path: string | RegExp;
|
||||
children?: boolean;
|
||||
files?: RegExp;
|
||||
}
|
||||
|
||||
enum ProjectEntryType {
|
||||
FILE,
|
||||
DIRECTORY
|
||||
}
|
||||
|
||||
const project_files: ProjectEntry[] = [];
|
||||
{ /* general required files*/
|
||||
project_files.push({
|
||||
type: ProjectEntryType.FILE,
|
||||
path: "/",
|
||||
name: "package.json"
|
||||
} as ProjectFile);
|
||||
project_files.push({
|
||||
type: ProjectEntryType.FILE,
|
||||
path: "/",
|
||||
name: "main.js"
|
||||
} as ProjectFile);
|
||||
|
||||
project_files.push({
|
||||
type: ProjectEntryType.DIRECTORY,
|
||||
path: "/node_modules",
|
||||
children: true,
|
||||
files: /\.(js|css|html|node|json)$/
|
||||
} as ProjectDirectory);
|
||||
|
||||
project_files.push({
|
||||
type: ProjectEntryType.DIRECTORY,
|
||||
path: "/node_modules/electron",
|
||||
children: true,
|
||||
files: /.*/
|
||||
} as ProjectDirectory);
|
||||
}
|
||||
|
||||
/* TeaClient modules */
|
||||
project_files.push({
|
||||
type: ProjectEntryType.DIRECTORY,
|
||||
path: "/modules",
|
||||
children: true,
|
||||
files: /.*\.(js|css|html|png|svg)$/
|
||||
} as ProjectDirectory);
|
||||
|
||||
/* resource files */
|
||||
project_files.push({
|
||||
type: ProjectEntryType.DIRECTORY,
|
||||
path: "/resources"
|
||||
} as ProjectDirectory);
|
||||
|
||||
|
||||
if (process.argv[2] == "linux") {
|
||||
options.arch = "x64";
|
||||
options.platform = "linux";
|
||||
options.icon = "resources/logo.svg";
|
||||
} else if (process.argv[2] == "win32") {
|
||||
options.arch = "x64";
|
||||
options.platform = "win32";
|
||||
options.icon = "resources/logo.ico";
|
||||
} else {
|
||||
console.error("Invalid system");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const path_validator = (path: string) => {
|
||||
path = path.replace(/\\/g,"/");
|
||||
|
||||
const IGNORE = true;
|
||||
const APPEND = false;
|
||||
|
||||
const ppath = path_helper.parse(path);
|
||||
const is_directory = ppath.ext == '' && fs.statSync(path_helper.join('.', ppath.dir, ppath.name)).isDirectory();
|
||||
const directory = (is_directory ? path_helper.join(ppath.dir, ppath.name) : ppath.dir).replace(/\\/g,"/");
|
||||
|
||||
//console.log("Is directory %o => %s", is_directory, directory);
|
||||
//console.dir(ppath);
|
||||
|
||||
for(const entry of project_files) {
|
||||
if(entry.type == ProjectEntryType.DIRECTORY) {
|
||||
const dir_entry = <ProjectDirectory>entry;
|
||||
|
||||
if(typeof(dir_entry.path) === 'string') {
|
||||
//ppath.dir == dir_entry.path
|
||||
//console.log("'" + dir_entry.path + "' | '" + directory + "'");
|
||||
if(dir_entry.path.startsWith(directory)) {
|
||||
if(is_directory)
|
||||
return APPEND;
|
||||
}
|
||||
if(directory == dir_entry.path) {
|
||||
//console.log("Math: " + ppath.base + " to " + dir_entry.files);
|
||||
if(dir_entry.files)
|
||||
return ppath.base.match(dir_entry.files) ? APPEND : IGNORE;
|
||||
return APPEND;
|
||||
}
|
||||
if(directory.startsWith(dir_entry.path)) {
|
||||
if(dir_entry.children) {
|
||||
if(is_directory)
|
||||
return APPEND;
|
||||
|
||||
const sub_path = directory.substr(dir_entry.path.length);
|
||||
//console.log("Sub path: " + sub_path + ". Test: " + (path_helper.join(sub_path, ppath.base) + " against " + dir_entry.files);
|
||||
|
||||
if(dir_entry.files)
|
||||
return path_helper.join(sub_path, ppath.base).match(dir_entry.files) ? APPEND : IGNORE;
|
||||
return APPEND;
|
||||
}
|
||||
//TODO test for regex
|
||||
}
|
||||
} else {
|
||||
//TODO
|
||||
}
|
||||
} else if(entry.type == ProjectEntryType.FILE) {
|
||||
const file_entry = <ProjectFile>entry;
|
||||
if(file_entry.path.startsWith(directory) && is_directory)
|
||||
return APPEND;
|
||||
|
||||
if(directory == file_entry.path) {
|
||||
if(typeof(file_entry.name) === 'string' && ppath.base == file_entry.name)
|
||||
return APPEND;
|
||||
else if (ppath.base.match(file_entry.name))
|
||||
return APPEND;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
options.ignore = path => {
|
||||
if(path.length == 0) return false; //Dont ignore root paths
|
||||
|
||||
const ignore_path = path_validator(path);
|
||||
if(!ignore_path) {
|
||||
console.log(" + " + path);
|
||||
}
|
||||
return ignore_path;
|
||||
};
|
||||
|
||||
async function copy_striped(source: string, target: string, symbol_directory: string) {
|
||||
const copy_file = util.promisify(fs.copyFile);
|
||||
const exec = util.promisify(child_process.exec);
|
||||
|
||||
if(process.argv[2] == "win32") {
|
||||
await copy_file(source, target);
|
||||
return;
|
||||
}
|
||||
if(process.argv[2] != "linux") throw "invalid target type";
|
||||
|
||||
await copy_file(source, target);
|
||||
|
||||
{
|
||||
const symbols_command = await exec("dump_syms " + target, {
|
||||
maxBuffer: 1024 * 1024 * 512
|
||||
});
|
||||
if(symbols_command.stderr.length != 0) {
|
||||
console.error("Failed to create sys dump: %o", symbols_command.stderr.toString());
|
||||
throw "symbol create failed";
|
||||
}
|
||||
|
||||
const symbols = symbols_command.stdout.toString();
|
||||
const header = symbols.substr(0, symbols.indexOf('\n'));
|
||||
const binary_name = path_helper.basename(target);
|
||||
const dump_id = header.split(" ")[3];
|
||||
if(binary_name != header.split(" ")[4])
|
||||
throw "binary name missmatch";
|
||||
|
||||
const dump_dir = path_helper.join(symbol_directory, binary_name, dump_id);
|
||||
const dump_file = path_helper.join(dump_dir, binary_name + ".sym");
|
||||
if(!(await fs.pathExists(dump_dir)))
|
||||
await fs.mkdirp(dump_dir);
|
||||
await fs.ensureDir(dump_dir);
|
||||
|
||||
console.log("Writing file to %s", dump_file);
|
||||
await fs.writeFile(dump_file, symbols);
|
||||
console.log("Created dump file for binary %s (%s).", binary_name, dump_id);
|
||||
}
|
||||
|
||||
{
|
||||
console.log("Striping file");
|
||||
|
||||
//TODO: Keep node module names!
|
||||
const strip_command = await exec("strip -s " + target, {
|
||||
maxBuffer: 1024 * 1024 * 512
|
||||
});
|
||||
if(strip_command.stderr.length != 0) {
|
||||
console.error("Failed to strip binary: %o", strip_command.stderr.toString());
|
||||
throw "strip failed";
|
||||
}
|
||||
|
||||
const nm_command = await exec("nm " + target, {
|
||||
maxBuffer: 1024 * 1024 * 512
|
||||
});
|
||||
console.log("File stripped. Symbols left: \n%s", nm_command.stderr.toString().trim() || nm_command.stdout.toString().trim());
|
||||
}
|
||||
}
|
||||
async function create_native_addons(target_directory: string, symbol_directory: string) {
|
||||
let node_source = "native/build/" + os.platform() + "_" + os.arch() + "/";
|
||||
console.log("Native addon path: %s", node_source);
|
||||
await fs.ensureDir(target_directory);
|
||||
for(const file of await fs.readdir(node_source)) {
|
||||
if(!file.endsWith(".node")) {
|
||||
console.warn("Discovered non node file within node file out dir");
|
||||
continue;
|
||||
}
|
||||
|
||||
await copy_striped(path_helper.join(node_source, file), path_helper.join(target_directory, file), symbol_directory);
|
||||
}
|
||||
}
|
||||
|
||||
interface UIVersion {
|
||||
channel: string;
|
||||
version: string;
|
||||
git_hash: string;
|
||||
timestamp: number;
|
||||
|
||||
required_client?: string;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
async function create_default_ui_pack(target_directory: string) {
|
||||
const remote_url = "https://clientapi.teaspeak.de/";
|
||||
const channel = "release";
|
||||
|
||||
const file = path_helper.join(target_directory, "default_ui.tar.gz");
|
||||
console.log("Creating default UI pack. Downloading from %s (channel: %s)", remote_url, channel);
|
||||
await fs.ensureDir(target_directory);
|
||||
|
||||
let ui_info: UIVersion;
|
||||
await new Promise((resolve, reject) => {
|
||||
request.get(remote_url + "api.php?" + querystring.stringify({
|
||||
type: "ui-download",
|
||||
channel: channel,
|
||||
version: "latest"
|
||||
}), {
|
||||
timeout: 5000
|
||||
}).on('response', function(response) {
|
||||
if(response.statusCode != 200)
|
||||
reject("Failed to download UI files (Status code " + response.statusCode + ")");
|
||||
|
||||
ui_info = {
|
||||
channel: channel,
|
||||
version: response.headers["x-ui-version"] as string,
|
||||
git_hash: response.headers["x-ui-git-ref"] as string,
|
||||
required_client: response.headers["x-ui-required_client"] as string,
|
||||
timestamp: parseInt(response.headers["x-ui-timestamp"] as string),
|
||||
filename: path_helper.basename(file)
|
||||
}
|
||||
}).on('error', error => {
|
||||
reject("Failed to download UI files: " + error);
|
||||
}).pipe(fs.createWriteStream(file)).on('finish', resolve);
|
||||
});
|
||||
|
||||
if(!ui_info)
|
||||
throw "failed to generate ui info!";
|
||||
|
||||
await fs.writeJson(path_helper.join(target_directory, "default_ui_info.json"), ui_info);
|
||||
console.log("UI-Pack downloaded!");
|
||||
}
|
||||
|
||||
let path;
|
||||
packager(options).then(async app_paths => {
|
||||
console.log("Copying changelog file!");
|
||||
await util.promisify(fs.copyFile)(path_helper.join(options.dir, "github", "ChangeLog.txt"), path_helper.join(app_paths[0], "ChangeLog.txt"));
|
||||
return app_paths;
|
||||
}).then(async app_paths => {
|
||||
await create_native_addons(path_helper.join(app_paths[0], "resources", "natives"), "build/symbols");
|
||||
return app_paths;
|
||||
}).then(async app_paths => {
|
||||
await create_default_ui_pack(path_helper.join(app_paths[0], "resources", "ui"));
|
||||
return app_paths;
|
||||
}).then(async appPaths => {
|
||||
///native/build/linux_amd64
|
||||
path = appPaths[0];
|
||||
if(process.argv[2] == "linux") {
|
||||
await copy_striped(options.dir + "/native/build/exe/update-installer", path + "/update-installer", "build/symbols");
|
||||
} else if (process.argv[2] == "win32") {
|
||||
await copy_striped(options.dir + "/native/build/exe/update-installer.exe", path + "/update-installer.exe", "build/symbols");
|
||||
}
|
||||
return fs.writeJson(path + "/app_version.json", {
|
||||
version: version.toString(true),
|
||||
timestamp: version.timestamp
|
||||
});
|
||||
}).then(() => {
|
||||
console.log("Package created");
|
||||
});
|
||||
|
||||
export {}
|
143
installer/deploy/crash_dumps.ts
Normal file
143
installer/deploy/crash_dumps.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as _node_ssh from "node-ssh";
|
||||
import * as ssh2 from "ssh2";
|
||||
import * as util from "util";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
declare namespace node_ssh {
|
||||
export type PutFilesOptions = {
|
||||
sftp?: Object,
|
||||
sftpOptions?: Object,
|
||||
concurrency?: number,
|
||||
}
|
||||
export type PutDirectoryOptions = {
|
||||
sftp?: Object,
|
||||
sftpOptions?: Object,
|
||||
concurrency?: number,
|
||||
recursive?: boolean,
|
||||
tick?: ((localPath: string, remotePath: string, error?: Error) => void),
|
||||
validate?: ((localPath: string) => boolean),
|
||||
}
|
||||
export type ExecOptions = {
|
||||
cwd?: string,
|
||||
options?: Object // passed to ssh2.exec
|
||||
stdin?: string,
|
||||
stream?: 'stdout' | 'stderr' | 'both',
|
||||
onStdout?: ((chunk: Buffer) => void),
|
||||
onStderr?: ((chunk: Buffer) => void),
|
||||
}
|
||||
|
||||
export class Instance {
|
||||
connect(config: ssh2.ConnectConfig): Promise<this>
|
||||
requestSFTP(): Promise<ssh2.SFTPWrapper>
|
||||
requestShell(): Promise<ssh2.ClientChannel>
|
||||
mkdir(path: string, method: 'sftp' | 'exec', givenSftp?: Object): Promise<string>
|
||||
exec(command: string, parameters: Array<string>, options: ExecOptions): Promise<Object | string>
|
||||
execCommand(command: string, options?: { cwd: string, stdin: string }): Promise<{ stdout: string, options?: Object, stderr: string, signal?: string, code: number }>
|
||||
putFile(localFile: string, remoteFile: string, sftp?: Object, opts?: Object): Promise<void>
|
||||
getFile(localFile: string, remoteFile: string, sftp?: Object, opts?: Object): Promise<void>
|
||||
putFiles(files: Array<{ local: string, remote: string }>, options: PutFilesOptions): Promise<void>
|
||||
putDirectory(localDirectory: string, remoteDirectory: string, options: PutDirectoryOptions): Promise<boolean>
|
||||
dispose(): void
|
||||
}
|
||||
}
|
||||
|
||||
let instance: node_ssh.Instance;
|
||||
export async function setup() {
|
||||
if(instance)
|
||||
throw "already initiaized";
|
||||
instance = new _node_ssh();
|
||||
try {
|
||||
await instance.connect({
|
||||
host: 'deploy.teaspeak.de',
|
||||
username: 'TeaSpeak-Jenkins-Client',
|
||||
privateKey: path.join(__dirname, "ssh_key")
|
||||
})
|
||||
} catch(error) {
|
||||
try { instance.dispose(); } finally { instance = undefined; }
|
||||
console.error("Failed to connect: %o", error);
|
||||
throw "failed to connect";
|
||||
}
|
||||
}
|
||||
async function update_remote_file(local_path: string, remote_path: string) {
|
||||
if(!instance)
|
||||
throw "Invalid instance";
|
||||
|
||||
let sftp: ssh2.SFTPWrapper;
|
||||
try {
|
||||
let sha512_remote, sha512_local;
|
||||
try {
|
||||
const sha512_remote_result = await instance.execCommand('sha512sum ' + remote_path);
|
||||
if(sha512_remote_result.code != 0)
|
||||
sha512_remote = undefined; /* file does not exists! */
|
||||
else {
|
||||
const result = sha512_remote_result.stdout.toString();
|
||||
sha512_remote = result.split(" ")[0];
|
||||
//console.log("File %s has a remote sha512: %o", remote_path, sha512_remote);
|
||||
}
|
||||
} catch(error) {
|
||||
console.log("Failed to calculate remote sha521 for file %s: %o", remote_path, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if(sha512_remote) { /* if the remote hasn't the file then we've def a "new" version */
|
||||
const hash_processor = crypto.createHash('sha512');
|
||||
const local_stream = fs.createReadStream(local_path);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
local_stream.on('error', reject);
|
||||
local_stream.on('data', chunk => hash_processor.update(chunk));
|
||||
local_stream.on('end', resolve);
|
||||
});
|
||||
sha512_local = hash_processor.digest('hex');
|
||||
local_stream.close();
|
||||
}
|
||||
|
||||
if(sha512_remote) {
|
||||
if(sha512_remote == sha512_local) {
|
||||
console.log("File %s (%s) is already up to date.", path.basename(local_path), local_path);
|
||||
return;
|
||||
} else {
|
||||
console.log("Updating file %s (%s) at %s. Local sum: %s Remote sum: %s", path.basename(local_path), local_path, remote_path, sha512_local, sha512_remote);
|
||||
}
|
||||
} else {
|
||||
console.log("Uploading file %s (%s) to %s.", path.basename(local_path), local_path, remote_path);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await instance.putFile(local_path, remote_path);
|
||||
} catch(error) {
|
||||
console.error("Failed to upload file %s (%s): %s", path.basename(local_path), local_path, error);
|
||||
throw "Upload failed";
|
||||
}
|
||||
} finally {
|
||||
if(sftp)
|
||||
sftp.end();
|
||||
}
|
||||
}
|
||||
|
||||
export async function deploy_crash_dumps(local_path: string, remote_path: string) {
|
||||
console.log("Uploading crash dumps from %s to %s", local_path, remote_path);
|
||||
|
||||
const do_dir = async (local_path, remote_path) => {
|
||||
for(const file of await util.promisify(fs.readdir)(local_path)) {
|
||||
const local_file = path.join(local_path, file);
|
||||
const remote_file = remote_path + "/" + file;
|
||||
|
||||
if((await util.promisify(fs.stat)(local_file)).isDirectory())
|
||||
await do_dir(local_file, remote_file);
|
||||
else
|
||||
await update_remote_file(local_file, remote_file);
|
||||
}
|
||||
};
|
||||
|
||||
await do_dir(local_path, remote_path);
|
||||
}
|
||||
|
||||
const test = async () => {
|
||||
await setup();
|
||||
await deploy_crash_dumps(path.join(__dirname, "../../build/symbols/"), "symbols");
|
||||
};
|
||||
test();
|
3
installer/deploy/generate_keys.md
Normal file
3
installer/deploy/generate_keys.md
Normal file
@ -0,0 +1,3 @@
|
||||
```$bash
|
||||
ssh-keygen -t rsa -b 4096 -f ssh_key -N ''
|
||||
```
|
239
installer/deploy/index.ts
Normal file
239
installer/deploy/index.ts
Normal file
@ -0,0 +1,239 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as _node_ssh from "node-ssh";
|
||||
import * as ssh2 from "ssh2";
|
||||
import * as stream from "stream";
|
||||
import * as child_process from "child_process";
|
||||
import * as util from "util";
|
||||
import {FileEntry} from "ssh2-streams";
|
||||
|
||||
declare namespace node_ssh {
|
||||
export type PutFilesOptions = {
|
||||
sftp?: Object,
|
||||
sftpOptions?: Object,
|
||||
concurrency?: number,
|
||||
}
|
||||
export type PutDirectoryOptions = {
|
||||
sftp?: Object,
|
||||
sftpOptions?: Object,
|
||||
concurrency?: number,
|
||||
recursive?: boolean,
|
||||
tick?: ((localPath: string, remotePath: string, error?: Error) => void),
|
||||
validate?: ((localPath: string) => boolean),
|
||||
}
|
||||
export type ExecOptions = {
|
||||
cwd?: string,
|
||||
options?: Object // passed to ssh2.exec
|
||||
stdin?: string,
|
||||
stream?: 'stdout' | 'stderr' | 'both',
|
||||
onStdout?: ((chunk: Buffer) => void),
|
||||
onStderr?: ((chunk: Buffer) => void),
|
||||
}
|
||||
|
||||
export class Instance {
|
||||
connect(config: ssh2.ConnectConfig): Promise<this>
|
||||
requestSFTP(): Promise<ssh2.SFTPWrapper>
|
||||
requestShell(): Promise<ssh2.ClientChannel>
|
||||
mkdir(path: string, method: 'sftp' | 'exec', givenSftp?: Object): Promise<string>
|
||||
exec(command: string, parameters: Array<string>, options: ExecOptions): Promise<Object | string>
|
||||
execCommand(command: string, options?: { cwd: string, stdin: string }): Promise<{ stdout: string, options?: Object, stderr: string, signal?: string, code: number }>
|
||||
putFile(localFile: string, remoteFile: string, sftp?: Object, opts?: Object): Promise<void>
|
||||
getFile(localFile: string, remoteFile: string, sftp?: Object, opts?: Object): Promise<void>
|
||||
putFiles(files: Array<{ local: string, remote: string }>, options: PutFilesOptions): Promise<void>
|
||||
putDirectory(localDirectory: string, remoteDirectory: string, options: PutDirectoryOptions): Promise<boolean>
|
||||
dispose(): void
|
||||
}
|
||||
}
|
||||
|
||||
let instance: node_ssh.Instance;
|
||||
export async function setup() {
|
||||
if(instance)
|
||||
throw "already initiaized";
|
||||
instance = new _node_ssh();
|
||||
try {
|
||||
await instance.connect({
|
||||
host: 'deploy.teaspeak.de',
|
||||
username: 'TeaSpeak-Jenkins-Client',
|
||||
privateKey: path.join(__dirname, "ssh_key")
|
||||
})
|
||||
} catch(error) {
|
||||
try { instance.dispose(); } finally { instance = undefined; }
|
||||
console.error("Failed to connect: %o", error);
|
||||
throw "failed to connect";
|
||||
}
|
||||
}
|
||||
|
||||
function read_stream_full(stream: stream.Readable) : Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const buffers = [];
|
||||
stream.on('data', buffer => buffers.push(buffer));
|
||||
stream.on('end', () => resolve(Buffer.concat(buffers)));
|
||||
stream.on('error', error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
export type PlatformSpecs = {
|
||||
system: 'linux' | 'windows' | 'osx';
|
||||
arch: 'amd64' | 'x86';
|
||||
type: 'indev' | 'debug' | 'optimized' | 'stable';
|
||||
}
|
||||
|
||||
export type Version = {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
type?: 'indev' | 'beta';
|
||||
}
|
||||
|
||||
//<system>/<arch>_<type>/
|
||||
function platform_path(platform: PlatformSpecs) {
|
||||
return platform.system + "/" + platform.arch + "_" + platform.type + "/";
|
||||
}
|
||||
|
||||
function version_string(version: Version) {
|
||||
return version.major + "." + version.minor + "." + version.patch + (version.type ? "-" + version.type : "");
|
||||
}
|
||||
|
||||
export async function latest_version(platform: PlatformSpecs) {
|
||||
const path = "versions/" + platform_path(platform);
|
||||
if(!instance)
|
||||
throw "Invalid instance";
|
||||
const sftp = await instance.requestSFTP();
|
||||
try {
|
||||
if(!sftp)
|
||||
throw "failed to request sftp";
|
||||
|
||||
try {
|
||||
const data_stream = sftp.createReadStream(path + "latest");
|
||||
const data = await read_stream_full(data_stream);
|
||||
return data.toString();
|
||||
} catch(error) {
|
||||
if(error instanceof Error && error.message == "No such file")
|
||||
return undefined;
|
||||
|
||||
console.log("Failed to receive last version: %o", error);
|
||||
return undefined;
|
||||
}
|
||||
} finally {
|
||||
if(sftp)
|
||||
sftp.end();
|
||||
}
|
||||
}
|
||||
|
||||
export async function generate_build_index(platform: PlatformSpecs, version: Version) : Promise<number> {
|
||||
const path = "versions/" + platform_path(platform);
|
||||
const version_str = version_string(version);
|
||||
if(!instance)
|
||||
throw "Invalid instance";
|
||||
const sftp = await instance.requestSFTP();
|
||||
try {
|
||||
if(!sftp)
|
||||
throw "failed to request sftp";
|
||||
|
||||
try {
|
||||
const files = await new Promise<FileEntry[]>((resolve, reject) => sftp.readdir(path, (error, result) => error ? reject(error) : resolve(result)));
|
||||
const version_files = files.filter(e => e.filename.startsWith(version_str));
|
||||
if(version_files.length == 0)
|
||||
return 0;
|
||||
let index = 1;
|
||||
while(version_files.find(e => e.filename.toLowerCase() === version_str + "-" + index)) index++;
|
||||
return index;
|
||||
} catch(error) {
|
||||
if(error instanceof Error && error.message == "No such file")
|
||||
return 0;
|
||||
|
||||
console.log("Failed to receive versions list: %o", error);
|
||||
return undefined;
|
||||
}
|
||||
} finally {
|
||||
if(sftp)
|
||||
sftp.end();
|
||||
}
|
||||
}
|
||||
|
||||
export type WinDbgFile = {
|
||||
binary: string,
|
||||
pdb: string;
|
||||
};
|
||||
export async function deploy_win_dbg_files(files: WinDbgFile[], version: Version) : Promise<void> {
|
||||
//symstore add /r /f .\*.node /s \\deploy.teaspeak.de\symbols /t "TeaClient-Windows-amd64" /v "x.y.z"
|
||||
//symstore add /r /f .\*.* /s \\deploy.teaspeak.de\symbols /t "TeaClient-Windows-amd64" /v "1.0.0"
|
||||
const server_path = "\\\\deploy.teaspeak.de\\symbols\\symbols";
|
||||
const vstring = version_string(version);
|
||||
const exec = util.promisify(child_process.exec);
|
||||
for(const file of files) {
|
||||
console.log("Deploying %s to %s", file, server_path);
|
||||
let current_file;
|
||||
try {
|
||||
{
|
||||
const result = await exec("symstore add /r /f " + file.binary + " /s " + server_path + " /t \"TeaClient-Windows-amd64\" /v \"" + vstring + "\"");
|
||||
if(result.stdout)
|
||||
console.log("Stdout: %s", result.stdout);
|
||||
if(result.stderr)
|
||||
console.log("Stderr: %s", result.stderr);
|
||||
}
|
||||
{
|
||||
const result = await exec("symstore add /r /f " + file.pdb + " /s " + server_path + " /t \"TeaClient-Windows-amd64\" /v \"" + vstring + "\"");
|
||||
if(result.stdout)
|
||||
console.log("Stdout: %s", result.stdout);
|
||||
if(result.stderr)
|
||||
console.log("Stderr: %s", result.stderr);
|
||||
}
|
||||
} catch(error) {
|
||||
if('killed' in error && 'code' in error) {
|
||||
const perror: {
|
||||
killed: boolean,
|
||||
code: number,
|
||||
signal: any,
|
||||
cmd: string,
|
||||
stdout: string,
|
||||
stderr: string
|
||||
} = error;
|
||||
console.error("Failed to deploy %s file %s:", current_file, file);
|
||||
console.log(" Code: %d", perror.code)
|
||||
{
|
||||
console.error(" Stdout: ");
|
||||
for(const element of perror.stdout.split("\n"))
|
||||
console.error(" %s", element);
|
||||
}
|
||||
{
|
||||
console.error(" Stderr: ");
|
||||
for(const element of perror.stderr.split("\n"))
|
||||
console.error(" %s", element);
|
||||
}
|
||||
} else
|
||||
console.error("Failed to deploy %s file %s: %o", current_file, file, error);
|
||||
throw "deploy failed";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const test = async () => {
|
||||
await setup();
|
||||
console.log(await latest_version({
|
||||
arch: 'amd64',
|
||||
system: 'linux',
|
||||
type: 'optimized'
|
||||
}));
|
||||
console.log(await generate_build_index({
|
||||
arch: 'amd64',
|
||||
system: 'linux',
|
||||
type: 'optimized'
|
||||
}, {
|
||||
type: 'beta',
|
||||
patch: 19,
|
||||
minor: 3,
|
||||
major: 1
|
||||
}));
|
||||
/*
|
||||
console.log(await deploy_pdb_files(
|
||||
[path.join(__dirname, "..", "..", "native", "build", "symbols", "teaclient_crash_handler.pdb")], {
|
||||
type: 'beta',
|
||||
patch: 19,
|
||||
minor: 3,
|
||||
major: 1
|
||||
}
|
||||
))
|
||||
*/
|
||||
};
|
||||
test();
|
24
installer/deploy/setup_symbol_device.bat
Normal file
24
installer/deploy/setup_symbol_device.bat
Normal file
@ -0,0 +1,24 @@
|
||||
@echo off
|
||||
|
||||
set drive=T:
|
||||
|
||||
|
||||
net use z: /dele > nul
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
if %ERRORLEVEL% neq 2 (
|
||||
echo Failed to unmount old drive [%drive%\]: %errorlevel%.
|
||||
exit /b %errorlevel%
|
||||
) else (
|
||||
echo Drive [%drive%\] hasn't been mapped. Mapping device!
|
||||
)
|
||||
) else (
|
||||
echo Unmounted old device. Mapping new device!
|
||||
)
|
||||
rem /persistent:yes
|
||||
net use z: \\deploy.teaspeak.de\symbols T4j7CTADvCMev4Kr /user:deploy.teaspeak.de\TeaSpeak-Jenkins-Client
|
||||
if %errorlevel% neq 0 (
|
||||
echo Failed to mount drive [%drive%\]: %errorlevel%
|
||||
exit /b %errorlevel%
|
||||
) else (
|
||||
echo New device has been mapped successfully!
|
||||
)
|
51
installer/deploy/ssh_key
Normal file
51
installer/deploy/ssh_key
Normal file
@ -0,0 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIJKAIBAAKCAgEAnr2tvPgMv9uLdFFHCV6sX9uUZyK57cQNHLSVK/OGjdilf95q
|
||||
RMjI8AkYxM0nc9ljnVNPuhSYWSASTCeCPaBS/VkvUn3krEJQw1Z9GPOWbZo0vLbE
|
||||
LzhZ8uyCgdOZSuWJpHui4Tchz10pxcKACi5wL4MFaouoJUcGX1MgFJ1Dcd/BxuVj
|
||||
1hKrOYLBJy1cDuocmP4aTu8mgtzluRMieTjH1fRq7SAbJDx5RmzsEy5S6+6XDuLq
|
||||
uidXbsoM3GEYhcIHjtBTcbKVsOLmelqR/byGUSFP0tmOLcq85nGuOf0Uccb1DuKA
|
||||
RDZ53OfsGus2/Gbb0z9nipOtEkP6abgvhas3VNWI+JQ+YhLnwfBPy7hNytQFVN5q
|
||||
Q0+HaQ1SXdp+Ze3moKPqCHXrlbCvk8FpQoGhzEIteP/xq6DPeHfHZ/T7mH6T8JzF
|
||||
H3HfGuAfaDMk6f1dg5jHs1L+vaWtWI3SZohj/LeN31TQsPJJbQ9ltK2Eu1hsy9mY
|
||||
BAXv39yR+os1gpsx5imIQfVMFQ5Fg8W2lNyXjn2S/zWfY76G0vEDOomE4BhMjd2b
|
||||
Tn2ZzpAba9bPfgmyv6Kjo4mDXv+qVddHzsivKaV3u7bTAYZMpekcEDs7wLPHKC6+
|
||||
Ca4Z0jXXEtwAeqW/wqop3Hi5gNruj4+FEWsRvC/431AxXnmyo1WEenx27o0CAwEA
|
||||
AQKCAgBvKjfsGO2cwhuj5nNPzXv7WSNRIpGAP4ZLLu80K0N0PF6R8GkNKGsHJeex
|
||||
klXpnDhVaY1wq5GRAJOvvw9HJupXP0iThVRJidtTIFNU0OjsckwyR++VfeoH47b1
|
||||
QgCc4agFhwummlxUAlMJic7u0lx/+UomtgyXpmiBAw55QTSFH4RtTCEhPkuoZ7fq
|
||||
Pqq0SyChx3kXBAU9KYK6m/rNV4UigRsMWiqHss3fEtI0EIHDdX9VznVAzeI91MMy
|
||||
5dAtg5aVXbDB86U+jXeIAbsxLQAG+sQSzYXy7YJiAwRW2bOihgkBVn8qxdeLauL+
|
||||
avBDy6hwBHv3ILnYC8DmnjSxcBi7do/MNIEm5DEs90Lt8Oyh033e+riw7NpLyzY0
|
||||
ZFJ/ScfLAzq/UIooCSGIIbpdPs7c8oER6kPj4X5Gv1unh3u9A0ZGGcrDtxS/f/eo
|
||||
/05ohAFwb31v4KM4eCvHqoaSqKVypFplrslObqxgPQsR/Ecw+gezBAqYmJnbMVRU
|
||||
8RjNnPGbLulG4HnXxA2dLpqOEU7/FJN7bN8U7rdCqcm/WDbBlVUhNm7jjzDr2OFU
|
||||
exJMRZvXfRgdqgk59+RPizanRQIMs7GULCtVcJGWlSUfdo2KIuhfFJ/LVIcgm7oo
|
||||
dFTZVGMOc2KqoRfBnZz6yr74HcexHSlmQZSfIoQhbiiDNexOIQKCAQEA0UMDYrum
|
||||
FVzkisI0QSdq3h/9KFexYnp4ZYYP83FaZl2js7PAYIIcRcXxVb/i9AJx38FmPH9s
|
||||
1uMb/PyLGdHGF/qvYrjYqZLZ64eKVlnQGWcU5eOQ9560l8Je5FUfR9IwC3LTG+3R
|
||||
3QByPANOn19D0GunCJ7cTmwdoedTGPeUxyK7ZTodXn3YHBikLX9atroiRkMe+h4U
|
||||
fg8R2WV8Hul1PPcVqQ3Lz2aNm1mjfVe+nOghcNrmcI0t03BdXCCTcz3NLTJxwAeC
|
||||
4TcXlENUo78OcxsZj4hq84xhauN3npT7sa/5JGcC7c7N+EULh/CTH/CK+zsFUYfD
|
||||
IHC88Vu9fnXcVQKCAQEAwjIHcVkru0apkF65f1AVIyEQJ2sWpndHslAAfSMxEUbL
|
||||
sMOii5QH5MKA8LRAGpDEoZY2XZrrOpuvnvxKIJt7Ijj4wfYF8ffYTyI0oK7gsR6v
|
||||
nvY7i+n18pzHZFJLKq5Z9n4zgEi2YBj802v0GfQyCjk3+8we7Man/Uz4jYZr8nxZ
|
||||
YPy+viBLP5nGp4R7y6nef93prfTL6iApXyr3MdFDMnJBGmiKNGW9UrpKFyFkwvE4
|
||||
7sdHUXj2JW/AM8P7CeNA3x2GQ1l0qS8QJ3R0X9GzTeq5tuIKUH2mDrPiBXisRG6k
|
||||
IYwKb0l8iv7sTLyfMv60T5uUCrS8hompHDtWsh0BWQKCAQBMSOSsEoIaGZIK738D
|
||||
HW586SZtlYJJxyGqyPN5qKHu3UX3FZkU1XmfCejPfLMshtOiYSt29HDl6Ubjs+C1
|
||||
md5gEXfsQjxhnPIqRW/tyLHvAMACijHnwwhMpoPXMxzDHuF62vIQpWKy8R2zuPTp
|
||||
bl4XVZc/skHXqNwokF6fpGmtKoEsBsJ8Ft44Z9c56spUAIjMGl3pihuoVLAKE0/r
|
||||
KOofPme8CBZ7VgRbVJMf92O6aXj/Xh1RfHXvNXAjTJDUGvx39IK5IUPZ/C5xUxZA
|
||||
1z5aQc/Qnkd2338H60JJIkCa5u6pEZBkxtYZInpwpQfNRfA0Y7CtpxM/+Tk3t1ze
|
||||
A/M9AoIBABmp7Ovg4fOk+gG3UwJtPe3fj7f14g9r0hDRm87t2K000vRwVknl7Ukh
|
||||
H1MwLwyTtzi3lkW2lIGxU3tKUi2O/q3eI5nWfqCkpXSHy7a0hcNCj+kNF399EuDW
|
||||
MU+jxIVGd2Mo+HtqoJeAleEG8kJ/0CEjwK9JIYkfE9JY2rwxWJC6OEGmBTsxH2Cv
|
||||
XN6ElquqrlntpNU1dcFiMLWAAx0VT7EaAlqQGDumeme1cNcvtZZBtMlxko5E0xrN
|
||||
cvQkYUfEPa1+xGCgMNeu/Y6JSFvlZbHVZGez5bMPd+OXiDY65WFB0fURAcwFRS1F
|
||||
VUsq3ksp+ABRSjZD/mo1RSETAnkVdjkCggEBAJwC6yt1x9yIAh9RL6AKN2VUAopJ
|
||||
Ot2vnAp6dHMEwkKQY3hseAJx7YkAb5KaTpEZ50VhXOxe5lOz+4eagXL2CelZG1xN
|
||||
jDTjIsReQflGn+jwYT1w35u+CPnDCizlA82cLaR5dlaOc/Np3hKWzvc6sAXzjmxN
|
||||
05+YfB70gbz/u4GE+KFiylArQfJ+2WRxoyUpy3YmbOq7LhadV7GOzYb2ufYgv1cs
|
||||
US58bqWNx9NXSnJQ1uPDUaaSSfUvGJVJHd602h5m3PLxGIzBFrSsgwx9r6qLXPZ3
|
||||
P6XiwAuMOqqEbFfBtJnCxUta9hNoAg4Vo8NV89c92uSFSEJ4xjlKHwOu0U8=
|
||||
-----END RSA PRIVATE KEY-----
|
1
installer/deploy/ssh_key.pub
Normal file
1
installer/deploy/ssh_key.pub
Normal file
@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCeva28+Ay/24t0UUcJXqxf25RnIrntxA0ctJUr84aN2KV/3mpEyMjwCRjEzSdz2WOdU0+6FJhZIBJMJ4I9oFL9WS9SfeSsQlDDVn0Y85ZtmjS8tsQvOFny7IKB05lK5Ymke6LhNyHPXSnFwoAKLnAvgwVqi6glRwZfUyAUnUNx38HG5WPWEqs5gsEnLVwO6hyY/hpO7yaC3OW5EyJ5OMfV9GrtIBskPHlGbOwTLlLr7pcO4uq6J1duygzcYRiFwgeO0FNxspWw4uZ6WpH9vIZRIU/S2Y4tyrzmca45/RRxxvUO4oBENnnc5+wa6zb8ZtvTP2eKk60SQ/ppuC+FqzdU1Yj4lD5iEufB8E/LuE3K1AVU3mpDT4dpDVJd2n5l7eago+oIdeuVsK+TwWlCgaHMQi14//GroM94d8dn9PuYfpPwnMUfcd8a4B9oMyTp/V2DmMezUv69pa1YjdJmiGP8t43fVNCw8kltD2W0rYS7WGzL2ZgEBe/f3JH6izWCmzHmKYhB9UwVDkWDxbaU3JeOfZL/NZ9jvobS8QM6iYTgGEyN3ZtOfZnOkBtr1s9+CbK/oqOjiYNe/6pV10fOyK8ppXe7ttMBhkyl6RwQOzvAs8coLr4JrhnSNdcS3AB6pb/CqinceLmA2u6Pj4URaxG8L/jfUDFeebKjVYR6fHbujQ== wolverindev@WolverinDEV
|
178
installer/package.ts
Normal file
178
installer/package.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import * as fs from "fs-extra";
|
||||
import * as tar from "tar-stream";
|
||||
import * as zlib from "zlib";
|
||||
import * as path from "path";
|
||||
import * as asar from "asar";
|
||||
|
||||
import {Pack} from "tar-stream";
|
||||
import * as request from "request";
|
||||
import {Version} from "../modules/shared/version";
|
||||
|
||||
async function append_dir(parent: string, path: string, pack: Pack, excludes: (string | RegExp)[]) {
|
||||
const entries = await fs.readdir(parent + "/" + path);
|
||||
for(let entry of entries) {
|
||||
console.log(entry);
|
||||
const stat = await fs.stat(parent + "/" + path + "/" + entry);
|
||||
if(stat.isDirectory()) {
|
||||
console.log("Add sub: %s", entry);
|
||||
await append_dir(parent, path + "/" + entry, pack, excludes);
|
||||
} else {
|
||||
let exclude = false;
|
||||
for(const pattern of excludes) {
|
||||
if((path + "/" + entry).match(pattern)) {
|
||||
console.log("Excluding file %s", path + "/" + entry);
|
||||
exclude = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(exclude) continue;
|
||||
|
||||
let pentry = pack.entry({
|
||||
name: path + "/" + entry,
|
||||
size: stat.size,
|
||||
mode: stat.mode
|
||||
}, error => {
|
||||
if(error) throw error;
|
||||
});
|
||||
|
||||
if(!pentry) throw "Failed to create new file";
|
||||
|
||||
const pipe = fs.createReadStream(parent + "/" + path + "/" + entry).pipe(pentry);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
pipe.on('finish', resolve);
|
||||
pipe.on('error', reject);
|
||||
});
|
||||
pentry.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function pack_update(source: string, dest: string) : Promise<string> {
|
||||
await fs.mkdirs(path.dirname(dest));
|
||||
const target = fs.createWriteStream(dest);
|
||||
const pack = tar.pack();
|
||||
const compress = zlib.createGzip();
|
||||
|
||||
pack.pipe(compress).pipe(target);
|
||||
|
||||
await append_dir(source, ".", pack, [/.\/app_versions\/.*/]); ///.\/postzip($|.exe)/,
|
||||
pack.finalize();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
target.on('close', resolve);
|
||||
target.on('error', reject);
|
||||
});
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
export async function pack_info(src: string) : Promise<any> {
|
||||
const appAsarPath = path.join(src, 'resources/app.asar');
|
||||
const appPackageJSONPath = path.join(src, 'resources/app/package.json');
|
||||
|
||||
if(await fs.pathExists(appAsarPath))
|
||||
return JSON.parse(asar.extractFile(appAsarPath, "package.json"));
|
||||
else
|
||||
return await fs.readJson(appPackageJSONPath);
|
||||
}
|
||||
|
||||
interface InfoEntry {
|
||||
platform: string;
|
||||
arch: string;
|
||||
update: string;
|
||||
install: string;
|
||||
}
|
||||
|
||||
export async function write_info(file: string, platform: string, arch: string, update_file: string, install_file: string) {
|
||||
let infos: InfoEntry[] = fs.existsSync(file) ? await fs.readJson(file) as InfoEntry[] : [];
|
||||
for(const entry of infos.slice()) {
|
||||
if(entry.platform == platform && entry.arch == arch)
|
||||
infos.splice(infos.indexOf(entry),1);
|
||||
}
|
||||
infos.push({
|
||||
"platform": platform,
|
||||
"arch": arch,
|
||||
"update": update_file,
|
||||
"install": install_file
|
||||
});
|
||||
await fs.writeJson(file, infos);
|
||||
}
|
||||
|
||||
interface VersionFile {
|
||||
release: VersionEntry[];
|
||||
beta: VersionEntry[];
|
||||
}
|
||||
|
||||
interface VersionEntry {
|
||||
platform: string;
|
||||
arch: string;
|
||||
version: Version;
|
||||
}
|
||||
export async function write_version(file: string, platform: string, arch: string, channel: string, version: Version) {
|
||||
let versions: VersionFile = fs.existsSync(file) ? await fs.readJson(file) as VersionFile : {} as any;
|
||||
|
||||
versions[channel] = versions[channel] || [];
|
||||
let channel_data = versions[channel];
|
||||
|
||||
for(const entry of channel_data.slice()) {
|
||||
if(entry.platform == platform && entry.arch == arch)
|
||||
channel_data.splice(channel_data.indexOf(entry), 1);
|
||||
}
|
||||
|
||||
channel_data.push({
|
||||
platform: platform,
|
||||
arch: arch,
|
||||
version: version
|
||||
});
|
||||
await fs.writeJson(file, versions);
|
||||
}
|
||||
export async function deploy(platform: string, arch: string, channel: string, version: Version, update_file: string, install_file: string, install_suffix: string) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const url = (process.env["teaclient_deploy_url"] || "http://clientapi.teaspeak.de/") + "api.php";
|
||||
console.log("Requesting " + url);
|
||||
console.log("Uploading update file " + update_file);
|
||||
console.log("Uploading install file " + install_file);
|
||||
console.log("Secret (env key: teaclient_deploy_secret): " + process.env["teaclient_deploy_secret"]);
|
||||
if(!process.env["teaclient_deploy_secret"]) throw "Missing secret!";
|
||||
|
||||
request.post(url, {
|
||||
formData: {
|
||||
type: "deploy-build",
|
||||
secret: process.env["teaclient_deploy_secret"],
|
||||
|
||||
platform: platform,
|
||||
arch: arch,
|
||||
version: JSON.stringify(version),
|
||||
channel: channel,
|
||||
|
||||
update: fs.createReadStream(update_file),
|
||||
update_suffix: "tar.gz",
|
||||
|
||||
installer: fs.createReadStream(install_file),
|
||||
installer_suffix: install_suffix
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
if(error) {
|
||||
console.error("Failed to upload:");
|
||||
console.error(error);
|
||||
throw "Failed to upload: " + error;
|
||||
}
|
||||
console.log("Response code: " + (response ? response.statusCode : 0));
|
||||
let info;
|
||||
if(response && response.statusCode == 413) {
|
||||
info = {msg: "Files too large! Increase limits!"};
|
||||
} else {
|
||||
try {
|
||||
info = JSON.parse(body);
|
||||
} catch (error) {
|
||||
info = {};
|
||||
console.dir(body);
|
||||
}
|
||||
}
|
||||
console.dir(info);
|
||||
if(!info["success"]) throw info["msg"] || "Could not deploy files!";
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
}
|
78
installer/package_linux.ts
Normal file
78
installer/package_linux.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import * as installer from "electron-installer-debian";
|
||||
import * as packager from "./package";
|
||||
import {parse_version, Version} from "../modules/shared/version";
|
||||
|
||||
const package_path = "build/TeaClient-linux-x64/";
|
||||
const filename_update = "TeaClient-linux-x64.tar.gz";
|
||||
|
||||
let options = {
|
||||
src: package_path,
|
||||
dest: undefined,
|
||||
dest_file: undefined,
|
||||
arch: 'amd64',
|
||||
|
||||
rename: (directory, name) => {
|
||||
console.log("Destination directory: " + directory);
|
||||
console.log("Destination name : " + name);
|
||||
options.dest_file = directory + "/" + name;
|
||||
return directory + "/" + name;
|
||||
},
|
||||
options: {
|
||||
name: "TeaClient",
|
||||
productName: "TeaClient",
|
||||
genericName: "TeaSpeak - Client",
|
||||
description: "TeaClient by TeaSpeak",
|
||||
version: undefined,
|
||||
homepage: "https://teaspeak.de",
|
||||
maintainer: "WolverinDEV <client@teaspeak.de>",
|
||||
|
||||
icon: 'resources/logo.svg',
|
||||
categories: [
|
||||
"Utility"
|
||||
],
|
||||
bin: 'TeaClient'
|
||||
}
|
||||
};
|
||||
|
||||
if(process.argv.length < 3) {
|
||||
console.error("Missing build channel!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let version: Version;
|
||||
const alive = setInterval(() => {}, 1000);
|
||||
packager.pack_info(package_path).then(package_info => {
|
||||
options.options.version = (version = parse_version(package_info["version"])).toString();
|
||||
options.dest = "build/output/" + process.argv[2] + "/" + options.options.version + "/";
|
||||
console.log('Creating package for version ' + options.options.version + ' (this may take a while)');
|
||||
|
||||
//return Promise.resolve();
|
||||
return installer(options);
|
||||
}).then(() => {
|
||||
if(!options.dest_file)
|
||||
options.dest_file = options.dest + "TeaClient_" + options.options.version + "_amd64.deb";
|
||||
|
||||
console.log(`Successfully created package at ${options.dest} (${options.dest_file})`);
|
||||
return packager.pack_update(options.src, options.dest + "/" + filename_update);
|
||||
}).then(() => {
|
||||
return packager.write_info(options.dest + "info.json", "linux", "x64", filename_update, options.dest_file)
|
||||
}).then(() => {
|
||||
return packager.write_version("build/output/version.json", "linux", "x64", process.argv[2], version);
|
||||
}).then(() => {
|
||||
console.log("Deploying symbol files");
|
||||
//FIXME!
|
||||
}).then(() => {
|
||||
//Fixup in case of skip of the packaging
|
||||
|
||||
console.log("Deploying build");
|
||||
return packager.deploy("linux", "x64", process.argv[2], version, options.dest + filename_update, options.dest_file, "deb");
|
||||
}).then(() => {
|
||||
console.log("Build version (" + options.options.version + ") created!");
|
||||
clearInterval(alive);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to pack package!");
|
||||
console.error(err, err.stack);
|
||||
process.exit(1)
|
||||
});
|
||||
|
88
installer/package_windows.ts
Normal file
88
installer/package_windows.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import * as packager from "./package";
|
||||
import * as deployer from "./deploy";
|
||||
import {parse_version, Version} from "../modules/shared/version";
|
||||
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const util = require('util');
|
||||
const cproc = require('child_process');
|
||||
const proc = require('process');
|
||||
const ejs = require('ejs');
|
||||
const exec = util.promisify(cproc.exec);
|
||||
const ejs_render = util.promisify(ejs.renderFile);
|
||||
|
||||
const filename_update = "TeaClient-windows-x64.tar.gz";
|
||||
const filename_installer = "TeaClient-windows-x64.exe";
|
||||
const package_path = "build/TeaClient-win32-x64/";
|
||||
const symbol_pdb_path = "native/build/symbols/";
|
||||
const symbol_binary_path = package_path + "/resources/natives/";
|
||||
let dest_path = undefined;
|
||||
let info;
|
||||
let version: Version;
|
||||
async function make_template() : Promise<string> {
|
||||
const content = await ejs_render("installer/WinInstall.ejs", {
|
||||
source_dir: path.resolve(package_path) + "/*",
|
||||
dest_dir: path.resolve(dest_path),
|
||||
icon_file: path.resolve("resources/logo.ico"),
|
||||
version: info["version"],
|
||||
executable_name: filename_installer.substr(0, filename_installer.length - 4) //Remove the .exe
|
||||
}, {});
|
||||
|
||||
await fs.mkdirs(dest_path);
|
||||
fs.writeFileSync(dest_path + "/" + "installer.iss", content);
|
||||
return dest_path + "/" + "installer.iss";
|
||||
}
|
||||
|
||||
async function make_installer(path: string) {
|
||||
console.log("Compiling path %s", path);
|
||||
const { stdout, stderr } = await exec("\"C:\\Program Files (x86)\\Inno Setup 5\\iscc.exe\" " + path, {maxBuffer: 1024 * 1024 * 1024}); //FIXME relative path?
|
||||
}
|
||||
if(process.argv.length < 3) {
|
||||
console.error("Missing build channel!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
packager.pack_info(package_path).then(async _info => {
|
||||
info = _info;
|
||||
version = parse_version(_info["version"]);
|
||||
dest_path = "build/output/" + process.argv[2] + "/" + version.toString() + "/";
|
||||
await packager.pack_update(package_path, dest_path + "TeaClient-windows-x64.tar.gz");
|
||||
}).then(async () => {
|
||||
await packager.write_info(dest_path + "info.json", "win32", "x64", filename_update, filename_installer)
|
||||
}).then(async () => {
|
||||
await packager.write_version("build/output/version.json", "win32", "x64", process.argv[2], version);
|
||||
}).then(async () => await make_template())
|
||||
.then(async path => await make_installer(path))
|
||||
.then(async () => {
|
||||
console.log("Deploying PDB files");
|
||||
const files = [];
|
||||
for(const file of await fs.readdir(symbol_binary_path)) {
|
||||
if(!file.endsWith(".node"))
|
||||
continue;
|
||||
let file_name = path.basename(file);
|
||||
if(file_name.endsWith(".node"))
|
||||
file_name = file_name.substr(0, file_name.length - 5);
|
||||
const binary_path = path.join(symbol_binary_path, file);
|
||||
const pdb_path = path.join(symbol_pdb_path, file_name + ".pdb");
|
||||
if(!fs.existsSync(pdb_path)) {
|
||||
console.warn("Missing PDB file for binary %s", file);
|
||||
continue;
|
||||
}
|
||||
files.push({
|
||||
binary: binary_path,
|
||||
pdb: pdb_path
|
||||
});
|
||||
}
|
||||
await deployer.deploy_win_dbg_files(files, version);
|
||||
console.log("PDB files deployed");
|
||||
})
|
||||
.then(async () => {
|
||||
console.log("Deploying build");
|
||||
await packager.deploy("win32", "x64", process.argv[2], version, dest_path + filename_update, dest_path + filename_installer, "exe");
|
||||
}).then(() => {
|
||||
console.log("Succeed");
|
||||
proc.exit(0);
|
||||
}).catch(error => {
|
||||
console.log(error);
|
||||
proc.exit(1);
|
||||
});
|
11
libraries.txt
Normal file
11
libraries.txt
Normal file
@ -0,0 +1,11 @@
|
||||
https://git.assembla.com/portaudio.git
|
||||
https://github.com/WolverinDEV/libfvad
|
||||
|
||||
|
||||
git clone https://git.code.sf.net/p/soxr/code soxr
|
||||
mkdir -p out/linux_x64
|
||||
cd out/linux_x674
|
||||
|
||||
cmake ../../ -DWITH_OPENMP=OFF -DBUILD_TESTS=OFF -DBUILD_SHARED_LIBS=OFF -DCMAKE_C_FLAGS="-fPIC"
|
||||
make -j 12
|
||||
sudo make install
|
30
main.ts
Normal file
30
main.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import * as electron from "electron";
|
||||
import * as os from "os";
|
||||
|
||||
{
|
||||
const app_path = electron.app.getAppPath();
|
||||
console.log("Native module path: %s", app_path + "/native/build/" + os.platform() + "_" + os.arch() + "/");
|
||||
}
|
||||
|
||||
import * as crash_handler from "./modules/crash_handler";
|
||||
|
||||
const is_electron_run = process.argv[0].endsWith("electron") || process.argv[0].endsWith("electron.exe");
|
||||
const process_arguments = is_electron_run ? process.argv.slice(2) : process.argv.slice(1);
|
||||
if(process_arguments.length > 0 && process_arguments[0] === "crash-handler") {
|
||||
/* crash handler callback */
|
||||
crash_handler.handle_crash_callback(process_arguments.slice(1));
|
||||
} else {
|
||||
crash_handler.initialize_handler("main", is_electron_run);
|
||||
/* app execute */
|
||||
{
|
||||
const versions = process.versions;
|
||||
console.log("Versions:");
|
||||
console.log(" TeaSpeak Client: " + electron.app.getVersion());
|
||||
|
||||
for (const key of Object.keys(versions))
|
||||
console.log(" %s: %s", key, versions[key]);
|
||||
}
|
||||
|
||||
const tea_client = require("./modules/core/main.js");
|
||||
tea_client.execute();
|
||||
}
|
42
modules/core/app-updater/changelog/index.ts
Normal file
42
modules/core/app-updater/changelog/index.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import {BrowserWindow} from "electron";
|
||||
import * as electron from "electron";
|
||||
import * as path from "path";
|
||||
|
||||
let changelog_window: BrowserWindow;
|
||||
export function open() {
|
||||
if(changelog_window) {
|
||||
changelog_window.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
changelog_window = new BrowserWindow({
|
||||
show: false
|
||||
});
|
||||
|
||||
changelog_window.setMenu(null);
|
||||
|
||||
let file = "";
|
||||
{
|
||||
const app_path = electron.app.getAppPath();
|
||||
if(app_path.endsWith(".asar"))
|
||||
file = path.join(path.dirname(app_path), "..", "ChangeLog.txt");
|
||||
else
|
||||
file = path.join(app_path, "github", "ChangeLog.txt"); /* We've the source master :D */
|
||||
}
|
||||
|
||||
changelog_window.loadFile(file);
|
||||
changelog_window.setTitle("TeaClient ChangeLog");
|
||||
changelog_window.on('ready-to-show', () => {
|
||||
changelog_window.show();
|
||||
});
|
||||
changelog_window.on('close', () => {
|
||||
changelog_window = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
export function close() {
|
||||
if(changelog_window) {
|
||||
changelog_window.close();
|
||||
changelog_window = undefined;
|
||||
}
|
||||
}
|
860
modules/core/app-updater/index.ts
Normal file
860
modules/core/app-updater/index.ts
Normal file
@ -0,0 +1,860 @@
|
||||
import * as querystring from "querystring";
|
||||
import * as request from "request";
|
||||
import {app, dialog, ipcMain} from "electron";
|
||||
import * as fs from "fs-extra";
|
||||
import * as ofs from "original-fs";
|
||||
import * as os from "os";
|
||||
import * as tar from "tar-stream";
|
||||
import * as path from "path";
|
||||
import * as zlib from "zlib";
|
||||
import * as child_process from "child_process";
|
||||
import * as progress from "request-progress";
|
||||
import * as util from "util";
|
||||
|
||||
import {parse_version, Version} from "../../shared/version";
|
||||
|
||||
import Timer = NodeJS.Timer;
|
||||
import MessageBoxOptions = Electron.MessageBoxOptions;
|
||||
import {Headers} from "tar-stream";
|
||||
import {Arguments, process_args} from "../../shared/process-arguments";
|
||||
import * as electron from "electron";
|
||||
import {PassThrough} from "stream";
|
||||
import {prevent_instant_close} from "../../core/main_window";
|
||||
import ErrnoException = NodeJS.ErrnoException;
|
||||
import {EPERM} from "constants";
|
||||
|
||||
const is_debug = false;
|
||||
export function server_url() : string {
|
||||
const default_path = is_debug ? "http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/client-api/environment/" : "http://clientapi.teaspeak.de/";
|
||||
return process_args.has_value(...Arguments.SERVER_URL) ? process_args.value(...Arguments.SERVER_URL) : default_path;
|
||||
}
|
||||
|
||||
export interface UpdateVersion {
|
||||
channel: string;
|
||||
platform: string,
|
||||
arch: string;
|
||||
version: Version;
|
||||
}
|
||||
|
||||
export interface UpdateData {
|
||||
versions: UpdateVersion[];
|
||||
updater_version: UpdateVersion;
|
||||
}
|
||||
|
||||
let version_cache: UpdateData = undefined;
|
||||
export async function load_data(allow_cached: boolean = true) : Promise<UpdateData> {
|
||||
if(version_cache && allow_cached) return Promise.resolve(version_cache);
|
||||
|
||||
return new Promise<UpdateData>((resolve, reject) => {
|
||||
const request_url = server_url() + "/api.php?" + querystring.stringify({
|
||||
type: "update-info"
|
||||
});
|
||||
console.log("request: %s", request_url);
|
||||
request.get(request_url, {
|
||||
timeout: 2000
|
||||
}, (error, response, body) => {
|
||||
if(!response || response.statusCode != 200) {
|
||||
let info;
|
||||
try {
|
||||
info = JSON.parse(body) || {msg: error};
|
||||
} catch(e) {
|
||||
info = {msg: "!-- failed to parse json --!"};
|
||||
}
|
||||
setImmediate(reject, "Invalid status code (" + (response || {statusCode: -1}).statusCode + " | " + (info || {msg: "undefined"}).msg + ")");
|
||||
return;
|
||||
}
|
||||
const data = JSON.parse(body);
|
||||
if(!data) {
|
||||
setImmediate(reject, "Invalid response");
|
||||
return;
|
||||
}
|
||||
if(!data["success"]) {
|
||||
setImmediate(reject, "Action failed (" + data["msg"] + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
let resp: UpdateData = {} as any;
|
||||
resp.versions = [];
|
||||
for(const channel of Object.keys(data)) {
|
||||
if(channel == "success") continue;
|
||||
|
||||
for(const entry of data[channel]) {
|
||||
let version: UpdateVersion = {} as any;
|
||||
version.channel = channel;
|
||||
version.arch = entry["arch"];
|
||||
version.platform = entry["platform"];
|
||||
version.version = new Version(entry["version"]["major"], entry["version"]["minor"], entry["version"]["patch"], entry["version"]["build"], entry["version"]["timestamp"]);
|
||||
if(version.channel == 'updater')
|
||||
resp.updater_version = version;
|
||||
else
|
||||
resp.versions.push(version);
|
||||
}
|
||||
}
|
||||
resolve(resp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function newest_version(current_version: Version, channel?: string) : Promise<UpdateVersion | undefined> {
|
||||
if(!app.getAppPath().endsWith(".asar")) {
|
||||
throw "You cant run an update when you're executing the source code!";
|
||||
}
|
||||
const data = await load_data();
|
||||
let had_data = false;
|
||||
for(const version of data.versions) {
|
||||
if(version.arch == os.arch() && version.platform == os.platform()) {
|
||||
if(!channel || version.channel == channel) {
|
||||
if(!current_version || version.version.newer_than(current_version))
|
||||
return version;
|
||||
else
|
||||
had_data = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!had_data)
|
||||
throw "Missing data";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function extract_updater(update_file: string) {
|
||||
if(!fs.existsSync(update_file)) throw "Missing update file!";
|
||||
|
||||
|
||||
let parent_path = app.getAppPath();
|
||||
if(parent_path.endsWith(".asar")) {
|
||||
parent_path = path.join(parent_path, "..", "..");
|
||||
parent_path = fs.realpathSync(parent_path);
|
||||
}
|
||||
|
||||
let post_path;
|
||||
if(os.platform() == "linux")
|
||||
post_path = parent_path + "/update-installer";
|
||||
else
|
||||
post_path = parent_path + "/update-installer.exe";
|
||||
|
||||
const source = fs.createReadStream(update_file);
|
||||
const extract = tar.extract();
|
||||
await new Promise(resolve => {
|
||||
let updater_found = false;
|
||||
source.on('end', () => {
|
||||
if(!updater_found) {
|
||||
console.error("Failed to extract the updater (Updater hasn't been found!)");
|
||||
resolve(); //FIXME use reject!
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
extract.on('entry', (header: Headers, stream, callback) => {
|
||||
stream.on('end', callback);
|
||||
console.log("Got entry " + header.name);
|
||||
|
||||
if(header.name == "./update-installer" || header.name == "./update-installer.exe") {
|
||||
console.log("Found updater! (" + header.size + ")");
|
||||
console.log("Extracting to %s", post_path);
|
||||
const s = fs.createWriteStream(post_path);
|
||||
stream.pipe(s).on('finish', event => {
|
||||
console.log("Updater extracted and written!");
|
||||
updater_found = true;
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
stream.resume(); //Drain the stream
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
source.pipe(extract);
|
||||
});
|
||||
}
|
||||
|
||||
export async function update_updater() : Promise<void> {
|
||||
//TODO here
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function data_directory() : string {
|
||||
return electron.app.getPath('userData');
|
||||
}
|
||||
|
||||
function get_update_file(channel: string, version: Version) : string {
|
||||
let _path = fs.realpathSync(data_directory());
|
||||
|
||||
const name = channel + "_" + version.major + "_" + version.minor + "_" + version.patch + "_" + version.build + ".tar";
|
||||
return path.join(_path, "app_versions", name);
|
||||
}
|
||||
|
||||
export interface ProgressState {
|
||||
percent: number, // Overall percent (between 0 to 1)
|
||||
speed: number, // The download speed in bytes/sec
|
||||
size: {
|
||||
total: number, // The total payload size in bytes
|
||||
transferred: number// The transferred payload size in bytes
|
||||
},
|
||||
time: {
|
||||
elapsed: number,// The total elapsed seconds since the start (3 decimals)
|
||||
remaining: number // The remaining seconds to finish (3 decimals)
|
||||
}
|
||||
}
|
||||
|
||||
export async function download_version(channel: string, version: Version, status?: (state: ProgressState) => any) : Promise<string> {
|
||||
const target_path = get_update_file(channel, version);
|
||||
console.log("Downloading version %s to %s", version.toString(false), target_path);
|
||||
if(fs.existsSync(target_path)) {
|
||||
/* TODO test if this file is valid and can be used */
|
||||
try {
|
||||
await fs.remove(target_path);
|
||||
} catch(error) {
|
||||
throw "Failed to remove old file: " + error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.mkdirp(path.dirname(target_path));
|
||||
} catch(error) {
|
||||
throw "Failed to make target directory: " + path.dirname(target_path);
|
||||
}
|
||||
|
||||
const url = server_url() + "/api.php?" + querystring.stringify({
|
||||
type: "update-download",
|
||||
platform: os.platform(),
|
||||
arch: os.arch(),
|
||||
version: version.toString(),
|
||||
channel: channel
|
||||
});
|
||||
console.log("Downloading update from %s. (%s)", server_url(), url);
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let fired = false;
|
||||
let stream = progress(request.get(url, {
|
||||
timeout: 2000
|
||||
}, (error, response, body) => {
|
||||
if(!response || response.statusCode != 200) {
|
||||
let info;
|
||||
try {
|
||||
info = JSON.parse(body)
|
||||
} catch(e) {
|
||||
info = {"msg": "!-- failed to parse json --!"};
|
||||
}
|
||||
if(!fired && (fired = true))
|
||||
setImmediate(reject, "Invalid status code (" + (response || {statusCode: -1}).statusCode + "|" + (info || {"msg": "undefined"}).msg + ")");
|
||||
return;
|
||||
}
|
||||
})).on('progress', _state => status ? status(_state) : {}).on('error', error => {
|
||||
console.warn("Encountered error within download pipe. Ignoring error: %o", error);
|
||||
}).on('end', function () {
|
||||
console.log("Update downloaded successfully. Waiting for write stream to finish.");
|
||||
if(status)
|
||||
status({
|
||||
percent: 1,
|
||||
speed: 0,
|
||||
size: { total: 0, transferred: 0},
|
||||
time: { elapsed: 0, remaining: 0}
|
||||
})
|
||||
});
|
||||
|
||||
console.log("Decompressing update package while streaming!");
|
||||
stream = stream.pipe(zlib.createGunzip());
|
||||
stream.pipe(fs.createWriteStream(target_path, {
|
||||
autoClose: true
|
||||
})).on('finish', () => {
|
||||
console.log("Write stream has finished. Download successfully.");
|
||||
if(!fired && (fired = true))
|
||||
setImmediate(resolve, target_path);
|
||||
}).on('error', error => {
|
||||
console.log("Write stream encountered an error while downloading update. Error: %o", error);
|
||||
if(!fired && (fired = true))
|
||||
setImmediate(reject,"failed to write");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if(typeof(String.prototype.trim) === "undefined")
|
||||
{
|
||||
String.prototype.trim = function()
|
||||
{
|
||||
return String(this).replace(/^\s+|\s+$/g, '');
|
||||
};
|
||||
}
|
||||
|
||||
export async function test_file_accessibility(update_file: string) : Promise<string[]> {
|
||||
if(os.platform() === "win32")
|
||||
return []; /* within windows the update installer request admin privileges if required */
|
||||
|
||||
const original_fs = require('original-fs');
|
||||
if(!fs.existsSync(update_file))
|
||||
throw "Missing update file (" + update_file + ")";
|
||||
|
||||
let parent_path = app.getAppPath();
|
||||
if(parent_path.endsWith(".asar")) {
|
||||
parent_path = path.join(parent_path, "..", "..");
|
||||
parent_path = fs.realpathSync(parent_path);
|
||||
}
|
||||
|
||||
const test_access = async (file: string, mode: number) => {
|
||||
return await new Promise<NodeJS.ErrnoException>(resolve => original_fs.access(file, mode, resolve));
|
||||
};
|
||||
|
||||
let code = await test_access(update_file, original_fs.constants.R_OK);
|
||||
if(code)
|
||||
throw "Failed test read for update file. (" + update_file + " results in " + code.code + ")";
|
||||
|
||||
const fstream = original_fs.createReadStream(update_file);
|
||||
const tar_stream = tar.extract();
|
||||
|
||||
const errors: string[] = [];
|
||||
const tester = async (header: Headers) => {
|
||||
const entry_path = path.normalize(path.join(parent_path, header.name));
|
||||
if(header.type == "file") {
|
||||
if(original_fs.existsSync(entry_path)) {
|
||||
code = await test_access(entry_path, original_fs.constants.W_OK);
|
||||
if(code)
|
||||
errors.push("Failed to acquire write permissions for file " + entry_path + " (Code " + code.code + ")");
|
||||
} else {
|
||||
let directory = path.dirname(entry_path);
|
||||
while(directory.length != 0 && !original_fs.existsSync(directory))
|
||||
directory = path.normalize(path.join(directory, ".."));
|
||||
|
||||
code = await test_access(directory, original_fs.constants.W_OK);
|
||||
if(code) errors.push("Failed to acquire write permissions for directory " + entry_path + " (Code " + code.code + ". Target directory " + directory + ")");
|
||||
}
|
||||
} else if(header.type == "directory") {
|
||||
let directory = path.dirname(entry_path);
|
||||
while(directory.length != 0 && !original_fs.existsSync(directory))
|
||||
directory = path.normalize(path.join(directory, ".."));
|
||||
|
||||
code = await test_access(directory, original_fs.constants.W_OK);
|
||||
if(code) errors.push("Failed to acquire write permissions for directory " + entry_path + " (Code " + code.code + ". Target directory " + directory + ")");
|
||||
}
|
||||
};
|
||||
|
||||
tar_stream.on('entry', (header: Headers, stream, next) => {
|
||||
tester(header).catch(error => {
|
||||
console.log("Emit out of tar_stream.on('entry' ...)");
|
||||
tar_stream.emit('error', error);
|
||||
}).then(() => {
|
||||
stream.on('end', next);
|
||||
stream.resume();
|
||||
});
|
||||
});
|
||||
|
||||
fstream.pipe(tar_stream);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
tar_stream.on('finish', resolve);
|
||||
tar_stream.on('error', error => { reject(error); });
|
||||
});
|
||||
} catch(error) {
|
||||
throw "Failed to list files within tar: " + error;
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
namespace install_config {
|
||||
export interface LockFile {
|
||||
filename: string;
|
||||
timeout: number;
|
||||
"error-id": string;
|
||||
}
|
||||
export interface MoveFile {
|
||||
source: string;
|
||||
target: string;
|
||||
"error-id": string;
|
||||
}
|
||||
export interface ConfigFile {
|
||||
version: number;
|
||||
|
||||
backup: boolean;
|
||||
"backup-directory": string;
|
||||
|
||||
"callback_file": string;
|
||||
"callback_argument_fail": string;
|
||||
"callback_argument_success": string;
|
||||
|
||||
moves: MoveFile[];
|
||||
locks: LockFile[];
|
||||
}
|
||||
}
|
||||
|
||||
async function build_install_config(source_root: string, target_root: string) : Promise<install_config.ConfigFile> {
|
||||
console.log("Building update install config for target directory: %s. Update source: %o", target_root, source_root);
|
||||
const result: install_config.ConfigFile = { } as any;
|
||||
|
||||
result.version = 1;
|
||||
|
||||
result.backup = true;
|
||||
|
||||
{
|
||||
const data = path.parse(source_root);
|
||||
result["backup-directory"] = path.join(data.dir, data.name + "_backup");
|
||||
}
|
||||
|
||||
result.callback_file = app.getPath("exe");
|
||||
result.callback_argument_fail = "--no-single-instance --update-failed-new=";
|
||||
result.callback_argument_success = "--no-single-instance --update-succeed-new=";
|
||||
|
||||
result.moves = [];
|
||||
result.locks = [
|
||||
{
|
||||
"error-id": "main-exe-lock",
|
||||
filename: app.getPath("exe"),
|
||||
timeout: 5000
|
||||
}
|
||||
];
|
||||
|
||||
const ignore_list = [
|
||||
"update-installer.exe", "update-installer"
|
||||
];
|
||||
|
||||
const dir_walker = async (relative_path: string) => {
|
||||
const source_directory = path.join(source_root, relative_path);
|
||||
const target_directory = path.join(target_root, relative_path);
|
||||
|
||||
let files: string[];
|
||||
try {
|
||||
files = await util.promisify(ofs.readdir)(source_directory);
|
||||
} catch(error) {
|
||||
console.warn("Failed to iterate over source directory \"%s\": %o", source_directory, error);
|
||||
return;
|
||||
}
|
||||
|
||||
for(const file of files) {
|
||||
let _exclude = false;
|
||||
for(const exclude of ignore_list) {
|
||||
if(exclude == file) {
|
||||
console.debug("Ignoring file to update (%s/%s)", relative_path, file);
|
||||
_exclude = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(_exclude)
|
||||
continue;
|
||||
|
||||
const source_file = path.join(source_directory, file);
|
||||
const target_file = path.join(target_directory, file);
|
||||
|
||||
//TODO check if file content has changed else ignore?
|
||||
|
||||
const info = await util.promisify(ofs.stat)(source_file);
|
||||
if(info.isDirectory()) {
|
||||
await dir_walker(path.join(relative_path, file));
|
||||
} else {
|
||||
/* TODO: ensure its a file! */
|
||||
result.moves.push({
|
||||
"error-id": "move-file-" + result.moves.length,
|
||||
source: source_file,
|
||||
target: target_file
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await dir_walker(".");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function execute_update(update_file: string, restart_callback: (callback: () => void) => any) : Promise<void> {
|
||||
let application_path = app.getAppPath();
|
||||
if(application_path.endsWith(".asar")) {
|
||||
console.log("App path points to ASAR file (Going up to root directory)");
|
||||
application_path = await fs.realpath(path.join(application_path, "..", ".."));
|
||||
} else if(await fs.pathExists(application_path) && (await fs.stat(application_path)).isFile())
|
||||
application_path = path.dirname(application_path);
|
||||
|
||||
console.log("Located target app path: %s", application_path);
|
||||
console.log("Using update file: %s", update_file);
|
||||
|
||||
const temp_directory = path.join(app.getPath("temp"), "teaclient_update_" + Math.random().toString(36).substring(7));
|
||||
{
|
||||
console.log("Preparing update source directory at %s", temp_directory);
|
||||
try {
|
||||
await fs.mkdirp(temp_directory)
|
||||
} catch(error) {
|
||||
console.error("failed to create update source directory: %o", error);
|
||||
throw "failed to create update source directory";
|
||||
}
|
||||
|
||||
const source = fs.createReadStream(update_file);
|
||||
const extract = tar.extract();
|
||||
|
||||
extract.on('entry', (header: Headers, stream: PassThrough, callback) => {
|
||||
const extract = async (header: Headers, stream: PassThrough) => {
|
||||
const target_file = path.join(temp_directory, header.name);
|
||||
console.debug("Extracting entry %s of type %s to %s", header.name, header.type, target_file);
|
||||
|
||||
if(header.type == "directory") {
|
||||
await util.promisify(ofs.mkdir)(target_file, {recursive: true});
|
||||
} else if(header.type == "file") {
|
||||
{
|
||||
const directory = path.parse(target_file).dir;
|
||||
console.debug("Testing for directory: %s", directory);
|
||||
if(!(await util.promisify(ofs.exists)(directory)) || !(await util.promisify(ofs.stat)(directory)).isDirectory()) {
|
||||
console.log("Creating directory %s", directory);
|
||||
try {
|
||||
await util.promisify(ofs.mkdir)(directory, {recursive: true});
|
||||
} catch(error) {
|
||||
console.warn("failed to create directory for file %s", header.type);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
const write_stream = ofs.createWriteStream(target_file);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
stream.pipe(write_stream)
|
||||
.on('error', reject)
|
||||
.on('finish', resolve);
|
||||
});
|
||||
return; /* success */
|
||||
} catch(error) {
|
||||
console.error("Failed to extract update file %s: %o", header.name, error);
|
||||
}
|
||||
} else {
|
||||
console.debug("Skipping this unknown file type");
|
||||
}
|
||||
stream.resume(); /* drain the stream */
|
||||
};
|
||||
extract(header, stream).catch(error => {
|
||||
console.log("Ignoring file %s due to an error: %o", header.name, error);
|
||||
}).then(() => {
|
||||
callback();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
source.pipe(extract);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
extract.on('finish', resolve);
|
||||
extract.on('error', reject);
|
||||
});
|
||||
} catch(error) {
|
||||
console.error("Failed to unpack update: %o", error);
|
||||
throw "update unpacking failed";
|
||||
}
|
||||
}
|
||||
/* the "new" environment should now be available at 'temp_directory' */
|
||||
console.log("Update unpacked successfully. Building update extractor file.");
|
||||
|
||||
let install_config;
|
||||
try {
|
||||
install_config = await build_install_config(temp_directory, application_path);
|
||||
} catch(error) {
|
||||
console.error("Failed to build update installer config: %o", error);
|
||||
throw "failed to build update installer config";
|
||||
}
|
||||
|
||||
const log_file = path.join(temp_directory, "update-log.txt");
|
||||
const config_file = path.join(temp_directory, "update_install.json");
|
||||
console.log("Writing config to %s", config_file);
|
||||
try {
|
||||
await fs.writeJSON(config_file, install_config);
|
||||
} catch(error) {
|
||||
console.error("Failed to write update install config file: %s", error);
|
||||
throw "failed to write update install config file";
|
||||
}
|
||||
|
||||
const update_installer = path.join(application_path, "update-installer" + (os.platform() === "win32" ? ".exe" : ""));
|
||||
if(!(await fs.pathExists(update_installer))) {
|
||||
console.error("Missing update installer! Supposed to be at %s", update_installer);
|
||||
throw "Missing update installer!";
|
||||
} else {
|
||||
console.log("Using update installer located at %s", update_installer);
|
||||
}
|
||||
|
||||
if(os.platform() == "linux") {
|
||||
console.log("Executing update install on linux");
|
||||
|
||||
//We have to unpack it later
|
||||
const rest_callback = () => {
|
||||
console.log("Executing command %s with args %o", update_installer, [log_file, config_file]);
|
||||
try {
|
||||
let result = child_process.spawnSync(update_installer, [log_file, config_file]);
|
||||
if(result.status != 0) {
|
||||
console.error("Failed to execute update installer! Return code: %d", result.status);
|
||||
dialog.showMessageBox({
|
||||
buttons: ["update now", "remind me later"],
|
||||
title: "Update available",
|
||||
message:
|
||||
"Failed to execute update installer\n" +
|
||||
"Installer exited with code " + result.status
|
||||
} as MessageBoxOptions);
|
||||
}
|
||||
} catch(error) {
|
||||
console.error("Failed to execute update installer (%o)", error);
|
||||
if("errno" in error) {
|
||||
const errno = error as ErrnoException;
|
||||
if(errno.errno == EPERM) {
|
||||
dialog.showMessageBox({
|
||||
buttons: ["quit"],
|
||||
title: "Update execute failed",
|
||||
message: "Failed to execute update installer. (No permissions)\nPlease execute the client with admin privileges!"
|
||||
} as MessageBoxOptions);
|
||||
return;
|
||||
}
|
||||
dialog.showMessageBox({
|
||||
buttons: ["quit"],
|
||||
title: "Update execute failed",
|
||||
message: "Failed to execute update installer.\nError: " + errno.message
|
||||
} as MessageBoxOptions);
|
||||
return;
|
||||
}
|
||||
dialog.showMessageBox({
|
||||
buttons: ["quit"],
|
||||
title: "Update execute failed",
|
||||
message: "Failed to execute update installer.\nLookup console for more detail"
|
||||
} as MessageBoxOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
if(electron.app.hasSingleInstanceLock())
|
||||
electron.app.releaseSingleInstanceLock();
|
||||
|
||||
const ids = child_process.execSync("pgrep TeaClient").toString().split(os.EOL).map(e => e.trim()).reverse().join(" ");
|
||||
console.log("Executing %s", "kill -9 " + ids);
|
||||
child_process.execSync("kill -9 " + ids);
|
||||
};
|
||||
restart_callback(rest_callback);
|
||||
} else {
|
||||
console.log("Executing update install on windows");
|
||||
|
||||
//We have to unpack it later
|
||||
const rest_callback = () => {
|
||||
let pipe = child_process.spawn(update_installer, [log_file, config_file], {
|
||||
detached: true,
|
||||
cwd: application_path,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
pipe.unref();
|
||||
app.quit();
|
||||
};
|
||||
restart_callback(rest_callback);
|
||||
}
|
||||
}
|
||||
|
||||
export async function current_version() : Promise<Version> {
|
||||
if(process_args.has_value(Arguments.UPDATER_LOCAL_VERSION))
|
||||
return parse_version(process_args.value(Arguments.UPDATER_LOCAL_VERSION));
|
||||
|
||||
let parent_path = app.getAppPath();
|
||||
if(parent_path.endsWith(".asar")) {
|
||||
parent_path = path.join(parent_path, "..", "..");
|
||||
parent_path = fs.realpathSync(parent_path);
|
||||
}
|
||||
try {
|
||||
const info = await fs.readJson(path.join(parent_path, "app_version.json"));
|
||||
let result = parse_version(info["version"]);
|
||||
result.timestamp = info["timestamp"];
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.log("Got no version!");
|
||||
return new Version(0, 0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
async function minawait<T>(object: Promise<T>, time: number) : Promise<T> {
|
||||
const begin = Date.now();
|
||||
const r = await object;
|
||||
const end = Date.now();
|
||||
if(end - begin < time)
|
||||
await new Promise(resolve => setTimeout(resolve, time + begin - end));
|
||||
return r;
|
||||
}
|
||||
|
||||
export let update_restart_pending = false;
|
||||
export async function execute_graphical(channel: string, ask_install: boolean) : Promise<Boolean> {
|
||||
const electron = require('electron');
|
||||
|
||||
const ui_debug = process_args.has_flag(Arguments.UPDATER_UI_DEBUG);
|
||||
const window = new electron.BrowserWindow({
|
||||
show: false,
|
||||
width: ui_debug ? 1200 : 600,
|
||||
height: ui_debug ? 800 : 400,
|
||||
|
||||
webPreferences: {
|
||||
devTools: true,
|
||||
nodeIntegration: true,
|
||||
javascript: true
|
||||
}
|
||||
});
|
||||
|
||||
window.loadFile(path.join(path.dirname(module.filename), "ui", "index.html"));
|
||||
if(ui_debug) {
|
||||
window.webContents.openDevTools();
|
||||
}
|
||||
await new Promise(resolve => window.on('ready-to-show', resolve));
|
||||
window.show();
|
||||
|
||||
const current_vers = await current_version();
|
||||
console.log("Current version: " + current_vers.toString(true));
|
||||
|
||||
console.log("Showed");
|
||||
const set_text = text => window.webContents.send('status-update-text', text);
|
||||
const set_error = text => window.webContents.send('status-error', text);
|
||||
const set_progress = progress => window.webContents.send('status-update', progress);
|
||||
const await_exit = () => { return new Promise(resolve => window.on('closed', resolve))};
|
||||
const await_version_confirm = version => {
|
||||
const id = "version-accept-" + Date.now();
|
||||
window.webContents.send('status-confirm-update', id, current_vers, version);
|
||||
return new Promise((resolve, reject) => {
|
||||
window.on('closed', () => resolve(false));
|
||||
ipcMain.once(id, (event, result) => {
|
||||
console.log("Got response %o", result);
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
};
|
||||
const await_confirm_execute = () => {
|
||||
const id = "status-confirm-execute-" + Date.now();
|
||||
window.webContents.send('status-confirm-execute', id);
|
||||
return new Promise((resolve, reject) => {
|
||||
window.on('closed', () => resolve(false));
|
||||
ipcMain.once(id, (event, result) => {
|
||||
console.log("Got response %o", result);
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
set_text("Loading data");
|
||||
let version: UpdateVersion;
|
||||
try {
|
||||
version = await minawait(newest_version(process_args.has_flag(Arguments.UPDATER_ENFORCE) ? undefined : current_vers, channel), 3000);
|
||||
} catch (error) {
|
||||
set_error("Failed to get newest information:<br>" + error);
|
||||
await await_exit();
|
||||
return false;
|
||||
}
|
||||
console.log("Got version %o", version);
|
||||
|
||||
if(!version) {
|
||||
set_error("You're already on the newest version!");
|
||||
await await_exit();
|
||||
return false;
|
||||
}
|
||||
|
||||
if(ask_install) {
|
||||
try {
|
||||
const test = await await_version_confirm(version.version);
|
||||
if(!test) {
|
||||
window.close();
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.dir(error);
|
||||
window.close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
set_text("Updating to version " + version.version.toString() + "<br>Downloading....");
|
||||
let update_path: string;
|
||||
try {
|
||||
update_path = await download_version(version.channel, version.version, status => { setImmediate(set_progress, status.percent); });
|
||||
} catch (error) {
|
||||
set_error("Failed to download version: <br>" + error);
|
||||
console.error(error);
|
||||
await await_exit();
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const inaccessible = await test_file_accessibility(update_path);
|
||||
if(inaccessible.length > 0) {
|
||||
console.log("Failed to access the following files:");
|
||||
for(const fail of inaccessible)
|
||||
console.log(" - " + fail);
|
||||
|
||||
if(os.platform() == "linux") {
|
||||
set_error("Failed to access target files.<br>Please execute this app with administrator (sudo) privileges.<br>Use the following command:<br><p>" +
|
||||
"sudo " + path.normalize(app.getAppPath()) + " --update-execute=\"" + path.normalize(update_path) + "\"</p>");
|
||||
await await_exit();
|
||||
return false;
|
||||
} else if(os.platform() == "win32") {
|
||||
/* the updater asks for admin rights anyway :/ */
|
||||
}
|
||||
}
|
||||
} catch(error) {
|
||||
set_error("Failed to access target files.<br>You may need to execute the TeaClient as Administrator!<br>Error: " + error);
|
||||
await await_exit();
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!await await_confirm_execute()) {
|
||||
window.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
set_text("Extracting update installer...<br>Please wait");
|
||||
try {
|
||||
await extract_updater(update_path);
|
||||
} catch(error) {
|
||||
console.error("Failed to update the updater! (%o)", error);
|
||||
set_error("Failed to update the update installer.\nUpdate failed!");
|
||||
await await_exit();
|
||||
return false;
|
||||
}
|
||||
set_text("Executing update...<br>Please wait");
|
||||
|
||||
try {
|
||||
await execute_update(update_path, callback => {
|
||||
prevent_instant_close = true;
|
||||
update_restart_pending = true;
|
||||
window.close();
|
||||
callback();
|
||||
});
|
||||
} catch (error) {
|
||||
dialog.showErrorBox("Update error", "Failed to execute update!\n" + error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export let update_question_open = false;
|
||||
async function check_update(channel: string) {
|
||||
let version: UpdateVersion;
|
||||
try {
|
||||
version = await newest_version(await current_version(), channel);
|
||||
} catch(error) {
|
||||
console.warn("failed check for newer versions!");
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
if(version) {
|
||||
update_question_open = true;
|
||||
dialog.showMessageBox({
|
||||
buttons: ["update now", "remind me later"],
|
||||
title: "Update available",
|
||||
message:
|
||||
"There is an update available!\n" +
|
||||
"Should we update now?\n" +
|
||||
"\n" +
|
||||
"Current version: " + (await current_version()).toString() + "\n" +
|
||||
"Target version: " + version.version.toString()
|
||||
} as MessageBoxOptions, result => {
|
||||
if(result == 0) {
|
||||
execute_graphical(channel, false).then(() => {
|
||||
update_question_open = false;
|
||||
});
|
||||
} else {
|
||||
update_question_open = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let update_task: Timer;
|
||||
export function start_auto_update_check() {
|
||||
if(update_task) return;
|
||||
update_task = setInterval(check_update, 2 * 60 * 60 * 1000);
|
||||
setImmediate(check_update);
|
||||
}
|
||||
|
||||
export function stop_auto_update_check() {
|
||||
clearInterval(update_task);
|
||||
update_task = undefined;
|
||||
}
|
||||
|
||||
export async function selected_channel() : Promise<string> {
|
||||
return process_args.has_value(Arguments.UPDATER_CHANNEL) ? process_args.value(Arguments.UPDATER_CHANNEL) : "release";
|
||||
}
|
9
modules/core/app-updater/ui/index.css
Normal file
9
modules/core/app-updater/ui/index.css
Normal file
@ -0,0 +1,9 @@
|
||||
.page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=index.css.map */
|
1
modules/core/app-updater/ui/index.css.map
Normal file
1
modules/core/app-updater/ui/index.css.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["index.scss"],"names":[],"mappings":"AAAA;EACC;;;AAGD;EACC","file":"index.css"}
|
36
modules/core/app-updater/ui/index.html
Normal file
36
modules/core/app-updater/ui/index.html
Normal file
@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Updating app</title>
|
||||
|
||||
<script type="application/javascript">
|
||||
const exports = {};
|
||||
</script>
|
||||
<script src="index.js" type="application/javascript"></script>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="page info">
|
||||
<div class="state">Downloading update. Please wait!</div>
|
||||
<progress class="progress" value="0" max="100"></progress>
|
||||
</div>
|
||||
<div class="page error">
|
||||
<div class="message"></div>
|
||||
</div>
|
||||
<div class="page confirm-restart">
|
||||
Download succeeded.<br>
|
||||
Update installation requires client restart!<br>
|
||||
Please ensure that <b>all instances</b> are closed!<br>
|
||||
If not this app would kill them!<br>
|
||||
<button class="button-execute">Execute update</button>
|
||||
</div>
|
||||
<div class="page config-update">
|
||||
Update available.<br>
|
||||
Current version: <a class="current-version"></a><br>
|
||||
Target version: <a class="target-version"></a><br>
|
||||
<button class="button-update">update</button>
|
||||
<button class="button-cancel">cancel</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
7
modules/core/app-updater/ui/index.scss
Normal file
7
modules/core/app-updater/ui/index.scss
Normal file
@ -0,0 +1,7 @@
|
||||
.page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: block;
|
||||
}
|
51
modules/core/app-updater/ui/index.ts
Normal file
51
modules/core/app-updater/ui/index.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import * as electron from "electron";
|
||||
import * as $ from "jquery";
|
||||
import {Version} from "../../../shared/version";
|
||||
|
||||
electron.ipcRenderer.on('status-update', (event, progress) => {
|
||||
if(!$(".info").is(":visible")) {
|
||||
$(".page").hide();
|
||||
$(".info").show()
|
||||
}
|
||||
$(".progress").attr("value", progress * 100);
|
||||
});
|
||||
|
||||
electron.ipcRenderer.on('status-update-text', (event, text) => {
|
||||
if(!$(".info").is(":visible")) {
|
||||
$(".page").hide();
|
||||
$(".info").show()
|
||||
}
|
||||
$(".state").html(text);
|
||||
});
|
||||
|
||||
electron.ipcRenderer.on('status-error', (event, text) => {
|
||||
console.log("Got error %s", text);
|
||||
$(".page").hide();
|
||||
$(".error").show().html(text);
|
||||
});
|
||||
electron.ipcRenderer.on('status-confirm-execute', (event, callback_name) => {
|
||||
$(".page").hide();
|
||||
$(".confirm-restart").show();
|
||||
$(".button-execute").on('click', event => electron.ipcRenderer.send(callback_name, true))
|
||||
});
|
||||
electron.ipcRenderer.on('status-confirm-update', (event, callback_name, current: Version, version: Version) => {
|
||||
console.dir(callback_name);
|
||||
console.dir(version);
|
||||
|
||||
$(".page").hide();
|
||||
$(".config-update").show();
|
||||
|
||||
$(".target-version").text(version.major + "." + version.minor + "." + version.patch + (version.build > 0 ? " (" + version.build + ")" : ""));
|
||||
$(".current-version").text(current.major + "." + current.minor + "." + current.patch + (current.build > 0 ? " (" + current.build + ")" : ""));
|
||||
|
||||
$(".button-update").on('click', event => electron.ipcRenderer.send(callback_name, true));
|
||||
$(".button-cancel").on('click', event => electron.ipcRenderer.send(callback_name, false));
|
||||
});
|
||||
|
||||
/*
|
||||
const set_text = text => window.webContents.send('status-update-text', text);
|
||||
const set_error = text => window.webContents.send('status-error', text);
|
||||
const set_confirm_restart = () => window.webContents.send('status-confirm-restart');
|
||||
const set_progress = progress => window.webContents.send('status-update', progress);
|
||||
const await_exit = () => { return new Promise(resolve => window.on('closed', resolve))};
|
||||
*/
|
223
modules/core/main.ts
Normal file
223
modules/core/main.ts
Normal file
@ -0,0 +1,223 @@
|
||||
// Quit when all windows are closed.
|
||||
import * as electron from "electron";
|
||||
import * as app_updater from "./app-updater";
|
||||
import * as forum from "./teaspeak-forum";
|
||||
|
||||
import { app } from "electron";
|
||||
import MessageBoxOptions = electron.MessageBoxOptions;
|
||||
|
||||
import {process_args, parse_arguments, Arguments} from "../shared/process-arguments";
|
||||
import {open as open_changelog} from "./app-updater/changelog";
|
||||
import * as crash_handler from "../crash_handler";
|
||||
|
||||
async function execute_app() {
|
||||
/* legacy, will be removed soon */
|
||||
if(process_args.has_value("update-failed")) {
|
||||
const result = electron.dialog.showMessageBox({
|
||||
type: "error",
|
||||
message: "Failed to execute update:\n" + process_args.value("update-failed"),
|
||||
title: "Update failed!",
|
||||
buttons: ["retry", "ignore"]
|
||||
} as MessageBoxOptions);
|
||||
if(result == 0)
|
||||
if(await app_updater.execute_graphical(await app_updater.selected_channel(), false))
|
||||
return;
|
||||
} else if(process_args.has_value("update-succeed")) {
|
||||
const result = electron.dialog.showMessageBox({
|
||||
type: "info",
|
||||
message: "Update successfully installed!\nShould we launch TeaClient?",
|
||||
title: "Update succeeded!",
|
||||
buttons: ["yes", "no"]
|
||||
} as MessageBoxOptions);
|
||||
if(result != 0) {
|
||||
electron.app.exit(0);
|
||||
return; //Not really required here!
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if(process_args.has_value("update-execute")) {
|
||||
console.log("Executing update " + process_args.value("update-execute"));
|
||||
await app_updater.execute_update(process_args.value("update-execute"), callback => {
|
||||
console.log("Update preconfig successful. Extracting update. (The client should start automatically)");
|
||||
app.quit();
|
||||
setImmediate(callback);
|
||||
});
|
||||
return;
|
||||
} else if(process_args.has_value("update-failed-new") || process_args.has_value("update-succeed-new")) {
|
||||
const success = process_args.has_value("update-succeed-new");
|
||||
let data: {
|
||||
parse_success: boolean;
|
||||
log_file?: string;
|
||||
error_id?: string;
|
||||
error_message?: string;
|
||||
} = {
|
||||
parse_success: false
|
||||
};
|
||||
try {
|
||||
let encoded_data = Buffer.from(process_args.value("update-failed-new") || process_args.value("update-succeed-new"), "base64").toString();
|
||||
for(const part of encoded_data.split(";")) {
|
||||
const index = part.indexOf(':');
|
||||
if(index == -1)
|
||||
data[part] = true;
|
||||
else {
|
||||
data[part.substr(0, index)] = Buffer.from(part.substr(index + 1), "base64").toString();
|
||||
}
|
||||
}
|
||||
data.parse_success = true;
|
||||
} catch($) {
|
||||
console.error($);
|
||||
}
|
||||
console.log("Update success: %o. Update data: %o", success, data);
|
||||
|
||||
let title = "";
|
||||
let type = "";
|
||||
let message = "";
|
||||
|
||||
const buttons: ({
|
||||
key: string,
|
||||
callback: () => Promise<boolean>
|
||||
})[] = [];
|
||||
|
||||
if(success) {
|
||||
open_changelog();
|
||||
|
||||
type = "info";
|
||||
title = "Update succeeded!";
|
||||
|
||||
message = "Update has been successfully installed!\nWhat do you want to do next?";
|
||||
|
||||
buttons.push({
|
||||
key: "Launch client",
|
||||
callback: async () => false
|
||||
});
|
||||
if(data.parse_success && data.log_file) {
|
||||
buttons.push({
|
||||
key: "Open update log",
|
||||
callback: async () => {
|
||||
electron.shell.openItem(data.log_file);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
type = "error";
|
||||
title = "Update failed!";
|
||||
|
||||
message = "Failed to install update.";
|
||||
if(data.parse_success) {
|
||||
message += "\n\n";
|
||||
message += "Error ID: " + (data.error_id || "undefined") + "\n";
|
||||
message += "Error Message: " + (data.error_message || "undefined") + "\n";
|
||||
message += "Installer log: " + (data.log_file || "undefined");
|
||||
} else {
|
||||
message += "\nUnknown error! Lookup the console for more details.";
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
key: "Ignore",
|
||||
callback: async () => false
|
||||
});
|
||||
buttons.push({
|
||||
key: "Retry update",
|
||||
callback: async () => {
|
||||
await app_updater.execute_graphical(await app_updater.selected_channel(), false);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if(data.parse_success && data.log_file) {
|
||||
buttons.push({
|
||||
key: "Open update log",
|
||||
callback: async () => {
|
||||
electron.shell.openItem(data.log_file);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
buttons.push({
|
||||
key: "Close",
|
||||
callback: async () => true
|
||||
});
|
||||
|
||||
const result = await new Promise<number>(resolve => electron.dialog.showMessageBox({
|
||||
type: type,
|
||||
message: message,
|
||||
title: title,
|
||||
buttons: buttons.map(e => e.key)
|
||||
} as MessageBoxOptions, resolve));
|
||||
if(buttons[result].callback) {
|
||||
if(await buttons[result].callback())
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
forum.setup();
|
||||
try {
|
||||
await forum.initialize();
|
||||
}catch(error) {
|
||||
console.error("Failed to initialize forum connection: %o", error);
|
||||
const result = electron.dialog.showMessageBox({
|
||||
type: "error",
|
||||
message: "Failed to initialize forum connection\nLookup the console for more info",
|
||||
title: "Main execution failed!",
|
||||
buttons: ["close"]
|
||||
} as MessageBoxOptions);
|
||||
electron.app.exit(1);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
{
|
||||
const version = await app_updater.current_version();
|
||||
global["app_version_client"] = version.toString();
|
||||
}
|
||||
const main = require("./main_window");
|
||||
main.execute();
|
||||
|
||||
app_updater.start_auto_update_check();
|
||||
} catch (error) {
|
||||
console.dir(error);
|
||||
const result = electron.dialog.showMessageBox({
|
||||
type: "error",
|
||||
message: "Failed to execute app main!\n" + error,
|
||||
title: "Main execution failed!",
|
||||
buttons: ["close"]
|
||||
} as MessageBoxOptions);
|
||||
electron.app.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
process.on('uncaughtException', err => {
|
||||
console.error(err, 'Uncaught Exception thrown');
|
||||
console.dir(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
/*
|
||||
if(false) {
|
||||
SegfaultHandler = require('segfault-handler');
|
||||
|
||||
SegfaultHandler.registerHandler("crash.log"); // With no argument, SegfaultHandler will generate a generic log file name
|
||||
}
|
||||
|
||||
const SegfaultHandler = require('segfault-handler');
|
||||
SegfaultHandler.registerHandler("crash.log"); // With no argument, SegfaultHandler will generate a generic log file name
|
||||
*/
|
||||
if(app) { //We're executed!
|
||||
parse_arguments();
|
||||
if(process_args.has_value(Arguments.DISABLE_HARDWARE_ACCELERATION))
|
||||
app.disableHardwareAcceleration();
|
||||
if(process_args.has_value(Arguments.DUMMY_CRASH_MAIN))
|
||||
crash_handler.handler.crash();
|
||||
if(!process_args.has_value(Arguments.DEBUG) && !process_args.has_value(Arguments.NO_SINGLE_INSTANCE)) {
|
||||
if(!app.requestSingleInstanceLock()) {
|
||||
console.log("Another instance is already running. Closing this instance");
|
||||
app.exit(0);
|
||||
}
|
||||
}
|
||||
app.on('ready', execute_app);
|
||||
}
|
||||
}
|
||||
export const execute = main;
|
262
modules/core/main_window.ts
Normal file
262
modules/core/main_window.ts
Normal file
@ -0,0 +1,262 @@
|
||||
import {BrowserWindow, Menu, MenuItem, MessageBoxOptions, app, dialog} from "electron";
|
||||
import * as electron from "electron";
|
||||
export let prevent_instant_close: boolean = true;
|
||||
export let is_debug: boolean;
|
||||
export let allow_dev_tools: boolean;
|
||||
|
||||
import {Arguments, parse_arguments, process_args} from "../shared/process-arguments";
|
||||
import * as updater from "./app-updater";
|
||||
import * as loader from "./ui-loader";
|
||||
import {open as open_changelog} from "./app-updater/changelog";
|
||||
import * as crash_handler from "../crash_handler";
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
export let main_window: BrowserWindow = null;
|
||||
|
||||
function create_menu() : Menu {
|
||||
const menu = new Menu();
|
||||
|
||||
if(allow_dev_tools) {
|
||||
menu.append(new MenuItem({
|
||||
id: "developer-tools",
|
||||
enabled: true,
|
||||
label: "Developer",
|
||||
|
||||
submenu: [
|
||||
{
|
||||
id: "tool-dev-tools",
|
||||
label: "Open developer tools",
|
||||
enabled: true,
|
||||
click: event => {
|
||||
main_window.webContents.openDevTools();
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: "tool-page-reload",
|
||||
label: "Reload current page",
|
||||
enabled: true,
|
||||
click: event => {
|
||||
main_window.reload();
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
}
|
||||
menu.append(new MenuItem({
|
||||
id: "help",
|
||||
enabled: true,
|
||||
label: "Help",
|
||||
|
||||
submenu: [
|
||||
{
|
||||
id: "update-check",
|
||||
label: "Check for updates",
|
||||
click: () => updater.selected_channel().then(channel => updater.execute_graphical(channel, true))
|
||||
},
|
||||
{
|
||||
id: "changelog",
|
||||
label: "View ChangeLog file",
|
||||
click: open_changelog
|
||||
},
|
||||
{
|
||||
id: "hr-01",
|
||||
type: "separator"
|
||||
},
|
||||
{
|
||||
id: "visit-home",
|
||||
label: "Visit TeaSpeak.de",
|
||||
click: () => electron.shell.openExternal("https://teaspeak.de")
|
||||
},
|
||||
{
|
||||
id: "visit-support",
|
||||
label: "Get support",
|
||||
click: () => electron.shell.openExternal("https://forum.teaspeak.de")
|
||||
},
|
||||
{
|
||||
id: "about-teaclient",
|
||||
label: "About TeaClient",
|
||||
click: () => {
|
||||
updater.current_version().then(version => {
|
||||
dialog.showMessageBox({
|
||||
title: "TeaClient info",
|
||||
message: "TeaClient by TeaSpeak (WolverinDEV)\nVersion: " + version.toString(true),
|
||||
buttons: ["close"]
|
||||
} as MessageBoxOptions, result => {});
|
||||
});
|
||||
}
|
||||
},
|
||||
]
|
||||
}));
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
function spawn_main_window(entry_point: string) {
|
||||
// Create the browser window.
|
||||
console.log("Spawning main window");
|
||||
main_window = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
nodeIntegrationInWorker: true
|
||||
},
|
||||
});
|
||||
|
||||
const menu = create_menu();
|
||||
if(menu.items.length > 0)
|
||||
main_window.setMenu(menu);
|
||||
|
||||
main_window.webContents.on('devtools-closed', event => {
|
||||
console.log("Dev tools destroyed!");
|
||||
});
|
||||
|
||||
main_window.on('closed', () => {
|
||||
main_window = null;
|
||||
prevent_instant_close = false;
|
||||
});
|
||||
|
||||
|
||||
main_window.loadFile(loader.ui.preloading_page(entry_point));
|
||||
|
||||
main_window.once('ready-to-show', () => {
|
||||
main_window.show();
|
||||
main_window.focus();
|
||||
loader.ui.cleanup();
|
||||
if(allow_dev_tools && !main_window.webContents.isDevToolsOpened())
|
||||
main_window.webContents.openDevTools();
|
||||
prevent_instant_close = false; /* just to ensure that the client could be unloaded */
|
||||
});
|
||||
|
||||
main_window.webContents.on('new-window', (event, url, frameName, disposition, options, additionalFeatures) => {
|
||||
console.log("Got new window " + frameName);
|
||||
if (frameName === 'teaforo-login') {
|
||||
// open window as modal
|
||||
Object.assign(options, {
|
||||
modal: true,
|
||||
parent: main_window,
|
||||
width: 100,
|
||||
height: 100
|
||||
});
|
||||
|
||||
let a = new BrowserWindow(options);
|
||||
a.show();
|
||||
} else {
|
||||
const url_preview = require("./url-preview");
|
||||
url_preview.open_preview(url);
|
||||
}
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
main_window.webContents.on('crashed', event => {
|
||||
console.error("UI thread crashed! Closing app!");
|
||||
if(!process_args.has_flag(Arguments.DEBUG)) {
|
||||
main_window.close();
|
||||
prevent_instant_close = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handle_error(message: string) {
|
||||
console.log("Caught loading error: %s", message);
|
||||
//"A critical error happened while loading TeaClient!", "A critical error happened while loading TeaClient!<br>" + message
|
||||
dialog.showMessageBox({
|
||||
type: "error",
|
||||
buttons: ["exit"],
|
||||
title: "A critical error happened while loading TeaClient!",
|
||||
message: message
|
||||
});
|
||||
loader.ui.cancel();
|
||||
}
|
||||
|
||||
function init_listener() {
|
||||
app.on('quit', () => {
|
||||
console.debug("Finalizing crash handler");
|
||||
crash_handler.finalize_handler();
|
||||
console.log("RUNNING quit!");
|
||||
loader.cleanup();
|
||||
console.log("RUNNING quit 2!");
|
||||
loader.ui.cleanup();
|
||||
console.log("RUNNING quit done!");
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
console.log("RUNNING all win closed!");
|
||||
// On macOS it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
if(!prevent_instant_close) {
|
||||
console.log("All windows have been closed, closing app.");
|
||||
app.quit();
|
||||
} else {
|
||||
console.log("All windows have been closed, but we dont want to quit instantly. Waiting 10 seconds if something happens");
|
||||
setTimeout(() => {
|
||||
if(BrowserWindow.getAllWindows().length == 0) {
|
||||
console.log("All windows have been closed for over an minute. Exiting app!");
|
||||
app.quit();
|
||||
}
|
||||
}, 10 * 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (main_window === null) {
|
||||
//spawn_loading_screen();
|
||||
//createWindow()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
||||
console.log("Allowing untrusted certificate for %o", url);
|
||||
event.preventDefault();
|
||||
callback(true);
|
||||
});
|
||||
}
|
||||
|
||||
export function execute() {
|
||||
console.log("Main app executed!");
|
||||
|
||||
|
||||
parse_arguments();
|
||||
is_debug = process_args.has_flag(...Arguments.DEBUG);
|
||||
allow_dev_tools = process_args.has_flag(...Arguments.DEV_TOOLS);
|
||||
if(is_debug) {
|
||||
console.log("Enabled debug!");
|
||||
console.log("Arguments: %o", process_args);
|
||||
}
|
||||
|
||||
init_listener();
|
||||
console.log("Spawn loading screen");
|
||||
loader.ui.execute_loader().then(async (entry_point: string) => {
|
||||
/* test if the updater may have an update found */
|
||||
let awaiting_update_set = false;
|
||||
while(updater.update_question_open) {
|
||||
if(!awaiting_update_set) {
|
||||
awaiting_update_set = true;
|
||||
loader.ui.show_await_update();
|
||||
console.log("Awaiting update stuff to be finished");
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
if(updater.update_restart_pending)
|
||||
return undefined;
|
||||
|
||||
return entry_point;
|
||||
}).then((entry_point: string) => {
|
||||
loader.ui.cleanup(); /* close the window */
|
||||
|
||||
if(entry_point) //has not been canceled
|
||||
spawn_main_window(entry_point);
|
||||
else {
|
||||
console.warn("Missing entry point!");
|
||||
}
|
||||
}).catch(handle_error);
|
||||
}
|
127
modules/core/teaspeak-forum/index.ts
Normal file
127
modules/core/teaspeak-forum/index.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import * as path from "path";
|
||||
import * as electron from "electron";
|
||||
import * as fs from "fs-extra";
|
||||
|
||||
import {BrowserWindow, ipcMain as ipc} from "electron";
|
||||
import {Arguments, process_args} from "../../shared/process-arguments";
|
||||
import UserData = forum.UserData;
|
||||
import {main_window} from "../main_window";
|
||||
|
||||
let current_window: BrowserWindow;
|
||||
let _current_data: UserData;
|
||||
|
||||
function update_data(data?: UserData) {
|
||||
_current_data = data;
|
||||
electron.webContents.getAllWebContents().forEach(content => {
|
||||
content.send('teaforo-update', data);
|
||||
});
|
||||
}
|
||||
|
||||
function config_file_path() {
|
||||
return path.join(electron.app.getPath('userData'), "forum_data.json");
|
||||
}
|
||||
|
||||
async function load_data() {
|
||||
try {
|
||||
const file = config_file_path();
|
||||
if((await fs.stat(file)).isFile()) {
|
||||
const raw_data = await fs.readFile(config_file_path());
|
||||
const data = JSON.parse(raw_data.toString());
|
||||
update_data(data as UserData);
|
||||
console.log("Initialized forum account from config!");
|
||||
} else {
|
||||
console.log("Missing forum config file. Ignoring forum auth");
|
||||
}
|
||||
} catch(error) {
|
||||
console.error("Failed to load forum account connection: %o", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function save_data() {
|
||||
const file = config_file_path();
|
||||
try {
|
||||
await fs.ensureFile(file);
|
||||
} catch(error) {
|
||||
console.error("Failed to ensure forum config file as file %o", error);
|
||||
}
|
||||
try {
|
||||
await fs.writeJSON(file, _current_data);
|
||||
} catch(error) {
|
||||
console.error("Failed to save forum config: %o", error);
|
||||
}
|
||||
}
|
||||
|
||||
export function open_login(enforce: boolean = false) : Promise<UserData> {
|
||||
if(_current_data && !enforce) return Promise.resolve(_current_data);
|
||||
|
||||
if(current_window) {
|
||||
current_window.close();
|
||||
current_window = undefined;
|
||||
}
|
||||
current_window = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 400,
|
||||
show: true,
|
||||
parent: main_window,
|
||||
webPreferences: {
|
||||
webSecurity: false
|
||||
},
|
||||
});
|
||||
current_window.setMenu(null);
|
||||
console.log("Main: " + main_window);
|
||||
|
||||
current_window.loadFile(path.join(path.dirname(module.filename), "ui", "index.html"));
|
||||
if(process_args.has_flag(...Arguments.DEV_TOOLS))
|
||||
current_window.webContents.openDevTools();
|
||||
|
||||
return new Promise<UserData>((resolve, reject) => {
|
||||
let response = false;
|
||||
ipc.once("teaforo-callback", (event, data) => {
|
||||
if(response) return;
|
||||
response = true;
|
||||
current_window.close();
|
||||
current_window = undefined;
|
||||
|
||||
update_data(data);
|
||||
save_data();
|
||||
if(data)
|
||||
resolve(data);
|
||||
else
|
||||
reject();
|
||||
});
|
||||
|
||||
current_window.on('closed', event => {
|
||||
if(response) return;
|
||||
response = true;
|
||||
|
||||
current_window = undefined;
|
||||
reject();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function current_data() : UserData | undefined {
|
||||
return this._current_data;
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
update_data(undefined);
|
||||
save_data();
|
||||
}
|
||||
|
||||
export async function initialize() {
|
||||
await load_data();
|
||||
}
|
||||
|
||||
export function setup() {
|
||||
ipc.on('teaforo-login', event => {
|
||||
open_login().catch(error => {}); //TODO may local notify
|
||||
});
|
||||
|
||||
ipc.on('teaforo-logout', event => {
|
||||
logout();
|
||||
});
|
||||
ipc.on('teaforo-update', event => {
|
||||
update_data(_current_data);
|
||||
});
|
||||
}
|
99
modules/core/teaspeak-forum/ui/index.css
Normal file
99
modules/core/teaspeak-forum/ui/index.css
Normal file
@ -0,0 +1,99 @@
|
||||
html {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.inner {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.inner-container {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
position: absolute;
|
||||
top: calc(50vh - 200px);
|
||||
left: calc(50vw - 200px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-family: Helvetica, serif;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.13);
|
||||
padding: 30px 0px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.box h1 {
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.box input {
|
||||
display: block;
|
||||
width: 300px;
|
||||
margin: 20px auto;
|
||||
padding: 15px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
color: #fff;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.box input:focus, .box input:active, .box button:focus, .box button:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.box button {
|
||||
background: #742ECC;
|
||||
border: 0;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
font-size: 20px;
|
||||
width: 330px;
|
||||
margin: 20px auto;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.box button:disabled {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.box button:active {
|
||||
background: #27ae60;
|
||||
}
|
||||
|
||||
.box p {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.box p span {
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.box .error {
|
||||
color: darkred;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#login {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#success {
|
||||
margin-top: 50px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=index.css.map */
|
1
modules/core/teaspeak-forum/ui/index.css.map
Normal file
1
modules/core/teaspeak-forum/ui/index.css.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["index.scss"],"names":[],"mappings":"AAAA;EACI;;;AAEJ;EACI;EACA;EACA;;;AAEJ;EACI;;;AAEJ;EACI;EACA;EACA;EACA;EACA;EACA;;;AAEJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEJ;EACI;EACA;EACA;;;AAEJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEJ;EACI;;;AAEJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEJ;EACI;;;AAEJ;EACI;;;AAEJ;EACI;EACA;;;AAEJ;EACI;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAEJ;EACI;EACA","file":"index.css"}
|
28
modules/core/teaspeak-forum/ui/index.html
Normal file
28
modules/core/teaspeak-forum/ui/index.html
Normal file
@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<title>TeaSpeak forum login</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="inner-container">
|
||||
<div class="box">
|
||||
<h1>Login</h1>
|
||||
<div id="login">
|
||||
<a class="error">some error code</a>
|
||||
<input type="text" placeholder="Username" id="user"/>
|
||||
<input type="password" placeholder="Password" id="pass"/>
|
||||
<button id="btn_login" target="#">Login</button>
|
||||
<p>Create a account on <a href="https://forum.teaspeak.de">forum.teaspeak.de</a></p>
|
||||
</div>
|
||||
<div id="success">
|
||||
<a> Successful logged in!</a><br>
|
||||
<a>You will be redirected in 3 seconds</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>const exports = {};</script>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
84
modules/core/teaspeak-forum/ui/index.scss
Normal file
84
modules/core/teaspeak-forum/ui/index.scss
Normal file
@ -0,0 +1,84 @@
|
||||
html {
|
||||
overflow: visible;
|
||||
}
|
||||
body{
|
||||
padding:0;
|
||||
margin:0;
|
||||
overflow: visible;
|
||||
}
|
||||
.inner {
|
||||
position: absolute;
|
||||
}
|
||||
.inner-container{
|
||||
width:400px;
|
||||
height:400px;
|
||||
position:absolute;
|
||||
top:calc(50vh - 200px);
|
||||
left:calc(50vw - 200px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.box{
|
||||
position:absolute;
|
||||
height:100%;
|
||||
width:100%;
|
||||
font-family: Helvetica, serif;
|
||||
color:#fff;
|
||||
background:rgba(0,0,0,0.13);
|
||||
padding:30px 0px;
|
||||
text-align: center;
|
||||
}
|
||||
.box h1{
|
||||
text-align:center;
|
||||
margin:30px 0;
|
||||
font-size:30px;
|
||||
}
|
||||
.box input{
|
||||
display:block;
|
||||
width:300px;
|
||||
margin:20px auto;
|
||||
padding:15px;
|
||||
background:rgba(0,0,0,0.2);
|
||||
color:#fff;
|
||||
border:0;
|
||||
}
|
||||
.box input:focus,.box input:active,.box button:focus,.box button:active{
|
||||
outline:none;
|
||||
}
|
||||
.box button {
|
||||
background:#742ECC;
|
||||
border:0;
|
||||
color:#fff;
|
||||
padding:10px;
|
||||
font-size:20px;
|
||||
width:330px;
|
||||
margin:20px auto;
|
||||
display:block;
|
||||
cursor:pointer;
|
||||
}
|
||||
.box button:disabled {
|
||||
background:rgba(0,0,0,0.2);
|
||||
}
|
||||
.box button:active{
|
||||
background:#27ae60;
|
||||
}
|
||||
.box p{
|
||||
font-size:14px;
|
||||
text-align:center;
|
||||
}
|
||||
.box p span{
|
||||
cursor:pointer;
|
||||
color:#666;
|
||||
}
|
||||
|
||||
.box .error {
|
||||
color: darkred;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#login {
|
||||
display: block;
|
||||
}
|
||||
#success {
|
||||
margin-top: 50px;
|
||||
display: none;
|
||||
}
|
84
modules/core/teaspeak-forum/ui/index.ts
Normal file
84
modules/core/teaspeak-forum/ui/index.ts
Normal file
@ -0,0 +1,84 @@
|
||||
window.$ = require("jquery");
|
||||
{
|
||||
const request = require('request');
|
||||
const util = require('util');
|
||||
const request_post = util.promisify(request.post);
|
||||
|
||||
|
||||
const api_url = "https://web.teaspeak.de/";
|
||||
|
||||
|
||||
const btn_login = $("#btn_login");
|
||||
btn_login.on('click', () => {
|
||||
btn_login
|
||||
.prop("disabled", true)
|
||||
.empty()
|
||||
.append($(document.createElement("i")).addClass("fa fa-circle-o-notch fa-spin"));
|
||||
submit_login($("#user").val() as string, $("#pass").val() as string).then(data => {
|
||||
$("#login").hide(500);
|
||||
$("#success").show(500);
|
||||
|
||||
const ipc = require("electron").ipcRenderer;
|
||||
ipc.send('teaforo-callback', data);
|
||||
}).catch(error => {
|
||||
console.log("Failed: " + error);
|
||||
loginFailed(error);
|
||||
});
|
||||
});
|
||||
|
||||
async function submit_login(user: string, pass: string) : Promise<UserData> {
|
||||
const {error, response, body} = await request_post(api_url + "auth.php", {
|
||||
timeout: 5000,
|
||||
form: {
|
||||
action: "login",
|
||||
user: user,
|
||||
pass: pass
|
||||
}
|
||||
|
||||
});
|
||||
console.log("Error: %o", error);
|
||||
console.log("response: %o", response);
|
||||
console.log("body: %o", body);
|
||||
|
||||
const data = JSON.parse(body);
|
||||
if(!data["success"]) throw data["msg"];
|
||||
|
||||
let user_data: UserData = {} as any;
|
||||
user_data.session_id = data["sessionId"];
|
||||
user_data.username = data["user_name"];
|
||||
user_data.application_data = data["user_data"];
|
||||
user_data.application_data_sign = data["user_sign"];
|
||||
return user_data;
|
||||
}
|
||||
|
||||
function loginFailed(err: string = "") {
|
||||
btn_login
|
||||
.prop("disabled", false)
|
||||
.empty()
|
||||
.append($(document.createElement("a")).text("Login"));
|
||||
|
||||
let errTag = $(".box .error");
|
||||
if(err !== "") {
|
||||
errTag.text(err).show(500);
|
||||
} else errTag.hide(500);
|
||||
}
|
||||
|
||||
//<i class="fa fa-circle-o-notch fa-spin" id="login-loader"></i>
|
||||
|
||||
$("#user").on('keydown', event => {
|
||||
if(event.key == "Enter") $("#pass").focus();
|
||||
});
|
||||
|
||||
$("#pass").on('keydown', event => {
|
||||
if(event.key == "Enter") $("#btn_login").trigger("click");
|
||||
});
|
||||
|
||||
//Patch for the external URL
|
||||
$('body').on('click', 'a', (event) => {
|
||||
event.preventDefault();
|
||||
let link = (<any>event.target).href;
|
||||
require("electron").shell.openExternal(link);
|
||||
});
|
||||
}
|
||||
|
||||
|
135
modules/core/ui-loader/graphical.ts
Normal file
135
modules/core/ui-loader/graphical.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import * as electron from "electron";
|
||||
import * as path from "path";
|
||||
import {screen} from "electron";
|
||||
|
||||
import {Arguments, process_args} from "../../shared/process-arguments";
|
||||
import * as loader from "./loader";
|
||||
import * as updater from "../app-updater";
|
||||
|
||||
export namespace ui {
|
||||
let gui: electron.BrowserWindow;
|
||||
let promise: Promise<String>;
|
||||
let resolve: any;
|
||||
let reject: any;
|
||||
|
||||
export function running() : boolean {
|
||||
return promise !== undefined;
|
||||
}
|
||||
|
||||
export function cancel() : boolean {
|
||||
if(resolve)
|
||||
resolve();
|
||||
|
||||
cleanup();
|
||||
return true;
|
||||
}
|
||||
|
||||
export function cleanup() {
|
||||
if(gui) {
|
||||
gui.destroy();
|
||||
gui = undefined;
|
||||
|
||||
promise = undefined;
|
||||
resolve = undefined;
|
||||
reject = error => {
|
||||
if(error)
|
||||
console.error("Received error from loader after it had been closed... Error: %o", error);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function load_files() {
|
||||
const channel = await updater.selected_channel();
|
||||
try {
|
||||
const entry_point = await loader.load_files(channel, (status, index) => {
|
||||
if(gui) {
|
||||
gui.webContents.send('progress-update', index);
|
||||
}
|
||||
});
|
||||
|
||||
const resolved = () => {
|
||||
resolve(entry_point);
|
||||
|
||||
promise = undefined;
|
||||
resolve = undefined;
|
||||
reject = error => {
|
||||
if(error)
|
||||
console.error("Received error from loader after it had been closed... Error: %o", error);
|
||||
};
|
||||
};
|
||||
if(!process_args.has_flag(...Arguments.DISABLE_ANIMATION))
|
||||
setTimeout(resolved, 250);
|
||||
else
|
||||
setImmediate(resolved);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function show_await_update() {
|
||||
if(gui)
|
||||
gui.webContents.send('await-update');
|
||||
}
|
||||
|
||||
function spawn_gui(close_callback: () => any) {
|
||||
console.log("Spawn window!");
|
||||
|
||||
const WINDOW_WIDTH = 340;
|
||||
const WINDOW_HEIGHT = 400;
|
||||
|
||||
let bounds = screen.getPrimaryDisplay().bounds;
|
||||
let x = (bounds.width - WINDOW_WIDTH) / 2;
|
||||
let y = (bounds.height - WINDOW_HEIGHT) / 2;
|
||||
|
||||
let dev_tools = false;
|
||||
|
||||
gui = new electron.BrowserWindow({
|
||||
width: dev_tools ? WINDOW_WIDTH + 1000 : WINDOW_WIDTH,
|
||||
height: WINDOW_HEIGHT + (process.platform == "win32" ? 40 : 0),
|
||||
frame: true,
|
||||
resizable: dev_tools,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
//frame: false,
|
||||
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
nodeIntegrationInWorker: true
|
||||
}
|
||||
});
|
||||
gui.setMenu(null);
|
||||
gui.loadFile(path.join(path.dirname(module.filename), "ui", "loading_screen.html"));
|
||||
gui.on('closed', close_callback);
|
||||
|
||||
gui.on('ready-to-show', () => {
|
||||
gui.show();
|
||||
|
||||
const call_loader = () => load_files().catch(reject);
|
||||
if(!process_args.has_flag(...Arguments.DISABLE_ANIMATION))
|
||||
setTimeout(call_loader, 1000);
|
||||
else
|
||||
setImmediate(call_loader);
|
||||
|
||||
if(dev_tools)
|
||||
gui.webContents.openDevTools();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export async function execute_loader() : Promise<String> {
|
||||
return promise = new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve;
|
||||
reject = _reject || (error => {
|
||||
console.error("Failed to load UI files! Error: %o", error)
|
||||
});
|
||||
|
||||
spawn_gui(() => reject(undefined));
|
||||
});
|
||||
}
|
||||
|
||||
export function preloading_page(entry_point: string) : string {
|
||||
global["browser-root"] = entry_point; /* setup entry point */
|
||||
|
||||
return path.join(path.dirname(module.filename), "ui", "preload_page.html");
|
||||
}
|
||||
}
|
2
modules/core/ui-loader/index.ts
Normal file
2
modules/core/ui-loader/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./loader.js";
|
||||
export * from "./graphical.js";
|
512
modules/core/ui-loader/loader.ts
Normal file
512
modules/core/ui-loader/loader.ts
Normal file
@ -0,0 +1,512 @@
|
||||
import {is_debug} from "../main_window";
|
||||
|
||||
const request = require('request');
|
||||
const querystring = require('querystring');
|
||||
const fs = require('fs-extra');
|
||||
const os = require('os');
|
||||
const UUID = require('pure-uuid');
|
||||
import * as path from "path";
|
||||
import * as zlib from "zlib";
|
||||
import * as tar from "tar-stream";
|
||||
import {Arguments, process_args} from "../../shared/process-arguments";
|
||||
import {parse_version} from "../../shared/version";
|
||||
|
||||
import * as electron from "electron";
|
||||
import MessageBoxOptions = Electron.MessageBoxOptions;
|
||||
import {current_version, execute_graphical} from "../app-updater";
|
||||
|
||||
const TIMEOUT = 10000;
|
||||
let local_path = undefined;
|
||||
|
||||
interface RemoteURL {
|
||||
(): string;
|
||||
cached?: string;
|
||||
}
|
||||
const remote_url: RemoteURL = () => {
|
||||
if(remote_url.cached)
|
||||
return remote_url.cached;
|
||||
const default_path = is_debug ? "http://localhost/home/TeaSpeak/Web-Client/client-api/environment/" : "https://clientapi.teaspeak.de/";
|
||||
return remote_url.cached = (process_args.has_value(...Arguments.SERVER_URL) ? process_args.value(...Arguments.SERVER_URL) : default_path);
|
||||
};
|
||||
|
||||
function data_directory() : string {
|
||||
return electron.app.getPath('userData');
|
||||
}
|
||||
|
||||
function cache_directory() : string {
|
||||
return path.join(data_directory(), "cache", "ui");
|
||||
}
|
||||
|
||||
function working_directory() : string {
|
||||
return path.join(data_directory(), "tmp", "ui");
|
||||
}
|
||||
|
||||
export interface VersionedFile {
|
||||
name: string,
|
||||
hash: string,
|
||||
path: string,
|
||||
type: string,
|
||||
|
||||
local_url: () => Promise<String>
|
||||
}
|
||||
|
||||
function generate_tmp() : Promise<String> {
|
||||
if(local_path) return Promise.resolve(local_path);
|
||||
|
||||
const id = new UUID(4).format();
|
||||
const directory = path.join(os.tmpdir(), "TeaClient-" + id) + "/";
|
||||
|
||||
return fs.mkdirs(directory).then(() => {
|
||||
local_path = directory;
|
||||
global["browser-root"] = local_path;
|
||||
console.log("Local browser path: %s", local_path);
|
||||
return Promise.resolve(local_path);
|
||||
});
|
||||
}
|
||||
|
||||
function get_raw_app_files() : Promise<VersionedFile[]> {
|
||||
return generate_tmp().then(path => new Promise<VersionedFile[]>((resolve, reject) => {
|
||||
const url = remote_url() + "api.php?" + querystring.stringify({
|
||||
type: "files",
|
||||
});
|
||||
console.debug("Requesting file list from %s", url);
|
||||
request.get(url, {
|
||||
timeout: TIMEOUT
|
||||
}, (error, response, body: string) => {
|
||||
response = response || {statusCode: -1};
|
||||
|
||||
if(error) { reject(error); return; }
|
||||
if(response.statusCode != 200) { setImmediate(reject, "invalid status code " + response.statusCode + " for " + url); return; }
|
||||
if(response.headers["info-version"] != 1) { setImmediate(reject, "Invalid response version (" + response.headers["info-version"] + "). Update your app manually!"); return; }
|
||||
if(!body) {
|
||||
setImmediate(reject, "invalid body. (Missing)");
|
||||
return;
|
||||
}
|
||||
let result: VersionedFile[] = [];
|
||||
|
||||
body.split("\n").forEach(entry => {
|
||||
if(entry.length == 0) return;
|
||||
|
||||
let info = entry.split("\t");
|
||||
if(info[0] == "type") return;
|
||||
|
||||
result.push({
|
||||
type: info[0],
|
||||
hash: info[1],
|
||||
path: info[2],
|
||||
name: info[3]
|
||||
} as VersionedFile);
|
||||
});
|
||||
setImmediate(resolve, result);
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function download_raw_app_files() : Promise<VersionedFile[]> {
|
||||
return get_raw_app_files().then(response => {
|
||||
for(let file of response) {
|
||||
file.local_url = () => fs.mkdirs(local_path + file.path + "/").then(() => new Promise<String>((resolve, reject) => {
|
||||
request.get(remote_url() + "api.php?" + querystring.stringify({
|
||||
type: "file",
|
||||
path: file.path,
|
||||
name: file.name
|
||||
}), {
|
||||
timeout: TIMEOUT
|
||||
}).on('response', function(response) {
|
||||
if(response.statusCode != 200) {
|
||||
setImmediate(reject, "invalid status code " + response.statusCode + " for file " + file.name + " (" + file.path + ")");
|
||||
return;
|
||||
}
|
||||
}).on('complete', event => {
|
||||
}).on('error', error => {
|
||||
setImmediate(reject, error);
|
||||
}).pipe(fs.createWriteStream(local_path + file.path + "/" + file.name))
|
||||
.on('finish', event => {
|
||||
setImmediate(resolve, file.path + "/" + file.name);
|
||||
});
|
||||
}));
|
||||
}
|
||||
return Promise.resolve(response);
|
||||
}).catch(error => {
|
||||
console.log("Failed to get file list: %o", error);
|
||||
return Promise.reject("Failed to get file list (" + error + ")");
|
||||
})
|
||||
}
|
||||
|
||||
interface LocalUICache {
|
||||
fetch_history?: FetchStatus;
|
||||
versions?: LocalUICacheEntry[];
|
||||
|
||||
remote_index?: UIVersion[] | UIVersion;
|
||||
remote_index_channel?: string; /* only set if the last status was a channel only*/
|
||||
|
||||
local_index?: UIVersion;
|
||||
}
|
||||
|
||||
interface FetchStatus {
|
||||
timestamp: number;
|
||||
/**
|
||||
* 0 = success
|
||||
* 1 = connect fail
|
||||
* 2 = internal fail
|
||||
*/
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface LocalUICacheEntry {
|
||||
version: UIVersion;
|
||||
download_timestamp: number;
|
||||
tar_file: string;
|
||||
checksum: string; /* SHA512 */
|
||||
}
|
||||
|
||||
export interface UIVersion {
|
||||
channel: string;
|
||||
version: string;
|
||||
git_hash: string;
|
||||
timestamp: number;
|
||||
|
||||
required_client?: string;
|
||||
filename?: string;
|
||||
|
||||
client_shipped?: boolean;
|
||||
}
|
||||
|
||||
function ui_file_path(version: UIVersion) : string {
|
||||
if(version.client_shipped) {
|
||||
const app_path = electron.app.getAppPath();
|
||||
if(!app_path.endsWith(".asar"))
|
||||
return undefined;
|
||||
|
||||
return path.join(path.join(path.dirname(app_path), "ui"), version.filename);
|
||||
}
|
||||
|
||||
const file_name = "ui_" + version.channel + "_" + version.version + "_" + version.git_hash + "_" + version.timestamp + ".tar.gz";
|
||||
return path.join(cache_directory(), file_name);
|
||||
}
|
||||
|
||||
let _ui_load_cache: LocalUICache;
|
||||
async function ui_load_cache() : Promise<LocalUICache> {
|
||||
if(_ui_load_cache) return _ui_load_cache;
|
||||
|
||||
const file = path.join(cache_directory(), "data.json");
|
||||
if(!fs.existsSync(file)) return {} as LocalUICache;
|
||||
|
||||
console.log("Loading UI cache file %s", file);
|
||||
_ui_load_cache = await fs.readJson(file) as LocalUICache;
|
||||
return _ui_load_cache;
|
||||
}
|
||||
|
||||
async function client_shipped_ui() : Promise<UIVersion | undefined> {
|
||||
const app_path = electron.app.getAppPath();
|
||||
if(!app_path.endsWith(".asar"))
|
||||
return undefined;
|
||||
|
||||
const base_path = path.join(path.dirname(app_path), "ui");
|
||||
console.debug("Looking for client shipped UI pack at %s", base_path);
|
||||
if(!(await fs.pathExists(base_path)))
|
||||
return undefined;
|
||||
|
||||
const info: {
|
||||
channel: string,
|
||||
version: string,
|
||||
git_hash: string,
|
||||
required_client: string,
|
||||
timestamp: number,
|
||||
filename: string
|
||||
} = await fs.readJson(path.join(base_path, "default_ui_info.json")) as any;
|
||||
|
||||
return {
|
||||
channel: info.channel,
|
||||
client_shipped: true,
|
||||
|
||||
filename: info.filename,
|
||||
git_hash: info.git_hash,
|
||||
required_client: info.required_client,
|
||||
timestamp: info.timestamp,
|
||||
version: info.version,
|
||||
}
|
||||
}
|
||||
|
||||
async function ui_save_cache(cache: LocalUICache) {
|
||||
const file = path.join(cache_directory(), "data.json");
|
||||
if(!fs.existsSync(path.dirname(file)))
|
||||
await fs.mkdirs(path.dirname(file));
|
||||
await fs.writeJson(file, cache);
|
||||
}
|
||||
|
||||
async function get_ui_pack(channel?: string) : Promise<UIVersion[] | UIVersion> {
|
||||
return await new Promise<UIVersion[] | UIVersion>((resolve, reject) => {
|
||||
const url = remote_url() + "api.php?" + querystring.stringify({
|
||||
type: "ui-info"
|
||||
});
|
||||
request.get(url, {
|
||||
timeout: TIMEOUT
|
||||
}, (error, response, body: string) => {
|
||||
try {
|
||||
response = response || {statusCode: -1};
|
||||
|
||||
if(error) { throw error; }
|
||||
if(response.statusCode != 200) { throw "invalid status code " + response.statusCode + " for " + url; }
|
||||
if(!body) throw "invalid response body";
|
||||
|
||||
let result: UIVersion[] = [];
|
||||
|
||||
const json = JSON.parse(body) || {success: false, msg: "invalid body"};
|
||||
if(!json["success"]) throw "Failed to get ui info: " + json["msg"];
|
||||
|
||||
for(const entry of json["versions"]) {
|
||||
if(!channel || entry["channel"] == channel)
|
||||
result.push({
|
||||
channel: entry["channel"],
|
||||
version: entry["version"],
|
||||
git_hash: entry["git-ref"],
|
||||
timestamp: entry["timestamp"],
|
||||
required_client: entry["required_client"]
|
||||
});
|
||||
}
|
||||
|
||||
if(result.length == 0 && channel) result.push(undefined);
|
||||
const res = channel ? result[0] : result;
|
||||
ui_load_cache().then(async cache => {
|
||||
cache.fetch_history = cache.fetch_history || {} as any;
|
||||
cache.fetch_history.timestamp = Date.now();
|
||||
cache.fetch_history.status = 0;
|
||||
cache.remote_index = res as any;
|
||||
cache.remote_index_channel = channel;
|
||||
await ui_save_cache(cache);
|
||||
}).catch(error => {
|
||||
console.warn("Failed to save UI cache info: %o", error);
|
||||
resolve(res);
|
||||
}).then(err => resolve(res));
|
||||
} catch(error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
async function download_ui_pack(version: UIVersion) : Promise<void> {
|
||||
const directory = cache_directory();
|
||||
const file = ui_file_path(version);
|
||||
await fs.mkdirs(directory);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
request.get(remote_url() + "api.php?" + querystring.stringify({
|
||||
type: "ui-download",
|
||||
"git-ref": version.git_hash,
|
||||
version: version.version,
|
||||
timestamp: version.timestamp,
|
||||
channel: version.channel
|
||||
}), {
|
||||
timeout: TIMEOUT
|
||||
}).on('response', function(response) {
|
||||
if(response.statusCode != 200) { reject("Failed to download UI files (Status code " + response.statusCode + ")"); }
|
||||
}).on('error', error => {
|
||||
reject("Failed to download UI files: " + error);
|
||||
}).pipe(fs.createWriteStream(file)).on('finish', () => {
|
||||
ui_load_cache().then(cache => {
|
||||
cache.versions.push({
|
||||
checksum: "undefined",
|
||||
tar_file: file,
|
||||
download_timestamp: Date.now(),
|
||||
version: version
|
||||
});
|
||||
return ui_save_cache(cache);
|
||||
}).catch(error => resolve()).then(() => resolve());
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ui_pack_exists(version: UIVersion) : boolean {
|
||||
return fs.existsSync(ui_file_path(version));
|
||||
}
|
||||
|
||||
async function unpack_cached(version: UIVersion) : Promise<string> {
|
||||
const file = ui_file_path(version);
|
||||
if(!fs.existsSync(file)) throw "missing file";
|
||||
|
||||
const target_dir = path.join(working_directory(), version.channel + "_" + version.timestamp);
|
||||
if(fs.existsSync(target_dir)) fs.removeSync(target_dir);
|
||||
|
||||
await fs.mkdirs(target_dir);
|
||||
|
||||
const gunzip = zlib.createGunzip();
|
||||
const extract = tar.extract();
|
||||
const fpipe = fs.createReadStream(file);
|
||||
|
||||
extract.on('entry', function(header: tar.Headers, stream, next) {
|
||||
if(header.type == 'file') {
|
||||
const target_file = path.join(target_dir, header.name);
|
||||
if(!fs.existsSync(path.dirname(target_file))) fs.mkdirsSync(path.dirname(target_file));
|
||||
|
||||
stream.on('end', () => setImmediate(next));
|
||||
const wfpipe = fs.createWriteStream(target_file);
|
||||
stream.pipe(wfpipe);
|
||||
} else if(header.type == 'directory') {
|
||||
if(fs.existsSync(path.join(target_dir, header.name)))
|
||||
setImmediate(next);
|
||||
fs.mkdirs(path.join(target_dir, header.name)).catch(error => {
|
||||
console.warn("Failed to create unpacking fir " + path.join(target_dir, header.name));
|
||||
console.error(error);
|
||||
}).then(() => setImmediate(next));
|
||||
} else {
|
||||
console.warn("Invalid ui tar ball entry type (" + header.type + ")");
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const finish_promise = new Promise(resolve => {
|
||||
extract.on('finish', resolve);
|
||||
extract.on('error', event => {
|
||||
if(!event) return;
|
||||
throw event;
|
||||
});
|
||||
});
|
||||
|
||||
fpipe.pipe(gunzip).pipe(extract);
|
||||
await finish_promise;
|
||||
|
||||
return target_dir;
|
||||
}
|
||||
|
||||
export async function cleanup() {
|
||||
if(await fs.pathExists(local_path))
|
||||
await fs.remove(local_path);
|
||||
}
|
||||
|
||||
export async function load_files(channel: string, static_cb: (message: string, index: number) => any) : Promise<String> {
|
||||
const type = parseInt(process_args.has_value(Arguments.UPDATER_UI_LOAD_TYPE) ? process_args.value(Arguments.UPDATER_UI_LOAD_TYPE) : "-1");
|
||||
if(type == 0 || !is_debug) {
|
||||
console.log("Loading ui package");
|
||||
|
||||
static_cb("Fetching info", 0);
|
||||
const cache = await ui_load_cache();
|
||||
console.log("Local cache: %o", cache);
|
||||
|
||||
let ui_info: UIVersion;
|
||||
try {
|
||||
ui_info = await get_ui_pack(channel) as UIVersion;
|
||||
} catch(error) {
|
||||
if(error instanceof Error)
|
||||
console.error("Failed to fetch ui info: %s. Using cached info!", error.message);
|
||||
else
|
||||
console.error("Failed to fetch ui info: %o. Using cached info!", error);
|
||||
}
|
||||
if(!ui_info) {
|
||||
if(cache && !process_args.has_flag(Arguments.UPDATER_UI_NO_CACHE)) {
|
||||
if(Array.isArray(cache.remote_index)) {
|
||||
for(const index of cache.remote_index) {
|
||||
if(index && index.channel == "release") {
|
||||
ui_info = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//TODO: test channel?
|
||||
ui_info = cache.remote_index;
|
||||
}
|
||||
}
|
||||
if(ui_info) {
|
||||
console.debug("Found local UI pack.");
|
||||
} else {
|
||||
//Test for the client shipped ui pack
|
||||
try {
|
||||
console.info("Looking for client shipped UI pack.");
|
||||
ui_info = await client_shipped_ui();
|
||||
if(!ui_info)
|
||||
throw "failed to load info";
|
||||
console.info("Using client shipped UI pack because we've no active internet connection.")
|
||||
} catch(error) {
|
||||
console.warn("Failed to load client shipped UI pack: %o", error);
|
||||
throw "Failed to load UI pack from cache!\nPlease ensure a valid internet connection.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static_cb("Searching cache for file", .33);
|
||||
console.log("Loading UI from data: %o. Target path: %s", ui_info, ui_file_path(ui_info));
|
||||
if(ui_info.required_client) {
|
||||
const ui_vers = parse_version(ui_info.required_client);
|
||||
console.log("Checking required client version (Required: %s, Version: %s)", ui_vers.toString(true), (await current_version()).toString(true));
|
||||
if(ui_vers.newer_than(await current_version())) {
|
||||
const local_available = cache && cache.local_index ? ui_pack_exists(cache.local_index) : undefined;
|
||||
|
||||
const result = electron.dialog.showMessageBox({
|
||||
type: "question",
|
||||
message:
|
||||
"Local client is outdated.\n" +
|
||||
"Newer UI packs (>= " + ui_info.version + ") require client " + ui_info.required_client + "\n" +
|
||||
"Do you want to upgrade?",
|
||||
title: "Client outdated!",
|
||||
buttons: ["yes", local_available ? "ignore and use last possible (" + cache.local_index.version + ")" : "close client"]
|
||||
} as MessageBoxOptions);
|
||||
if(result == 0) {
|
||||
await execute_graphical(channel, true);
|
||||
throw "client outdated";
|
||||
} else {
|
||||
if(!local_available) {
|
||||
electron.app.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
ui_info = cache.local_index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!ui_pack_exists(ui_info)) {
|
||||
console.log("Ui version does not locally exists. Downloading new one");
|
||||
static_cb("Downloading files", .34);
|
||||
await download_ui_pack(ui_info);
|
||||
console.log("Download completed!");
|
||||
}
|
||||
|
||||
console.log("Unpacking cached ui info");
|
||||
static_cb("Unpacking files", .66);
|
||||
const target_path = await unpack_cached(ui_info);
|
||||
cache.local_index = ui_info;
|
||||
await ui_save_cache(cache);
|
||||
|
||||
console.log("Unpacked. Target path: %s", target_path);
|
||||
static_cb("UI loaded", 1);
|
||||
|
||||
return path.join(target_path, "index.html");
|
||||
} else {
|
||||
console.log("Loading file by file");
|
||||
|
||||
static_cb("Fetching files", 0);
|
||||
let files;
|
||||
try {
|
||||
files = await download_raw_app_files()
|
||||
} catch (error) {
|
||||
throw "Failed to get file list: " + error;
|
||||
}
|
||||
console.log("Get raw files:");
|
||||
let futures: Promise<void>[] = [];
|
||||
let finish_count = 0;
|
||||
static_cb("Downloading files", 0);
|
||||
|
||||
for(const file of files) {
|
||||
console.log("Start downloading %s (%s)", file.name, file.path);
|
||||
|
||||
const start = Date.now();
|
||||
futures.push(file.local_url().then(data => {
|
||||
finish_count++;
|
||||
console.log("Downloaded %s (%s) (%ims)", file.name, file.path, Date.now() - start);
|
||||
static_cb("Downloading files", finish_count / files.length);
|
||||
}));
|
||||
|
||||
//await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(futures);
|
||||
} catch (error) {
|
||||
throw "Failed to download files: " + error;
|
||||
}
|
||||
return await generate_tmp() + "index.html"; /* entry point */
|
||||
}
|
||||
}
|
1
modules/core/ui-loader/ui/img/logo.svg
Normal file
1
modules/core/ui-loader/ui/img/logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 13 KiB |
BIN
modules/core/ui-loader/ui/img/smoke.png
Normal file
BIN
modules/core/ui-loader/ui/img/smoke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
21
modules/core/ui-loader/ui/loader.ts
Normal file
21
modules/core/ui-loader/ui/loader.ts
Normal file
@ -0,0 +1,21 @@
|
||||
const icp = require("electron").ipcRenderer;
|
||||
|
||||
interface Window {
|
||||
$: JQuery;
|
||||
}
|
||||
(window as any).$ = require("jquery");
|
||||
|
||||
icp.on('progress-update', (event, count) => {
|
||||
console.log("Process update to %f", count);
|
||||
|
||||
$(".container-bar .bar").css("width", (count * 100) + "%");
|
||||
});
|
||||
|
||||
icp.on('await-update', (event) => {
|
||||
console.log("Received update notification");
|
||||
|
||||
$(".container-bar .bar").css("width", "100%");
|
||||
$("#loading-text").html("Awaiting client update response<br>(User input required)");
|
||||
});
|
||||
|
||||
export {}
|
80
modules/core/ui-loader/ui/loading_screen.html
Normal file
80
modules/core/ui-loader/ui/loading_screen.html
Normal file
@ -0,0 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>TeaClient</title>
|
||||
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
background: #18BC9C;
|
||||
}
|
||||
|
||||
body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
display: block;
|
||||
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.smoke {
|
||||
z-index: 2;
|
||||
}
|
||||
.logo {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.container-logo {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.container-info a {
|
||||
display: inline-block;
|
||||
color: #FFFFFF;
|
||||
font-family: "Arial",serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.container-bar {
|
||||
position: relative;
|
||||
margin-top: 5px;
|
||||
border: white solid 2px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.container-bar .bar {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
display: block;
|
||||
|
||||
background: whitesmoke;
|
||||
border: none;
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="application/ecmascript">const exports = {};</script>
|
||||
<script type="application/ecmascript" src="loader.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-logo">
|
||||
<img class="logo" src="img/logo.svg">
|
||||
<img class="smoke" src="img/smoke.png">
|
||||
</div>
|
||||
<div class="container-info">
|
||||
<a id="loading-text">Loading... Please wait!</a>
|
||||
<div class="container-bar">
|
||||
<div class="bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
25
modules/core/ui-loader/ui/preload_page.html
Normal file
25
modules/core/ui-loader/ui/preload_page.html
Normal file
@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script type="application/javascript">
|
||||
const remote = require('electron').remote;
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const target_file = remote.getGlobal("browser-root");
|
||||
console.log("Navigate to %s", target_file);
|
||||
|
||||
if(fs.existsSync(target_file))
|
||||
window.location.href = target_file;
|
||||
else {
|
||||
console.error("Failed to find target file!");
|
||||
if(!remote.getCurrentWebContents().isDevToolsOpened())
|
||||
remote.getCurrentWebContents().openDevTools();
|
||||
}
|
||||
</script>
|
||||
<title>TeaClient - loading files</title>
|
||||
</head>
|
||||
<body>
|
||||
An unknown error happened!<br>
|
||||
Please report this!
|
||||
</body>
|
||||
</html>
|
37
modules/core/url-preview/index.ts
Normal file
37
modules/core/url-preview/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import * as electron from "electron";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
export function open_preview(url: string) {
|
||||
console.log("Open URL as preview: %s", url);
|
||||
const window = new electron.BrowserWindow();
|
||||
window.loadURL(url);
|
||||
|
||||
//FIXME try catch?
|
||||
const inject_file = path.join(path.dirname(module.filename), "inject.js");
|
||||
window.webContents.once('dom-ready', e => {
|
||||
const code_inject = fs.readFileSync(inject_file).toString();
|
||||
window.webContents.executeJavaScript(code_inject, true);
|
||||
});
|
||||
}
|
||||
|
||||
electron.ipcMain.on('preview-action', (event, args) => {
|
||||
const sender: electron.WebContents = event.sender;
|
||||
if(!args || !args.action) {
|
||||
console.warn("Received preview action without a valid action type!");
|
||||
return;
|
||||
}
|
||||
|
||||
if(args.action === "open-url") {
|
||||
console.log("Opening " +args.url);
|
||||
electron.shell.openExternal(args.url, {
|
||||
activate: true
|
||||
});
|
||||
|
||||
const browser = electron.BrowserWindow.fromWebContents(sender);
|
||||
if(!browser)
|
||||
console.warn("Failed to find browser handle");
|
||||
else
|
||||
browser.close();
|
||||
}
|
||||
});
|
78
modules/core/url-preview/inject.ts
Normal file
78
modules/core/url-preview/inject.ts
Normal file
@ -0,0 +1,78 @@
|
||||
const log_prefix = "[TeaSpeak::Preview] ";
|
||||
|
||||
const object =
|
||||
"<div style='position: fixed; top: 0; bottom: 0; left: 0; right: 0; z-index: 99999999999999999999999999;'>" +
|
||||
"<div style='\n" +
|
||||
"font-family: \"Open Sans\"," +
|
||||
"sans-serif;\n" +
|
||||
"width: 100%;\n" +
|
||||
"margin: 0;\n" +
|
||||
"height: 40px;\n" +
|
||||
"font-size: 17px;\n" +
|
||||
"font-weight: 400;\n" +
|
||||
"padding: .33em .5em;\n" +
|
||||
"color: #5c5e60;\n" +
|
||||
"position: fixed;\n" +
|
||||
"background-color: white;\n" +
|
||||
"box-shadow: 0 1px 3px 2px rgba(0,0,0,0.15);" +
|
||||
"display: flex;\n" +
|
||||
"flex-direction: row;\n" +
|
||||
"justify-content: center;" +
|
||||
"align-items: center;'" +
|
||||
">" +
|
||||
"<div style='margin-right: .67em;display: inline-block;line-height: 1.3;text-align: center'>You're in TeaWeb website preview mode. Click <a href='#' class='button-open'>here</a> to open the website in the browser</div>" +
|
||||
"</div>" +
|
||||
"<div style='display: table-cell;width: 1.6em;'>" +
|
||||
"<a style='font-size: 14px;\n" +
|
||||
"top: 13px;\n" +
|
||||
"right: 25px;\n" +
|
||||
"width: 15px;\n" +
|
||||
"height: 15px;\n" +
|
||||
"opacity: .3;\n" +
|
||||
"color: #000;\n" +
|
||||
"cursor: pointer;\n" +
|
||||
"position: absolute;\n" +
|
||||
"text-align: center;\n" +
|
||||
"line-height: 15px;\n" +
|
||||
"z-index: 1000;\n" +
|
||||
"text-decoration: none;'" +
|
||||
"class='button-close'>" +
|
||||
"✖" +
|
||||
"</a>" +
|
||||
"</div>" +
|
||||
"</div>";
|
||||
|
||||
const element = document.createElement("div");
|
||||
element.id = "TeaClient-Overlay-Container";
|
||||
document.body.append(element);
|
||||
element.innerHTML = object;
|
||||
|
||||
{
|
||||
const buttons = element.getElementsByClassName("button-close");
|
||||
if(buttons.length < 1) {
|
||||
console.warn(log_prefix + "Failed to find close button for preview notice!");
|
||||
} else {
|
||||
for(const button of buttons) {
|
||||
(<HTMLElement>button).onclick = event => {
|
||||
console.trace(log_prefix + "Closing preview notice");
|
||||
element.remove();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
const buttons = element.getElementsByClassName("button-open");
|
||||
if(buttons.length < 1) {
|
||||
console.warn(log_prefix + "Failed to find open button for preview notice!");
|
||||
} else {
|
||||
for(const element of buttons) {
|
||||
(<HTMLElement>element).onclick = event => {
|
||||
console.info(log_prefix + "Opening URL with default browser");
|
||||
require("electron").ipcRenderer.send('preview-action', {
|
||||
action: 'open-url',
|
||||
url: document.documentURI
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
97
modules/crash_handler/index.ts
Normal file
97
modules/crash_handler/index.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import {app, BrowserWindow, remote} from "electron";
|
||||
import * as path from "path";
|
||||
import * as electron from "electron";
|
||||
import * as os from "os";
|
||||
|
||||
export function handle_crash_callback(args: string[]) {
|
||||
const parameter = {};
|
||||
for(const argument of args) {
|
||||
const colon_index = argument.indexOf('=');
|
||||
if(colon_index == -1) {
|
||||
console.warn("Crash callback contains invalid argument! (%s)", argument);
|
||||
continue;
|
||||
}
|
||||
|
||||
parameter[argument.substr(0, colon_index)] = argument.substr(colon_index + 1);
|
||||
}
|
||||
console.log("Received crash dump callback. Arguments: %o", parameter);
|
||||
|
||||
let error = undefined;
|
||||
let crash_file = undefined;
|
||||
|
||||
if(parameter["success"] == true) {
|
||||
/* okey we have an crash dump */
|
||||
crash_file = parameter["dump_path"];
|
||||
if(typeof(crash_file) === "string") {
|
||||
try {
|
||||
crash_file = Buffer.from(crash_file, 'base64').toString();
|
||||
} catch(error) {
|
||||
console.warn("Failed to decode dump path: %o", error);
|
||||
crash_file = undefined;
|
||||
error = "failed to decode dump path!";
|
||||
}
|
||||
}
|
||||
} else if(typeof(parameter["error"]) === "string") {
|
||||
try {
|
||||
error = Buffer.from(crash_file, 'base64').toString();
|
||||
} catch(error) {
|
||||
console.warn("Failed to decode error: %o", error);
|
||||
error = "failed to decode error";
|
||||
}
|
||||
} else {
|
||||
error = "missing parameters";
|
||||
}
|
||||
|
||||
app.on('ready', () => {
|
||||
const crash_window = new BrowserWindow({
|
||||
show: false,
|
||||
width: 1000,
|
||||
height: 300 + (os.platform() === "win32" ? 50 : 0),
|
||||
|
||||
webPreferences: {
|
||||
devTools: true,
|
||||
nodeIntegration: true,
|
||||
javascript: true
|
||||
}
|
||||
});
|
||||
crash_window.setMenu(null);
|
||||
crash_window.loadFile(path.join(path.dirname(module.filename), "ui", "index.html"));
|
||||
crash_window.on('ready-to-show', () => {
|
||||
if(error)
|
||||
crash_window.webContents.send('dump-error', error);
|
||||
else if(!crash_file)
|
||||
crash_window.webContents.send('dump-error', "Missing crash file");
|
||||
else
|
||||
crash_window.webContents.send('dump-url', crash_file);
|
||||
crash_window.show();
|
||||
});
|
||||
app.on('window-all-closed', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.paths.push(...(() => {
|
||||
const app_path = (remote || electron).app.getAppPath();
|
||||
const result = [];
|
||||
result.push(app_path + "/native/build/" + os.platform() + "_" + os.arch() + "/");
|
||||
if(app_path.endsWith(".asar"))
|
||||
result.push(path.join(path.dirname(app_path), "natives"));
|
||||
return result;
|
||||
})());
|
||||
|
||||
export const handler = require( "teaclient_crash_handler");
|
||||
|
||||
export function initialize_handler(component_name: string, requires_file: boolean) {
|
||||
const start_path = requires_file ? (" " + path.join(__dirname, "..", "..")) : "";
|
||||
handler.setup_crash_handler(
|
||||
component_name,
|
||||
path.join((remote || electron).app.getPath('userData'), "crash_dumps"),
|
||||
process.argv[0] + start_path + " crash-handler success=1 dump_path=%crash_path%",
|
||||
process.argv[0] + start_path + " crash-handler success=0 error=%error_message%"
|
||||
);
|
||||
}
|
||||
|
||||
export function finalize_handler() {
|
||||
handler.finalize();
|
||||
}
|
1
modules/crash_handler/ui/crash_logo.svg
Normal file
1
modules/crash_handler/ui/crash_logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 13 KiB |
34
modules/crash_handler/ui/index.css
Normal file
34
modules/crash_handler/ui/index.css
Normal file
@ -0,0 +1,34 @@
|
||||
body {
|
||||
background-color: grey;
|
||||
min-width: 750px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center; }
|
||||
|
||||
.container .container-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
width: fit-content; }
|
||||
.container .container-header > * {
|
||||
flex-shrink: 0; }
|
||||
.container .container-header img {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
width: 128px;
|
||||
height: 128px; }
|
||||
.container .container-header .text {
|
||||
margin-left: 20px;
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
align-self: center; }
|
||||
.container .container-header .text h1 {
|
||||
color: darkred;
|
||||
margin-bottom: 0; }
|
||||
.container .container-header .text h2 {
|
||||
font-size: 1.25em;
|
||||
margin-top: .2em; }
|
||||
.container .error-dump {
|
||||
color: red; }
|
||||
|
||||
/*# sourceMappingURL=index.css.map */
|
7
modules/crash_handler/ui/index.css.map
Normal file
7
modules/crash_handler/ui/index.css.map
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"mappings": "AAAA,IAAK;EACJ,gBAAgB,EAAE,IAAI;EACtB,SAAS,EAAE,KAAK;EAEhB,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,GAAG;EACnB,eAAe,EAAE,MAAM;;AAIvB,4BAAkB;EACjB,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,GAAG;EACnB,eAAe,EAAE,MAAM;EACvB,KAAK,EAAE,WAAW;EAElB,gCAAI;IACH,WAAW,EAAE,CAAC;EAGf,gCAAI;IACH,cAAc,EAAE,MAAM;IACtB,OAAO,EAAE,YAAY;IAErB,KAAK,EAAE,KAAK;IACZ,MAAM,EAAE,KAAK;EAGd,kCAAM;IACL,WAAW,EAAE,IAAI;IAEjB,OAAO,EAAE,YAAY;IACrB,UAAU,EAAE,IAAI;IAChB,UAAU,EAAE,MAAM;IAElB,qCAAG;MACF,KAAK,EAAE,OAAO;MACd,aAAa,EAAE,CAAC;IAGjB,qCAAG;MACF,SAAS,EAAE,MAAM;MACjB,UAAU,EAAE,IAAI;AAKnB,sBAAY;EACX,KAAK,EAAE,GAAG",
|
||||
"sources": ["index.scss"],
|
||||
"names": [],
|
||||
"file": "index.css"
|
||||
}
|
36
modules/crash_handler/ui/index.html
Normal file
36
modules/crash_handler/ui/index.html
Normal file
@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Application crashed!</title>
|
||||
<link rel="stylesheet" type="text/css" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="container-header">
|
||||
<img src="crash_logo.svg">
|
||||
<div class="text">
|
||||
<h1>Ooops, something went incredible wrong!</h1>
|
||||
<h2>It seems like your TeaSpeak Client has been crashed.</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-body">
|
||||
<p>
|
||||
Please report this crash to TeaSpeak and help improving the client!<br>
|
||||
Official issue and bug tracker url: <a href="#" onclick="open_issue_tracker(); return false;">https://github.com/TeaSpeak/TeaClient/issues</a><br>
|
||||
<b>Attention:</b> Crash reports without a crash dump file will be ignored!
|
||||
</p>
|
||||
<p class="error-hide">
|
||||
Crash dump file: <a href="#" class="crash-dump-directory">undefined</a>
|
||||
</p>
|
||||
<p class="error-dump error-show">
|
||||
Failed to create crash dump file: <a class="crash-dump-error"></a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let exports = {};
|
||||
</script>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
51
modules/crash_handler/ui/index.scss
Normal file
51
modules/crash_handler/ui/index.scss
Normal file
@ -0,0 +1,51 @@
|
||||
body {
|
||||
background-color: grey;
|
||||
min-width: 750px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
.container-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
|
||||
> * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 20px;
|
||||
|
||||
display: inline-block;;
|
||||
text-align: left;
|
||||
align-self: center;
|
||||
|
||||
h1 {
|
||||
color: darkred;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25em;
|
||||
margin-top: .2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-dump {
|
||||
color: red;
|
||||
}
|
||||
}
|
30
modules/crash_handler/ui/index.ts
Normal file
30
modules/crash_handler/ui/index.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { shell, ipcRenderer } from "electron";
|
||||
|
||||
function open_issue_tracker() {
|
||||
shell.openExternal("https://github.com/TeaSpeak/TeaClient/issues");
|
||||
}
|
||||
|
||||
function set_dump_error_flag(flag: boolean) {
|
||||
for(const node of document.getElementsByClassName("error-show") as HTMLCollectionOf<HTMLElement>)
|
||||
node.style.display = flag ? "block" : "none";
|
||||
|
||||
for(const node of document.getElementsByClassName("error-hide") as HTMLCollectionOf<HTMLElement>)
|
||||
node.style.display = flag ? "none" : "block";
|
||||
}
|
||||
|
||||
function set_dump_url(url: string) {
|
||||
for(const crash_path_node of document.getElementsByClassName("crash-dump-directory") as HTMLCollectionOf<HTMLElement>) {
|
||||
crash_path_node.textContent = url;
|
||||
crash_path_node.onclick = () => shell.showItemInFolder(url);
|
||||
}
|
||||
set_dump_error_flag(false);
|
||||
}
|
||||
|
||||
function set_dump_error(error: string) {
|
||||
set_dump_error_flag(true);
|
||||
for(const node of document.getElementsByClassName("crash-dump-error") as HTMLCollectionOf<HTMLElement>)
|
||||
node.textContent = error;
|
||||
}
|
||||
|
||||
ipcRenderer.on('dump-url', (event, url) => set_dump_url(url));
|
||||
ipcRenderer.on('dump-error', (event, error) => set_dump_error(error));
|
82
modules/renderer/PersistentLocalStorage.ts
Normal file
82
modules/renderer/PersistentLocalStorage.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import * as electron from "electron";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs-extra";
|
||||
|
||||
const APP_DATA = electron.remote.app.getPath("userData");
|
||||
const SETTINGS_DIR = path.join(APP_DATA, "settings");
|
||||
|
||||
let _local_storage: {[key: string]: any} = {};
|
||||
let _local_storage_save: {[key: string]: boolean} = {};
|
||||
export async function initialize() {
|
||||
await fs.mkdirs(SETTINGS_DIR);
|
||||
|
||||
const files = await fs.readdir(SETTINGS_DIR);
|
||||
for(const file of files) {
|
||||
const key = decodeURIComponent(file);
|
||||
console.log("Load settings: %s", key);
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(path.join(SETTINGS_DIR, file));
|
||||
const decoded = JSON.parse(data.toString() || "{}");
|
||||
|
||||
_local_storage[key] = decoded;
|
||||
} catch(error) {
|
||||
console.error("Failed to load settings for %s: %o", key, error);
|
||||
}
|
||||
}
|
||||
|
||||
let _new_storage: Storage = {} as any;
|
||||
|
||||
_new_storage.getItem = key => _local_storage[key] || null;
|
||||
_new_storage.setItem = (key, value) => {
|
||||
_local_storage[key] = value;
|
||||
_local_storage_save[key] = true;
|
||||
save_key(key).catch(error => {
|
||||
console.warn("Failed to save key: %s => %o", key, error);
|
||||
});
|
||||
(_new_storage as any)["length"] = Object.keys(_local_storage).length;
|
||||
};
|
||||
|
||||
_new_storage.clear = () => {
|
||||
_local_storage = {};
|
||||
_local_storage_save = {};
|
||||
|
||||
try {
|
||||
fs.emptyDirSync(SETTINGS_DIR);
|
||||
} catch(error) {
|
||||
console.warn("Failed to empty settings dir");
|
||||
}
|
||||
(_new_storage as any)["length"] = 0;
|
||||
};
|
||||
|
||||
_new_storage.key = index => Object.keys(_local_storage)[index];
|
||||
_new_storage.removeItem = key => {
|
||||
delete _local_storage[key];
|
||||
delete_key(key).catch(error => {
|
||||
console.warn("Failed to delete key on fs: %s => %o", key, error);
|
||||
});
|
||||
(_new_storage as any)["length"] = Object.keys(_local_storage).length;
|
||||
};
|
||||
|
||||
Object.assign(window.localStorage, _new_storage);
|
||||
}
|
||||
|
||||
export async function save_all() {
|
||||
let promises: Promise<void>[] = [];
|
||||
for(const key of Object.keys(_local_storage))
|
||||
promises.push(save_key(key));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
export async function save_key(key: string) {
|
||||
if(!_local_storage_save[key])
|
||||
return;
|
||||
|
||||
_local_storage_save[key] = false;
|
||||
await fs.writeJson(path.join(SETTINGS_DIR, encodeURIComponent(key)), _local_storage[key], {spaces: 0});
|
||||
}
|
||||
|
||||
export async function delete_key(key: string) {
|
||||
delete _local_storage_save[key];
|
||||
await fs.remove(path.join(SETTINGS_DIR, encodeURIComponent(key)));
|
||||
}
|
121
modules/renderer/audio/AudioPlayer.ts
Normal file
121
modules/renderer/audio/AudioPlayer.ts
Normal file
@ -0,0 +1,121 @@
|
||||
window["require_setup"](module);
|
||||
|
||||
import {audio as naudio} from "teaclient_connection";
|
||||
|
||||
namespace audio.player {
|
||||
export interface Device {
|
||||
device_id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Navigator {
|
||||
mozGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void;
|
||||
webkitGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void;
|
||||
}
|
||||
|
||||
let _initialized_callbacks: (() => any)[] = [];
|
||||
export let _initialized = false;
|
||||
export let _audioContext: AudioContext;
|
||||
export let _processor: ScriptProcessorNode;
|
||||
export let _output_stream: naudio.playback.OwnedAudioOutputStream;
|
||||
export let _current_device: naudio.AudioDevice;
|
||||
|
||||
export function initialized() : boolean {
|
||||
return _initialized;
|
||||
}
|
||||
|
||||
|
||||
export function context() : AudioContext {
|
||||
if(!_audioContext) throw "Initialize first!";
|
||||
return _audioContext;
|
||||
}
|
||||
|
||||
export function destination() : AudioNode {
|
||||
if(!_initialized)
|
||||
throw "Audio player hasn't yet be initialized";
|
||||
return _processor || _audioContext.destination;
|
||||
}
|
||||
|
||||
export function on_ready(cb: () => any) {
|
||||
if(_initialized)
|
||||
cb();
|
||||
else
|
||||
_initialized_callbacks.push(cb);
|
||||
}
|
||||
|
||||
export function initialize() {
|
||||
_output_stream = naudio.playback.create_stream();
|
||||
_output_stream.set_buffer_max_latency(0.08);
|
||||
_output_stream.set_buffer_latency(0.02);
|
||||
|
||||
_output_stream.callback_overflow = () => {
|
||||
console.warn("Main audio overflow");
|
||||
_output_stream.clear();
|
||||
};
|
||||
_output_stream.callback_underflow = () => {
|
||||
console.warn("Main audio underflow");
|
||||
};
|
||||
|
||||
_audioContext = new AudioContext();
|
||||
_processor = _audioContext.createScriptProcessor(1024, _output_stream.channels, _output_stream.channels);
|
||||
|
||||
_processor.onaudioprocess = function(event) {
|
||||
const buffer = event.inputBuffer;
|
||||
//console.log("Received %d channels of %d with a rate of %d", buffer.numberOfChannels, buffer.length, buffer.sampleRate);
|
||||
const target_buffer = new Float32Array(buffer.numberOfChannels * buffer.length);
|
||||
|
||||
for(let channel = 0; channel < buffer.numberOfChannels; channel++) {
|
||||
const channel_data = buffer.getChannelData(channel);
|
||||
target_buffer.set(channel_data, channel * buffer.length);
|
||||
}
|
||||
_output_stream.write_data_rated(target_buffer.buffer, false, buffer.sampleRate);
|
||||
};
|
||||
_processor.connect(_audioContext.destination);
|
||||
|
||||
|
||||
_initialized = true;
|
||||
for(const callback of _initialized_callbacks)
|
||||
callback();
|
||||
_initialized_callbacks = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function available_devices() : Promise<Device[]> {
|
||||
return naudio.available_devices().filter(e => e.output_supported || e.output_default).map(e => {
|
||||
return {
|
||||
device_id: e.device_id,
|
||||
name: e.name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function set_device(device_id?: string) : Promise<void> {
|
||||
const dev = naudio.available_devices().filter(e => e.device_id == device_id);
|
||||
if(dev.length == 0) {
|
||||
console.warn("Missing audio device with is %s", device_id)
|
||||
throw "invalid device id";
|
||||
}
|
||||
|
||||
await naudio.playback.set_device(dev[0].device_index);
|
||||
_current_device = dev[0];
|
||||
}
|
||||
|
||||
export function current_device() : Device {
|
||||
if(_current_device)
|
||||
return _current_device;
|
||||
|
||||
const dev = naudio.available_devices().filter(e => e.output_default);
|
||||
if(dev.length > 0)
|
||||
return dev[0];
|
||||
return {device_id: "default", name: "default"} as Device;
|
||||
}
|
||||
|
||||
export function get_master_volume() : number {
|
||||
return naudio.playback.get_master_volume();
|
||||
}
|
||||
export function set_master_volume(volume: number) {
|
||||
naudio.playback.set_master_volume(volume);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(window["audio"] || (window["audio"] = {}), audio);
|
393
modules/renderer/audio/AudioRecorder.ts
Normal file
393
modules/renderer/audio/AudioRecorder.ts
Normal file
@ -0,0 +1,393 @@
|
||||
/// <reference path="../imports/imports_shared.d.ts" />
|
||||
window["require_setup"](module);
|
||||
|
||||
import {audio as naudio} from "teaclient_connection";
|
||||
|
||||
export namespace _audio.recorder {
|
||||
import InputDevice = audio.recorder.InputDevice;
|
||||
import AbstractInput = audio.recorder.AbstractInput;
|
||||
|
||||
interface NativeDevice extends InputDevice {
|
||||
device_index: number;
|
||||
}
|
||||
|
||||
let _device_cache: NativeDevice[] = undefined;
|
||||
export function devices() : InputDevice[] {
|
||||
return _device_cache || (_device_cache = naudio.available_devices().filter(e => e.input_supported || e.input_default).map(e => {
|
||||
return {
|
||||
unique_id: e.device_id,
|
||||
channels: 2, /* TODO */
|
||||
default_input: e.input_default,
|
||||
supported: e.input_supported,
|
||||
name: e.name,
|
||||
sample_rate: 44100, /* TODO! */
|
||||
device_index: e.device_index
|
||||
} as NativeDevice
|
||||
}));
|
||||
}
|
||||
|
||||
export function device_refresh_available() : boolean { return false; }
|
||||
export function refresh_devices() : Promise<void> { throw "not supported yet!"; }
|
||||
|
||||
export function create_input() : AbstractInput {
|
||||
return new NativeInput();
|
||||
}
|
||||
|
||||
namespace filter {
|
||||
export abstract class NativeFilter implements audio.recorder.filter.Filter {
|
||||
type: audio.recorder.filter.Type;
|
||||
handle: NativeInput;
|
||||
enabled: boolean = false;
|
||||
|
||||
protected constructor(handle, type) { this.handle = handle; this.type = type; }
|
||||
|
||||
abstract initialize();
|
||||
abstract finalize();
|
||||
|
||||
|
||||
is_enabled(): boolean { return this.enabled; }
|
||||
}
|
||||
|
||||
export class NThresholdFilter extends NativeFilter implements audio.recorder.filter.ThresholdFilter {
|
||||
private filter: naudio.record.ThresholdConsumeFilter;
|
||||
|
||||
private _margin_frames: number = 6; /* 120ms */
|
||||
private _threshold: number = 50;
|
||||
private _callback_level: any;
|
||||
|
||||
private _attack_smooth = 0;
|
||||
private _release_smooth = 0;
|
||||
|
||||
callback_level: (level: number) => any;
|
||||
|
||||
constructor(handle) {
|
||||
super(handle, audio.recorder.filter.Type.THRESHOLD);
|
||||
|
||||
Object.defineProperty(this, 'callback_level', {
|
||||
get(): any {
|
||||
return this._callback_level;
|
||||
}, set(v: any): void {
|
||||
console.log("SET CALLBACK LEVEL! %o", v);
|
||||
if(v === this._callback_level)
|
||||
return;
|
||||
|
||||
this._callback_level = v;
|
||||
if(this.filter)
|
||||
this.filter.set_analyze_filter(v);
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
})
|
||||
}
|
||||
|
||||
get_margin_frames(): number {
|
||||
return this.filter ? this.filter.get_margin_frames() : this._margin_frames;
|
||||
}
|
||||
|
||||
get_threshold(): number {
|
||||
return this.filter ? this.filter.get_threshold() : this._threshold;
|
||||
}
|
||||
|
||||
set_margin_frames(value: number) {
|
||||
this._margin_frames = value;
|
||||
if(this.filter)
|
||||
this.filter.set_margin_frames(value);
|
||||
}
|
||||
|
||||
get_attack_smooth(): number {
|
||||
return this.filter ? this.filter.get_attack_smooth() : this._attack_smooth;
|
||||
}
|
||||
|
||||
get_release_smooth(): number {
|
||||
return this.filter ? this.filter.get_release_smooth() : this._release_smooth;
|
||||
}
|
||||
|
||||
set_attack_smooth(value: number) {
|
||||
this._attack_smooth = value;
|
||||
if(this.filter)
|
||||
this.filter.set_attack_smooth(value);
|
||||
}
|
||||
|
||||
set_release_smooth(value: number) {
|
||||
this._release_smooth = value;
|
||||
if(this.filter)
|
||||
this.filter.set_release_smooth(value);
|
||||
}
|
||||
|
||||
set_threshold(value: number): Promise<void> {
|
||||
if(typeof(value) === "string")
|
||||
value = parseInt(value); /* yes... this happens */
|
||||
this._threshold = value;
|
||||
if(this.filter)
|
||||
this.filter.set_threshold(value);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if(this.filter) {
|
||||
if(this.handle.consumer)
|
||||
this.handle.consumer.unregister_filter(this.filter);
|
||||
this.filter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if(!this.handle.consumer)
|
||||
return;
|
||||
|
||||
this.finalize();
|
||||
this.filter = this.handle.consumer.create_filter_threshold(this._threshold);
|
||||
if(this._callback_level)
|
||||
this.filter.set_analyze_filter(this._callback_level);
|
||||
this.filter.set_margin_frames(this._margin_frames);
|
||||
this.filter.set_attack_smooth(this._attack_smooth);
|
||||
this.filter.set_release_smooth(this._release_smooth);
|
||||
}
|
||||
}
|
||||
|
||||
export class NStateFilter extends NativeFilter implements audio.recorder.filter.StateFilter {
|
||||
private filter: naudio.record.StateConsumeFilter;
|
||||
private active = false;
|
||||
|
||||
constructor(handle) {
|
||||
super(handle, audio.recorder.filter.Type.STATE);
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if(this.filter) {
|
||||
if(this.handle.consumer)
|
||||
this.handle.consumer.unregister_filter(this.filter);
|
||||
this.filter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if(!this.handle.consumer)
|
||||
return;
|
||||
|
||||
this.finalize();
|
||||
this.filter = this.handle.consumer.create_filter_state();
|
||||
this.filter.set_consuming(this.active);
|
||||
}
|
||||
|
||||
is_active(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
async set_state(state: boolean): Promise<void> {
|
||||
if(this.active === state)
|
||||
return;
|
||||
this.active = state;
|
||||
if(this.filter)
|
||||
this.filter.set_consuming(state);
|
||||
}
|
||||
}
|
||||
|
||||
export class NVoiceLevelFilter extends NativeFilter implements audio.recorder.filter.VoiceLevelFilter {
|
||||
private filter: naudio.record.VADConsumeFilter;
|
||||
private level = 3;
|
||||
private _margin_frames = 5;
|
||||
|
||||
constructor(handle) {
|
||||
super(handle, audio.recorder.filter.Type.VOICE_LEVEL);
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if(this.filter) {
|
||||
if(this.handle.consumer)
|
||||
this.handle.consumer.unregister_filter(this.filter);
|
||||
this.filter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if(!this.handle.consumer)
|
||||
return;
|
||||
|
||||
this.finalize();
|
||||
this.filter = this.handle.consumer.create_filter_vad(this.level);
|
||||
this.filter.set_margin_frames(this._margin_frames);
|
||||
}
|
||||
|
||||
get_level(): number {
|
||||
return this.level;
|
||||
}
|
||||
|
||||
set_level(value: number) {
|
||||
if(this.level === value)
|
||||
return;
|
||||
|
||||
this.level = value;
|
||||
if(this.filter) {
|
||||
this.finalize();
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
set_margin_frames(value: number) {
|
||||
this._margin_frames = value;
|
||||
if(this.filter)
|
||||
this.filter.set_margin_frames(value);
|
||||
}
|
||||
|
||||
get_margin_frames(): number {
|
||||
return this.filter ? this.filter.get_margin_frames() : this._margin_frames;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NativeInput implements AbstractInput {
|
||||
private handle: naudio.record.AudioRecorder;
|
||||
consumer: naudio.record.AudioConsumer;
|
||||
|
||||
private _current_device: audio.recorder.InputDevice;
|
||||
private _current_state: audio.recorder.InputState = audio.recorder.InputState.PAUSED;
|
||||
|
||||
callback_begin: () => any;
|
||||
callback_end: () => any;
|
||||
|
||||
private filters: filter.NativeFilter[] = [];
|
||||
|
||||
constructor() {
|
||||
this.handle = naudio.record.create_recorder();
|
||||
this._current_state = audio.recorder.InputState.PAUSED;
|
||||
}
|
||||
|
||||
/* TODO: some kind of finalize? */
|
||||
current_consumer(): audio.recorder.InputConsumer | undefined {
|
||||
return {
|
||||
type: audio.recorder.InputConsumerType.NATIVE
|
||||
};
|
||||
}
|
||||
|
||||
async set_consumer(consumer: audio.recorder.InputConsumer): Promise<void> {
|
||||
if(typeof(consumer) !== "undefined")
|
||||
throw "we only support native consumers!"; /* TODO: May create a general wrapper? */
|
||||
return;
|
||||
}
|
||||
|
||||
async set_device(_device: audio.recorder.InputDevice | undefined): Promise<void> {
|
||||
if(_device === this._current_device)
|
||||
return;
|
||||
|
||||
const device = _device as NativeDevice; /* TODO: test for? */
|
||||
this._current_device = _device;
|
||||
this.handle.set_device(device ? device.device_index : -1);
|
||||
try {
|
||||
this.handle.start(); /* TODO: Test for state! */
|
||||
} catch(error) {
|
||||
console.warn(tr("Failed to start playback on new input device (%o)"), error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
current_device(): audio.recorder.InputDevice | undefined {
|
||||
return this._current_device;
|
||||
}
|
||||
|
||||
current_state(): audio.recorder.InputState {
|
||||
return this._current_state;
|
||||
}
|
||||
|
||||
disable_filter(type: audio.recorder.filter.Type) {
|
||||
const filter = this.get_filter(type) as filter.NativeFilter;
|
||||
if(filter.is_enabled())
|
||||
filter.enabled = false;
|
||||
filter.finalize();
|
||||
}
|
||||
|
||||
enable_filter(type: audio.recorder.filter.Type) {
|
||||
const filter = this.get_filter(type) as filter.NativeFilter;
|
||||
if(!filter.is_enabled()) {
|
||||
filter.enabled = true;
|
||||
filter.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
clear_filter() {
|
||||
for(const filter of this.filters) {
|
||||
filter.enabled = false;
|
||||
filter.finalize();
|
||||
}
|
||||
}
|
||||
|
||||
get_filter(type: audio.recorder.filter.Type): audio.recorder.filter.Filter | undefined {
|
||||
for(const filter of this.filters)
|
||||
if(filter.type === type)
|
||||
return filter;
|
||||
|
||||
let _filter: filter.NativeFilter;
|
||||
switch (type) {
|
||||
case audio.recorder.filter.Type.THRESHOLD:
|
||||
_filter = new filter.NThresholdFilter(this);
|
||||
break;
|
||||
case audio.recorder.filter.Type.STATE:
|
||||
_filter = new filter.NStateFilter(this);
|
||||
break;
|
||||
case audio.recorder.filter.Type.VOICE_LEVEL:
|
||||
_filter = new filter.NVoiceLevelFilter(this);
|
||||
break;
|
||||
default:
|
||||
throw "this filter isn't supported!";
|
||||
}
|
||||
this.filters.push(_filter);
|
||||
return _filter;
|
||||
}
|
||||
|
||||
supports_filter(type: audio.recorder.filter.Type) : boolean {
|
||||
switch (type) {
|
||||
case audio.recorder.filter.Type.THRESHOLD:
|
||||
case audio.recorder.filter.Type.STATE:
|
||||
case audio.recorder.filter.Type.VOICE_LEVEL:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
try {
|
||||
await this.stop();
|
||||
} catch(error) {
|
||||
console.warn(tr("Failed to stop old record session before start (%o)"), error);
|
||||
}
|
||||
|
||||
this._current_state = audio.recorder.InputState.DRY;
|
||||
try {
|
||||
if(!this.consumer) {
|
||||
this.consumer = this.handle.create_consumer();
|
||||
this.consumer.callback_ended = () => {
|
||||
this._current_state = audio.recorder.InputState.RECORDING;
|
||||
if(this.callback_end)
|
||||
this.callback_end();
|
||||
};
|
||||
this.consumer.callback_started = () => {
|
||||
this._current_state = audio.recorder.InputState.DRY;
|
||||
if(this.callback_begin)
|
||||
this.callback_begin();
|
||||
};
|
||||
}
|
||||
|
||||
this.handle.start();
|
||||
for(const filter of this.filters)
|
||||
if(filter.is_enabled())
|
||||
filter.initialize();
|
||||
} catch(error) {
|
||||
this._current_state = audio.recorder.InputState.PAUSED;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.handle.stop();
|
||||
for(const filter of this.filters)
|
||||
filter.finalize();
|
||||
if(this.callback_end)
|
||||
this.callback_end();
|
||||
this._current_state = audio.recorder.InputState.PAUSED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(window["audio"] || (window["audio"] = {}), _audio);
|
||||
_audio.recorder.devices(); /* query devices */
|
96
modules/renderer/connection/FileTransfer.ts
Normal file
96
modules/renderer/connection/FileTransfer.ts
Normal file
@ -0,0 +1,96 @@
|
||||
/// <reference path="../imports/imports_shared.d.ts" />
|
||||
|
||||
|
||||
window["require_setup"](module);
|
||||
import * as native from "teaclient_connection";
|
||||
|
||||
namespace _transfer {
|
||||
class NativeFileDownload implements transfer.DownloadTransfer {
|
||||
readonly key: transfer.DownloadKey;
|
||||
private _handle: native.ft.NativeFileTransfer;
|
||||
private _buffer: Uint8Array;
|
||||
|
||||
private _result: Promise<void>;
|
||||
private _response: Response;
|
||||
|
||||
private _result_success: () => any;
|
||||
private _result_error: (error: any) => any;
|
||||
|
||||
constructor(key: transfer.DownloadKey) {
|
||||
this.key = key;
|
||||
this._buffer = new Uint8Array(key.total_size);
|
||||
this._handle = native.ft.spawn_connection({
|
||||
client_transfer_id: key.client_transfer_id,
|
||||
server_transfer_id: key.server_transfer_id,
|
||||
|
||||
remote_address: key.peer.hosts[0],
|
||||
remote_port: key.peer.port,
|
||||
|
||||
transfer_key: key.key,
|
||||
|
||||
object: native.ft.download_transfer_object_from_buffer(this._buffer.buffer)
|
||||
});
|
||||
}
|
||||
|
||||
get_key(): transfer.DownloadKey {
|
||||
return this.key;
|
||||
}
|
||||
|
||||
async request_file(): Promise<Response> {
|
||||
if(this._response)
|
||||
return this._response;
|
||||
|
||||
try {
|
||||
await (this._result || this._start_transfer());
|
||||
} catch(error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if(this._response)
|
||||
return this._response;
|
||||
|
||||
const buffer = this._buffer.buffer.slice(this._buffer.byteOffset, this._buffer.byteOffset + Math.min(64, this._buffer.byteLength));
|
||||
|
||||
/* may another task has been stepped by and already set the response */
|
||||
return this._response || (this._response = new Response(this._buffer, {
|
||||
status: 200,
|
||||
statusText: "success",
|
||||
headers: {
|
||||
"X-media-bytes": base64ArrayBuffer(buffer)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
_start_transfer() : Promise<void> {
|
||||
return this._result = new Promise((resolve, reject) => {
|
||||
this._result_error = (error) => {
|
||||
this._result_error = undefined;
|
||||
this._result_success = undefined;
|
||||
reject(error);
|
||||
};
|
||||
this._result_success = () => {
|
||||
this._result_error = undefined;
|
||||
this._result_success = undefined;
|
||||
resolve();
|
||||
};
|
||||
|
||||
this._handle.callback_failed = this._result_error;
|
||||
this._handle.callback_finished = aborted => {
|
||||
if(aborted)
|
||||
this._result_error("aborted");
|
||||
else
|
||||
this._result_success();
|
||||
};
|
||||
|
||||
this._handle.start();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function spawn_download_transfer(key: transfer.DownloadKey) : transfer.DownloadTransfer {
|
||||
return new NativeFileDownload(key);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(window["transfer"] || (window["transfer"] = {}), _transfer);
|
302
modules/renderer/connection/ServerConnection.ts
Normal file
302
modules/renderer/connection/ServerConnection.ts
Normal file
@ -0,0 +1,302 @@
|
||||
/// <reference path="../imports/imports_shared.d.ts" />
|
||||
|
||||
window["require_setup"](module);
|
||||
import {
|
||||
destroy_server_connection as _destroy_server_connection,
|
||||
NativeServerConnection,
|
||||
ServerType,
|
||||
spawn_server_connection as _spawn_server_connection
|
||||
} from "teaclient_connection";
|
||||
import {_audio} from "./VoiceConnection";
|
||||
|
||||
export namespace _connection {
|
||||
export namespace native {
|
||||
import VoiceConnection = _audio.native.VoiceConnection;
|
||||
|
||||
class ErrorCommandHandler extends connection.AbstractCommandHandler {
|
||||
private _handle: ServerConnection;
|
||||
|
||||
constructor(handle: ServerConnection) {
|
||||
super(handle);
|
||||
this._handle = handle;
|
||||
}
|
||||
|
||||
handle_command(command: connection.ServerCommand): boolean {
|
||||
if(command.command === "error") {
|
||||
const return_listener: {[key: string]: (result: CommandResult) => any} = this._handle["_return_listener"];
|
||||
const data = command.arguments[0];
|
||||
|
||||
let return_code : string = data["return_code"];
|
||||
if(!return_code) {
|
||||
const listener = return_listener["last_command"] || return_listener["_clientinit"];
|
||||
if(typeof(listener) === "function") {
|
||||
console.warn(tr("Received error without return code. Using last command (%o)"), listener);
|
||||
listener(new CommandResult(data));
|
||||
delete return_listener["last_command"];
|
||||
delete return_listener["_clientinit"];
|
||||
} else {
|
||||
console.warn(tr("Received error without return code."), data);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if(return_listener[return_code]) {
|
||||
return_listener[return_code](new CommandResult(data));
|
||||
} else {
|
||||
console.warn(tr("Error received for no handler! (%o)"), data);
|
||||
}
|
||||
return true;
|
||||
} else if(command.command == "initivexpand") {
|
||||
if(command.arguments[0]["teaspeak"] == true) {
|
||||
console.log("Using TeaSpeak identity type");
|
||||
this._handle.handshake_handler().startHandshake();
|
||||
}
|
||||
return true;
|
||||
} else if(command.command == "initivexpand2") {
|
||||
/* its TeamSpeak or TeaSpeak with experimental 3.1 and not up2date */
|
||||
this._handle["_do_teamspeak"] = true;
|
||||
} else if(command.command == "initserver") {
|
||||
const return_listener: {[key: string]: (result: CommandResult) => any} = this._handle["_return_listener"];
|
||||
|
||||
if(typeof(return_listener["_clientinit"]) === "function") {
|
||||
return_listener["_clientinit"](new CommandResult({id: 0, message: ""}));
|
||||
delete return_listener["_clientinit"];
|
||||
}
|
||||
|
||||
if(this._handle.onconnectionstatechanged)
|
||||
this._handle.onconnectionstatechanged(ConnectionState.INITIALISING, ConnectionState.CONNECTING);
|
||||
} else if(command.command == "notifyconnectioninforequest") {
|
||||
this._handle.send_command("setconnectioninfo",
|
||||
{
|
||||
//TODO calculate
|
||||
connection_ping: 0.0000,
|
||||
connection_ping_deviation: 0.0,
|
||||
|
||||
connection_packets_sent_speech: 0,
|
||||
connection_packets_sent_keepalive: 0,
|
||||
connection_packets_sent_control: 0,
|
||||
connection_bytes_sent_speech: 0,
|
||||
connection_bytes_sent_keepalive: 0,
|
||||
connection_bytes_sent_control: 0,
|
||||
connection_packets_received_speech: 0,
|
||||
connection_packets_received_keepalive: 0,
|
||||
connection_packets_received_control: 0,
|
||||
connection_bytes_received_speech: 0,
|
||||
connection_bytes_received_keepalive: 0,
|
||||
connection_bytes_received_control: 0,
|
||||
connection_server2client_packetloss_speech: 0.0000,
|
||||
connection_server2client_packetloss_keepalive: 0.0000,
|
||||
connection_server2client_packetloss_control: 0.0000,
|
||||
connection_server2client_packetloss_total: 0.0000,
|
||||
connection_bandwidth_sent_last_second_speech: 0,
|
||||
connection_bandwidth_sent_last_second_keepalive: 0,
|
||||
connection_bandwidth_sent_last_second_control: 0,
|
||||
connection_bandwidth_sent_last_minute_speech: 0,
|
||||
connection_bandwidth_sent_last_minute_keepalive: 0,
|
||||
connection_bandwidth_sent_last_minute_control: 0,
|
||||
connection_bandwidth_received_last_second_speech: 0,
|
||||
connection_bandwidth_received_last_second_keepalive: 0,
|
||||
connection_bandwidth_received_last_second_control: 0,
|
||||
connection_bandwidth_received_last_minute_speech: 0,
|
||||
connection_bandwidth_received_last_minute_keepalive: 0,
|
||||
connection_bandwidth_received_last_minute_control: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerConnection extends connection.AbstractServerConnection {
|
||||
private _native_handle: NativeServerConnection;
|
||||
private _voice_connection: VoiceConnection;
|
||||
|
||||
private _do_teamspeak: boolean;
|
||||
private _return_listener: {[key: string]: (result: CommandResult) => any} = {};
|
||||
|
||||
private _command_handler: NativeConnectionCommandBoss;
|
||||
private _command_error_handler: ErrorCommandHandler;
|
||||
private _command_handler_default: connection.ConnectionCommandHandler;
|
||||
|
||||
private _remote_address: ServerAddress;
|
||||
private _handshake_handler: connection.HandshakeHandler;
|
||||
|
||||
private _return_code_index: number = 0;
|
||||
|
||||
onconnectionstatechanged: connection.ConnectionStateListener;
|
||||
|
||||
constructor(props: ConnectionHandler) {
|
||||
super(props);
|
||||
|
||||
this._command_handler = new NativeConnectionCommandBoss(this);
|
||||
this._command_error_handler = new ErrorCommandHandler(this);
|
||||
this._command_handler_default = new connection.ConnectionCommandHandler(this);
|
||||
|
||||
this._command_handler.register_handler(this._command_error_handler);
|
||||
this._command_handler.register_handler(this._command_handler_default);
|
||||
|
||||
this._native_handle = _spawn_server_connection();
|
||||
this._native_handle.callback_disconnect = reason => {
|
||||
this.client.handleDisconnect(DisconnectReason.CONNECTION_CLOSED, {
|
||||
reason: reason,
|
||||
event: event
|
||||
});
|
||||
};
|
||||
this._native_handle.callback_command = (command, args, switches) => {
|
||||
console.log("Received: %o %o %o", command, args, switches);
|
||||
//FIXME catch error
|
||||
|
||||
this._command_handler.invoke_handle({
|
||||
command: command,
|
||||
arguments: args
|
||||
});
|
||||
};
|
||||
this._voice_connection = new VoiceConnection(this, this._native_handle._voice_connection);
|
||||
|
||||
this.command_helper.initialize();
|
||||
this._voice_connection.setup();
|
||||
}
|
||||
|
||||
native_handle() : NativeServerConnection {
|
||||
return this._native_handle;
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if(this._native_handle)
|
||||
_destroy_server_connection(this._native_handle);
|
||||
this._native_handle = undefined;
|
||||
}
|
||||
|
||||
connect(address: ServerAddress, handshake: connection.HandshakeHandler, timeout?: number): Promise<void> {
|
||||
this._remote_address = address;
|
||||
this._handshake_handler = handshake;
|
||||
this._do_teamspeak = false;
|
||||
handshake.setConnection(this);
|
||||
handshake.initialize();
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this._native_handle.connect({
|
||||
remote_host: address.host,
|
||||
remote_port: address.port,
|
||||
|
||||
timeout: typeof(timeout) === "number" ? timeout : -1,
|
||||
|
||||
|
||||
callback: error => {
|
||||
if(error != 0)
|
||||
reject(this._native_handle.error_message(error));
|
||||
else {
|
||||
resolve();
|
||||
}
|
||||
|
||||
console.log("Remote server type: %o (%s)", this._native_handle.server_type, ServerType[this._native_handle.server_type]);
|
||||
if(this._native_handle.server_type == ServerType.TEAMSPEAK || this._do_teamspeak) {
|
||||
console.log("Trying to use TeamSpeak's identity system");
|
||||
this.handshake_handler().on_teamspeak();
|
||||
}
|
||||
},
|
||||
|
||||
identity_key: (handshake.get_identity_handler() as profiles.identities.TeaSpeakHandshakeHandler).identity.private_key,
|
||||
teamspeak: false
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
remote_address(): ServerAddress {
|
||||
return this._remote_address;
|
||||
}
|
||||
|
||||
handshake_handler(): connection.HandshakeHandler {
|
||||
return this._handshake_handler;
|
||||
}
|
||||
|
||||
connected(): boolean {
|
||||
return typeof(this._native_handle) !== "undefined" && this._native_handle.connected();
|
||||
}
|
||||
|
||||
disconnect(reason?: string): Promise<void> {
|
||||
console.trace("Disconnect: %s",reason);
|
||||
return new Promise<void>((resolve, reject) => this._native_handle.disconnect(reason || "", error => {
|
||||
if(error == 0)
|
||||
resolve();
|
||||
else
|
||||
reject(this._native_handle.error_message(error));
|
||||
}));
|
||||
}
|
||||
|
||||
support_voice(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
voice_connection(): connection.voice.AbstractVoiceConnection {
|
||||
return this._voice_connection;
|
||||
}
|
||||
|
||||
command_handler_boss(): connection.AbstractCommandHandlerBoss {
|
||||
return this._command_handler;
|
||||
}
|
||||
|
||||
private generate_return_code() : string {
|
||||
return (this._return_code_index++).toString();
|
||||
}
|
||||
|
||||
send_command(command: string, data?: any, _options?: connection.CommandOptions): Promise<CommandResult> {
|
||||
if(!this.connected()) {
|
||||
console.warn(tr("Tried to send a command without a valid connection."));
|
||||
return Promise.reject(tr("not connected"));
|
||||
}
|
||||
|
||||
const options: connection.CommandOptions = {};
|
||||
Object.assign(options, connection.CommandOptionDefaults);
|
||||
Object.assign(options, _options);
|
||||
|
||||
data = $.isArray(data) ? data : [data || {}];
|
||||
if(data.length == 0) /* we require min one arg to append return_code */
|
||||
data.push({});
|
||||
|
||||
let return_code = data[0]["return_code"] !== undefined ? data[0].return_code : this.generate_return_code();
|
||||
data[0]["return_code"] = return_code;
|
||||
|
||||
console.log("Sending %s (%o)", command, data);
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const timeout_id = setTimeout(() => {
|
||||
delete this._return_listener[return_code];
|
||||
reject("timeout");
|
||||
}, 5000);
|
||||
|
||||
this._return_listener[return_code] = result => {
|
||||
clearTimeout(timeout_id);
|
||||
delete this._return_listener[return_code];
|
||||
|
||||
(result.success ? resolve : reject)(result);
|
||||
};
|
||||
|
||||
if(command == "clientinit")
|
||||
this._return_listener["_clientinit"] = this._return_listener[return_code]; /* fix for TS3 (clientinit does not accept a return code) */
|
||||
|
||||
try {
|
||||
this._native_handle.send_command(command, data, options.flagset || []);
|
||||
} catch(error) {
|
||||
console.warn(tr("Failed to send command: %o"), error);
|
||||
}
|
||||
});
|
||||
return this._command_handler_default.proxy_command_promise(promise, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NativeConnectionCommandBoss extends connection.AbstractCommandHandlerBoss {
|
||||
constructor(connection: connection.AbstractServerConnection) {
|
||||
super(connection);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* override the "normal" connection */
|
||||
export function spawn_server_connection(handle: ConnectionHandler) : connection.AbstractServerConnection {
|
||||
console.log("Spawning native connection");
|
||||
return new native.ServerConnection(handle); /* will be overridden by the client */
|
||||
}
|
||||
}
|
||||
Object.assign(window["connection"] || (window["connection"] = {}), _connection);
|
141
modules/renderer/connection/VoiceConnection.ts
Normal file
141
modules/renderer/connection/VoiceConnection.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import {_connection} from "./ServerConnection";
|
||||
import {_audio as _recorder} from "../audio/AudioRecorder";
|
||||
|
||||
import {
|
||||
NativeVoiceConnection,
|
||||
NativeVoiceClient
|
||||
} from "teaclient_connection";
|
||||
|
||||
export namespace _audio {
|
||||
export namespace native {
|
||||
import ServerConnection = _connection.native.ServerConnection;
|
||||
|
||||
export class VoiceConnection extends connection.voice.AbstractVoiceConnection {
|
||||
readonly connection: ServerConnection;
|
||||
readonly handle: NativeVoiceConnection;
|
||||
|
||||
private _audio_source: RecorderProfile;
|
||||
|
||||
constructor(connection: ServerConnection, voice: NativeVoiceConnection) {
|
||||
super(connection);
|
||||
this.connection = connection;
|
||||
this.handle = voice;
|
||||
}
|
||||
|
||||
setup() { }
|
||||
|
||||
async acquire_voice_recorder(recorder: RecorderProfile | undefined, enforce?: boolean) {
|
||||
if(this._audio_source === recorder && !enforce)
|
||||
return;
|
||||
|
||||
if(this._audio_source)
|
||||
await this._audio_source.unmount();
|
||||
|
||||
if(recorder) {
|
||||
if(!(recorder.input instanceof _recorder.recorder.NativeInput))
|
||||
throw "Recorder input must be an instance of NativeInput!";
|
||||
await recorder.unmount();
|
||||
}
|
||||
|
||||
this.handleVoiceEnded();
|
||||
this._audio_source = recorder;
|
||||
|
||||
if(recorder) {
|
||||
recorder.current_handler = this.connection.client;
|
||||
|
||||
recorder.callback_unmount = () => {
|
||||
this._audio_source = undefined;
|
||||
this.handle.set_audio_source(undefined);
|
||||
this.connection.client.update_voice_status(undefined);
|
||||
};
|
||||
|
||||
recorder.callback_start = this.handleVoiceStarted.bind(this);
|
||||
recorder.callback_stop = this.handleVoiceEnded.bind(this);
|
||||
|
||||
recorder.callback_support_change = () => {
|
||||
this.connection.client.update_voice_status(undefined);
|
||||
};
|
||||
|
||||
this.handle.set_audio_source((recorder.input as _recorder.recorder.NativeInput).consumer);
|
||||
}
|
||||
this.connection.client.update_voice_status(undefined);
|
||||
}
|
||||
|
||||
voice_playback_support() : boolean {
|
||||
return this.connection.connected();
|
||||
}
|
||||
|
||||
voice_send_support() : boolean {
|
||||
return this.connection.connected();
|
||||
}
|
||||
|
||||
private current_channel_codec() : number {
|
||||
const chandler = this.connection.client;
|
||||
return (chandler.getClient().currentChannel() || {properties: { channel_codec: 4}}).properties.channel_codec;
|
||||
}
|
||||
|
||||
private handleVoiceEnded() {
|
||||
const chandler = this.connection.client;
|
||||
chandler.getClient().speaking = false;
|
||||
|
||||
if(!chandler.connected)
|
||||
return false;
|
||||
|
||||
if(chandler.client_status.input_muted)
|
||||
return false;
|
||||
|
||||
console.log(tr("Local voice ended"));
|
||||
//TODO
|
||||
}
|
||||
|
||||
private handleVoiceStarted() {
|
||||
const chandler = this.connection.client;
|
||||
console.log(tr("Local voice started"));
|
||||
chandler.getClient().speaking = true;
|
||||
}
|
||||
|
||||
connected(): boolean {
|
||||
return true; /* we cant be disconnected at any time! */
|
||||
}
|
||||
|
||||
voice_recorder(): RecorderProfile {
|
||||
return this._audio_source;
|
||||
}
|
||||
|
||||
available_clients(): connection.voice.VoiceClient[] {
|
||||
return this.handle.available_clients();
|
||||
}
|
||||
|
||||
find_client(client_id: number) : connection.voice.VoiceClient | undefined {
|
||||
for(const client of this.available_clients())
|
||||
if(client.client_id === client_id)
|
||||
return client;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
unregister_client(client: connection.voice.VoiceClient): Promise<void> {
|
||||
this.handle.unregister_client(client.client_id);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
register_client(client_id: number): connection.voice.VoiceClient {
|
||||
const client = this.handle.register_client(client_id);
|
||||
if(!client)
|
||||
return client;
|
||||
|
||||
const stream = client.get_stream();
|
||||
stream.set_buffer_latency(0.02);
|
||||
stream.set_buffer_max_latency(0.2);
|
||||
return client;
|
||||
}
|
||||
|
||||
decoding_supported(codec: number): boolean {
|
||||
return this.handle.decoding_supported(codec);
|
||||
}
|
||||
|
||||
encoding_supported(codec: number): boolean {
|
||||
return this.handle.encoding_supported(codec);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
245
modules/renderer/dns/dns_resolver.ts
Normal file
245
modules/renderer/dns/dns_resolver.ts
Normal file
@ -0,0 +1,245 @@
|
||||
/// <reference path="../imports/imports_shared.d.ts" />
|
||||
|
||||
|
||||
window["require_setup"](module);
|
||||
|
||||
import * as dns_handler from "dns";
|
||||
|
||||
namespace _dns {
|
||||
type Lookup<R> = (hostname: string, callback: (err: NodeJS.ErrnoException, result: R) => void) => void;
|
||||
type AsyncLookup<R> = (hostname: string, timeout: number) => Promise<R>;
|
||||
function make_async<R>(fun: Lookup<R>) : AsyncLookup<R> {
|
||||
return (hostname, timeout) => {
|
||||
return new Promise<R>((resolve, reject) => {
|
||||
const timeout_id = setTimeout(() => {
|
||||
reject("timeout");
|
||||
}, timeout);
|
||||
fun(hostname, (err, result) => {
|
||||
clearTimeout(timeout_id);
|
||||
if(err) {
|
||||
if(err.errno as any == "ENOTFOUND" || err.errno as any == "ENODATA") {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(err);
|
||||
} else
|
||||
resolve(result);
|
||||
})
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const async_resolve_srv = make_async(dns_handler.resolveSrv);
|
||||
const async_resolve_cname = make_async(dns_handler.resolveCname);
|
||||
const async_resolve_a = make_async(dns_handler.resolve4);
|
||||
const async_resolve_aaaa = make_async(dns_handler.resolve6);
|
||||
const async_resolve_any = make_async(dns_handler.resolveAny);
|
||||
|
||||
export interface AddressTarget {
|
||||
target_ip: string;
|
||||
target_port?: number;
|
||||
}
|
||||
export interface ResolveOptions extends dns.ResolveOptions {
|
||||
log?: (message: string, ...args: any[]) => void;
|
||||
override_port?: number;
|
||||
}
|
||||
|
||||
export function supported() { return true; }
|
||||
export async function resolve_address(address: string, _options?: ResolveOptions) : Promise<AddressTarget> {
|
||||
if(address === "localhost") {
|
||||
return {
|
||||
target_ip: "localhost"
|
||||
};
|
||||
}
|
||||
|
||||
const options: ResolveOptions = {};
|
||||
Object.assign(options, dns.default_options);
|
||||
Object.assign(options, _options || {});
|
||||
|
||||
if(options.max_depth <= 0)
|
||||
throw "max depth exceeded";
|
||||
|
||||
if(typeof(options.log) !== "function")
|
||||
options.log = (message, ...args) => console.debug("[DNS] " + message, ...args);
|
||||
|
||||
const mod_options: ResolveOptions = {};
|
||||
Object.assign(mod_options, options);
|
||||
mod_options.max_depth = mod_options.max_depth - 1;
|
||||
mod_options.allow_srv = false;
|
||||
mod_options.log = (message, ...args) => options.log(" " + message, ...args);
|
||||
|
||||
options.log("Resolving %s", address);
|
||||
|
||||
let response: AddressTarget;
|
||||
if(typeof(options.allow_aaaa) !== "boolean" || options.allow_aaaa) {
|
||||
const aaaa_response: string[] | undefined | null = await async_resolve_aaaa(address, options.timeout).catch(error => {
|
||||
options.log("AAAA record resolved unsuccessfully (%o)", error);
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
|
||||
if(typeof(aaaa_response) !== "undefined") {
|
||||
if(!aaaa_response || aaaa_response.length == 0)
|
||||
options.log("No AAAA records found");
|
||||
else {
|
||||
options.log("Resolved AAAA records: %o. Returning: %s", aaaa_response, aaaa_response[0]);
|
||||
response = {
|
||||
target_ip: aaaa_response[0],
|
||||
target_port: options.override_port
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!response && (typeof(options.allow_a) !== "boolean" || options.allow_a)) {
|
||||
const a_response: string[] | undefined | null = await async_resolve_a(address, options.timeout).catch(error => {
|
||||
options.log("A record resolved unsuccessfully (%o)", error);
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
if(typeof(a_response) !== "undefined") {
|
||||
if(!a_response || a_response.length == 0)
|
||||
options.log("No A records found");
|
||||
else {
|
||||
options.log("Resolved A records: %o. Returning: %s", a_response, a_response[0]);
|
||||
response = {
|
||||
target_ip: a_response[0],
|
||||
target_port: options.override_port
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!response && (typeof(options.allow_any) !== "boolean" || options.allow_any)) {
|
||||
const any_response: dns_handler.AnyRecord[] = await async_resolve_any(address, options.timeout).catch(error => {
|
||||
options.log("ANY record resolved unsuccessfully (%o)", error);
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
|
||||
if(typeof(any_response) !== "undefined") {
|
||||
if(!any_response || any_response.length == 0)
|
||||
options.log("No ANY records found");
|
||||
else {
|
||||
options.log("Resolved ANY records: %o.", any_response);
|
||||
for(const record of any_response) {
|
||||
if(record.type === "A") {
|
||||
const a_record = record as dns_handler.AnyARecord;
|
||||
options.log("Returning A record from ANY query: %s", a_record.address);
|
||||
return {
|
||||
target_ip: a_record.address,
|
||||
target_port: options.override_port
|
||||
};
|
||||
} else if(record.type === "AAAA") {
|
||||
const aaaa_record = record as dns_handler.AnyAaaaRecord;
|
||||
options.log("Returning AAAA record from ANY query: %s", aaaa_record.address);
|
||||
return {
|
||||
target_ip: aaaa_record.address,
|
||||
target_port: options.override_port
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(typeof(options.allow_srv) !== "boolean" || options.allow_srv) {
|
||||
const response: dns_handler.SrvRecord[] = await async_resolve_srv("_ts3._udp." + address, options.timeout).catch(error => {
|
||||
options.log("SRV resolve unsuccessfully (%o)", error);
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
if(typeof(response) !== "undefined") {
|
||||
if(!response || response.length == 0)
|
||||
options.log("No SRV records found");
|
||||
else {
|
||||
const sorted = response.sort((a, b) => b.weight - a.weight);
|
||||
const original_port = mod_options.override_port;
|
||||
options.log("Resolved SRV records: %o", sorted);
|
||||
for(const entry of sorted) {
|
||||
options.log("Resolving SRV record: %o", entry);
|
||||
mod_options.override_port = entry.port || mod_options.override_port;
|
||||
const resp = await resolve_address(entry.name, mod_options).catch(error => {
|
||||
options.log("SRV entry resolved unsuccessfully (%o)", error);
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
if(resp) {
|
||||
options.log("SRV entry resolved to %o. Found result", resp);
|
||||
return resp;
|
||||
} else {
|
||||
options.log("No response for SRV record");
|
||||
}
|
||||
}
|
||||
mod_options.override_port = original_port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* resolve CNAME in the last step, else may the A records will be empty! */
|
||||
if(typeof(options.allow_cname) !== "boolean" || options.allow_cname) {
|
||||
const cname_response: string[] = await async_resolve_cname(address, options.timeout).catch(error => {
|
||||
options.log("CName resolved unsuccessfully (%o)", error);
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
if(typeof(cname_response) !== "undefined") {
|
||||
if(!cname_response || cname_response.length == 0)
|
||||
options.log("No CNAME records found");
|
||||
else {
|
||||
options.log("Resolved %d CNAME records", cname_response.length);
|
||||
for(const entry of cname_response) {
|
||||
options.log("Resolving CNAME record: %o", entry);
|
||||
const resp = await resolve_address(entry, mod_options).catch(error => {
|
||||
options.log("Failed to resolve resolved CName (%o)", error);
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
if(resp) {
|
||||
options.log("CName entry resolved to %o. Found result", resp);
|
||||
return resp;
|
||||
} else {
|
||||
options.log("No response for CName record");
|
||||
}
|
||||
}
|
||||
response = undefined; /* overridden by a CNAME */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
const lookup_result = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject("timeout");
|
||||
}, options.timeout);
|
||||
dns_handler.lookup(address, {
|
||||
hints: dns_handler.ADDRCONFIG | dns_handler.V4MAPPED,
|
||||
all: true,
|
||||
family: 0
|
||||
}, (error, result, family) => {
|
||||
clearTimeout(timeout);
|
||||
console.log(result);
|
||||
if(error) {
|
||||
if(error.errno as any == "ENOTFOUND" || error.errno as any == "ENODATA") {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(error);
|
||||
} else
|
||||
resolve(result);
|
||||
});
|
||||
}).catch(error => {
|
||||
options.log("General lookup failed: %o", error);
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
console.log(lookup_result);
|
||||
*/
|
||||
if(response)
|
||||
return response;
|
||||
options.log("No records found, no result.");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
dns_handler.setServers(["8.8.8.8", "8.8.8.4", "1.1.1.1"]);
|
||||
}
|
||||
|
||||
Object.assign(window["dns"] || (window["dns"] = {}), _dns);
|
225
modules/renderer/index.ts
Normal file
225
modules/renderer/index.ts
Normal file
@ -0,0 +1,225 @@
|
||||
/// <reference path="imports/imports_shared.d.ts" />
|
||||
|
||||
import {Arguments, process_args, parse_arguments} from "../shared/process-arguments";
|
||||
import {remote} from "electron";
|
||||
import * as crash_handler from "../crash_handler";
|
||||
import * as electron from "electron";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
|
||||
/* first of all setup crash handler */
|
||||
{
|
||||
const is_electron_run = process.argv[0].endsWith("electron") || process.argv[0].endsWith("electron.exe");
|
||||
crash_handler.initialize_handler("renderer", is_electron_run);
|
||||
}
|
||||
|
||||
interface Window {
|
||||
$: any;
|
||||
jQuery: any;
|
||||
jsrender: any;
|
||||
|
||||
impl_display_critical_error: any;
|
||||
displayCriticalError: any;
|
||||
teaclient_initialize: any;
|
||||
|
||||
open_connected_question: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
declare const window: Window;
|
||||
|
||||
export const require_native: NodeRequireFunction = id => require(id);
|
||||
export const initialize = async () => {
|
||||
/* we use out own jquery resource */
|
||||
loader.register_task(loader.Stage.JAVASCRIPT, {
|
||||
name: "teaclient jquery",
|
||||
function: jquery_initialize,
|
||||
priority: 80
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT, {
|
||||
name: "teaclient general",
|
||||
function: load_basic_modules,
|
||||
priority: 10
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "teaclient javascript init",
|
||||
function: load_modules,
|
||||
priority: 50
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "teaclient initialize modules",
|
||||
function: module_loader_setup,
|
||||
priority: 60
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "teaclient initialize persistent storage",
|
||||
function: async () => {
|
||||
const storage = require("./PersistentLocalStorage");
|
||||
await storage.initialize();
|
||||
},
|
||||
priority: 90
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "teaclient initialize logging",
|
||||
function: initialize_logging,
|
||||
priority: 80
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "teaclient initialize error",
|
||||
function: initialize_error_handler,
|
||||
priority: 100
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "teaclient initialize arguments",
|
||||
function: async () => {
|
||||
parse_arguments();
|
||||
if(process_args.has_value(Arguments.DUMMY_CRASH_RENDERER))
|
||||
crash_handler.handler.crash();
|
||||
if(!process_args.has_flag(Arguments.DEBUG)) {
|
||||
window.open_connected_question = () => {
|
||||
return new Promise(resolve => {
|
||||
remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'question',
|
||||
buttons: ['Yes', 'No'],
|
||||
title: 'Confirm',
|
||||
message: 'Are you really sure?\nYou\'re still connected!'
|
||||
}, choice => {
|
||||
resolve(choice === 0);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
priority: 110
|
||||
});
|
||||
};
|
||||
|
||||
const jquery_initialize = async () => {
|
||||
window.$ = require("jquery");
|
||||
window.jQuery = window.$;
|
||||
Object.assign(window.$, window.jsrender = require('jsrender'));
|
||||
};
|
||||
|
||||
const initialize_logging = async () => {
|
||||
const logger = require("./logger");
|
||||
logger.setup();
|
||||
};
|
||||
|
||||
const initialize_error_handler = async () => {
|
||||
const _impl = message => {
|
||||
if(!process_args.has_flag(Arguments.DEBUG)) {
|
||||
console.error("Displaying critical error: %o", message);
|
||||
message = message.replace(/<br>/i, "\n");
|
||||
|
||||
const win = remote.getCurrentWindow();
|
||||
remote.dialog.showMessageBox({
|
||||
type: "error",
|
||||
buttons: ["exit"],
|
||||
title: "A critical error happened!",
|
||||
message: message
|
||||
});
|
||||
|
||||
win.close();
|
||||
} else {
|
||||
console.error("Received critical error: %o", message);
|
||||
console.error("Ignoring error due to the debug mode");
|
||||
}
|
||||
};
|
||||
|
||||
if(window.impl_display_critical_error)
|
||||
window.impl_display_critical_error = _impl;
|
||||
else
|
||||
window.displayCriticalError = _impl;
|
||||
};
|
||||
|
||||
const module_loader_setup = async () => {
|
||||
const native_paths = (() => {
|
||||
const app_path = (remote || electron).app.getAppPath();
|
||||
const result = [];
|
||||
result.push(app_path + "/native/build/" + os.platform() + "_" + os.arch() + "/");
|
||||
if(app_path.endsWith(".asar"))
|
||||
result.push(path.join(path.dirname(app_path), "natives"));
|
||||
return result;
|
||||
})();
|
||||
window["require_setup"] = _mod => {
|
||||
if(!_mod || !_mod.paths) return;
|
||||
|
||||
console.dir(_mod);
|
||||
|
||||
_mod.paths.push(...native_paths);
|
||||
const org_req = _mod.__proto__.require;
|
||||
if(!_mod.proxied) {
|
||||
_mod.require = function a(m) {
|
||||
let stack = new Error().stack;
|
||||
if(stack.startsWith("Error"))
|
||||
stack = stack.substr(6);
|
||||
//console.log("require \"%s\"\nStack:\n%s", m, stack);
|
||||
return org_req.apply(_mod, [m]);
|
||||
};
|
||||
_mod.proxied = true;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const load_basic_modules = async () => {
|
||||
console.dir(require("./audio/AudioPlayer")); /* setup audio */
|
||||
console.dir(require("./audio/AudioRecorder")); /* setup audio */
|
||||
require("./logger");
|
||||
|
||||
|
||||
};
|
||||
|
||||
const load_modules = async () => {
|
||||
window["require_setup"](this);
|
||||
|
||||
console.log(module.paths);
|
||||
console.log("Loading native extensions...");
|
||||
try {
|
||||
try {
|
||||
require("./ppt");
|
||||
} catch(error) {
|
||||
console.error("Failed to load ppt");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
require("./version");
|
||||
} catch(error) {
|
||||
console.error("Failed to load version extension");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
require("./connection/ServerConnection");
|
||||
} catch(error) {
|
||||
console.error("Failed to load server connection extension");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
require("./connection/FileTransfer");
|
||||
} catch(error) {
|
||||
console.error("Failed to load file transfer extension");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
require("./dns/dns_resolver");
|
||||
} catch(error) {
|
||||
console.error("Failed to load dns extension");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
} catch(error){
|
||||
console.log(error);
|
||||
window.displayCriticalError("Failed to load native extensions: " + error);
|
||||
throw error;
|
||||
}
|
||||
console.log("Loaded native extensions");
|
||||
};
|
79
modules/renderer/logger.ts
Normal file
79
modules/renderer/logger.ts
Normal file
@ -0,0 +1,79 @@
|
||||
enum LogType {
|
||||
TRACE,
|
||||
DEBUG,
|
||||
INFO,
|
||||
WARNING,
|
||||
ERROR
|
||||
}
|
||||
|
||||
export interface Logger {
|
||||
trace(message: string, ...args);
|
||||
debug(message: string, ...args);
|
||||
info(message: string, ...args);
|
||||
log(message: string, ...args);
|
||||
warning(message: string, ...args);
|
||||
error(message: string, ...args);
|
||||
|
||||
dir_error(error: any, message?: string);
|
||||
}
|
||||
|
||||
let loggers = {};
|
||||
const original_console: Console = {} as any;
|
||||
|
||||
export function setup() {
|
||||
Object.assign(original_console, console);
|
||||
Object.assign(console, logger("console"));
|
||||
}
|
||||
|
||||
export function logger(name: string = "console") : Logger {
|
||||
if(loggers[name])
|
||||
return loggers[name];
|
||||
|
||||
return loggers[name] = create_logger(name);
|
||||
}
|
||||
|
||||
import * as util from "util";
|
||||
function create_logger(name: string) : Logger {
|
||||
const log = (type, message: string, ...args) => {
|
||||
switch (type) {
|
||||
case LogType.TRACE:
|
||||
original_console.trace(message, ...args);
|
||||
break;
|
||||
case LogType.DEBUG:
|
||||
original_console.debug(message, ...args);
|
||||
break;
|
||||
case LogType.INFO:
|
||||
original_console.info(message, ...args);
|
||||
break;
|
||||
case LogType.WARNING:
|
||||
original_console.warn(message, ...args);
|
||||
break;
|
||||
case LogType.ERROR:
|
||||
original_console.error(message, ...args);
|
||||
break;
|
||||
}
|
||||
|
||||
const log_message = util.format(message, ...args);
|
||||
process.stdout.write(util.format("[%s][%s] %s", name, LogType[type], log_message) + "\n");
|
||||
};
|
||||
|
||||
return {
|
||||
trace: (m, ...a) => log(LogType.TRACE, m, ...a),
|
||||
debug: (m, ...a) => log(LogType.DEBUG, m, ...a),
|
||||
info: (m, ...a) => log(LogType.INFO, m, ...a),
|
||||
log: (m, ...a) => log(LogType.INFO, m, ...a),
|
||||
warning: (m, ...a) => log(LogType.WARNING, m, ...a),
|
||||
error: (m, ...a) => log(LogType.ERROR, m, ...a),
|
||||
|
||||
dir_error: (e, m) => {
|
||||
log(LogType.ERROR, "Caught exception: " + m);
|
||||
log(LogType.ERROR, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
(window as any).logger = {
|
||||
log: function (category, level, message) {
|
||||
console.log("%d | %d | %s", category, level, message);
|
||||
}
|
||||
};
|
134
modules/renderer/ppt.ts
Normal file
134
modules/renderer/ppt.ts
Normal file
@ -0,0 +1,134 @@
|
||||
window["require_setup"](module);
|
||||
|
||||
import {KeyEvent as NKeyEvent} from "teaclient_ppt";
|
||||
namespace _ppt {
|
||||
let key_listener: ((_: ppt.KeyEvent) => any)[] = [];
|
||||
|
||||
let native_ppt;
|
||||
function listener_key(type: ppt.EventType, nevent: NKeyEvent) {
|
||||
if(nevent.key_code === 'VoidSymbol' || nevent.key_code === 'error')
|
||||
nevent.key_code = undefined; /* trigger event for state update */
|
||||
|
||||
let event: ppt.KeyEvent = {
|
||||
type: type,
|
||||
|
||||
key: nevent.key_code,
|
||||
key_code: nevent.key_code,
|
||||
|
||||
key_ctrl: nevent.key_ctrl,
|
||||
key_shift: nevent.key_shift,
|
||||
key_alt: nevent.key_alt,
|
||||
key_windows: nevent.key_windows
|
||||
} as any;
|
||||
|
||||
//console.debug("Trigger key event %o", key_event);
|
||||
|
||||
for (const listener of key_listener)
|
||||
listener(event);
|
||||
}
|
||||
|
||||
function native_keyhook(event: NKeyEvent) {
|
||||
//console.log("Native event!: %o", event);
|
||||
|
||||
if(event.type == 0)
|
||||
listener_key(ppt.EventType.KEY_PRESS, event);
|
||||
else if(event.type == 1)
|
||||
listener_key(ppt.EventType.KEY_RELEASE, event);
|
||||
else if(event.type == 2)
|
||||
listener_key(ppt.EventType.KEY_TYPED, event);
|
||||
}
|
||||
|
||||
export async function initialize() : Promise<void> {
|
||||
native_ppt = require("teaclient_ppt");
|
||||
|
||||
register_key_listener(listener_hook);
|
||||
native_ppt.RegisterCallback(native_keyhook);
|
||||
}
|
||||
|
||||
export function finalize() {
|
||||
unregister_key_listener(listener_hook);
|
||||
native_ppt.UnregisterCallback(native_keyhook);
|
||||
}
|
||||
|
||||
export function register_key_listener(listener: (_: ppt.KeyEvent) => any) {
|
||||
key_listener.push(listener);
|
||||
}
|
||||
|
||||
export function unregister_key_listener(listener: (_: ppt.KeyEvent) => any) {
|
||||
key_listener.remove(listener);
|
||||
}
|
||||
|
||||
interface CurrentState {
|
||||
event: ppt.KeyEvent;
|
||||
code: string;
|
||||
|
||||
special: { [key:number]:boolean };
|
||||
}
|
||||
|
||||
let current_state: CurrentState = {
|
||||
special: []
|
||||
} as any;
|
||||
|
||||
let key_hooks: ppt.KeyHook[] = [];
|
||||
let key_hooks_active: ppt.KeyHook[] = [];
|
||||
|
||||
function listener_hook(event: ppt.KeyEvent) {
|
||||
if(event.type == ppt.EventType.KEY_TYPED)
|
||||
return;
|
||||
|
||||
let old_hooks = [...key_hooks_active];
|
||||
let new_hooks = [];
|
||||
|
||||
current_state.special[ppt.SpecialKey.ALT] = event.key_alt;
|
||||
current_state.special[ppt.SpecialKey.CTRL] = event.key_ctrl;
|
||||
current_state.special[ppt.SpecialKey.SHIFT] = event.key_shift;
|
||||
current_state.special[ppt.SpecialKey.WINDOWS] = event.key_windows;
|
||||
|
||||
current_state.code = undefined;
|
||||
current_state.event = undefined;
|
||||
|
||||
if(event.type == ppt.EventType.KEY_PRESS) {
|
||||
current_state.event = event;
|
||||
current_state.code = event.key_code;
|
||||
|
||||
for(const hook of key_hooks) {
|
||||
if(hook.key_code != event.key_code) continue;
|
||||
if(hook.key_alt != event.key_alt) continue;
|
||||
if(hook.key_ctrl != event.key_ctrl) continue;
|
||||
if(hook.key_shift != event.key_shift) continue;
|
||||
if(hook.key_windows != event.key_windows) continue;
|
||||
|
||||
new_hooks.push(hook);
|
||||
if(!old_hooks.remove(hook) && hook.callback_press) {
|
||||
hook.callback_press();
|
||||
console.debug("Trigger key press for %o!", hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//We have a new situation
|
||||
for(const hook of old_hooks)
|
||||
if(hook.callback_release) {
|
||||
hook.callback_release();
|
||||
console.debug("Trigger key release for %o!", hook);
|
||||
}
|
||||
key_hooks_active = new_hooks;
|
||||
}
|
||||
|
||||
export function register_key_hook(hook: ppt.KeyHook) {
|
||||
key_hooks.push(hook);
|
||||
}
|
||||
|
||||
export function unregister_key_hook(hook: ppt.KeyHook) {
|
||||
key_hooks.remove(hook);
|
||||
key_hooks_active.remove(hook);
|
||||
}
|
||||
|
||||
export function key_pressed(code: string | ppt.SpecialKey) : boolean {
|
||||
if(typeof(code) === 'string')
|
||||
return current_state.code == code;
|
||||
return current_state.special[code];
|
||||
}
|
||||
}
|
||||
Object.assign(window["ppt"] || (window["ppt"] = {}), _ppt);
|
||||
console.dir(_ppt);
|
21
modules/renderer/version.ts
Normal file
21
modules/renderer/version.ts
Normal file
@ -0,0 +1,21 @@
|
||||
namespace native {
|
||||
const remote = require('electron').remote;
|
||||
export async function client_version() : Promise<string> {
|
||||
const version = remote.getGlobal("app_version_client");
|
||||
return version || "?.?.?";
|
||||
}
|
||||
}
|
||||
|
||||
window["native"] = native;
|
||||
|
||||
namespace forum {
|
||||
export interface UserData {
|
||||
session_id: string;
|
||||
username: string;
|
||||
|
||||
application_data: string;
|
||||
application_data_sign: string;
|
||||
}
|
||||
}
|
||||
|
||||
//window["forum"] = forum;
|
98
modules/shared/process-arguments/index.ts
Normal file
98
modules/shared/process-arguments/index.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import * as electron from "electron";
|
||||
import {app} from "electron";
|
||||
|
||||
export class Arguments {
|
||||
static readonly DEV_TOOLS = ["t", "dev-tools"];
|
||||
static readonly DEBUG = ["d", "debug"];
|
||||
static readonly DISABLE_ANIMATION = ["a", "disable-animation"];
|
||||
static readonly SERVER_URL = ["u", "server-url"];
|
||||
static readonly UPDATER_UI_DEBUG = ["updater-debug-ui"];
|
||||
static readonly UPDATER_ENFORCE = ["updater-enforce"];
|
||||
static readonly UPDATER_CHANNEL = ["updater-channel"];
|
||||
static readonly UPDATER_LOCAL_VERSION = ["updater-local-version"];
|
||||
static readonly UPDATER_UI_LOAD_TYPE = ["updater-ui-loader_type"];
|
||||
static readonly UPDATER_UI_NO_CACHE = ["updater-ui-no-cache"];
|
||||
static readonly DISABLE_HARDWARE_ACCELERATION = ["disable-hardware-acceleration"];
|
||||
static readonly NO_SINGLE_INSTANCE = ["no-single-instance"];
|
||||
static readonly DUMMY_CRASH_MAIN = ["dummy-crash-main"];
|
||||
static readonly DUMMY_CRASH_RENDERER = ["dummy-crash-renderer"];
|
||||
|
||||
has_flag: (...keys: (string | string[])[]) => boolean;
|
||||
has_value: (...keys: (string | string[])[]) => boolean;
|
||||
value: (...keys: (string | string[])[]) => string;
|
||||
}
|
||||
|
||||
export interface Window {
|
||||
process_args: Arguments;
|
||||
}
|
||||
|
||||
export const process_args: Arguments = {} as Arguments;
|
||||
|
||||
export function parse_arguments() {
|
||||
if(!process || !process.type || process.type === 'renderer') {
|
||||
Object.assign(process_args, electron.remote.getGlobal("process_arguments"));
|
||||
(window as any).process_args = process_args;
|
||||
} else {
|
||||
const is_electron_run = process.argv[0].endsWith("electron") || process.argv[0].endsWith("electron.exe");
|
||||
|
||||
{
|
||||
const minimist: <T> (args, opts) => T = require("./minimist") as any;
|
||||
let args = minimist<Arguments>(is_electron_run ? process.argv.slice(2) : process.argv.slice(1), {
|
||||
boolean: true,
|
||||
stopEarly: true
|
||||
}) as Arguments;
|
||||
args.has_flag = (...keys) => {
|
||||
for(const key of [].concat(...Array.of(...keys).map(e => Array.isArray(e) ? Array.of(...e) : [e])))
|
||||
if(typeof process_args[key as any as string] === "boolean")
|
||||
return process_args[key as any as string];
|
||||
return false;
|
||||
};
|
||||
|
||||
args.value = (...keys) => {
|
||||
for(const key of [].concat(...Array.of(...keys).map(e => Array.isArray(e) ? Array.of(...e) : [e])))
|
||||
if(typeof process_args[key] !== "undefined")
|
||||
return process_args[key];
|
||||
return undefined;
|
||||
};
|
||||
|
||||
args.has_value = (...keys) => {
|
||||
return args.value(...keys) !== undefined;
|
||||
};
|
||||
|
||||
if(args.has_flag(Arguments.DEBUG)) {
|
||||
const _has_flag = args.has_flag;
|
||||
args.has_flag = (...keys) => {
|
||||
const result = _has_flag(...keys);
|
||||
console.log("Process argument test for parameter %o results in %o", keys, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
const _value = args.value;
|
||||
args.value = (...keys) => {
|
||||
const result = _value(...keys);
|
||||
console.log("Process argument test for parameter %o results in %o", keys, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
console.log("Parsed CMD arguments %o as %o", process.argv, args);
|
||||
Object.assign(process_args, args);
|
||||
Object.assign(global["process_arguments"] = {}, args);
|
||||
}
|
||||
|
||||
if(process_args.has_flag("help", "h")) {
|
||||
console.log("TeaClient command line help page");
|
||||
console.log(" -h --help => Displays this page");
|
||||
console.log(" -d --debug => Enabled the application debug");
|
||||
console.log(" -t --dev-tools => Enables dev tools");
|
||||
console.log(" -u --server-url => Sets the remote client api server url");
|
||||
console.log(" --updater-channel => Set the updater channel");
|
||||
console.log(" -a --disable-animation => Disables some cosmetic animations and loadings");
|
||||
console.log(" --disable-hardware-acceleration => Disables the hardware acceleration for the UI");
|
||||
console.log(" --no-single-instance => Disable multi instance testing");
|
||||
|
||||
//is_debug = process_args.has_flag("debug", "d");
|
||||
//open_dev_tools = process_args.has_flag("dev-tools", "dt");
|
||||
app.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
299
modules/shared/process-arguments/minimist.ts
Normal file
299
modules/shared/process-arguments/minimist.ts
Normal file
@ -0,0 +1,299 @@
|
||||
namespace minimist {
|
||||
export interface Opts {
|
||||
/**
|
||||
* A string or array of strings argument names to always treat as strings
|
||||
*/
|
||||
string?: string | string[];
|
||||
|
||||
/**
|
||||
* A boolean, string or array of strings to always treat as booleans. If true will treat
|
||||
* all double hyphenated arguments without equals signs as boolean (e.g. affects `--foo`, not `-f` or `--foo=bar`)
|
||||
*/
|
||||
boolean?: boolean | string | string[];
|
||||
|
||||
/**
|
||||
* An object mapping string names to strings or arrays of string argument names to use as aliases
|
||||
*/
|
||||
alias?: { [key: string]: string | string[] };
|
||||
|
||||
/**
|
||||
* An object mapping string argument names to default values
|
||||
*/
|
||||
default?: { [key: string]: any };
|
||||
|
||||
/**
|
||||
* When true, populate argv._ with everything after the first non-option
|
||||
*/
|
||||
stopEarly?: boolean;
|
||||
|
||||
/**
|
||||
* A function which is invoked with a command line parameter not defined in the opts
|
||||
* configuration object. If the function returns false, the unknown option is not added to argv
|
||||
*/
|
||||
unknown?: (arg: string) => boolean;
|
||||
|
||||
/**
|
||||
* When true, populate argv._ with everything before the -- and argv['--'] with everything after the --.
|
||||
* Note that with -- set, parsing for arguments still stops after the `--`.
|
||||
*/
|
||||
'--'?: boolean;
|
||||
}
|
||||
|
||||
export interface ParsedArgs {
|
||||
[arg: string]: any;
|
||||
|
||||
/**
|
||||
* If opts['--'] is true, populated with everything after the --
|
||||
*/
|
||||
'--'?: string[];
|
||||
|
||||
/**
|
||||
* Contains all the arguments that didn't have an option associated with them
|
||||
*/
|
||||
_: string[];
|
||||
}
|
||||
}
|
||||
|
||||
const parse = (args: string[], options: minimist.Opts) => {
|
||||
options = options || {};
|
||||
|
||||
var flags = { bools : {}, strings : {}, unknownFn: null, allBools: false };
|
||||
|
||||
if (typeof options['unknown'] === 'function') {
|
||||
flags.unknownFn = options['unknown'];
|
||||
}
|
||||
|
||||
if (typeof options['boolean'] === 'boolean' && options['boolean']) {
|
||||
flags.allBools = true;
|
||||
} else {
|
||||
[].concat(options['boolean']).filter(Boolean).forEach(function (key) {
|
||||
flags.bools[key] = true;
|
||||
});
|
||||
}
|
||||
|
||||
var aliases = {};
|
||||
Object.keys(options.alias || {}).forEach(function (key) {
|
||||
aliases[key] = [].concat(options.alias[key]);
|
||||
aliases[key].forEach(function (x) {
|
||||
aliases[x] = [key].concat(aliases[key].filter(function (y) {
|
||||
return x !== y;
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
[].concat(options.string).filter(Boolean).forEach(function (key) {
|
||||
flags.strings[key] = true;
|
||||
if (aliases[key]) {
|
||||
flags.strings[aliases[key]] = true;
|
||||
}
|
||||
});
|
||||
|
||||
var defaults = options['default'] || {};
|
||||
|
||||
var argv = { _ : [] };
|
||||
Object.keys(flags.bools).forEach(function (key) {
|
||||
setArg(key, defaults[key] === undefined ? false : defaults[key]);
|
||||
});
|
||||
|
||||
var notFlags = [];
|
||||
|
||||
if (args.indexOf('--') !== -1) {
|
||||
notFlags = args.slice(args.indexOf('--')+1);
|
||||
args = args.slice(0, args.indexOf('--'));
|
||||
}
|
||||
|
||||
function argDefined(key, arg) {
|
||||
return (flags.allBools && /^--[^=]+$/.test(arg)) ||
|
||||
flags.strings[key] || flags.bools[key] || aliases[key];
|
||||
}
|
||||
|
||||
function setArg (key, val, arg) {
|
||||
if (arg && flags.unknownFn && !argDefined(key, arg)) {
|
||||
if (flags.unknownFn(arg) === false) return;
|
||||
}
|
||||
|
||||
var value = !flags.strings[key] && isNumber(val)
|
||||
? Number(val) : val
|
||||
;
|
||||
setKey(argv, key.split('.'), value);
|
||||
|
||||
(aliases[key] || []).forEach(function (x) {
|
||||
setKey(argv, x.split('.'), value);
|
||||
});
|
||||
}
|
||||
|
||||
function setKey (obj, keys, value) {
|
||||
var o = obj;
|
||||
keys.slice(0,-1).forEach(function (key) {
|
||||
if (o[key] === undefined) o[key] = {};
|
||||
o = o[key];
|
||||
});
|
||||
|
||||
var key = keys[keys.length - 1];
|
||||
if (o[key] === undefined || flags.bools[key] || typeof o[key] === 'boolean') {
|
||||
o[key] = value;
|
||||
}
|
||||
else if (Array.isArray(o[key])) {
|
||||
o[key].push(value);
|
||||
}
|
||||
else {
|
||||
o[key] = [ o[key], value ];
|
||||
}
|
||||
}
|
||||
|
||||
function aliasIsBoolean(key) {
|
||||
return aliases[key].some(function (x) {
|
||||
return flags.bools[x];
|
||||
});
|
||||
}
|
||||
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
var arg = args[i];
|
||||
|
||||
if (/^--.+=/.test(arg)) {
|
||||
// Using [\s\S] instead of . because js doesn't support the
|
||||
// 'dotall' regex modifier. See:
|
||||
// http://stackoverflow.com/a/1068308/13216
|
||||
var m = arg.match(/^--([^=]+)=([\s\S]*)$/);
|
||||
var key = m[1];
|
||||
var value = m[2];
|
||||
if (flags.bools[key]) {
|
||||
value = value !== 'false';
|
||||
}
|
||||
setArg(key, value, arg);
|
||||
}
|
||||
/*
|
||||
else if (/^--no-.+/.test(arg)) {
|
||||
var key = arg.match(/^--no-(.+)/)[1];
|
||||
setArg(key, false, arg);
|
||||
}
|
||||
*/
|
||||
else if (/^--.+/.test(arg)) {
|
||||
var key = arg.match(/^--(.+)/)[1];
|
||||
var next = args[i + 1];
|
||||
if (next !== undefined && !/^-/.test(next)
|
||||
&& !flags.bools[key]
|
||||
&& !flags.allBools
|
||||
&& (aliases[key] ? !aliasIsBoolean(key) : true)) {
|
||||
setArg(key, next, arg);
|
||||
i++;
|
||||
}
|
||||
else if (/^(true|false)$/.test(next)) {
|
||||
setArg(key, next === 'true', arg);
|
||||
i++;
|
||||
}
|
||||
else {
|
||||
setArg(key, flags.strings[key] ? '' : true, arg);
|
||||
}
|
||||
}
|
||||
else if (/^-[^-]+/.test(arg)) {
|
||||
var letters = arg.slice(1,-1).split('');
|
||||
|
||||
var broken = false;
|
||||
for (var j = 0; j < letters.length; j++) {
|
||||
var next = arg.slice(j+2);
|
||||
|
||||
if (next === '-') {
|
||||
setArg(letters[j], next, arg)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/[A-Za-z]/.test(letters[j]) && /=/.test(next)) {
|
||||
setArg(letters[j], next.split('=')[1], arg);
|
||||
broken = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (/[A-Za-z]/.test(letters[j])
|
||||
&& /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
|
||||
setArg(letters[j], next, arg);
|
||||
broken = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (letters[j+1] && letters[j+1].match(/\W/)) {
|
||||
setArg(letters[j], arg.slice(j+2), arg);
|
||||
broken = true;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg);
|
||||
}
|
||||
}
|
||||
|
||||
var key = arg.slice(-1)[0];
|
||||
if (!broken && key !== '-') {
|
||||
if (args[i+1] && !/^(-|--)[^-]/.test(args[i+1])
|
||||
&& !flags.bools[key]
|
||||
&& (aliases[key] ? !aliasIsBoolean(key) : true)) {
|
||||
setArg(key, args[i+1], arg);
|
||||
i++;
|
||||
}
|
||||
else if (args[i+1] && /true|false/.test(args[i+1])) {
|
||||
setArg(key, args[i+1] === 'true', arg);
|
||||
i++;
|
||||
}
|
||||
else {
|
||||
setArg(key, flags.strings[key] ? '' : true, arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
|
||||
argv._.push(
|
||||
flags.strings['_'] || !isNumber(arg) ? arg : Number(arg)
|
||||
);
|
||||
}
|
||||
if (options.stopEarly) {
|
||||
argv._.push.apply(argv._, args.slice(i + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(defaults).forEach(function (key) {
|
||||
if (!hasKey(argv, key.split('.'))) {
|
||||
setKey(argv, key.split('.'), defaults[key]);
|
||||
|
||||
(aliases[key] || []).forEach(function (x) {
|
||||
setKey(argv, x.split('.'), defaults[key]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (options['--']) {
|
||||
argv['--'] = new Array();
|
||||
notFlags.forEach(function(key) {
|
||||
argv['--'].push(key);
|
||||
});
|
||||
}
|
||||
else {
|
||||
notFlags.forEach(function(key) {
|
||||
argv._.push(key);
|
||||
});
|
||||
}
|
||||
|
||||
return argv;
|
||||
};
|
||||
|
||||
function hasKey (obj, keys) {
|
||||
var o = obj;
|
||||
keys.slice(0,-1).forEach(function (key) {
|
||||
o = (o[key] || {});
|
||||
});
|
||||
|
||||
var key = keys[keys.length - 1];
|
||||
return key in o;
|
||||
}
|
||||
|
||||
function isNumber (x) {
|
||||
if (typeof x === 'number') return true;
|
||||
if (/^0x[0-9a-f]+$/i.test(x)) return true;
|
||||
return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x);
|
||||
}
|
||||
|
||||
|
||||
function minimist<T>(args?: string[], opts?: minimist.Opts): T & minimist.ParsedArgs {
|
||||
return parse(args || [], opts) as any;
|
||||
}
|
||||
export = minimist;
|
77
modules/shared/version/index.ts
Normal file
77
modules/shared/version/index.ts
Normal file
@ -0,0 +1,77 @@
|
||||
export class Version {
|
||||
major: number = 0;
|
||||
minor: number = 0;
|
||||
patch: number = 0;
|
||||
build: number = 0;
|
||||
timestamp: number = 0;
|
||||
|
||||
constructor(major: number, minor: number, patch: number, build: number, timestamp: number) {
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
this.patch = patch;
|
||||
this.build = build;
|
||||
}
|
||||
|
||||
toString(timestamp: boolean = false) {
|
||||
let result = "";
|
||||
result += this.major + ".";
|
||||
result += this.minor + ".";
|
||||
result += this.patch;
|
||||
if(this.build > 0)
|
||||
result += "-" + this.build;
|
||||
if(timestamp && this.timestamp > 0)
|
||||
result += " [" + this.timestamp + "]";
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
equals(other: Version) : boolean {
|
||||
if(other == this) return true;
|
||||
if(typeof(other) != typeof(this)) return false;
|
||||
|
||||
if(other.major != this.major) return false;
|
||||
if(other.minor != this.minor) return false;
|
||||
if(other.patch != this.patch) return false;
|
||||
if(other.build != this.build) return false;
|
||||
if(other.timestamp != this.timestamp) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
newer_than(other: Version) : boolean {
|
||||
if(other.major > this.major) return false;
|
||||
else if(other.major < this.major) return true;
|
||||
|
||||
if(other.minor > this.minor) return false;
|
||||
else if(other.minor < this.minor) return true;
|
||||
|
||||
else if(other.patch < this.patch) return true;
|
||||
if(other.patch > this.patch) return false;
|
||||
|
||||
if(other.build > this.build) return false;
|
||||
else if(other.build < this.build) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//1.0.0-2 [1000]
|
||||
export function parse_version(version: string) : Version {
|
||||
let result: Version = new Version(0, 0, 0, 0, 0);
|
||||
|
||||
const roots = version.split(" ");
|
||||
{
|
||||
const parts = roots[0].split("-");
|
||||
const numbers = parts[0].split(".");
|
||||
|
||||
if(numbers.length > 0) result.major = parseInt(numbers[0]);
|
||||
if(numbers.length > 1) result.minor = parseInt(numbers[1]);
|
||||
if(numbers.length > 2) result.patch = parseInt(numbers[2]);
|
||||
if(parts.length > 1) result.build = parseInt(parts[1]);
|
||||
}
|
||||
if(roots.length > 1 && ((roots[1] = roots[1].trim()).startsWith("[") && roots[1].endsWith("]"))) {
|
||||
result.timestamp = parseInt(roots[1].substr(1, roots[1].length - 2));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
2
native/.gitignore
vendored
Normal file
2
native/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
build/
|
||||
cmake-build-*
|
163
native/CMakeLists.txt
Normal file
163
native/CMakeLists.txt
Normal file
@ -0,0 +1,163 @@
|
||||
cmake_minimum_required(VERSION 3.1)
|
||||
|
||||
project(TeaClient-Natives VERSION 1.0.0)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_VERBOSE_MAKEFILE ON)
|
||||
|
||||
#Setup NodeJS
|
||||
function(setup_nodejs)
|
||||
set(NodeJS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake/")
|
||||
set(NODEJS_URL "https://atom.io/download/atom-shell")
|
||||
set(NODEJS_VERSION "v4.0.5")
|
||||
|
||||
find_package(NodeJS REQUIRED)
|
||||
|
||||
set(NODEJS_NAN_DIR "node_modules/nan")
|
||||
nodejs_init()
|
||||
|
||||
#Fix nan include headers
|
||||
set(NAN_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/../node_modules/nan")
|
||||
if(NOT EXISTS "${NAN_INCLUDE_DIR}")
|
||||
message(FATAL_ERROR "Failed to find nan headers.")
|
||||
endif()
|
||||
list(APPEND NODEJS_INCLUDE_DIRS ${NAN_INCLUDE_DIR})
|
||||
|
||||
set(EXE_DIRECTORY "${CMAKE_SOURCE_DIR}/build/exe/" PARENT_SCOPE)
|
||||
if (MSVC)
|
||||
set(NODE_LIB_DIRECTORY "${CMAKE_SOURCE_DIR}/build/win32_x64/" PARENT_SCOPE)
|
||||
set(NODE_PDB_DIRECTORY "${CMAKE_SOURCE_DIR}/build/symbols/" PARENT_SCOPE)
|
||||
else()
|
||||
set(NODE_LIB_DIRECTORY "${CMAKE_SOURCE_DIR}/build/linux_x64/" PARENT_SCOPE)
|
||||
endif()
|
||||
|
||||
#Set some more variables
|
||||
set(NODEJS_INCLUDE_DIRS "${NODEJS_INCLUDE_DIRS}" PARENT_SCOPE)
|
||||
set(NODEJS_INIT ${NODEJS_INIT} PARENT_SCOPE)
|
||||
|
||||
include_directories("dist/ext_nan")
|
||||
message("${NODEJS_INCLUDE_DIRS}")
|
||||
|
||||
|
||||
function(add_nodejs_module NAME)
|
||||
message("Registering module ${NAME}")
|
||||
_add_nodejs_module(${NAME} ${ARGN})
|
||||
target_compile_features(${NAME} PUBLIC cxx_std_17)
|
||||
set_target_properties(${NAME}
|
||||
PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${NODE_LIB_DIRECTORY}/"
|
||||
)
|
||||
target_include_directories(${NAME} PUBLIC ${NODEJS_INCLUDE_DIRS})
|
||||
set_target_properties(${NAME} PROPERTIES CXX_STANDARD 17) #Needs to be overridden after _add_nodejs_module sets it to 11
|
||||
message("Add lib: ${NODEJS_LIBRARIES}")
|
||||
|
||||
if(MSVC)
|
||||
add_custom_command(TARGET ${NAME} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E
|
||||
copy "$<TARGET_FILE:${NAME}>" "${NODE_LIB_DIRECTORY}/${NAME}.node"
|
||||
)
|
||||
add_custom_command(TARGET ${NAME} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E
|
||||
copy "$<TARGET_PDB_FILE:${NAME}>" "${NODE_PDB_DIRECTORY}/${NAME}.pdb"
|
||||
)
|
||||
else()
|
||||
set_target_properties(${NAME}
|
||||
PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${NODE_LIB_DIRECTORY}/"
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
#Forward parameters to global scope
|
||||
set(NODEJS_VERSION ${NODEJS_VERSION} PARENT_SCOPE)
|
||||
set(NODEJS_SOURCES ${NODEJS_SOURCES} PARENT_SCOPE)
|
||||
set(NODEJS_INCLUDE_DIRS ${NODEJS_INCLUDE_DIRS} PARENT_SCOPE)
|
||||
set(NODEJS_LIBRARIES ${NODEJS_LIBRARIES} PARENT_SCOPE)
|
||||
set(NODEJS_LINK_FLAGS ${NODEJS_LINK_FLAGS} PARENT_SCOPE)
|
||||
set(NODEJS_DEFINITIONS ${NODEJS_DEFINITIONS} PARENT_SCOPE)
|
||||
|
||||
# Prevents this function from executing more than once
|
||||
set(NODEJS_INIT TRUE PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
#Setup the compiler (Cant be done within a function!)
|
||||
if (MSVC)
|
||||
set(CompilerFlags
|
||||
CMAKE_C_FLAGS_DEBUG
|
||||
CMAKE_C_FLAGS_MINSIZEREL
|
||||
CMAKE_C_FLAGS_RELEASE
|
||||
CMAKE_C_FLAGS_RELWITHDEBINFO
|
||||
CMAKE_CXX_FLAGS_DEBUG
|
||||
CMAKE_CXX_FLAGS_MINSIZEREL
|
||||
CMAKE_CXX_FLAGS_RELEASE
|
||||
CMAKE_CXX_FLAGS_RELWITHDEBINFO
|
||||
)
|
||||
foreach(CompilerFlag ${CompilerFlags})
|
||||
string(REPLACE "/MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}")
|
||||
endforeach()
|
||||
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /std:c++17")
|
||||
else()
|
||||
#This is a bad thing here!
|
||||
set(LIBRARY_PATH "/home/wolverindev/TeaSpeak/libraries/")
|
||||
set(LIBEVENT_PATH "${LIBRARY_PATH}/event/build/lib/")
|
||||
|
||||
function(resolve_library VARIABLE FALLBACK PATHS)
|
||||
set( _PATHS ${PATHS} ${ARGN} ) # Merge them together
|
||||
|
||||
foreach(PATH IN ITEMS ${_PATHS})
|
||||
message(STATUS "Try to use path ${PATH} for ${VARIABLE}")
|
||||
if(EXISTS ${PATH})
|
||||
message(STATUS "Setting ${VARIABLE} to ${PATH}")
|
||||
set(${VARIABLE} ${PATH} PARENT_SCOPE)
|
||||
return()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(FALLBACK)
|
||||
message(WARNING "Failed to resolve library path for ${VARIABLE}. Using default ${VARIABLE}")
|
||||
else()
|
||||
message(FATAL_ERROR "Failed to find requited library. Variable: ${VARIABLE} Paths: ${_PATHS}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -static-libgcc -static-libstdc++")
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
# Addd the module path
|
||||
include(${CMAKE_MODULE_PATH}/libraries_wolverin_lap.cmake)
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${CMAKE_SOURCE_DIR}/cmake/")
|
||||
endif()
|
||||
|
||||
setup_nodejs()
|
||||
if(NOT NODEJS_INCLUDE_DIRS OR NODEJS_INCLUDE_DIRS STREQUAL "")
|
||||
message(FATAL_ERROR "Failed to find node headers")
|
||||
endif()
|
||||
|
||||
function(build_update_installer)
|
||||
add_subdirectory(updater)
|
||||
endfunction()
|
||||
build_update_installer()
|
||||
|
||||
|
||||
function(build_codec)
|
||||
add_subdirectory(codec)
|
||||
endfunction()
|
||||
build_codec()
|
||||
|
||||
function(build_ppt)
|
||||
add_subdirectory(ppt)
|
||||
endfunction()
|
||||
build_ppt()
|
||||
|
||||
function(build_connection)
|
||||
add_subdirectory(serverconnection)
|
||||
endfunction()
|
||||
build_connection()
|
||||
|
||||
function(build_crash_handler)
|
||||
add_subdirectory(crash_handler)
|
||||
endfunction()
|
||||
build_crash_handler()
|
1284
native/cmake/nodejs-config.cmake
Normal file
1284
native/cmake/nodejs-config.cmake
Normal file
File diff suppressed because it is too large
Load Diff
14
native/codec/.gitignore
vendored
Normal file
14
native/codec/.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
# Default ignored files
|
||||
/.idea/shelf/
|
||||
/.idea/workspace.xml
|
||||
|
||||
# Datasource local storage ignored files
|
||||
/.idea/dataSources/
|
||||
dataSources.local.xml
|
||||
|
||||
# Editor-based HTTP Client requests
|
||||
/.idea/httpRequests/
|
||||
rest-client.private.env.json
|
||||
http-client.private.env.json
|
||||
|
||||
cmake-build-*/
|
60
native/codec/CMakeLists.txt
Normal file
60
native/codec/CMakeLists.txt
Normal file
@ -0,0 +1,60 @@
|
||||
set(MODULE_NAME "teaclient_codec")
|
||||
set(CMAKE_MODULE_DIR "${CMAKE_SOURCE_DIR}/codec")
|
||||
|
||||
set(SOURCE_FILES
|
||||
binding.cc
|
||||
codec/NativeCodec.cpp
|
||||
codec/OpusCodec.cpp
|
||||
codec/SpeexCodec.cpp
|
||||
codec/CeltCodec.cpp
|
||||
)
|
||||
|
||||
add_nodejs_module(${MODULE_NAME} ${SOURCE_FILES})
|
||||
|
||||
if(MSVC)
|
||||
set(OPUS_LIBRARY_PATH "${CMAKE_MODULE_DIR}/libraries/generated/opus/lib/opus.lib")
|
||||
set(SPEEX_LIBRARY_PATH "${CMAKE_MODULE_DIR}/libraries/generated/speex/lib/libspeex.lib")
|
||||
else()
|
||||
set(OPUS_LIBRARY_PATH "${CMAKE_MODULE_DIR}/libraries/generated/opus/lib/libopus.a")
|
||||
set(SPEEX_LIBRARY_PATH "${CMAKE_MODULE_DIR}/libraries/generated/speex/lib/libspeex.a")
|
||||
set(CELT_LIBRARY_PATH "${CMAKE_MODULE_DIR}/libraries/generated/celt/lib/libcelt0.a")
|
||||
endif()
|
||||
|
||||
#Detect opus
|
||||
if(EXISTS "${CMAKE_MODULE_DIR}/libraries/generated/opus/include" AND EXISTS ${OPUS_LIBRARY_PATH})
|
||||
set(HAVE_OPUS ON)
|
||||
|
||||
add_definitions(-DHAVE_OPUS)
|
||||
include_directories(${CMAKE_MODULE_DIR}/libraries/generated/opus/include)
|
||||
target_link_libraries(${MODULE_NAME} ${OPUS_LIBRARY_PATH})
|
||||
else()
|
||||
message(WARNING "Missing opus libraries. Building without opus support!\n" "Build opus with the build script given within the libraries foulder")
|
||||
endif()
|
||||
|
||||
#Detect speex
|
||||
if(EXISTS "${CMAKE_MODULE_DIR}/libraries/generated/speex/include" AND EXISTS ${SPEEX_LIBRARY_PATH})
|
||||
set(HAVE_SPEEX ON)
|
||||
|
||||
add_definitions(-DHAVE_SPEEX)
|
||||
include_directories(${CMAKE_MODULE_DIR}/libraries/generated/speex/include)
|
||||
target_link_libraries(${MODULE_NAME} ${SPEEX_LIBRARY_PATH})
|
||||
else()
|
||||
message(WARNING "Missing speex libraries. Building without speex support!\n" "Build speex with the build script given within the libraries foulder")
|
||||
endif()
|
||||
|
||||
#Detect celt
|
||||
set(BUILD_CELT OFF)
|
||||
if(EXISTS "${CMAKE_MODULE_DIR}/libraries/generated/celt/include" AND EXISTS "${CELT_LIBRARY_PATH}" AND BUILD_CELT)
|
||||
set(HAVE_CELT ON)
|
||||
|
||||
add_definitions(-DHAVE_CELT)
|
||||
include_directories(${CMAKE_MODULE_DIR}/libraries/generated/celt/include)
|
||||
|
||||
target_link_libraries(${MODULE_NAME} ${CELT_LIBRARY_PATH})
|
||||
else()
|
||||
message(WARNING "Missing celt libraries. Building without celt support!\n" "Build celt with the build script given within the libraries foulder")
|
||||
endif()
|
||||
|
||||
if(HAVE_OPUS AND HAVE_CELT)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wl,-no-whole-archive,--allow-multiple-definition")
|
||||
endif()
|
31
native/codec/binding.cc
Normal file
31
native/codec/binding.cc
Normal file
@ -0,0 +1,31 @@
|
||||
#include <nan.h>
|
||||
#include <node.h>
|
||||
#include <v8.h>
|
||||
#include <iostream>
|
||||
#include "codec/NativeCodec.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
tc::WorkerPool* tc::codec_workers = nullptr;
|
||||
|
||||
NAN_METHOD(finalize) {
|
||||
auto pool = tc::codec_workers;
|
||||
if(!pool) return;
|
||||
|
||||
tc::codec_workers = nullptr;
|
||||
|
||||
pool->finalize();
|
||||
delete pool;
|
||||
}
|
||||
|
||||
NAN_MODULE_INIT(init) {
|
||||
tc::codec_workers = new tc::WorkerPool();
|
||||
tc::codec_workers->initialize();
|
||||
|
||||
Nan::Set(target, Nan::New<v8::String>("new_instance").ToLocalChecked(), Nan::GetFunction(Nan::New<v8::FunctionTemplate>(tc::NativeCodec::NewInstance)).ToLocalChecked());
|
||||
tc::NativeCodec::Init(target);
|
||||
tc::NativeCodec::CodecType::Init(target);
|
||||
}
|
||||
|
||||
NODE_MODULE(MODULE_NAME, init)
|
203
native/codec/codec/CeltCodec.cpp
Normal file
203
native/codec/codec/CeltCodec.cpp
Normal file
@ -0,0 +1,203 @@
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include "CeltCodec.h"
|
||||
#include "NativeCodec.h"
|
||||
#include "NanException.h"
|
||||
#include "NanEventCallback.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
using namespace tc;
|
||||
using namespace v8;
|
||||
using namespace Nan;
|
||||
|
||||
bool CeltCodec::supported() {
|
||||
#ifdef HAVE_CELT
|
||||
return true;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef HAVE_CELT
|
||||
|
||||
CeltCodec::CeltCodec(NativeCodec::CodecType::value type) : NativeCodec(type) {
|
||||
cout << "Allocate celt instance" << endl;
|
||||
|
||||
}
|
||||
|
||||
CeltCodec::~CeltCodec() {
|
||||
cout << "Free celt instance" << endl;
|
||||
|
||||
if(this->decoder || this->encoder) {
|
||||
NAN_THROW_EXCEPTION(Error, "please finalize before releasing!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(CeltCodec::initialize) {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
//(≥20kHz; sample rates from 8 kHz to 48 kHz)
|
||||
int error = 0;
|
||||
|
||||
int sample_rate = 48000;
|
||||
this->encoder = celt_encoder_create(sample_rate, this->channels, &error);
|
||||
if(!this->encoder || error) {
|
||||
cout << this->encoder << " - " << error << endl;
|
||||
if(this->encoder)
|
||||
celt_encoder_destroy(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
|
||||
NAN_THROW_EXCEPTION(Error, ("Failed to create encoder (" + to_string(error) + ")").c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
this->decoder = celt_decoder_create(sample_rate, this->channels, &error);
|
||||
if(!this->decoder || error) {
|
||||
if(this->decoder)
|
||||
celt_decoder_destroy(this->decoder);
|
||||
this->decoder = nullptr;
|
||||
if(this->encoder)
|
||||
celt_encoder_destroy(this->encoder);
|
||||
|
||||
this->encoder = nullptr;
|
||||
NAN_THROW_EXCEPTION(Error, ("Failed to create decoder (" + to_string(error) + ")").c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(CeltCodec::finalize) {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
if(this->encoder) {
|
||||
celt_encoder_destroy(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
}
|
||||
|
||||
if(this->decoder) {
|
||||
celt_decoder_destroy(this->decoder);
|
||||
this->decoder = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(CeltCodec::encode) {
|
||||
Nan::HandleScope scope;
|
||||
|
||||
if(!info[0]->IsArrayBuffer()) {
|
||||
NAN_THROW_EXCEPTION(Error, "First argument isn't an array buffer!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto js_buffer = info[0].As<ArrayBuffer>()->GetContents();
|
||||
auto buffer = make_unique<Chunk>(max(js_buffer.ByteLength(), 256UL));
|
||||
buffer->length = js_buffer.ByteLength();
|
||||
memcpy(buffer->memory, js_buffer.Data(), js_buffer.ByteLength());
|
||||
|
||||
auto callback_success = make_unique<Nan::Callback>(info[1].As<Function>());
|
||||
auto callback_error = make_unique<Nan::Callback>(info[2].As<Function>());
|
||||
|
||||
auto codec = make_unique<v8::Persistent<Object>>(info.GetIsolate(), info.Holder());
|
||||
auto callback = Nan::async_callback([callback_success = move(callback_success), callback_error = move(callback_error), codec = move(codec)](std::unique_ptr<Chunk> result, std::string error) {
|
||||
Nan::HandleScope scope;
|
||||
if(result) {
|
||||
auto _buffer = v8::ArrayBuffer::New(v8::Isolate::GetCurrent(), result->length);
|
||||
memcpy(_buffer->GetContents().Data(), result->memory, result->length);
|
||||
|
||||
Local<Value> argv[] = { _buffer }; //_buffer
|
||||
callback_success->Call(1, argv);
|
||||
} else {
|
||||
Local<Value> argv[] = { Nan::New<String>(error).ToLocalChecked() }; //error
|
||||
callback_error->Call(1, argv);
|
||||
}
|
||||
codec->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
auto frame_size = buffer->length / sizeof(float) / this->channels;
|
||||
if(frame_size != 64 && frame_size != 128 && frame_size != 256 && frame_size != 512) {
|
||||
NAN_THROW_EXCEPTION(Error, ("Invalid frame size (" + to_string(frame_size) + "). Only allow 64, 128, 256, 512bytes").c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
tc::codec_workers->enqueue_task(
|
||||
[this, callback, buffer = move(buffer)]() mutable {
|
||||
{
|
||||
lock_guard lock(this->coder_lock);
|
||||
if(!this->encoder) {
|
||||
callback(nullptr, "Please initialize first!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto nbytes = celt_encode_float(this->encoder, (float*) buffer->memory, buffer->length / sizeof(float) / this->channels, (u_char*) buffer->memory, buffer->allocated_length);
|
||||
if(nbytes < 0) {
|
||||
callback(nullptr, "Invalid encode return code (" + to_string(nbytes) + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
buffer->length = nbytes;
|
||||
}
|
||||
callback(move(buffer), "");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
NAN_METHOD(CeltCodec::decode) {
|
||||
Nan::HandleScope scope;
|
||||
|
||||
if(!info[0]->IsArrayBuffer()) {
|
||||
NAN_THROW_EXCEPTION(Error, "First argument isn't an array buffer!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto js_buffer = info[0].As<ArrayBuffer>()->GetContents();
|
||||
auto buffer = make_unique<Chunk>(max(js_buffer.ByteLength(), this->max_frame_size * this->channels * sizeof(float)));
|
||||
buffer->length = js_buffer.ByteLength();
|
||||
memcpy(buffer->memory, js_buffer.Data(), js_buffer.ByteLength());
|
||||
|
||||
auto callback_success = make_unique<Nan::Callback>(info[1].As<Function>());
|
||||
auto callback_error = make_unique<Nan::Callback>(info[2].As<Function>());
|
||||
|
||||
auto codec = make_unique<v8::Persistent<Object>>(info.GetIsolate(), info.Holder());
|
||||
auto callback = Nan::async_callback([callback_success = move(callback_success), callback_error = move(callback_error), codec = move(codec)](std::unique_ptr<Chunk> result, std::string error) {
|
||||
Nan::HandleScope scope;
|
||||
if(result) {
|
||||
auto _buffer = v8::ArrayBuffer::New(v8::Isolate::GetCurrent(), result->length);
|
||||
memcpy(_buffer->GetContents().Data(), result->memory, result->length);
|
||||
Local<Value> argv[] = { _buffer };
|
||||
callback_success->Call(1, argv);
|
||||
} else {
|
||||
Local<Value> argv[] = { Nan::New<String>(error).ToLocalChecked() };
|
||||
callback_error->Call(1, argv);
|
||||
}
|
||||
codec->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
tc::codec_workers->enqueue_task(
|
||||
[this, callback, buffer = move(buffer)]() mutable {
|
||||
int result;
|
||||
|
||||
{
|
||||
lock_guard lock(this->coder_lock);
|
||||
if(!this->decoder) {
|
||||
callback(nullptr, "Please initialize first!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto code = celt_decode_float(this->decoder, (u_char*) buffer->memory, buffer->length, (float*) buffer->memory, buffer->allocated_length / sizeof(float) / this->channels);
|
||||
if(code < 0) {
|
||||
callback(nullptr, "Invalid decode return code (" + to_string(code) + ")");
|
||||
return;
|
||||
}
|
||||
cout << code << endl;
|
||||
|
||||
buffer->length = this->channels * this->max_frame_size * sizeof(float);
|
||||
}
|
||||
|
||||
callback(move(buffer), "");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#endif
|
34
native/codec/codec/CeltCodec.h
Normal file
34
native/codec/codec/CeltCodec.h
Normal file
@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "NativeCodec.h"
|
||||
|
||||
#ifdef HAVE_CELT
|
||||
#include <celt/celt.h>
|
||||
#endif
|
||||
|
||||
namespace tc {
|
||||
class NativeCodec;
|
||||
|
||||
|
||||
class CeltCodec : public NativeCodec {
|
||||
public:
|
||||
static bool supported();
|
||||
|
||||
#ifdef HAVE_CELT
|
||||
explicit CeltCodec(CodecType::value type);
|
||||
virtual ~CeltCodec();
|
||||
|
||||
virtual NAN_METHOD(initialize);
|
||||
virtual NAN_METHOD(finalize);
|
||||
virtual NAN_METHOD(encode);
|
||||
virtual NAN_METHOD(decode);
|
||||
private:
|
||||
std::mutex coder_lock;
|
||||
int max_frame_size = 512;
|
||||
int channels = 1; /* fixed */
|
||||
|
||||
CELTEncoder* encoder = nullptr;
|
||||
CELTDecoder* decoder = nullptr;
|
||||
#endif
|
||||
};
|
||||
}
|
205
native/codec/codec/NativeCodec.cpp
Normal file
205
native/codec/codec/NativeCodec.cpp
Normal file
@ -0,0 +1,205 @@
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include "NativeCodec.h"
|
||||
#include "OpusCodec.h"
|
||||
#include "SpeexCodec.h"
|
||||
#include "CeltCodec.h"
|
||||
#include "NanException.h"
|
||||
#include <iostream>
|
||||
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
using namespace tc;
|
||||
using namespace v8;
|
||||
using namespace Nan;
|
||||
|
||||
#define DEFINE_ENUM_ENTRY(name, value) \
|
||||
Nan::ForceSet(types, Nan::New<v8::String>(name).ToLocalChecked(), Nan::New<v8::Number>(value), static_cast<PropertyAttribute>(ReadOnly|DontDelete)); \
|
||||
//Nan::ForceSet(types, Nan::New<v8::Number>(value), Nan::New<v8::String>(name).ToLocalChecked(), static_cast<PropertyAttribute>(ReadOnly|DontDelete));
|
||||
|
||||
NAN_MODULE_INIT(NativeCodec::CodecType::Init) {
|
||||
auto types = Nan::New<v8::Object>();
|
||||
|
||||
DEFINE_ENUM_ENTRY("OPUS_VOICE", CodecType::OPUS_VOICE);
|
||||
DEFINE_ENUM_ENTRY("OPUS_MUSIC", CodecType::OPUS_MUSIC);
|
||||
DEFINE_ENUM_ENTRY("SPEEX_NARROWBAND", CodecType::SPEEX_NARROWBAND);
|
||||
DEFINE_ENUM_ENTRY("SPEEX_WIDEBAND", CodecType::SPEEX_WIDEBAND);
|
||||
DEFINE_ENUM_ENTRY("SPEEX_ULTRA_WIDEBAND", CodecType::SPEEX_ULTRA_WIDEBAND);
|
||||
DEFINE_ENUM_ENTRY("CELT_MONO", CodecType::CELT_MONO);
|
||||
|
||||
Nan::Set(target, Nan::New<v8::String>("CodecTypes").ToLocalChecked(), types);
|
||||
Nan::Set(target, Nan::New<v8::String>("codec_supported").ToLocalChecked(), Nan::New<v8::Function>(NativeCodec::CodecType::supported));
|
||||
}
|
||||
|
||||
NAN_METHOD(NativeCodec::CodecType::supported) {
|
||||
if(!info[0]->IsNumber()) {
|
||||
NAN_THROW_EXCEPTION(Error, "Argument 0 shall be a number!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto type = (CodecType::value) Nan::To<int>(info[0]).FromJust();
|
||||
if(type == CodecType::OPUS_MUSIC || type == CodecType::OPUS_VOICE) {
|
||||
info.GetReturnValue().Set(OpusCodec::supported());
|
||||
} else if(type == CodecType::SPEEX_NARROWBAND || type == CodecType::SPEEX_WIDEBAND || type == CodecType::SPEEX_ULTRA_WIDEBAND) {
|
||||
info.GetReturnValue().Set(SpeexCodec::supported());
|
||||
} else if(type == CodecType::CELT_MONO) {
|
||||
info.GetReturnValue().Set(CeltCodec::supported());
|
||||
} else {
|
||||
NAN_THROW_EXCEPTION(Error, "Invalid type!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NAN_MODULE_INIT(NativeCodec::Init) {
|
||||
auto klass = New<FunctionTemplate>(NewInstance);
|
||||
klass->SetClassName(Nan::New("NativeCodec").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "decode", NativeCodec::function_decode);
|
||||
Nan::SetPrototypeMethod(klass, "encode", NativeCodec::function_encode);
|
||||
Nan::SetPrototypeMethod(klass, "initialize", NativeCodec::function_initialize);
|
||||
Nan::SetPrototypeMethod(klass, "finalize", NativeCodec::function_finalize);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
//Nan::Set(target, Nan::New("NativeCodec").ToLocalChecked(), Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(NativeCodec::NewInstance) {
|
||||
if (info.IsConstructCall()) {
|
||||
if(!info[0]->IsNumber()) {
|
||||
NAN_THROW_EXCEPTION(Error, "Argument 0 shall be a number!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto type = (CodecType::value) Nan::To<int>(info[0]).FromJust();
|
||||
std::unique_ptr<NativeCodec> instance;
|
||||
if(type == CodecType::OPUS_MUSIC || type == CodecType::OPUS_VOICE) {
|
||||
#ifdef HAVE_OPUS
|
||||
instance = make_unique<OpusCodec>(type);
|
||||
#endif
|
||||
} else if(type == CodecType::SPEEX_NARROWBAND || type == CodecType::SPEEX_WIDEBAND || type == CodecType::SPEEX_ULTRA_WIDEBAND) {
|
||||
#ifdef HAVE_SPEEX
|
||||
instance = make_unique<SpeexCodec>(type);
|
||||
#endif
|
||||
} else if(type == CodecType::CELT_MONO) {
|
||||
#ifdef HAVE_CELT
|
||||
instance = make_unique<CeltCodec>(type);
|
||||
#endif
|
||||
} else {
|
||||
NAN_THROW_EXCEPTION(Error, "Invalid type!");
|
||||
return;
|
||||
}
|
||||
if(!instance) {
|
||||
NAN_THROW_EXCEPTION(Error, "Target codec isn't supported!");
|
||||
return;
|
||||
}
|
||||
instance.release()->Wrap(info.This());
|
||||
|
||||
Nan::Set(info.This(), New<String>("type").ToLocalChecked(), New<Number>(type));
|
||||
info.GetReturnValue().Set(info.This());
|
||||
} else {
|
||||
const int argc = 1;
|
||||
v8::Local<v8::Value> argv[argc] = {info[0]};
|
||||
v8::Local<v8::Function> cons = Nan::New(constructor());
|
||||
|
||||
Nan::TryCatch try_catch;
|
||||
auto result = Nan::NewInstance(cons, argc, argv);
|
||||
if(try_catch.HasCaught()) {
|
||||
try_catch.ReThrow();
|
||||
return;
|
||||
}
|
||||
info.GetReturnValue().Set(result.ToLocalChecked());
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(NativeCodec::function_decode) {
|
||||
if(info.Length() != 3) {
|
||||
NAN_THROW_EXCEPTION(Error, "Invalid argument count!");
|
||||
return;
|
||||
}
|
||||
if(!info[0]->IsArrayBuffer() || !info[1]->IsFunction() || !info[2]->IsFunction()) {
|
||||
NAN_THROW_EXCEPTION(Error, "Invalid argument types!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto codec = ObjectWrap::Unwrap<NativeCodec>(info.Holder());
|
||||
codec->decode(info);
|
||||
}
|
||||
|
||||
NAN_METHOD(NativeCodec::function_encode) {
|
||||
if(info.Length() != 3) {
|
||||
NAN_THROW_EXCEPTION(Error, "Invalid argument count!");
|
||||
return;
|
||||
}
|
||||
if(!info[0]->IsArrayBuffer() || !info[1]->IsFunction() || !info[2]->IsFunction()) {
|
||||
NAN_THROW_EXCEPTION(Error, "Invalid argument types!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto codec = ObjectWrap::Unwrap<NativeCodec>(info.Holder());
|
||||
codec->encode(info);
|
||||
}
|
||||
|
||||
NAN_METHOD(NativeCodec::function_initialize) {
|
||||
auto codec = ObjectWrap::Unwrap<NativeCodec>(info.Holder());
|
||||
codec->initialize(info);
|
||||
}
|
||||
NAN_METHOD(NativeCodec::function_finalize) {
|
||||
auto codec = ObjectWrap::Unwrap<NativeCodec>(info.Holder());
|
||||
codec->finalize(info);
|
||||
}
|
||||
|
||||
NativeCodec::NativeCodec(tc::NativeCodec::CodecType::value type) : type(type) {}
|
||||
NativeCodec::~NativeCodec() {}
|
||||
|
||||
|
||||
WorkerPool::WorkerPool() {}
|
||||
WorkerPool::~WorkerPool() {}
|
||||
|
||||
void WorkerPool::initialize() {
|
||||
assert(!this->_running);
|
||||
|
||||
this->_running = true;
|
||||
this->worker = thread([&]{
|
||||
while(this->_running) {
|
||||
function<void()> worker;
|
||||
{
|
||||
unique_lock lock(this->worker_lock);
|
||||
this->worker_wait.wait_for(lock, minutes(1), [&]{ return !this->_running || !this->tasks.empty(); });
|
||||
if(!this->_running) break;
|
||||
if(this->tasks.empty()) continue;
|
||||
|
||||
worker = move(this->tasks.front());
|
||||
this->tasks.pop_front();
|
||||
}
|
||||
|
||||
try {
|
||||
worker();
|
||||
} catch(std::exception& ex) {
|
||||
cerr << "failed to invoke opus worker! message: " << ex.what() << endl;
|
||||
} catch (...) {
|
||||
cerr << "failed to invoke opus worker!" << endl;
|
||||
}
|
||||
}
|
||||
});
|
||||
#ifndef WIN32
|
||||
auto worker_handle = this->worker.native_handle();
|
||||
pthread_setname_np(worker_handle, "Codec Worker");
|
||||
#endif
|
||||
}
|
||||
|
||||
void WorkerPool::finalize() {
|
||||
this->_running = false;
|
||||
this->worker_wait.notify_all();
|
||||
this->tasks.clear();
|
||||
|
||||
if(this->worker.joinable())
|
||||
this->worker.join();
|
||||
}
|
||||
|
||||
void WorkerPool::enqueue_task(std::function<void()> task) {
|
||||
lock_guard lock(this->worker_lock);
|
||||
this->tasks.push_back(std::move(task));
|
||||
this->worker_wait.notify_one();
|
||||
}
|
95
native/codec/codec/NativeCodec.h
Normal file
95
native/codec/codec/NativeCodec.h
Normal file
@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include <nan.h>
|
||||
#include <mutex>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <condition_variable>
|
||||
|
||||
namespace tc {
|
||||
struct WorkerPool {
|
||||
public:
|
||||
WorkerPool();
|
||||
virtual ~WorkerPool();
|
||||
|
||||
void initialize();
|
||||
void finalize();
|
||||
|
||||
void enqueue_task(std::function<void()> /* function */);
|
||||
|
||||
template <typename T>
|
||||
void enqueue_task(T&& closure) {
|
||||
auto handle = std::make_shared<T>(std::forward<T>(closure));
|
||||
this->enqueue_task(std::function<void()>([handle]{ (*handle)(); }));
|
||||
}
|
||||
|
||||
private:
|
||||
bool _running = false;
|
||||
|
||||
std::thread worker;
|
||||
std::mutex worker_lock;
|
||||
std::condition_variable worker_wait;
|
||||
|
||||
std::deque<std::function<void()>> tasks;
|
||||
};
|
||||
extern WorkerPool* codec_workers;
|
||||
|
||||
struct Chunk {
|
||||
char* memory;
|
||||
size_t length;
|
||||
size_t allocated_length;
|
||||
|
||||
Chunk(size_t length) {
|
||||
memory = (char*) malloc(length);
|
||||
this->allocated_length = length;
|
||||
this->length = 0;
|
||||
}
|
||||
|
||||
~Chunk() {
|
||||
free(memory);
|
||||
}
|
||||
};
|
||||
|
||||
class NativeCodec : public Nan::ObjectWrap {
|
||||
public:
|
||||
struct CodecType {
|
||||
enum value {
|
||||
SPEEX_NARROWBAND,
|
||||
SPEEX_WIDEBAND,
|
||||
SPEEX_ULTRA_WIDEBAND,
|
||||
CELT_MONO,
|
||||
OPUS_VOICE,
|
||||
OPUS_MUSIC
|
||||
};
|
||||
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(supported);
|
||||
};
|
||||
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(NewInstance);
|
||||
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
|
||||
explicit NativeCodec(CodecType::value type);
|
||||
virtual ~NativeCodec();
|
||||
|
||||
virtual NAN_METHOD(initialize) = 0;
|
||||
virtual NAN_METHOD(finalize) = 0;
|
||||
|
||||
virtual NAN_METHOD(encode) = 0;
|
||||
virtual NAN_METHOD(decode) = 0;
|
||||
|
||||
static NAN_METHOD(function_encode);
|
||||
static NAN_METHOD(function_decode);
|
||||
static NAN_METHOD(function_initialize);
|
||||
static NAN_METHOD(function_finalize);
|
||||
|
||||
protected:
|
||||
CodecType::value type;
|
||||
|
||||
};
|
||||
}
|
204
native/codec/codec/OpusCodec.cpp
Normal file
204
native/codec/codec/OpusCodec.cpp
Normal file
@ -0,0 +1,204 @@
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include "OpusCodec.h"
|
||||
#include "NativeCodec.h"
|
||||
#include "NanException.h"
|
||||
#include "NanEventCallback.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
using namespace tc;
|
||||
using namespace v8;
|
||||
using namespace Nan;
|
||||
|
||||
bool OpusCodec::supported() {
|
||||
#ifdef HAVE_OPUS
|
||||
return true;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef HAVE_OPUS
|
||||
OpusCodec::OpusCodec(NativeCodec::CodecType::value type) : NativeCodec(type) {
|
||||
cout << "New opus instance" << endl;
|
||||
|
||||
this->sampling_rate = 48000;
|
||||
this->frames = 960;
|
||||
if(type == CodecType::OPUS_MUSIC) {
|
||||
this->channels = 2;
|
||||
} else {
|
||||
this->channels = 1;
|
||||
}
|
||||
}
|
||||
|
||||
OpusCodec::~OpusCodec() {
|
||||
cout << "Free opus instance" << endl;
|
||||
|
||||
if(this->decoder || this->encoder) {
|
||||
NAN_THROW_EXCEPTION(Error, "please finalize before releasing!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(OpusCodec::initialize) {
|
||||
int error = 0;
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
this->encoder = opus_encoder_create(this->sampling_rate, this->channels, this->type == CodecType::OPUS_MUSIC ? OPUS_APPLICATION_AUDIO : OPUS_APPLICATION_VOIP, &error);
|
||||
if(!this->encoder || error) {
|
||||
NAN_THROW_EXCEPTION(Error, ("Failed to create encoder (" + to_string(error) + ")").c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
this->decoder = opus_decoder_create(this->sampling_rate, this->channels, &error);
|
||||
if(!this->encoder || error) {
|
||||
opus_encoder_destroy(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
|
||||
NAN_THROW_EXCEPTION(Error, ("Failed to create decoder (" + to_string(error) + ")").c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(OpusCodec::finalize) {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
opus_encoder_destroy(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
|
||||
opus_decoder_destroy(this->decoder);
|
||||
this->decoder = nullptr;
|
||||
}
|
||||
|
||||
NAN_METHOD(OpusCodec::encode) {
|
||||
Nan::HandleScope scope;
|
||||
|
||||
if(info.Length() != 3 || !info[0]->IsArrayBuffer()) {
|
||||
NAN_THROW_EXCEPTION(Error, "Invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
auto js_buffer = info[0].As<ArrayBuffer>()->GetContents();
|
||||
auto buffer = make_unique<Chunk>(max(js_buffer.ByteLength(), 256UL));
|
||||
buffer->length = js_buffer.ByteLength();
|
||||
memcpy(buffer->memory, js_buffer.Data(), js_buffer.ByteLength());
|
||||
|
||||
auto callback_success = make_unique<Nan::Callback>(info[1].As<Function>());
|
||||
auto callback_error = make_unique<Nan::Callback>(info[2].As<Function>());
|
||||
|
||||
auto codec = make_unique<v8::Persistent<Object>>(info.GetIsolate(), info.Holder());
|
||||
auto callback = Nan::async_callback([callback_success = move(callback_success), callback_error = move(callback_error), codec = move(codec)](std::unique_ptr<Chunk> result, std::string error) {
|
||||
if(result) {
|
||||
auto _buffer = v8::ArrayBuffer::New(v8::Isolate::GetCurrent(), result->length);
|
||||
memcpy(_buffer->GetContents().Data(), result->memory, result->length);
|
||||
|
||||
Local<Value> argv[] = { _buffer }; //_buffer
|
||||
callback_success->Call(1, argv);
|
||||
} else {
|
||||
Local<Value> argv[] = { Nan::New<String>(error).ToLocalChecked() }; //error
|
||||
callback_error->Call(1, argv);
|
||||
}
|
||||
codec->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
tc::codec_workers->enqueue_task(
|
||||
[this, callback, buffer = move(buffer)]() mutable {
|
||||
int result;
|
||||
{
|
||||
lock_guard lock(this->coder_lock);
|
||||
if(!this->encoder) {
|
||||
callback(nullptr, "Please initialize first!");
|
||||
return;
|
||||
}
|
||||
|
||||
if(this->channels == 1) {
|
||||
result = opus_encode_float(this->encoder, (float*) buffer->memory, (int) (buffer->length / sizeof(float) / this->channels), (u_char*) buffer->memory, (opus_int32) buffer->allocated_length);
|
||||
} else {
|
||||
auto samples = buffer->length / sizeof(float) / this->channels;
|
||||
float* local_buffer = new float[samples * this->channels];
|
||||
for(size_t channel = 0; channel < this->channels; channel++)
|
||||
for(size_t sample = 0; sample < samples; sample++)
|
||||
local_buffer[sample * this->channels + channel] = ((float*) buffer->memory)[channel * samples + sample];
|
||||
|
||||
result = opus_encode_float(this->encoder, local_buffer, samples, (u_char*) buffer->memory, (opus_int32) buffer->allocated_length);
|
||||
delete[] local_buffer;
|
||||
}
|
||||
}
|
||||
|
||||
if(result <= 0) {
|
||||
callback(nullptr, "Invalid return code (" + to_string(result) + ")");
|
||||
} else {
|
||||
buffer->length = result;
|
||||
callback(move(buffer), "");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
NAN_METHOD(OpusCodec::decode) {
|
||||
Nan::HandleScope scope;
|
||||
|
||||
if(!info[0]->IsArrayBuffer()) {
|
||||
NAN_THROW_EXCEPTION(Error, "First argument isn't an array buffer!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto js_buffer = info[0].As<ArrayBuffer>()->GetContents();
|
||||
auto buffer = make_unique<Chunk>(max(js_buffer.ByteLength(), this->channels * this->frames * sizeof(float)));
|
||||
buffer->length = js_buffer.ByteLength();
|
||||
memcpy(buffer->memory, js_buffer.Data(), js_buffer.ByteLength());
|
||||
|
||||
auto callback_success = make_unique<Nan::Callback>(info[1].As<Function>());
|
||||
auto callback_error = make_unique<Nan::Callback>(info[2].As<Function>());
|
||||
|
||||
auto codec = make_unique<v8::Persistent<Object>>(info.GetIsolate(), info.Holder());
|
||||
auto callback = Nan::async_callback([callback_success = move(callback_success), callback_error = move(callback_error), codec = move(codec)](std::unique_ptr<Chunk> result, std::string error) {
|
||||
Nan::HandleScope scope;
|
||||
if(result) {
|
||||
auto _buffer = v8::ArrayBuffer::New(v8::Isolate::GetCurrent(), result->length);
|
||||
memcpy(_buffer->GetContents().Data(), result->memory, result->length);
|
||||
Local<Value> argv[] = { _buffer };
|
||||
callback_success->Call(1, argv);
|
||||
} else {
|
||||
Local<Value> argv[] = { Nan::New<String>(error).ToLocalChecked() };
|
||||
callback_error->Call(1, argv);
|
||||
}
|
||||
codec->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
tc::codec_workers->enqueue_task(
|
||||
[this, buffer = move(buffer), callback]() mutable {
|
||||
int result;
|
||||
|
||||
{
|
||||
lock_guard lock(this->coder_lock);
|
||||
if(!this->decoder) {
|
||||
callback(nullptr, "Please initialize first!");
|
||||
return;
|
||||
}
|
||||
|
||||
if(this->channels == 1) {
|
||||
result = opus_decode_float(this->decoder, (u_char*) buffer->memory, (opus_int32) buffer->length, (float*) buffer->memory, this->frames, 0);
|
||||
} else {
|
||||
float* local_buffer = new float[this->frames * this->channels];
|
||||
result = opus_decode_float(this->decoder, (u_char*) buffer->memory, (opus_int32) buffer->length, (float*) local_buffer, this->frames, 0);
|
||||
|
||||
for(size_t channel = 0; channel < this->channels; channel++)
|
||||
for(size_t sample = 0; sample < result; sample++)
|
||||
((float*) buffer->memory)[channel * this->frames + sample] = local_buffer[sample * this->channels + channel];
|
||||
delete[] local_buffer;
|
||||
}
|
||||
}
|
||||
|
||||
if(result <= 0) {
|
||||
callback(nullptr, "Invalid return code (" + to_string(result) + ")");
|
||||
} else {
|
||||
buffer->length = result * sizeof(float) * this->channels;
|
||||
callback(move(buffer), "");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
#endif
|
33
native/codec/codec/OpusCodec.h
Normal file
33
native/codec/codec/OpusCodec.h
Normal file
@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "NativeCodec.h"
|
||||
|
||||
#ifdef HAVE_OPUS
|
||||
#include <opus/opus.h>
|
||||
#endif
|
||||
|
||||
namespace tc {
|
||||
class NativeCodec;
|
||||
|
||||
class OpusCodec : public NativeCodec {
|
||||
public:
|
||||
static bool supported();
|
||||
#ifdef HAVE_OPUS
|
||||
explicit OpusCodec(CodecType::value type);
|
||||
virtual ~OpusCodec();
|
||||
|
||||
virtual NAN_METHOD(initialize);
|
||||
virtual NAN_METHOD(finalize);
|
||||
virtual NAN_METHOD(encode);
|
||||
virtual NAN_METHOD(decode);
|
||||
private:
|
||||
uint16_t sampling_rate = 0;
|
||||
uint16_t frames = 0;
|
||||
uint32_t channels = 0;
|
||||
|
||||
std::mutex coder_lock;
|
||||
OpusDecoder* decoder = nullptr;
|
||||
OpusEncoder* encoder = nullptr;
|
||||
#endif
|
||||
};
|
||||
}
|
234
native/codec/codec/SpeexCodec.cpp
Normal file
234
native/codec/codec/SpeexCodec.cpp
Normal file
@ -0,0 +1,234 @@
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include "SpeexCodec.h"
|
||||
#include "NativeCodec.h"
|
||||
#include "NanException.h"
|
||||
#include "NanEventCallback.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
using namespace tc;
|
||||
using namespace v8;
|
||||
using namespace Nan;
|
||||
|
||||
bool SpeexCodec::supported() {
|
||||
#ifdef HAVE_SPEEX
|
||||
return true;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef HAVE_SPEEX
|
||||
|
||||
SpeexCodec::SpeexCodec(NativeCodec::CodecType::value type) : NativeCodec(type) {
|
||||
cout << "Allocate speex instance" << endl;
|
||||
|
||||
}
|
||||
|
||||
SpeexCodec::~SpeexCodec() {
|
||||
cout << "Free speex instance" << endl;
|
||||
|
||||
if(this->decoder || this->encoder) {
|
||||
NAN_THROW_EXCEPTION(Error, "please finalize before releasing!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(SpeexCodec::initialize) {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
const SpeexMode* speex_mode = nullptr;
|
||||
|
||||
if(this->type == CodecType::SPEEX_NARROWBAND)
|
||||
speex_mode = &speex_nb_mode;
|
||||
else if(this->type == CodecType::SPEEX_WIDEBAND)
|
||||
speex_mode = &speex_wb_mode;
|
||||
else if(this->type == CodecType::SPEEX_ULTRA_WIDEBAND)
|
||||
speex_mode = &speex_uwb_mode;
|
||||
|
||||
assert(speex_mode);
|
||||
{
|
||||
|
||||
this->encoder = speex_encoder_init(speex_mode);
|
||||
if(!this->encoder) {
|
||||
NAN_THROW_EXCEPTION(Error, "Failed to create encoder");
|
||||
return;
|
||||
}
|
||||
|
||||
/*Set the quality to 8 (15 kbps)*/
|
||||
int tmp = 8; //FIXME configurable
|
||||
speex_encoder_ctl(this->encoder, SPEEX_SET_QUALITY, &tmp);
|
||||
|
||||
speex_encoder_ctl(this->encoder, SPEEX_GET_FRAME_SIZE, &this->frame_size);
|
||||
}
|
||||
{
|
||||
this->decoder = speex_decoder_init(speex_mode);
|
||||
if(!this->decoder) {
|
||||
speex_encoder_destroy(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
|
||||
NAN_THROW_EXCEPTION(Error, "Failed to create decoder");
|
||||
return;
|
||||
}
|
||||
|
||||
int tmp = 1; //TODO What is this?
|
||||
speex_decoder_ctl(this->decoder, SPEEX_SET_ENH, &tmp);
|
||||
|
||||
|
||||
int tmp_frame_size;
|
||||
speex_encoder_ctl(this->encoder, SPEEX_GET_FRAME_SIZE, &tmp_frame_size);
|
||||
if(tmp_frame_size != this->frame_size) {
|
||||
NAN_THROW_EXCEPTION(Error, "Decoder and encoder have different frame sizes!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
speex_bits_init(&this->encoder_bits);
|
||||
speex_bits_init(&this->decoder_bits);
|
||||
}
|
||||
|
||||
NAN_METHOD(SpeexCodec::finalize) {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
if(this->encoder) {
|
||||
speex_encoder_destroy(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
|
||||
speex_bits_destroy(&this->encoder_bits);
|
||||
}
|
||||
|
||||
if(this->decoder) {
|
||||
speex_decoder_destroy(this->decoder);
|
||||
this->decoder = nullptr;
|
||||
|
||||
speex_bits_destroy(&this->decoder_bits);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(SpeexCodec::encode) {
|
||||
Nan::HandleScope scope;
|
||||
|
||||
if(!info[0]->IsArrayBuffer()) {
|
||||
NAN_THROW_EXCEPTION(Error, "First argument isn't an array buffer!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto js_buffer = info[0].As<ArrayBuffer>()->GetContents();
|
||||
auto buffer = make_unique<Chunk>(max(js_buffer.ByteLength(), 256UL));
|
||||
buffer->length = js_buffer.ByteLength();
|
||||
memcpy(buffer->memory, js_buffer.Data(), js_buffer.ByteLength());
|
||||
|
||||
auto callback_success = make_unique<Nan::Callback>(info[1].As<Function>());
|
||||
auto callback_error = make_unique<Nan::Callback>(info[2].As<Function>());
|
||||
|
||||
auto codec = make_unique<v8::Persistent<Object>>(info.GetIsolate(), info.Holder());
|
||||
auto callback = Nan::async_callback([callback_success = move(callback_success), callback_error = move(callback_error), codec = move(codec)](std::unique_ptr<Chunk> result, std::string error) {
|
||||
Nan::HandleScope scope;
|
||||
if(result) {
|
||||
auto _buffer = v8::ArrayBuffer::New(v8::Isolate::GetCurrent(), result->length);
|
||||
memcpy(_buffer->GetContents().Data(), result->memory, result->length);
|
||||
|
||||
Local<Value> argv[] = { _buffer }; //_buffer
|
||||
callback_success->Call(1, argv);
|
||||
} else {
|
||||
Local<Value> argv[] = { Nan::New<String>(error).ToLocalChecked() }; //error
|
||||
callback_error->Call(1, argv);
|
||||
}
|
||||
codec->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
tc::codec_workers->enqueue_task(
|
||||
[this, callback, buffer = move(buffer)]() mutable {
|
||||
{
|
||||
lock_guard lock(this->coder_lock);
|
||||
if(!this->encoder) {
|
||||
callback(nullptr, "Please initialize first!");
|
||||
return;
|
||||
}
|
||||
|
||||
if(buffer->length < this->frame_size * sizeof(float)) {
|
||||
callback(nullptr, "Input buffer to short! Received " + to_string(buffer->length) + ", Expected " + to_string(this->frame_size * sizeof(float)));
|
||||
return;
|
||||
}
|
||||
|
||||
for(size_t frame = 0; frame < this->frame_size; frame++)
|
||||
((float*) buffer->memory)[frame] *= 8000; //We want a range between 0 and 8000
|
||||
|
||||
speex_bits_reset(&this->encoder_bits);
|
||||
speex_encode(this->encoder, (float*) buffer->memory, &this->encoder_bits);
|
||||
auto nbytes = speex_bits_write(&this->encoder_bits, buffer->memory, buffer->allocated_length);
|
||||
|
||||
if(nbytes < 0) {
|
||||
callback(nullptr, "Invalid write?");
|
||||
return;
|
||||
}
|
||||
buffer->length = nbytes;
|
||||
}
|
||||
callback(move(buffer), "");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
NAN_METHOD(SpeexCodec::decode) {
|
||||
Nan::HandleScope scope;
|
||||
|
||||
if(!info[0]->IsArrayBuffer()) {
|
||||
NAN_THROW_EXCEPTION(Error, "First argument isn't an array buffer!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto js_buffer = info[0].As<ArrayBuffer>()->GetContents();
|
||||
auto buffer = make_unique<Chunk>(max(js_buffer.ByteLength(), this->frame_size * sizeof(float)));
|
||||
buffer->length = js_buffer.ByteLength();
|
||||
memcpy(buffer->memory, js_buffer.Data(), js_buffer.ByteLength());
|
||||
|
||||
auto callback_success = make_unique<Nan::Callback>(info[1].As<Function>());
|
||||
auto callback_error = make_unique<Nan::Callback>(info[2].As<Function>());
|
||||
|
||||
auto codec = make_unique<v8::Persistent<Object>>(info.GetIsolate(), info.Holder());
|
||||
auto callback = Nan::async_callback([callback_success = move(callback_success), callback_error = move(callback_error), codec = move(codec)](std::unique_ptr<Chunk> result, std::string error) {
|
||||
Nan::HandleScope scope;
|
||||
if(result) {
|
||||
auto _buffer = v8::ArrayBuffer::New(v8::Isolate::GetCurrent(), result->length);
|
||||
memcpy(_buffer->GetContents().Data(), result->memory, result->length);
|
||||
Local<Value> argv[] = { _buffer };
|
||||
callback_success->Call(1, argv);
|
||||
} else {
|
||||
Local<Value> argv[] = { Nan::New<String>(error).ToLocalChecked() };
|
||||
callback_error->Call(1, argv);
|
||||
}
|
||||
codec->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
tc::codec_workers->enqueue_task(
|
||||
[this, callback, buffer = move(buffer)]() mutable {
|
||||
int result;
|
||||
|
||||
{
|
||||
lock_guard lock(this->coder_lock);
|
||||
if(!this->decoder) {
|
||||
callback(nullptr, "Please initialize first!");
|
||||
return;
|
||||
}
|
||||
|
||||
speex_bits_reset(&this->decoder_bits);
|
||||
speex_bits_read_from(&this->decoder_bits, buffer->memory, buffer->length);
|
||||
auto state = speex_decode(this->decoder, &this->decoder_bits, (float*) buffer->memory);
|
||||
if(state != 0) {
|
||||
callback(nullptr, "decode failed (" + to_string(state) + ")");
|
||||
return;
|
||||
}
|
||||
buffer->length = this->frame_size * sizeof(float);
|
||||
for(size_t frame = 0; frame < this->frame_size; frame++)
|
||||
((float*) buffer->memory)[frame] /= 8000; //We want a range between 0 and 1
|
||||
}
|
||||
|
||||
callback(move(buffer), "");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#endif
|
36
native/codec/codec/SpeexCodec.h
Normal file
36
native/codec/codec/SpeexCodec.h
Normal file
@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "NativeCodec.h"
|
||||
|
||||
#ifdef HAVE_SPEEX
|
||||
#include <speex/speex.h>
|
||||
#endif
|
||||
|
||||
namespace tc {
|
||||
class NativeCodec;
|
||||
|
||||
|
||||
class SpeexCodec : public NativeCodec {
|
||||
public:
|
||||
static bool supported();
|
||||
|
||||
#ifdef HAVE_SPEEX
|
||||
explicit SpeexCodec(CodecType::value type);
|
||||
virtual ~SpeexCodec();
|
||||
|
||||
virtual NAN_METHOD(initialize);
|
||||
virtual NAN_METHOD(finalize);
|
||||
virtual NAN_METHOD(encode);
|
||||
virtual NAN_METHOD(decode);
|
||||
private:
|
||||
int frame_size = 0;
|
||||
|
||||
//TODO are two bits really necessary?
|
||||
std::mutex coder_lock;
|
||||
SpeexBits encoder_bits;
|
||||
void* encoder = nullptr;
|
||||
SpeexBits decoder_bits;
|
||||
void* decoder = nullptr;
|
||||
#endif
|
||||
};
|
||||
}
|
2
native/codec/libraries/.gitignore
vendored
Normal file
2
native/codec/libraries/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
generated/
|
||||
opus/build
|
94
native/codec/libraries/build_celt.sh
Normal file
94
native/codec/libraries/build_celt.sh
Normal file
@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd $(dirname "$0")
|
||||
install_directory="$(pwd)/generated/celt/"
|
||||
|
||||
machine="$(uname -s)"
|
||||
case "${machine}" in
|
||||
Linux*) machine=Linux;;
|
||||
# Darwin*) machine=Mac;;
|
||||
MINGW*) machine=MinGW;;
|
||||
*) machine="UNKNOWN:${machine}"
|
||||
esac
|
||||
|
||||
if [[ ${machine} == "UNKNOWN"* ]]; then
|
||||
echo "Unknown platform ${machine}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd celt
|
||||
|
||||
if [[ ${machine} == "Linux" ]]; then
|
||||
if [[ ! -e configure ]]; then
|
||||
echo "Generating configure file"
|
||||
./autogen.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate configure file"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
windows_build="win32/VS2015/"
|
||||
windows_build_type="x64"
|
||||
if [[ ( ! -d build ) && ( ! -d "${windows_build}/${windows_build_type}" ) ]] || [[ "$1" == "rebuild" ]]; then
|
||||
if [[ ${machine} == "Linux" ]]; then
|
||||
if [[ -e build ]]; then
|
||||
rm -r build
|
||||
fi
|
||||
mkdir build && cd build
|
||||
|
||||
export CFLAGS="-fPIC"
|
||||
../configure --prefix="${install_directory}" --with-pic
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to configure project!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ..
|
||||
elif [[ ${machine} == "MinGW" ]]; then
|
||||
#Only cleanup last shit
|
||||
if [[ -e "${windows_build}/${windows_build_type}" ]]; then
|
||||
rm -r "${windows_build}/${windows_build_type}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e "${install_directory}" ]]; then
|
||||
echo "deleting old install directory!"
|
||||
rm -r "${install_directory}"
|
||||
echo "rm -r '${install_directory}'"
|
||||
fi
|
||||
|
||||
if [[ ${machine} == "MinGW" ]]; then
|
||||
saved_pwd=$(pwd)
|
||||
cd "${windows_build}"
|
||||
MSBuild.exe -p:Platform=x64 -property:Configuration=Release opus.vcxproj
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build celt!"
|
||||
exit 1
|
||||
fi
|
||||
cd ${saved_pwd}
|
||||
|
||||
mkdir -p "${install_directory}/include/celt"
|
||||
mkdir -p "${install_directory}/lib/"
|
||||
|
||||
cp -r include/* "${install_directory}/include/celt/"
|
||||
cp -r ${windows_build}/${windows_build_type}/Release/*.lib "${install_directory}/lib/"
|
||||
elif [[ ${machine} == "Linux" ]]; then
|
||||
cd build
|
||||
|
||||
make -j 12
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build celt!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
make install
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to install celt!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Celt build successfully"
|
98
native/codec/libraries/build_opus.sh
Normal file
98
native/codec/libraries/build_opus.sh
Normal file
@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd $(dirname "$0")
|
||||
install_directory="$(pwd)/generated/opus/"
|
||||
|
||||
machine="$(uname -s)"
|
||||
case "${machine}" in
|
||||
Linux*) machine=Linux;;
|
||||
# Darwin*) machine=Mac;;
|
||||
MINGW*) machine=MinGW;;
|
||||
*) machine="UNKNOWN:${machine}"
|
||||
esac
|
||||
|
||||
if [[ ${machine} == "UNKNOWN"* ]]; then
|
||||
echo "Unknown platform ${machine}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd opus
|
||||
#if [ ! -e CMakeLists.txt ]; then
|
||||
# echo "Linking CMakeLists"
|
||||
# ln -s ../cmake/opus/CMakeLists.txt .
|
||||
#fi
|
||||
|
||||
if [[ ${machine} == "Linux" ]]; then
|
||||
if [[ ! -e configure ]]; then
|
||||
echo "Generating configure file"
|
||||
./autogen.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate configure file"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
windows_build="win32/VS2015/"
|
||||
windows_build_type="x64"
|
||||
if [[ ( ! -d build ) && ( ! -d "${windows_build}/${windows_build_type}" ) ]] || [[ "$1" == "rebuild" ]]; then
|
||||
if [[ ${machine} == "Linux" ]]; then
|
||||
if [[ -e build ]]; then
|
||||
rm -r build
|
||||
fi
|
||||
mkdir build && cd build
|
||||
|
||||
export CFLAGS="-fPIC"
|
||||
../configure --prefix="${install_directory}" --with-pic
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to configure project!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ..
|
||||
elif [[ ${machine} == "MinGW" ]]; then
|
||||
#Only cleanup last shit
|
||||
if [[ -e "${windows_build}/${windows_build_type}" ]]; then
|
||||
rm -r "${windows_build}/${windows_build_type}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e "${install_directory}" ]]; then
|
||||
echo "deleting old install directory!"
|
||||
rm -r "${install_directory}"
|
||||
echo "rm -r '${install_directory}'"
|
||||
fi
|
||||
|
||||
if [[ ${machine} == "MinGW" ]]; then
|
||||
saved_pwd=$(pwd)
|
||||
cd "${windows_build}"
|
||||
MSBuild.exe -p:Platform=x64 -property:Configuration=Release opus.vcxproj
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build opus!"
|
||||
exit 1
|
||||
fi
|
||||
cd ${saved_pwd}
|
||||
|
||||
mkdir -p "${install_directory}/include/opus"
|
||||
mkdir -p "${install_directory}/lib/"
|
||||
|
||||
cp -r include/* "${install_directory}/include/opus/"
|
||||
cp -r ${windows_build}/${windows_build_type}/Release/*.lib "${install_directory}/lib/"
|
||||
elif [[ ${machine} == "Linux" ]]; then
|
||||
cd build
|
||||
|
||||
make -j 12
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build opus!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
make install
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to install opus!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Opus build successfully"
|
97
native/codec/libraries/build_speex.sh
Normal file
97
native/codec/libraries/build_speex.sh
Normal file
@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd $(dirname "$0")
|
||||
install_directory="$(pwd)/generated/speex/"
|
||||
|
||||
machine="$(uname -s)"
|
||||
case "${machine}" in
|
||||
Linux*) machine=Linux;;
|
||||
# Darwin*) machine=Mac;;
|
||||
MINGW*) machine=MinGW;;
|
||||
*) machine="UNKNOWN:${machine}"
|
||||
esac
|
||||
|
||||
if [[ ${machine} == "UNKNOWN"* ]]; then
|
||||
echo "Unknown platform ${machine}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd speex
|
||||
if [[ ${machine} == "Linux" ]]; then
|
||||
if [[ ! -e configure ]]; then
|
||||
echo "Generating configure file"
|
||||
./autogen.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate configure file"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
windows_build="win32/VS2015/"
|
||||
windows_build_type="x64"
|
||||
if [[ ( ! -d build ) && ( ! -d "${windows_build}/${windows_build_type}" ) ]] || [[ "$1" == "rebuild" ]]; then
|
||||
if [[ ${machine} == "Linux" ]]; then
|
||||
if [[ -e build ]]; then
|
||||
rm -r build
|
||||
fi
|
||||
mkdir build && cd build
|
||||
|
||||
export CFLAGS="-fPIC"
|
||||
../configure --prefix="${install_directory}" --with-pic
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to configure project!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ..
|
||||
elif [[ ${machine} == "MinGW" ]]; then
|
||||
#Only cleanup last shit
|
||||
if [[ -e "${windows_build}" ]]; then
|
||||
rm -r "${windows_build}"
|
||||
fi
|
||||
|
||||
mkdir -p ${windows_build}
|
||||
cp -r ../template/speex_VS2015/* ${windows_build}/
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e "${install_directory}" ]]; then
|
||||
echo "deleting old install directory!"
|
||||
rm -r "${install_directory}"
|
||||
echo "rm -r '${install_directory}'"
|
||||
fi
|
||||
|
||||
if [[ ${machine} == "MinGW" ]]; then
|
||||
saved_pwd=$(pwd)
|
||||
cd "${windows_build}"
|
||||
MSBuild.exe -p:Platform=x64 -property:Configuration=Release libspeex/libspeex.vcxproj
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build speex!"
|
||||
exit 1
|
||||
fi
|
||||
cd ${saved_pwd}
|
||||
|
||||
mkdir -p "${install_directory}/include/speex"
|
||||
mkdir -p "${install_directory}/lib/"
|
||||
|
||||
cp -r include/speex/*.h "${install_directory}/include/speex/"
|
||||
cp -r ${windows_build}/libspeex/${windows_build_type}/Release/*.lib "${install_directory}/lib/"
|
||||
elif [[ ${machine} == "Linux" ]]; then
|
||||
cd build
|
||||
|
||||
make -j 12
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build speex!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
make install
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to install speex!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Speex build successfully"
|
33
native/codec/libraries/celt/.gitignore
vendored
Normal file
33
native/codec/libraries/celt/.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
Makefile
|
||||
Makefile.in
|
||||
aclocal.m4
|
||||
autom4te.cache
|
||||
*.kdevelop.pcs
|
||||
*.kdevses
|
||||
config.guess
|
||||
config.h
|
||||
config.h.in
|
||||
config.log
|
||||
config.status
|
||||
config.sub
|
||||
configure
|
||||
depcomp
|
||||
install-sh
|
||||
.deps
|
||||
.libs
|
||||
*.la
|
||||
testcelt
|
||||
libtool
|
||||
ltmain.sh
|
||||
missing
|
||||
stamp-h1
|
||||
*.sw
|
||||
*.o
|
||||
*.lo
|
||||
*~
|
||||
tests/*test
|
||||
tools/celtdec
|
||||
tools/celtenc
|
||||
celt.pc
|
||||
libcelt.spec
|
||||
libcelt/dump_modes
|
0
native/codec/libraries/celt/AUTHORS
Normal file
0
native/codec/libraries/celt/AUTHORS
Normal file
25
native/codec/libraries/celt/COPYING
Normal file
25
native/codec/libraries/celt/COPYING
Normal file
@ -0,0 +1,25 @@
|
||||
Copyright 2001-2009 Jean-Marc Valin, Timothy B. Terriberry,
|
||||
CSIRO, and other contributors
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
0
native/codec/libraries/celt/ChangeLog
Normal file
0
native/codec/libraries/celt/ChangeLog
Normal file
283
native/codec/libraries/celt/Doxyfile
Normal file
283
native/codec/libraries/celt/Doxyfile
Normal file
@ -0,0 +1,283 @@
|
||||
# Doxyfile 1.5.3
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Project related configuration options
|
||||
#---------------------------------------------------------------------------
|
||||
DOXYFILE_ENCODING = UTF-8
|
||||
PROJECT_NAME = CELT
|
||||
PROJECT_NUMBER = 0.11.4
|
||||
OUTPUT_DIRECTORY = doc/API
|
||||
CREATE_SUBDIRS = NO
|
||||
OUTPUT_LANGUAGE = English
|
||||
BRIEF_MEMBER_DESC = YES
|
||||
REPEAT_BRIEF = YES
|
||||
ABBREVIATE_BRIEF = "The $name class " \
|
||||
"The $name widget " \
|
||||
"The $name file " \
|
||||
is \
|
||||
provides \
|
||||
specifies \
|
||||
contains \
|
||||
represents \
|
||||
a \
|
||||
an \
|
||||
the
|
||||
ALWAYS_DETAILED_SEC = NO
|
||||
INLINE_INHERITED_MEMB = NO
|
||||
FULL_PATH_NAMES = YES
|
||||
STRIP_FROM_PATH =
|
||||
STRIP_FROM_INC_PATH =
|
||||
SHORT_NAMES = NO
|
||||
JAVADOC_AUTOBRIEF = NO
|
||||
QT_AUTOBRIEF = NO
|
||||
MULTILINE_CPP_IS_BRIEF = NO
|
||||
DETAILS_AT_TOP = NO
|
||||
INHERIT_DOCS = YES
|
||||
SEPARATE_MEMBER_PAGES = NO
|
||||
TAB_SIZE = 8
|
||||
ALIASES =
|
||||
OPTIMIZE_OUTPUT_FOR_C = YES
|
||||
OPTIMIZE_OUTPUT_JAVA = NO
|
||||
BUILTIN_STL_SUPPORT = NO
|
||||
CPP_CLI_SUPPORT = NO
|
||||
DISTRIBUTE_GROUP_DOC = NO
|
||||
SUBGROUPING = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# Build related configuration options
|
||||
#---------------------------------------------------------------------------
|
||||
EXTRACT_ALL = NO
|
||||
EXTRACT_PRIVATE = NO
|
||||
EXTRACT_STATIC = NO
|
||||
EXTRACT_LOCAL_CLASSES = YES
|
||||
EXTRACT_LOCAL_METHODS = NO
|
||||
EXTRACT_ANON_NSPACES = NO
|
||||
HIDE_UNDOC_MEMBERS = YES
|
||||
HIDE_UNDOC_CLASSES = YES
|
||||
HIDE_FRIEND_COMPOUNDS = NO
|
||||
HIDE_IN_BODY_DOCS = NO
|
||||
INTERNAL_DOCS = NO
|
||||
CASE_SENSE_NAMES = YES
|
||||
HIDE_SCOPE_NAMES = NO
|
||||
SHOW_INCLUDE_FILES = YES
|
||||
INLINE_INFO = YES
|
||||
SORT_MEMBER_DOCS = YES
|
||||
SORT_BRIEF_DOCS = NO
|
||||
SORT_BY_SCOPE_NAME = NO
|
||||
GENERATE_TODOLIST = YES
|
||||
GENERATE_TESTLIST = YES
|
||||
GENERATE_BUGLIST = YES
|
||||
GENERATE_DEPRECATEDLIST= YES
|
||||
ENABLED_SECTIONS =
|
||||
MAX_INITIALIZER_LINES = 30
|
||||
SHOW_USED_FILES = YES
|
||||
SHOW_DIRECTORIES = NO
|
||||
FILE_VERSION_FILTER =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to warning and progress messages
|
||||
#---------------------------------------------------------------------------
|
||||
QUIET = NO
|
||||
WARNINGS = YES
|
||||
WARN_IF_UNDOCUMENTED = YES
|
||||
WARN_IF_DOC_ERROR = YES
|
||||
WARN_NO_PARAMDOC = NO
|
||||
WARN_FORMAT = "$file:$line: $text "
|
||||
WARN_LOGFILE =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the input files
|
||||
#---------------------------------------------------------------------------
|
||||
INPUT = libcelt/celt.h \
|
||||
libcelt/celt_types.h \
|
||||
libcelt/celt_header.h
|
||||
INPUT_ENCODING = UTF-8
|
||||
FILE_PATTERNS = *.c \
|
||||
*.cc \
|
||||
*.cxx \
|
||||
*.cpp \
|
||||
*.c++ \
|
||||
*.d \
|
||||
*.java \
|
||||
*.ii \
|
||||
*.ixx \
|
||||
*.ipp \
|
||||
*.i++ \
|
||||
*.inl \
|
||||
*.h \
|
||||
*.hh \
|
||||
*.hxx \
|
||||
*.hpp \
|
||||
*.h++ \
|
||||
*.idl \
|
||||
*.odl \
|
||||
*.cs \
|
||||
*.php \
|
||||
*.php3 \
|
||||
*.inc \
|
||||
*.m \
|
||||
*.mm \
|
||||
*.dox \
|
||||
*.py \
|
||||
*.C \
|
||||
*.CC \
|
||||
*.C++ \
|
||||
*.II \
|
||||
*.I++ \
|
||||
*.H \
|
||||
*.HH \
|
||||
*.H++ \
|
||||
*.CS \
|
||||
*.PHP \
|
||||
*.PHP3 \
|
||||
*.M \
|
||||
*.MM \
|
||||
*.PY
|
||||
RECURSIVE = NO
|
||||
EXCLUDE =
|
||||
EXCLUDE_SYMLINKS = NO
|
||||
EXCLUDE_PATTERNS = *.c
|
||||
EXCLUDE_SYMBOLS =
|
||||
EXAMPLE_PATH =
|
||||
EXAMPLE_PATTERNS = *
|
||||
EXAMPLE_RECURSIVE = NO
|
||||
IMAGE_PATH =
|
||||
INPUT_FILTER =
|
||||
FILTER_PATTERNS =
|
||||
FILTER_SOURCE_FILES = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to source browsing
|
||||
#---------------------------------------------------------------------------
|
||||
SOURCE_BROWSER = YES
|
||||
INLINE_SOURCES = NO
|
||||
STRIP_CODE_COMMENTS = YES
|
||||
REFERENCED_BY_RELATION = YES
|
||||
REFERENCES_RELATION = YES
|
||||
REFERENCES_LINK_SOURCE = YES
|
||||
USE_HTAGS = NO
|
||||
VERBATIM_HEADERS = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the alphabetical class index
|
||||
#---------------------------------------------------------------------------
|
||||
ALPHABETICAL_INDEX = NO
|
||||
COLS_IN_ALPHA_INDEX = 5
|
||||
IGNORE_PREFIX =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the HTML output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_HTML = YES
|
||||
HTML_OUTPUT = html
|
||||
HTML_FILE_EXTENSION = .html
|
||||
HTML_HEADER =
|
||||
HTML_FOOTER =
|
||||
HTML_STYLESHEET =
|
||||
HTML_ALIGN_MEMBERS = YES
|
||||
GENERATE_HTMLHELP = NO
|
||||
HTML_DYNAMIC_SECTIONS = NO
|
||||
CHM_FILE =
|
||||
HHC_LOCATION =
|
||||
GENERATE_CHI = NO
|
||||
BINARY_TOC = NO
|
||||
TOC_EXPAND = NO
|
||||
DISABLE_INDEX = NO
|
||||
ENUM_VALUES_PER_LINE = 4
|
||||
GENERATE_TREEVIEW = NO
|
||||
TREEVIEW_WIDTH = 250
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the LaTeX output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_LATEX = YES
|
||||
LATEX_OUTPUT = latex
|
||||
LATEX_CMD_NAME = latex
|
||||
MAKEINDEX_CMD_NAME = makeindex
|
||||
COMPACT_LATEX = NO
|
||||
PAPER_TYPE = a4wide
|
||||
EXTRA_PACKAGES =
|
||||
LATEX_HEADER =
|
||||
PDF_HYPERLINKS = YES
|
||||
USE_PDFLATEX = YES
|
||||
LATEX_BATCHMODE = NO
|
||||
LATEX_HIDE_INDICES = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the RTF output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_RTF = NO
|
||||
RTF_OUTPUT = rtf
|
||||
COMPACT_RTF = NO
|
||||
RTF_HYPERLINKS = NO
|
||||
RTF_STYLESHEET_FILE =
|
||||
RTF_EXTENSIONS_FILE =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the man page output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_MAN = YES
|
||||
MAN_OUTPUT = man
|
||||
MAN_EXTENSION = .3
|
||||
MAN_LINKS = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the XML output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_XML = NO
|
||||
XML_OUTPUT = xml
|
||||
XML_SCHEMA =
|
||||
XML_DTD =
|
||||
XML_PROGRAMLISTING = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options for the AutoGen Definitions output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_AUTOGEN_DEF = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the Perl module output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_PERLMOD = NO
|
||||
PERLMOD_LATEX = NO
|
||||
PERLMOD_PRETTY = YES
|
||||
PERLMOD_MAKEVAR_PREFIX =
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration options related to the preprocessor
|
||||
#---------------------------------------------------------------------------
|
||||
ENABLE_PREPROCESSING = YES
|
||||
MACRO_EXPANSION = NO
|
||||
EXPAND_ONLY_PREDEF = NO
|
||||
SEARCH_INCLUDES = YES
|
||||
INCLUDE_PATH =
|
||||
INCLUDE_FILE_PATTERNS =
|
||||
PREDEFINED =
|
||||
EXPAND_AS_DEFINED =
|
||||
SKIP_FUNCTION_MACROS = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration::additions related to external references
|
||||
#---------------------------------------------------------------------------
|
||||
TAGFILES =
|
||||
GENERATE_TAGFILE =
|
||||
ALLEXTERNALS = NO
|
||||
EXTERNAL_GROUPS = YES
|
||||
PERL_PATH = /usr/bin/perl
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration options related to the dot tool
|
||||
#---------------------------------------------------------------------------
|
||||
CLASS_DIAGRAMS = NO
|
||||
MSCGEN_PATH =
|
||||
HIDE_UNDOC_RELATIONS = YES
|
||||
HAVE_DOT = YES
|
||||
CLASS_GRAPH = YES
|
||||
COLLABORATION_GRAPH = YES
|
||||
GROUP_GRAPHS = YES
|
||||
UML_LOOK = NO
|
||||
TEMPLATE_RELATIONS = NO
|
||||
INCLUDE_GRAPH = NO
|
||||
INCLUDED_BY_GRAPH = NO
|
||||
CALL_GRAPH = NO
|
||||
CALLER_GRAPH = NO
|
||||
GRAPHICAL_HIERARCHY = YES
|
||||
DIRECTORY_GRAPH = YES
|
||||
DOT_IMAGE_FORMAT = png
|
||||
DOT_PATH =
|
||||
DOTFILE_DIRS =
|
||||
DOT_GRAPH_MAX_NODES = 50
|
||||
MAX_DOT_GRAPH_DEPTH = 1000
|
||||
DOT_TRANSPARENT = NO
|
||||
DOT_MULTI_TARGETS = NO
|
||||
GENERATE_LEGEND = YES
|
||||
DOT_CLEANUP = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration::additions related to the search engine
|
||||
#---------------------------------------------------------------------------
|
||||
SEARCHENGINE = NO
|
281
native/codec/libraries/celt/Doxyfile.devel
Normal file
281
native/codec/libraries/celt/Doxyfile.devel
Normal file
@ -0,0 +1,281 @@
|
||||
# Doxyfile 1.5.3
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Project related configuration options
|
||||
#---------------------------------------------------------------------------
|
||||
DOXYFILE_ENCODING = UTF-8
|
||||
PROJECT_NAME = CELT
|
||||
PROJECT_NUMBER = 0.11.4
|
||||
OUTPUT_DIRECTORY = doc/devel
|
||||
CREATE_SUBDIRS = NO
|
||||
OUTPUT_LANGUAGE = English
|
||||
BRIEF_MEMBER_DESC = YES
|
||||
REPEAT_BRIEF = YES
|
||||
ABBREVIATE_BRIEF = "The $name class " \
|
||||
"The $name widget " \
|
||||
"The $name file " \
|
||||
is \
|
||||
provides \
|
||||
specifies \
|
||||
contains \
|
||||
represents \
|
||||
a \
|
||||
an \
|
||||
the
|
||||
ALWAYS_DETAILED_SEC = NO
|
||||
INLINE_INHERITED_MEMB = NO
|
||||
FULL_PATH_NAMES = YES
|
||||
STRIP_FROM_PATH =
|
||||
STRIP_FROM_INC_PATH =
|
||||
SHORT_NAMES = NO
|
||||
JAVADOC_AUTOBRIEF = NO
|
||||
QT_AUTOBRIEF = NO
|
||||
MULTILINE_CPP_IS_BRIEF = NO
|
||||
DETAILS_AT_TOP = NO
|
||||
INHERIT_DOCS = YES
|
||||
SEPARATE_MEMBER_PAGES = NO
|
||||
TAB_SIZE = 8
|
||||
ALIASES =
|
||||
OPTIMIZE_OUTPUT_FOR_C = YES
|
||||
OPTIMIZE_OUTPUT_JAVA = NO
|
||||
BUILTIN_STL_SUPPORT = NO
|
||||
CPP_CLI_SUPPORT = NO
|
||||
DISTRIBUTE_GROUP_DOC = NO
|
||||
SUBGROUPING = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# Build related configuration options
|
||||
#---------------------------------------------------------------------------
|
||||
EXTRACT_ALL = YES
|
||||
EXTRACT_PRIVATE = NO
|
||||
EXTRACT_STATIC = NO
|
||||
EXTRACT_LOCAL_CLASSES = YES
|
||||
EXTRACT_LOCAL_METHODS = NO
|
||||
EXTRACT_ANON_NSPACES = NO
|
||||
HIDE_UNDOC_MEMBERS = YES
|
||||
HIDE_UNDOC_CLASSES = YES
|
||||
HIDE_FRIEND_COMPOUNDS = NO
|
||||
HIDE_IN_BODY_DOCS = NO
|
||||
INTERNAL_DOCS = NO
|
||||
CASE_SENSE_NAMES = YES
|
||||
HIDE_SCOPE_NAMES = NO
|
||||
SHOW_INCLUDE_FILES = YES
|
||||
INLINE_INFO = YES
|
||||
SORT_MEMBER_DOCS = YES
|
||||
SORT_BRIEF_DOCS = NO
|
||||
SORT_BY_SCOPE_NAME = NO
|
||||
GENERATE_TODOLIST = YES
|
||||
GENERATE_TESTLIST = YES
|
||||
GENERATE_BUGLIST = YES
|
||||
GENERATE_DEPRECATEDLIST= YES
|
||||
ENABLED_SECTIONS =
|
||||
MAX_INITIALIZER_LINES = 30
|
||||
SHOW_USED_FILES = YES
|
||||
SHOW_DIRECTORIES = NO
|
||||
FILE_VERSION_FILTER =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to warning and progress messages
|
||||
#---------------------------------------------------------------------------
|
||||
QUIET = NO
|
||||
WARNINGS = YES
|
||||
WARN_IF_UNDOCUMENTED = YES
|
||||
WARN_IF_DOC_ERROR = YES
|
||||
WARN_NO_PARAMDOC = NO
|
||||
WARN_FORMAT = "$file:$line: $text "
|
||||
WARN_LOGFILE =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the input files
|
||||
#---------------------------------------------------------------------------
|
||||
INPUT = libcelt
|
||||
INPUT_ENCODING = UTF-8
|
||||
FILE_PATTERNS = *.c \
|
||||
*.cc \
|
||||
*.cxx \
|
||||
*.cpp \
|
||||
*.c++ \
|
||||
*.d \
|
||||
*.java \
|
||||
*.ii \
|
||||
*.ixx \
|
||||
*.ipp \
|
||||
*.i++ \
|
||||
*.inl \
|
||||
*.h \
|
||||
*.hh \
|
||||
*.hxx \
|
||||
*.hpp \
|
||||
*.h++ \
|
||||
*.idl \
|
||||
*.odl \
|
||||
*.cs \
|
||||
*.php \
|
||||
*.php3 \
|
||||
*.inc \
|
||||
*.m \
|
||||
*.mm \
|
||||
*.dox \
|
||||
*.py \
|
||||
*.C \
|
||||
*.CC \
|
||||
*.C++ \
|
||||
*.II \
|
||||
*.I++ \
|
||||
*.H \
|
||||
*.HH \
|
||||
*.H++ \
|
||||
*.CS \
|
||||
*.PHP \
|
||||
*.PHP3 \
|
||||
*.M \
|
||||
*.MM \
|
||||
*.PY
|
||||
RECURSIVE = NO
|
||||
EXCLUDE =
|
||||
EXCLUDE_SYMLINKS = NO
|
||||
EXCLUDE_PATTERNS =
|
||||
EXCLUDE_SYMBOLS =
|
||||
EXAMPLE_PATH =
|
||||
EXAMPLE_PATTERNS = *
|
||||
EXAMPLE_RECURSIVE = NO
|
||||
IMAGE_PATH =
|
||||
INPUT_FILTER =
|
||||
FILTER_PATTERNS =
|
||||
FILTER_SOURCE_FILES = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to source browsing
|
||||
#---------------------------------------------------------------------------
|
||||
SOURCE_BROWSER = YES
|
||||
INLINE_SOURCES = NO
|
||||
STRIP_CODE_COMMENTS = YES
|
||||
REFERENCED_BY_RELATION = YES
|
||||
REFERENCES_RELATION = YES
|
||||
REFERENCES_LINK_SOURCE = YES
|
||||
USE_HTAGS = NO
|
||||
VERBATIM_HEADERS = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the alphabetical class index
|
||||
#---------------------------------------------------------------------------
|
||||
ALPHABETICAL_INDEX = NO
|
||||
COLS_IN_ALPHA_INDEX = 5
|
||||
IGNORE_PREFIX =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the HTML output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_HTML = YES
|
||||
HTML_OUTPUT = html
|
||||
HTML_FILE_EXTENSION = .html
|
||||
HTML_HEADER =
|
||||
HTML_FOOTER =
|
||||
HTML_STYLESHEET =
|
||||
HTML_ALIGN_MEMBERS = YES
|
||||
GENERATE_HTMLHELP = NO
|
||||
HTML_DYNAMIC_SECTIONS = NO
|
||||
CHM_FILE =
|
||||
HHC_LOCATION =
|
||||
GENERATE_CHI = NO
|
||||
BINARY_TOC = NO
|
||||
TOC_EXPAND = NO
|
||||
DISABLE_INDEX = NO
|
||||
ENUM_VALUES_PER_LINE = 4
|
||||
GENERATE_TREEVIEW = NO
|
||||
TREEVIEW_WIDTH = 250
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the LaTeX output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_LATEX = YES
|
||||
LATEX_OUTPUT = latex
|
||||
LATEX_CMD_NAME = latex
|
||||
MAKEINDEX_CMD_NAME = makeindex
|
||||
COMPACT_LATEX = NO
|
||||
PAPER_TYPE = a4wide
|
||||
EXTRA_PACKAGES =
|
||||
LATEX_HEADER =
|
||||
PDF_HYPERLINKS = YES
|
||||
USE_PDFLATEX = YES
|
||||
LATEX_BATCHMODE = NO
|
||||
LATEX_HIDE_INDICES = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the RTF output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_RTF = NO
|
||||
RTF_OUTPUT = rtf
|
||||
COMPACT_RTF = NO
|
||||
RTF_HYPERLINKS = NO
|
||||
RTF_STYLESHEET_FILE =
|
||||
RTF_EXTENSIONS_FILE =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the man page output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_MAN = YES
|
||||
MAN_OUTPUT = man
|
||||
MAN_EXTENSION = .3
|
||||
MAN_LINKS = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the XML output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_XML = NO
|
||||
XML_OUTPUT = xml
|
||||
XML_SCHEMA =
|
||||
XML_DTD =
|
||||
XML_PROGRAMLISTING = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options for the AutoGen Definitions output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_AUTOGEN_DEF = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the Perl module output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_PERLMOD = NO
|
||||
PERLMOD_LATEX = NO
|
||||
PERLMOD_PRETTY = YES
|
||||
PERLMOD_MAKEVAR_PREFIX =
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration options related to the preprocessor
|
||||
#---------------------------------------------------------------------------
|
||||
ENABLE_PREPROCESSING = YES
|
||||
MACRO_EXPANSION = NO
|
||||
EXPAND_ONLY_PREDEF = NO
|
||||
SEARCH_INCLUDES = YES
|
||||
INCLUDE_PATH =
|
||||
INCLUDE_FILE_PATTERNS =
|
||||
PREDEFINED =
|
||||
EXPAND_AS_DEFINED =
|
||||
SKIP_FUNCTION_MACROS = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration::additions related to external references
|
||||
#---------------------------------------------------------------------------
|
||||
TAGFILES =
|
||||
GENERATE_TAGFILE =
|
||||
ALLEXTERNALS = NO
|
||||
EXTERNAL_GROUPS = YES
|
||||
PERL_PATH = /usr/bin/perl
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration options related to the dot tool
|
||||
#---------------------------------------------------------------------------
|
||||
CLASS_DIAGRAMS = NO
|
||||
MSCGEN_PATH =
|
||||
HIDE_UNDOC_RELATIONS = YES
|
||||
HAVE_DOT = YES
|
||||
CLASS_GRAPH = YES
|
||||
COLLABORATION_GRAPH = YES
|
||||
GROUP_GRAPHS = YES
|
||||
UML_LOOK = NO
|
||||
TEMPLATE_RELATIONS = NO
|
||||
INCLUDE_GRAPH = NO
|
||||
INCLUDED_BY_GRAPH = NO
|
||||
CALL_GRAPH = NO
|
||||
CALLER_GRAPH = NO
|
||||
GRAPHICAL_HIERARCHY = YES
|
||||
DIRECTORY_GRAPH = YES
|
||||
DOT_IMAGE_FORMAT = png
|
||||
DOT_PATH =
|
||||
DOTFILE_DIRS =
|
||||
DOT_GRAPH_MAX_NODES = 50
|
||||
MAX_DOT_GRAPH_DEPTH = 1000
|
||||
DOT_TRANSPARENT = NO
|
||||
DOT_MULTI_TARGETS = NO
|
||||
GENERATE_LEGEND = YES
|
||||
DOT_CLEANUP = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration::additions related to the search engine
|
||||
#---------------------------------------------------------------------------
|
||||
SEARCHENGINE = NO
|
5
native/codec/libraries/celt/INSTALL
Normal file
5
native/codec/libraries/celt/INSTALL
Normal file
@ -0,0 +1,5 @@
|
||||
To compile:
|
||||
|
||||
./configure
|
||||
make
|
||||
|
18
native/codec/libraries/celt/Makefile.am
Normal file
18
native/codec/libraries/celt/Makefile.am
Normal file
@ -0,0 +1,18 @@
|
||||
## Process this file with automake to produce Makefile.in. -*-Makefile-*-
|
||||
|
||||
# To disable automatic dependency tracking if using other tools than
|
||||
# gcc and gmake, add the option 'no-dependencies'
|
||||
AUTOMAKE_OPTIONS = 1.6
|
||||
|
||||
#Fools KDevelop into including all files
|
||||
SUBDIRS = libcelt tests @tools@
|
||||
|
||||
DIST_SUBDIRS = libcelt tests tools
|
||||
|
||||
pkgconfigdir = $(libdir)/pkgconfig
|
||||
pkgconfig_DATA = celt.pc
|
||||
|
||||
EXTRA_DIST = celt.pc.in Doxyfile Doxyfile.devel msvc/config.h
|
||||
|
||||
rpm: dist
|
||||
rpmbuild -ta ${PACKAGE}-${VERSION}.tar.gz
|
0
native/codec/libraries/celt/NEWS
Normal file
0
native/codec/libraries/celt/NEWS
Normal file
88
native/codec/libraries/celt/README
Normal file
88
native/codec/libraries/celt/README
Normal file
@ -0,0 +1,88 @@
|
||||
CELT is a very low delay audio codec designed for high-quality communications.
|
||||
|
||||
Traditional full-bandwidth codecs such as Vorbis and AAC can offer high
|
||||
quality but they require codec delays of hundreds of milliseconds, which
|
||||
makes them unsuitable for real-time interactive applications like tele-
|
||||
conferencing. Speech targeted codecs, such as Speex or G.722, have lower
|
||||
20-40ms delays but their speech focus and limited sampling rates
|
||||
restricts their quality, especially for music.
|
||||
|
||||
Additionally, the other mandatory components of a full network audio system—
|
||||
audio interfaces, routers, jitter buffers— each add their own delay. For lower
|
||||
speed networks the time it takes to serialize a packet onto the network cable
|
||||
takes considerable time, and over the long distances the speed of light
|
||||
imposes a significant delay.
|
||||
|
||||
In teleconferencing— it is important to keep delay low so that the participants
|
||||
can communicate fluidly without talking on top of each other and so that their
|
||||
own voices don't return after a round trip as an annoying echo.
|
||||
|
||||
For network music performance— research has show that the total one way delay
|
||||
must be kept under 25ms to avoid degrading the musicians performance.
|
||||
|
||||
Since many of the sources of delay in a complete system are outside of the
|
||||
user's control (such as the speed of light) it is often only possible to
|
||||
reduce the total delay by reducing the codec delay.
|
||||
|
||||
Low delay has traditionally been considered a challenging area in audio codec
|
||||
design, because as a codec is forced to work on the smaller chunks of audio
|
||||
required for low delay it has access to less redundancy and less perceptual
|
||||
information which it can use to reduce the size of the transmitted audio.
|
||||
|
||||
CELT is designed to bridge the gap between "music" and "speech" codecs,
|
||||
permitting new very high quality teleconferencing applications, and to go
|
||||
further, permitting latencies much lower than speech codecs normally provide
|
||||
to enable applications such as remote musical collaboration even over long
|
||||
distances.
|
||||
|
||||
In keeping with the Xiph.Org mission— CELT is also designed to accomplish
|
||||
this without copyright or patent encumbrance. Only by keeping the formats
|
||||
that drive our Internet communication free and unencumbered can we maximize
|
||||
innovation, collaboration, and interoperability. Fortunately, CELT is ahead
|
||||
of the adoption curve in its target application space, so there should be
|
||||
no reason for someone who needs what CELT provides to go with a proprietary
|
||||
codec.
|
||||
|
||||
CELT has been tested on x86, x86_64, ARM, and the TI C55x DSPs, and should
|
||||
be portable to any platform with a working C compiler and on the order of
|
||||
100 MIPS of processing power.
|
||||
|
||||
The code is still in early stage, so it may be broken from time to time, and
|
||||
the bit-stream is not frozen yet, so it is different from one version to
|
||||
another. Oh, and don't complain if it sets your house on fire.
|
||||
|
||||
Complaints and accolades can be directed to the CELT mailing list:
|
||||
http://lists.xiph.org/mailman/listinfo/celt-dev/
|
||||
|
||||
To compile:
|
||||
% ./configure
|
||||
% make
|
||||
|
||||
For platforms without fast floating point support (such as ARM) use the
|
||||
--enable-fixed argument to configure to build a fixed-point version of CELT.
|
||||
|
||||
There are Ogg-based encode/decode tools in tools/. These are quite similar to
|
||||
the speexenc/speexdec tools. Use the --help option for details.
|
||||
|
||||
There is also a basic tool for testing the encoder and decoder called
|
||||
"testcelt" located in libcelt/:
|
||||
|
||||
% testcelt <rate> <channels> <frame size> <bytes per packet> input.sw output.sw
|
||||
|
||||
where input.sw is a 16-bit (machine endian) audio file sampled at 32000 Hz to
|
||||
96000 Hz. The output file is already decompressed.
|
||||
|
||||
For example, for a 44.1 kHz mono stream at ~64kbit/sec and with 256 sample
|
||||
frames:
|
||||
|
||||
% testcelt 44100 1 256 46 intput.sw output.sw
|
||||
|
||||
Since 44100/256*46*8 = 63393.74 bits/sec.
|
||||
|
||||
All even frame sizes from 64 to 512 are currently supported, although
|
||||
power-of-two sizes are recommended and most CELT development is done
|
||||
using a size of 256. The delay imposed by CELT is 1.25x - 1.5x the
|
||||
frame duration depending on the frame size and some details of CELT's
|
||||
internal operation. For 256 sample frames the delay is 1.5x or 384
|
||||
samples, so the total codec delay in the above example is 8.70ms
|
||||
(1000/(44100/384)).
|
10
native/codec/libraries/celt/README.Win32
Normal file
10
native/codec/libraries/celt/README.Win32
Normal file
@ -0,0 +1,10 @@
|
||||
Here are a few tips for building on Windows:
|
||||
|
||||
1) Create a config.h file that defines out things that defines out all the
|
||||
features that your compiler doesn't understand (e.g. inline, restrict).
|
||||
It also needs to define the CELT_BUILD macro
|
||||
|
||||
2) Define the HAVE_CONFIG_H macro in the project build options (NOT in config.h)
|
||||
|
||||
3) If you want things to be a lot easier, just use a compiler that supports
|
||||
C99, such as gcc
|
10
native/codec/libraries/celt/REPOSITORY_HAS_MOVED
Normal file
10
native/codec/libraries/celt/REPOSITORY_HAS_MOVED
Normal file
@ -0,0 +1,10 @@
|
||||
The celt codec design and implementation have been merged into
|
||||
the IETF Codec Working Group's "Opus" codec. As such, this
|
||||
repository is no longer under active development.
|
||||
|
||||
Please see https://git.xiph.org/?p=opus.git
|
||||
and https://git.xiph.org/?p=users/jm/opus-tools.git for more
|
||||
current work. Visit http://opus-codec.org/ for more
|
||||
information.
|
||||
|
||||
We apologize for any inconvenience this has caused.
|
20
native/codec/libraries/celt/TODO
Normal file
20
native/codec/libraries/celt/TODO
Normal file
@ -0,0 +1,20 @@
|
||||
- Check minimum width of bands
|
||||
- Revisit energy resolution based on the bit-rate
|
||||
- Revisit static bit allocation (as a function of frame size and channels)
|
||||
- Dynamic adjustment of energy quantisation
|
||||
- Psychacoustics
|
||||
* Error shaping within each band
|
||||
* Decisions on the rate
|
||||
- Intensity stereo decisions
|
||||
- Dynamic (intra-frame) bit allocation
|
||||
- Joint encoding of stereo energy
|
||||
|
||||
- Encode band shape (or just tilt)?
|
||||
- Make energy encoding more robust to losses?
|
||||
|
||||
|
||||
Misc:
|
||||
Detect uint decoding and flag them in the decoder directly
|
||||
If we attempt to write too many bits on the encoder side, set a flag instead of
|
||||
aborting
|
||||
Save "raw bytes" at the end of the stream
|
111
native/codec/libraries/celt/autogen.sh
Executable file
111
native/codec/libraries/celt/autogen.sh
Executable file
@ -0,0 +1,111 @@
|
||||
#!/bin/sh
|
||||
# Run this to set up the build system: configure, makefiles, etc.
|
||||
# (based on the version in enlightenment's cvs)
|
||||
|
||||
package="celt"
|
||||
|
||||
olddir=`pwd`
|
||||
srcdir=`dirname $0`
|
||||
test -z "$srcdir" && srcdir=.
|
||||
|
||||
cd "$srcdir"
|
||||
DIE=0
|
||||
|
||||
echo "checking for autoconf... "
|
||||
(autoconf --version) < /dev/null > /dev/null 2>&1 || {
|
||||
echo
|
||||
echo "You must have autoconf installed to compile $package."
|
||||
echo "Download the appropriate package for your distribution,"
|
||||
echo "or get the source tarball at ftp://ftp.gnu.org/pub/gnu/"
|
||||
DIE=1
|
||||
}
|
||||
|
||||
VERSIONGREP="sed -e s/.*[^0-9\.]\([0-9]\.[0-9]*\).*/\1/"
|
||||
VERSIONMKINT="sed -e s/[^0-9]//"
|
||||
|
||||
# do we need automake?
|
||||
if test -r Makefile.am; then
|
||||
AM_NEEDED=`fgrep AUTOMAKE_OPTIONS Makefile.am | $VERSIONGREP`
|
||||
if test -z $AM_NEEDED; then
|
||||
echo -n "checking for automake... "
|
||||
AUTOMAKE=automake
|
||||
ACLOCAL=aclocal
|
||||
if ($AUTOMAKE --version < /dev/null > /dev/null 2>&1); then
|
||||
echo "no"
|
||||
AUTOMAKE=
|
||||
else
|
||||
echo "yes"
|
||||
fi
|
||||
else
|
||||
echo -n "checking for automake $AM_NEEDED or later... "
|
||||
for am in automake-$AM_NEEDED automake$AM_NEEDED automake; do
|
||||
($am --version < /dev/null > /dev/null 2>&1) || continue
|
||||
ver=`$am --version < /dev/null | head -n 1 | $VERSIONGREP | $VERSIONMKINT`
|
||||
verneeded=`echo $AM_NEEDED | $VERSIONMKINT`
|
||||
if test $ver -ge $verneeded; then
|
||||
AUTOMAKE=$am
|
||||
echo $AUTOMAKE
|
||||
break
|
||||
fi
|
||||
done
|
||||
test -z $AUTOMAKE && echo "no"
|
||||
echo -n "checking for aclocal $AM_NEEDED or later... "
|
||||
for ac in aclocal-$AM_NEEDED aclocal$AM_NEEDED aclocal; do
|
||||
($ac --version < /dev/null > /dev/null 2>&1) || continue
|
||||
ver=`$ac --version < /dev/null | head -n 1 | $VERSIONGREP | $VERSIONMKINT`
|
||||
verneeded=`echo $AM_NEEDED | $VERSIONMKINT`
|
||||
if test $ver -ge $verneeded; then
|
||||
ACLOCAL=$ac
|
||||
echo $ACLOCAL
|
||||
break
|
||||
fi
|
||||
done
|
||||
test -z $ACLOCAL && echo "no"
|
||||
fi
|
||||
test -z $AUTOMAKE || test -z $ACLOCAL && {
|
||||
echo
|
||||
echo "You must have automake installed to compile $package."
|
||||
echo "Download the appropriate package for your distribution,"
|
||||
echo "or get the source tarball at ftp://ftp.gnu.org/pub/gnu/"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
echo -n "checking for libtool... "
|
||||
for LIBTOOLIZE in libtoolize glibtoolize nope; do
|
||||
($LIBTOOLIZE --version) < /dev/null > /dev/null 2>&1 && break
|
||||
done
|
||||
if test x$LIBTOOLIZE = xnope; then
|
||||
echo "nope."
|
||||
LIBTOOLIZE=libtoolize
|
||||
else
|
||||
echo $LIBTOOLIZE
|
||||
fi
|
||||
($LIBTOOLIZE --version) < /dev/null > /dev/null 2>&1 || {
|
||||
echo
|
||||
echo "You must have libtool installed to compile $package."
|
||||
echo "Download the appropriate package for your system,"
|
||||
echo "or get the source from one of the GNU ftp sites"
|
||||
echo "listed in http://www.gnu.org/order/ftp.html"
|
||||
DIE=1
|
||||
}
|
||||
|
||||
if test "$DIE" -eq 1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Generating configuration files for $package, please wait...."
|
||||
|
||||
echo " $ACLOCAL $ACLOCAL_FLAGS"
|
||||
$ACLOCAL $ACLOCAL_FLAGS || exit 1
|
||||
echo " autoheader"
|
||||
autoheader || exit 1
|
||||
echo " $LIBTOOLIZE --automake"
|
||||
$LIBTOOLIZE --automake || exit 1
|
||||
echo " $AUTOMAKE --add-missing $AUTOMAKE_FLAGS"
|
||||
$AUTOMAKE --add-missing $AUTOMAKE_FLAGS || exit 1
|
||||
echo " autoconf"
|
||||
autoconf || exit 1
|
||||
|
||||
cd $olddir
|
||||
#$srcdir/configure "$@" && echo
|
205
native/codec/libraries/celt/celt.kdevelop
Normal file
205
native/codec/libraries/celt/celt.kdevelop
Normal file
@ -0,0 +1,205 @@
|
||||
<?xml version = '1.0'?>
|
||||
<kdevelop>
|
||||
<general>
|
||||
<author>Jean-Marc Valin</author>
|
||||
<email>Jean-Marc.Valin@USherbrooke.ca</email>
|
||||
<version>$VERSION</version>
|
||||
<projectmanagement>KDevAutoProject</projectmanagement>
|
||||
<primarylanguage>C</primarylanguage>
|
||||
<ignoreparts/>
|
||||
<projectname>celt</projectname>
|
||||
<projectdirectory>.</projectdirectory>
|
||||
<absoluteprojectpath>false</absoluteprojectpath>
|
||||
<description></description>
|
||||
<defaultencoding></defaultencoding>
|
||||
</general>
|
||||
<kdevautoproject>
|
||||
<general>
|
||||
<useconfiguration>default</useconfiguration>
|
||||
<activetarget>libcelt/libcelt.la</activetarget>
|
||||
</general>
|
||||
<run>
|
||||
<mainprogram/>
|
||||
<programargs/>
|
||||
<globaldebugarguments/>
|
||||
<globalcwd/>
|
||||
<useglobalprogram>true</useglobalprogram>
|
||||
<terminal>false</terminal>
|
||||
<autocompile>false</autocompile>
|
||||
<autoinstall>false</autoinstall>
|
||||
<autokdesu>false</autokdesu>
|
||||
<envvars/>
|
||||
</run>
|
||||
<configurations>
|
||||
<optimized>
|
||||
<builddir>optimized</builddir>
|
||||
<ccompiler>GccOptions</ccompiler>
|
||||
<cxxcompiler>GppOptions</cxxcompiler>
|
||||
<f77compiler>G77Options</f77compiler>
|
||||
<cflags>-O2 -g0</cflags>
|
||||
</optimized>
|
||||
<debug>
|
||||
<configargs>--enable-debug=full</configargs>
|
||||
<builddir>debug</builddir>
|
||||
<ccompiler>GccOptions</ccompiler>
|
||||
<cxxcompiler>GppOptions</cxxcompiler>
|
||||
<f77compiler>G77Options</f77compiler>
|
||||
<cflags>-O0 -g3</cflags>
|
||||
</debug>
|
||||
</configurations>
|
||||
<make>
|
||||
<envvars>
|
||||
<envvar value="1" name="WANT_AUTOCONF_2_5" />
|
||||
<envvar value="1" name="WANT_AUTOMAKE_1_6" />
|
||||
</envvars>
|
||||
<abortonerror>true</abortonerror>
|
||||
<runmultiplejobs>true</runmultiplejobs>
|
||||
<numberofjobs>4</numberofjobs>
|
||||
<dontact>false</dontact>
|
||||
<makebin></makebin>
|
||||
<prio>0</prio>
|
||||
</make>
|
||||
</kdevautoproject>
|
||||
<kdevdebugger>
|
||||
<general>
|
||||
<dbgshell>libtool</dbgshell>
|
||||
<gdbpath></gdbpath>
|
||||
<configGdbScript></configGdbScript>
|
||||
<runShellScript></runShellScript>
|
||||
<runGdbScript></runGdbScript>
|
||||
<breakonloadinglibs>true</breakonloadinglibs>
|
||||
<separatetty>false</separatetty>
|
||||
<floatingtoolbar>false</floatingtoolbar>
|
||||
<raiseGDBOnStart>false</raiseGDBOnStart>
|
||||
</general>
|
||||
<display>
|
||||
<staticmembers>false</staticmembers>
|
||||
<demanglenames>true</demanglenames>
|
||||
<outputradix>10</outputradix>
|
||||
</display>
|
||||
</kdevdebugger>
|
||||
<kdevdoctreeview>
|
||||
<ignoretocs>
|
||||
<toc>ada</toc>
|
||||
<toc>ada_bugs_gcc</toc>
|
||||
<toc>bash</toc>
|
||||
<toc>bash_bugs</toc>
|
||||
<toc>clanlib</toc>
|
||||
<toc>fortran_bugs_gcc</toc>
|
||||
<toc>gnome1</toc>
|
||||
<toc>gnustep</toc>
|
||||
<toc>gtk</toc>
|
||||
<toc>gtk_bugs</toc>
|
||||
<toc>haskell</toc>
|
||||
<toc>haskell_bugs_ghc</toc>
|
||||
<toc>java_bugs_gcc</toc>
|
||||
<toc>java_bugs_sun</toc>
|
||||
<toc>kde2book</toc>
|
||||
<toc>libstdc++</toc>
|
||||
<toc>opengl</toc>
|
||||
<toc>pascal_bugs_fp</toc>
|
||||
<toc>php</toc>
|
||||
<toc>php_bugs</toc>
|
||||
<toc>perl</toc>
|
||||
<toc>perl_bugs</toc>
|
||||
<toc>python</toc>
|
||||
<toc>python_bugs</toc>
|
||||
<toc>qt-kdev3</toc>
|
||||
<toc>ruby</toc>
|
||||
<toc>ruby_bugs</toc>
|
||||
<toc>sdl</toc>
|
||||
<toc>stl</toc>
|
||||
<toc>sw</toc>
|
||||
<toc>w3c-dom-level2-html</toc>
|
||||
<toc>w3c-svg</toc>
|
||||
<toc>w3c-uaag10</toc>
|
||||
<toc>wxwidgets_bugs</toc>
|
||||
</ignoretocs>
|
||||
<ignoreqt_xml>
|
||||
<toc>Guide to the Qt Translation Tools</toc>
|
||||
<toc>Qt Assistant Manual</toc>
|
||||
<toc>Qt Designer Manual</toc>
|
||||
<toc>Qt Reference Documentation</toc>
|
||||
<toc>qmake User Guide</toc>
|
||||
</ignoreqt_xml>
|
||||
<ignoredoxygen>
|
||||
<toc>KDE Libraries (Doxygen)</toc>
|
||||
</ignoredoxygen>
|
||||
</kdevdoctreeview>
|
||||
<kdevfilecreate>
|
||||
<filetypes/>
|
||||
<useglobaltypes>
|
||||
<type ext="c" />
|
||||
<type ext="h" />
|
||||
</useglobaltypes>
|
||||
</kdevfilecreate>
|
||||
<kdevcppsupport>
|
||||
<qt>
|
||||
<used>false</used>
|
||||
<version>3</version>
|
||||
<includestyle>3</includestyle>
|
||||
<root></root>
|
||||
<designerintegration>EmbeddedKDevDesigner</designerintegration>
|
||||
<qmake></qmake>
|
||||
<designer></designer>
|
||||
<designerpluginpaths/>
|
||||
</qt>
|
||||
<codecompletion>
|
||||
<automaticCodeCompletion>false</automaticCodeCompletion>
|
||||
<automaticArgumentsHint>true</automaticArgumentsHint>
|
||||
<automaticHeaderCompletion>true</automaticHeaderCompletion>
|
||||
<codeCompletionDelay>250</codeCompletionDelay>
|
||||
<argumentsHintDelay>400</argumentsHintDelay>
|
||||
<headerCompletionDelay>250</headerCompletionDelay>
|
||||
<showOnlyAccessibleItems>false</showOnlyAccessibleItems>
|
||||
<completionBoxItemOrder>0</completionBoxItemOrder>
|
||||
<howEvaluationContextMenu>true</howEvaluationContextMenu>
|
||||
<showCommentWithArgumentHint>true</showCommentWithArgumentHint>
|
||||
<statusBarTypeEvaluation>false</statusBarTypeEvaluation>
|
||||
<namespaceAliases>std=_GLIBCXX_STD;__gnu_cxx=std</namespaceAliases>
|
||||
<processPrimaryTypes>true</processPrimaryTypes>
|
||||
<processFunctionArguments>false</processFunctionArguments>
|
||||
<preProcessAllHeaders>false</preProcessAllHeaders>
|
||||
<parseMissingHeadersExperimental>false</parseMissingHeadersExperimental>
|
||||
<resolveIncludePathsUsingMakeExperimental>false</resolveIncludePathsUsingMakeExperimental>
|
||||
<alwaysParseInBackground>true</alwaysParseInBackground>
|
||||
<usePermanentCaching>true</usePermanentCaching>
|
||||
<alwaysIncludeNamespaces>false</alwaysIncludeNamespaces>
|
||||
<includePaths>.;</includePaths>
|
||||
</codecompletion>
|
||||
<creategettersetter>
|
||||
<prefixGet></prefixGet>
|
||||
<prefixSet>set</prefixSet>
|
||||
<prefixVariable>m_,_</prefixVariable>
|
||||
<parameterName>theValue</parameterName>
|
||||
<inlineGet>true</inlineGet>
|
||||
<inlineSet>true</inlineSet>
|
||||
</creategettersetter>
|
||||
<splitheadersource>
|
||||
<enabled>true</enabled>
|
||||
<synchronize>true</synchronize>
|
||||
<orientation>Horizontal</orientation>
|
||||
</splitheadersource>
|
||||
<references/>
|
||||
</kdevcppsupport>
|
||||
<cppsupportpart>
|
||||
<filetemplates>
|
||||
<interfacesuffix>.h</interfacesuffix>
|
||||
<implementationsuffix>.cpp</implementationsuffix>
|
||||
</filetemplates>
|
||||
</cppsupportpart>
|
||||
<kdevfileview>
|
||||
<groups/>
|
||||
<tree>
|
||||
<hidepatterns>*.o,*.lo,CVS</hidepatterns>
|
||||
<hidenonprojectfiles>false</hidenonprojectfiles>
|
||||
</tree>
|
||||
</kdevfileview>
|
||||
<kdevdocumentation>
|
||||
<projectdoc>
|
||||
<docsystem/>
|
||||
<docurl/>
|
||||
<usermanualurl/>
|
||||
</projectdoc>
|
||||
</kdevdocumentation>
|
||||
</kdevelop>
|
15
native/codec/libraries/celt/celt.pc.in
Normal file
15
native/codec/libraries/celt/celt.pc.in
Normal file
@ -0,0 +1,15 @@
|
||||
# libcelt pkg-config source file
|
||||
|
||||
prefix=@prefix@
|
||||
exec_prefix=@exec_prefix@
|
||||
libdir=@libdir@
|
||||
includedir=@includedir@
|
||||
|
||||
Name: celt
|
||||
Description: CELT is a low-delay audio codec
|
||||
Version: @CELT_VERSION@
|
||||
Requires:
|
||||
Conflicts:
|
||||
Libs: -L${libdir} -lcelt@LIBCELT_SUFFIX@
|
||||
Libs.private: -lm
|
||||
Cflags: -I${includedir}
|
241
native/codec/libraries/celt/configure.ac
Normal file
241
native/codec/libraries/celt/configure.ac
Normal file
@ -0,0 +1,241 @@
|
||||
dnl Process this file with autoconf to produce a configure script. -*-m4-*-
|
||||
|
||||
AC_INIT(libcelt/arch.h)
|
||||
|
||||
AM_CONFIG_HEADER([config.h])
|
||||
|
||||
CELT_MAJOR_VERSION=0
|
||||
CELT_MINOR_VERSION=11
|
||||
CELT_MICRO_VERSION=4
|
||||
CELT_EXTRA_VERSION=
|
||||
CELT_VERSION=$CELT_MAJOR_VERSION.$CELT_MINOR_VERSION.$CELT_MICRO_VERSION$CELT_EXTRA_VERSION
|
||||
LIBCELT_SUFFIX=0
|
||||
|
||||
CELT_LT_CURRENT=2
|
||||
CELT_LT_REVISION=0
|
||||
CELT_LT_AGE=0
|
||||
|
||||
AC_SUBST(CELT_LT_CURRENT)
|
||||
AC_SUBST(CELT_LT_REVISION)
|
||||
AC_SUBST(CELT_LT_AGE)
|
||||
AC_SUBST(LIBCELT_SUFFIX)
|
||||
|
||||
# For automake.
|
||||
VERSION=$CELT_VERSION
|
||||
PACKAGE=celt
|
||||
|
||||
AC_SUBST(CELT_VERSION)
|
||||
|
||||
AM_INIT_AUTOMAKE($PACKAGE, $VERSION, no-define)
|
||||
AM_MAINTAINER_MODE
|
||||
|
||||
AC_CANONICAL_HOST
|
||||
AM_PROG_LIBTOOL
|
||||
|
||||
AC_PROG_CC_C99
|
||||
AC_C_BIGENDIAN
|
||||
AC_C_CONST
|
||||
AC_C_INLINE
|
||||
AC_C_RESTRICT
|
||||
|
||||
AC_DEFINE([CELT_BUILD], [], [This is a build of CELT])
|
||||
|
||||
AC_MSG_CHECKING(for C99 variable-size arrays)
|
||||
AC_TRY_COMPILE( , [
|
||||
int foo=10;
|
||||
int array[foo];
|
||||
],
|
||||
[has_var_arrays=yes;AC_DEFINE([VAR_ARRAYS], [], [Use C99 variable-size arrays])
|
||||
],
|
||||
has_var_arrays=no
|
||||
)
|
||||
AC_MSG_RESULT($has_var_arrays)
|
||||
|
||||
AC_CHECK_HEADERS([alloca.h getopt.h])
|
||||
AC_MSG_CHECKING(for alloca)
|
||||
AC_TRY_COMPILE( [#include <alloca.h>], [
|
||||
int foo=10;
|
||||
int *array = alloca(foo);
|
||||
],
|
||||
[
|
||||
has_alloca=yes;
|
||||
if test x$has_var_arrays = "xno" ; then
|
||||
AC_DEFINE([USE_ALLOCA], [], [Make use of alloca])
|
||||
fi
|
||||
],
|
||||
has_alloca=no
|
||||
)
|
||||
AC_MSG_RESULT($has_alloca)
|
||||
|
||||
AC_CHECK_HEADERS(sys/soundcard.h sys/audioio.h)
|
||||
|
||||
AS_IF([test "x$with_ogg" != xno],
|
||||
[XIPH_PATH_OGG([tools="tools"], [tools=""])],
|
||||
[tools=""])
|
||||
AC_SUBST(tools)
|
||||
|
||||
AC_CHECK_LIB(m, sin)
|
||||
|
||||
# Check for getopt_long; if not found, use included source.
|
||||
AC_CHECK_FUNCS([getopt_long],,
|
||||
[# FreeBSD has a gnugetopt library.
|
||||
AC_CHECK_LIB([gnugetopt],[getopt_long],
|
||||
[AC_DEFINE([HAVE_GETOPT_LONG])],
|
||||
[# Use the GNU replacement.
|
||||
AC_LIBOBJ(getopt)
|
||||
AC_LIBOBJ(getopt1)])])
|
||||
|
||||
AC_CHECK_LIB(winmm, main)
|
||||
|
||||
AC_DEFINE_UNQUOTED(CELT_VERSION, "${CELT_VERSION}", [Complete version string])
|
||||
AC_DEFINE_UNQUOTED(CELT_MAJOR_VERSION, ${CELT_MAJOR_VERSION}, [Version major])
|
||||
AC_DEFINE_UNQUOTED(CELT_MINOR_VERSION, ${CELT_MINOR_VERSION}, [Version minor])
|
||||
AC_DEFINE_UNQUOTED(CELT_MICRO_VERSION, ${CELT_MICRO_VERSION}, [Version micro])
|
||||
AC_DEFINE_UNQUOTED(CELT_EXTRA_VERSION, "${CELT_EXTRA_VERSION}", [Version extra])
|
||||
|
||||
has_float_approx=no
|
||||
#case "$host_cpu" in
|
||||
#i[[3456]]86 | x86_64 | powerpc64 | powerpc32 | ia64)
|
||||
# has_float_approx=yes
|
||||
# ;;
|
||||
#esac
|
||||
|
||||
ac_enable_fixed="no";
|
||||
AC_ARG_ENABLE(fixed-point, [ --enable-fixed-point compile as fixed-point],
|
||||
[if test "$enableval" = yes; then
|
||||
ac_enable_fixed="yes";
|
||||
AC_DEFINE([FIXED_POINT], , [Compile as fixed-point])
|
||||
else
|
||||
AC_DEFINE([FLOATING_POINT], , [Compile as floating-point])
|
||||
fi],
|
||||
AC_DEFINE([FLOATING_POINT], , [Compile as floating-point]))
|
||||
|
||||
ac_enable_fixed_debug="no"
|
||||
AC_ARG_ENABLE(fixed-point-debug, [ --enable-fixed-point-debug debug fixed-point implementation],
|
||||
[if test "$enableval" = yes; then
|
||||
ac_enable_fixed_debug="yes"
|
||||
AC_DEFINE([FIXED_DEBUG], , [Debug fixed-point implementation])
|
||||
fi])
|
||||
|
||||
ac_enable_experimental_postfilter="no"
|
||||
AC_ARG_ENABLE(experimental-postfilter, [ --enable-experimental-postfilter Enable this for testing only if you know what you're doing ],
|
||||
[if test "$enableval" = yes; then
|
||||
ac_enable_experimental_postfilter="yes"
|
||||
AC_DEFINE([ENABLE_POSTFILTER], , [Postfilter])
|
||||
fi])
|
||||
|
||||
ac_enable_custom_modes="no"
|
||||
AC_ARG_ENABLE(custom-modes, [ --enable-custom-modes Enable non-Opus modes, like 44.1 kHz and powers of two ],
|
||||
[if test "$enableval" = yes; then
|
||||
ac_enable_custom_modes="yes"
|
||||
AC_DEFINE([CUSTOM_MODES], , [Custom modes])
|
||||
fi])
|
||||
|
||||
float_approx=$has_float_approx
|
||||
AC_ARG_ENABLE(float-approx, [ --enable-float-approx enable fast approximations for floating point],
|
||||
[ if test "$enableval" = yes; then
|
||||
AC_WARN([Floating point approximations are not supported on all platforms.])
|
||||
float_approx=yes
|
||||
else
|
||||
float_approx=no
|
||||
fi], [ float_approx=$has_float_approx ])
|
||||
|
||||
if test "x${float_approx}" = "xyes"; then
|
||||
AC_DEFINE([FLOAT_APPROX], , [Float approximations])
|
||||
fi
|
||||
|
||||
ac_enable_assertions="no"
|
||||
AC_ARG_ENABLE(assertions, [ --enable-assertions enable additional software error checking],
|
||||
[if test "$enableval" = yes; then
|
||||
ac_enable_assertions="yes"
|
||||
AC_DEFINE([ENABLE_ASSERTIONS], , [Assertions])
|
||||
fi])
|
||||
|
||||
if test "$OPUS_BUILD" != "true" ; then
|
||||
saved_CFLAGS="$CFLAGS"
|
||||
CFLAGS="$CFLAGS -fvisibility=hidden"
|
||||
AC_MSG_CHECKING([if ${CXX} supports -fvisibility=hidden])
|
||||
AC_COMPILE_IFELSE([char foo;],
|
||||
[ AC_MSG_RESULT([yes])
|
||||
SYMBOL_VISIBILITY="-fvisibility=hidden" ],
|
||||
AC_MSG_RESULT([no]))
|
||||
CFLAGS="$saved_CFLAGS $SYMBOL_VISIBILITY"
|
||||
AC_SUBST(SYMBOL_VISIBILITY)
|
||||
fi
|
||||
|
||||
if test $ac_cv_c_compiler_gnu = yes ; then
|
||||
CFLAGS="$CFLAGS -W -Wstrict-prototypes -Wall -Wextra -Wcast-align -Wnested-externs -Wshadow -Wno-parentheses -Wno-unused-parameter -Wno-sign-compare"
|
||||
fi
|
||||
|
||||
AC_CHECK_FUNCS([lrintf])
|
||||
AC_CHECK_FUNCS([lrint])
|
||||
|
||||
AC_CHECK_SIZEOF(short)
|
||||
AC_CHECK_SIZEOF(int)
|
||||
AC_CHECK_SIZEOF(long)
|
||||
AC_CHECK_SIZEOF(long long)
|
||||
|
||||
if test x$has_char16 = "xyes" ; then
|
||||
case 1 in
|
||||
$ac_cv_sizeof_short) SIZE16="short";;
|
||||
$ac_cv_sizeof_int) SIZE16="int";;
|
||||
esac
|
||||
else
|
||||
case 2 in
|
||||
$ac_cv_sizeof_short) SIZE16="short";;
|
||||
$ac_cv_sizeof_int) SIZE16="int";;
|
||||
esac
|
||||
fi
|
||||
|
||||
if test x$has_char16 = "xyes" ; then
|
||||
case 2 in
|
||||
$ac_cv_sizeof_int) SIZE32="int";;
|
||||
$ac_cv_sizeof_long) SIZE32="long";;
|
||||
$ac_cv_sizeof_short) SIZE32="short";;
|
||||
esac
|
||||
else
|
||||
case 4 in
|
||||
$ac_cv_sizeof_int) SIZE32="int";;
|
||||
$ac_cv_sizeof_long) SIZE32="long";;
|
||||
$ac_cv_sizeof_short) SIZE32="short";;
|
||||
esac
|
||||
fi
|
||||
|
||||
AC_SUBST(SIZE16)
|
||||
AC_SUBST(SIZE32)
|
||||
|
||||
if test "$OPUS_BUILD" = "true" ; then
|
||||
AC_DEFINE(OPUS_BUILD, [], [We're part of Opus])
|
||||
fi
|
||||
|
||||
AC_OUTPUT([Makefile libcelt/Makefile tests/Makefile
|
||||
celt.pc tools/Makefile libcelt.spec ])
|
||||
|
||||
AC_MSG_RESULT([
|
||||
------------------------------------------------------------------------
|
||||
$PACKAGE $VERSION: Automatic configuration OK.
|
||||
|
||||
Compiler support:
|
||||
|
||||
C99 var arrays: ................ ${has_var_arrays}
|
||||
C99 lrintf: .................... ${ac_cv_func_lrintf}
|
||||
Alloca: ........................ ${has_alloca}
|
||||
|
||||
General configuration:
|
||||
|
||||
Fast float approximations: ..... ${float_approx}
|
||||
Fixed point support: ........... ${ac_enable_fixed}
|
||||
Fixed point debugging: ......... ${ac_enable_fixed_debug}
|
||||
Custom modes: .................. ${ac_enable_custom_modes}
|
||||
Assertion checking: ............ ${ac_enable_assertions}
|
||||
------------------------------------------------------------------------
|
||||
])
|
||||
|
||||
if test "x$tools" = "x"; then
|
||||
echo "**IMPORTANT**"
|
||||
echo "You don't seem to have the development package for libogg (libogg-devel) available. Only the library will be built (no encoder/decoder executable)"
|
||||
echo "You can download libogg from http://www.vorbis.com/download.psp"
|
||||
fi
|
||||
|
||||
echo "Type \"make; make install\" to compile and install";
|
||||
echo "Type \"make check\" to run the test suite";
|
16
native/codec/libraries/celt/doc/ietf/Makefile.ietf
Normal file
16
native/codec/libraries/celt/doc/ietf/Makefile.ietf
Normal file
@ -0,0 +1,16 @@
|
||||
CC = gcc
|
||||
CFLAGS = -c -O2 -g
|
||||
LIBS = -lm
|
||||
|
||||
OBJS = bands.o celt.o cwrs.o entcode.o entdec.o entenc.o kiss_fft.o \
|
||||
kiss_fftr.o laplace.o mdct.o modes.o pitch.o \
|
||||
quant_bands.o rangedec.o rangeenc.o rate.o testcelt.o vq.o plc.o
|
||||
|
||||
.c.o:
|
||||
$(CC) $(CFLAGS) $<
|
||||
|
||||
testcelt: $(OBJS)
|
||||
$(CC) -o $@ $(OBJS) $(LIBS)
|
||||
|
||||
clean:
|
||||
rm -f testcelt *.o
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user