Initial commit

This commit is contained in:
WolverinDEV 2019-06-26 22:09:01 +02:00
commit 6b70b8d425
1004 changed files with 253070 additions and 0 deletions

19
.gitignore vendored Normal file
View 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
View 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
View 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
View 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

@ -0,0 +1 @@
Subproject commit 0866874725f393c2804f2926687ea3d858d88653

46
installer/WinInstall.ejs Normal file
View 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
View 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 {}

View 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();

View File

@ -0,0 +1,3 @@
```$bash
ssh-keygen -t rsa -b 4096 -f ssh_key -N ''
```

239
installer/deploy/index.ts Normal file
View 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();

View 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
View 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-----

View 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
View 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();
});
})
}

View 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)
});

View 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
View 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
View 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();
}

View 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;
}
}

View 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";
}

View File

@ -0,0 +1,9 @@
.page {
display: none;
}
.info {
display: block;
}
/*# sourceMappingURL=index.css.map */

View File

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["index.scss"],"names":[],"mappings":"AAAA;EACC;;;AAGD;EACC","file":"index.css"}

View 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>

View File

@ -0,0 +1,7 @@
.page {
display: none;
}
.info {
display: block;
}

View 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
View 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
View 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);
}

View 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);
});
}

View 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 */

View 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"}

View 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>

View 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;
}

View 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);
});
}

View 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");
}
}

View File

@ -0,0 +1,2 @@
export * from "./loader.js";
export * from "./graphical.js";

View 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 */
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View 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 {}

View 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>

View 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>

View 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();
}
});

View 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
});
};
}
}
}

View 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();
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View 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 */

View 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"
}

View 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>

View 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;
}
}

View 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));

View 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)));
}

View 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);

View 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 */

View 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);

View 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);

View 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);
}
}
}
}

View 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
View 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");
};

View 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
View 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);

View 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;

View 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);
}
}
}

View 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;

View 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
View File

@ -0,0 +1,2 @@
build/
cmake-build-*

163
native/CMakeLists.txt Normal file
View 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()

File diff suppressed because it is too large Load Diff

14
native/codec/.gitignore vendored Normal file
View 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-*/

View 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
View 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)

View 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

View 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
};
}

View 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();
}

View 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;
};
}

View 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

View 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
};
}

View 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

View 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
View File

@ -0,0 +1,2 @@
generated/
opus/build

View 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"

View 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"

View 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
View 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

View File

View 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.

View File

View 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

View 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

View File

@ -0,0 +1,5 @@
To compile:
./configure
make

View 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

View File

View 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)).

View 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

View 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.

View 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

View 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

View 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>

View 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}

View 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";

View 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