Udated a lot of stuff

This commit is contained in:
WolverinDEV 2019-06-30 17:24:10 +02:00
parent 6b70b8d425
commit 160c6d83db
25 changed files with 642 additions and 176 deletions

2
github

@ -1 +1 @@
Subproject commit 0866874725f393c2804f2926687ea3d858d88653
Subproject commit 06391c6cdd772c2f83c1387960f7224f7cd9f514

View File

@ -22,6 +22,7 @@ import {PassThrough} from "stream";
import {prevent_instant_close} from "../../core/main_window";
import ErrnoException = NodeJS.ErrnoException;
import {EPERM} from "constants";
import * as winmgr from "../window";
const is_debug = false;
export function server_url() : string {
@ -684,6 +685,8 @@ export async function execute_graphical(channel: string, ask_install: boolean) :
}
await new Promise(resolve => window.on('ready-to-show', resolve));
window.show();
await winmgr.apply_bounds('update-installer', window);
winmgr.track_bounds('update-installer', window);
const current_vers = await current_version();
console.log("Current version: " + current_vers.toString(true));
@ -824,7 +827,7 @@ async function check_update(channel: string) {
update_question_open = true;
dialog.showMessageBox({
buttons: ["update now", "remind me later"],
title: "Update available",
title: "TeaClient: Update available",
message:
"There is an update available!\n" +
"Should we update now?\n" +

View File

@ -1,5 +1,7 @@
import {BrowserWindow, Menu, MenuItem, MessageBoxOptions, app, dialog} from "electron";
import * as electron from "electron";
import * as winmgr from "./window";
export let prevent_instant_close: boolean = true;
export let is_debug: boolean;
export let allow_dev_tools: boolean;
@ -102,7 +104,8 @@ function spawn_main_window(entry_point: string) {
show: false,
webPreferences: {
webSecurity: false,
nodeIntegrationInWorker: true
nodeIntegrationInWorker: true,
nodeIntegration: true
},
});
@ -124,11 +127,15 @@ function spawn_main_window(entry_point: string) {
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 */
winmgr.apply_bounds('main-window', main_window).then(() => {
winmgr.track_bounds('main-window', main_window);
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) => {

View File

@ -6,6 +6,7 @@ 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";
import * as winmgr from "../window";
let current_window: BrowserWindow;
let _current_data: UserData;
@ -64,10 +65,13 @@ export function open_login(enforce: boolean = false) : Promise<UserData> {
show: true,
parent: main_window,
webPreferences: {
webSecurity: false
webSecurity: false,
nodeIntegration: true
},
});
current_window.setMenu(null);
winmgr.apply_bounds('forum-manager', current_window);
winmgr.track_bounds('forum-manager', current_window);
console.log("Main: " + main_window);
current_window.loadFile(path.join(path.dirname(module.filename), "ui", "index.html"));

View File

@ -5,6 +5,8 @@ import {screen} from "electron";
import {Arguments, process_args} from "../../shared/process-arguments";
import * as loader from "./loader";
import * as updater from "../app-updater";
import * as winmgr from "../window";
import {main_window} from "../main_window";
export namespace ui {
let gui: electron.BrowserWindow;
@ -26,11 +28,12 @@ export namespace ui {
export function cleanup() {
if(gui) {
promise = undefined;
resolve = undefined;
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);
@ -71,47 +74,56 @@ export namespace ui {
gui.webContents.send('await-update');
}
function spawn_gui(close_callback: () => any) {
function spawn_gui() {
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;
const WINDOW_WIDTH = 340 + (dev_tools ? 1000 : 0);
const WINDOW_HEIGHT = 400 + (process.platform == "win32" ? 40 : 0);
let bounds = screen.getPrimaryDisplay().bounds;
let x = bounds.x + (bounds.width - WINDOW_WIDTH) / 2;
let y = bounds.y + (bounds.height - WINDOW_HEIGHT) / 2;
console.log("Bounds: %o; Move loader window to %ox%o", bounds, x, y);
gui = new electron.BrowserWindow({
width: dev_tools ? WINDOW_WIDTH + 1000 : WINDOW_WIDTH,
height: WINDOW_HEIGHT + (process.platform == "win32" ? 40 : 0),
frame: true,
width: WINDOW_WIDTH,
height: WINDOW_HEIGHT,
frame: dev_tools,
resizable: dev_tools,
show: false,
autoHideMenuBar: true,
//frame: false,
webPreferences: {
webSecurity: false,
nodeIntegrationInWorker: true
nodeIntegrationInWorker: false,
nodeIntegration: true
}
});
gui.setMenu(null);
gui.loadFile(path.join(path.dirname(module.filename), "ui", "loading_screen.html"));
gui.on('closed', close_callback);
gui.on('closed', () => {
if(resolve)
resolve();
gui = undefined;
cleanup();
});
gui.on('ready-to-show', () => {
gui.show();
gui.setPosition(x, y);
winmgr.apply_bounds('ui-load-window', gui, undefined, { apply_size: false }).then(() => {
winmgr.track_bounds('ui-load-window', gui);
const call_loader = () => load_files().catch(reject);
if(!process_args.has_flag(...Arguments.DISABLE_ANIMATION))
setTimeout(call_loader, 1000);
else
setImmediate(call_loader);
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();
if(dev_tools)
gui.webContents.openDevTools();
});
});
}
@ -123,7 +135,7 @@ export namespace ui {
console.error("Failed to load UI files! Error: %o", error)
});
spawn_gui(() => reject(undefined));
spawn_gui();
});
}

View File

@ -7,10 +7,27 @@
<style type="text/css">
html, body {
background: #18BC9C;
user-select: none;
}
body {
text-align: center;
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
margin-left: 18px;
margin-right: 18px;
display: flex;
flex-direction: column;
justify-content: center;
-ms-overflow-style: none;
-webkit-app-region: drag;
}
img {
@ -29,6 +46,7 @@
}
.container-logo {
align-self: center;
position: relative;
display: inline-block;

View File

@ -1,14 +1,32 @@
import * as electron from "electron";
import * as fs from "fs";
import * as path from "path";
import * as winmgr from "../window";
export function open_preview(url: string) {
export async function open_preview(url: string) {
console.log("Open URL as preview: %s", url);
const window = new electron.BrowserWindow();
window.loadURL(url);
const window = new electron.BrowserWindow({
webPreferences: {
webSecurity: true,
nodeIntegration: false,
nodeIntegrationInWorker: false,
allowRunningInsecureContent: false,
},
skipTaskbar: true,
center: true,
});
await winmgr.apply_bounds('url-preview', window);
winmgr.track_bounds('url-preview', window);
window.setMenu(null);
window.loadURL(url).then(() => {
window.webContents.openDevTools();
});
//FIXME try catch?
const inject_file = path.join(path.dirname(module.filename), "inject.js");
const code_inject = fs.readFileSync(inject_file).toString();
window.webContents.once('dom-ready', e => {
const code_inject = fs.readFileSync(inject_file).toString();
window.webContents.executeJavaScript(code_inject, true);

109
modules/core/window.ts Normal file
View File

@ -0,0 +1,109 @@
import * as electron from "electron";
import * as fs from "fs-extra";
import * as path from "path";
/* We read/write to this file every time again because this file could be used by multible processes */
const data_file: string = path.join(electron.app.getPath('userData'), "window-bounds.json");
import BrowserWindow = Electron.BrowserWindow;
import Rectangle = Electron.Rectangle;
let _changed_data: {[key: string]:Rectangle} = {};
let _changed_saver: NodeJS.Timer;
export async function save_changes() {
clearTimeout(_changed_saver);
try {
const data = (await fs.pathExists(data_file) ? await fs.readJson(data_file) : {}) || {};
Object.assign(data, _changed_data);
await fs.ensureFile(data_file);
await fs.writeJson(data_file, data);
path_exists = true;
_changed_data = {};
} catch(error) {
console.warn("Failed to save window bounds: %o", error);
}
console.log("Window bounds have been successfully saved!");
}
let path_exists = undefined;
export async function get_last_bounds(key: string) : Promise<Rectangle> {
try {
if(typeof(path_exists) === "undefined" ? !(path_exists = await fs.pathExists(data_file)) : !path_exists)
throw "skip!";
const data = await fs.readJson(data_file) || {};
if(data[key])
return data[key];
} catch(error) {
if(error !== "skip!")
console.warn("Failed to load window bounds for %s: %o", key, error);
}
return {
height: undefined,
width: undefined,
x: undefined,
y: undefined
}
}
export function track_bounds(key: string, window: BrowserWindow) {
const events = ['move', 'moved', 'resize'];
const update_bounds = () => {
_changed_data[key] = window.getBounds();
clearTimeout(_changed_saver);
_changed_saver = setTimeout(save_changes, 1000);
};
for(const event of events)
window.on(event as any, update_bounds);
window.on('closed', () => {
for(const event of events)
window.removeListener(event as any, update_bounds);
})
}
export async function apply_bounds(key: string, window: BrowserWindow, bounds?: Rectangle, options?: { apply_size?: boolean; apply_position?: boolean }) {
const screen = electron.screen;
if(!bounds)
bounds = await get_last_bounds(key);
if(!options)
options = {};
const original_bounds = window.getBounds();
if(typeof(options.apply_size) !== "boolean" || options.apply_size) {
let height = bounds.height > 0 ? bounds.height : original_bounds.height;
let width = bounds.width > 0 ? bounds.width : original_bounds.width;
if(height != original_bounds.height || width != original_bounds.width)
window.setSize(width, height, true);
}
if(typeof(options.apply_position) !== "boolean" || options.apply_position) {
let x = typeof(bounds.x) === "number" ? bounds.x : original_bounds.x;
let y = typeof(bounds.y) === "number" ? bounds.y : original_bounds.y;
if(x != original_bounds.x || y != original_bounds.y) {
const display = screen.getDisplayNearestPoint({ x: x, y: y });
if(display) {
const bounds = display.workArea || display.bounds;
let flag_invalid = false;
flag_invalid = flag_invalid || bounds.x > x || (bounds.x + bounds.width) < x;
flag_invalid = flag_invalid || bounds.y > x || (bounds.y + bounds.height) < y;
if(!flag_invalid) {
window.setPosition(x, y, true);
console.log("Updating position for %s", key);
}
}
}
}
}

View File

@ -0,0 +1,181 @@
window["require_setup"](module);
import * as electron from "electron";
const remote = electron.remote;
const {Menu, MenuItem} = remote;
import {isFunction} from "util";
import NativeImage = electron.NativeImage;
class ElectronContextMenu implements contextmenu.ContextMenuProvider {
private _close_listeners: (() => any)[] = [];
private _current_menu: electron.Menu;
private _icon_mash_url: string;
private _icon_mask_img: NativeImage;
private _div: JQuery;
despawn_context_menu() {
if(!this._current_menu)
return;
this._current_menu.closePopup();
this._current_menu = undefined;
for(const listener of this._close_listeners)
listener();
this._close_listeners = [];
}
finalize() {
this._icon_mask_img = undefined;
this._icon_mash_url = undefined;
if(this._div) this._div.detach();
this._div = undefined;
this._cache_klass_map = undefined;
}
initialize() {
this.initialize_icons();
}
private async initialize_icons() {
if(!this._div) {
this._div = $.spawn("div");
this._div.css('display', 'none');
this._div.appendTo(document.body);
}
const image = new Image();
image.src = 'img/client_icon_sprite.svg';
await new Promise((resolve, reject) => {
image.onload = resolve;
image.onerror = reject;
});
/* TODO: Get a size! */
const canvas = document.createElement("canvas");
canvas.width = 1024;
canvas.height = 1024;
canvas.getContext("2d").drawImage(image, 0, 0);
this._icon_mash_url = canvas.toDataURL();
this._icon_mask_img = remote.nativeImage.createFromDataURL(this._icon_mash_url);
}
private _cache_klass_map: {[key: string]: NativeImage} = {};
private class_to_image(klass: string) : NativeImage {
if(!klass || !this._icon_mask_img)
return undefined;
if(this._cache_klass_map[klass])
return this._cache_klass_map[klass];
this._div[0].classList.value = 'icon ' + klass;
const data = window.getComputedStyle(this._div[0]);
const offset_x = parseInt(data.backgroundPositionX.split(",")[0]);
const offset_y = parseInt(data.backgroundPositionY.split(",")[0]);
//http://localhost/home/TeaSpeak/Web-Client/web/environment/development/img/client_icon_sprite.svg
//const hight = element.css('height');
//const width = element.css('width');
console.log("Offset: x: %o y: %o;", offset_x, offset_y);
return this._cache_klass_map[klass] = this._icon_mask_img.crop({
height: 16,
width: 16,
x: offset_x == 0 ? 0 : -offset_x,
y: offset_y == 0 ? 0 : -offset_y
});
}
private _entry_id = 0;
private build_menu(entry: contextmenu.MenuEntry) : electron.MenuItem {
if(entry.type == contextmenu.MenuEntryType.CLOSE) {
this._close_listeners.push(entry.callback);
return undefined;
}
const click_callback = () => {
if(entry.callback)
entry.callback();
this.despawn_context_menu();
};
const _id = "entry_" + (this._entry_id++);
if(entry.type == contextmenu.MenuEntryType.ENTRY) {
return new MenuItem({
id: _id,
label: (isFunction(entry.name) ? (entry.name as (() => string))() : entry.name) as string,
type: "normal",
click: click_callback,
icon: this.class_to_image(entry.icon_class),
visible: entry.visible,
enabled: !entry.disabled
});
} else if(entry.type == contextmenu.MenuEntryType.HR) {
if(typeof(entry.visible) === "boolean" && !entry.visible)
return undefined;
return new MenuItem({
id: _id,
type: "separator",
label: '',
click: click_callback
})
} else if(entry.type == contextmenu.MenuEntryType.CHECKBOX) {
return new MenuItem({
id: _id,
label: (isFunction(entry.name) ? (entry.name as (() => string))() : entry.name) as string,
type: "checkbox",
checked: !!entry.checkbox_checked,
click: click_callback,
icon: this.class_to_image(entry.icon_class),
visible: entry.visible,
enabled: !entry.disabled
});
} else if (entry.type == contextmenu.MenuEntryType.SUB_MENU) {
const sub_menu = new Menu();
for(const e of entry.sub_menu) {
const build = this.build_menu(e);
if(!build)
continue;
sub_menu.append(build);
}
return new MenuItem({
id: _id,
label: (isFunction(entry.name) ? (entry.name as (() => string))() : entry.name) as string,
type: "submenu",
submenu: sub_menu,
click: click_callback,
icon: this.class_to_image(entry.icon_class),
visible: entry.visible,
enabled: !entry.disabled
});
}
return undefined;
}
spawn_context_menu(x: number, y: number, ...entries: contextmenu.MenuEntry[]) {
this.despawn_context_menu();
this._current_menu = new Menu();
for(const entry of entries) {
const build = this.build_menu(entry);
if(!build)
continue;
this._current_menu.append(build);
}
this._current_menu.popup({
window: remote.getCurrentWindow(),
x: x,
y: y,
callback: () => this.despawn_context_menu()
});
}
html_format_enabled() { return false; }
}
contextmenu.set_provider(new ElectronContextMenu());
export {};

View File

@ -98,6 +98,26 @@ export const initialize = async () => {
},
priority: 110
});
loader.register_task(loader.Stage.INITIALIZING, {
name: 'gdb-waiter',
function: async () => {
if(process_args.has_flag(Arguments.DEV_TOOLS_GDB)) {
console.log("Process ID: %d", process.pid);
await new Promise(resolve => {
console.log("Waiting for continue!");
const listener = () => {
console.log("Continue");
document.removeEventListener('click', listener);
resolve();
};
document.addEventListener('click', listener);
});
}
},
priority: 100
});
};
const jquery_initialize = async () => {
@ -216,6 +236,13 @@ const load_modules = async () => {
console.dir(error);
throw error;
}
try {
require("./context-menu");
} catch(error) {
console.error("Failed to load context menu extension");
console.dir(error);
throw error;
}
} catch(error){
console.log(error);
window.displayCriticalError("Failed to load native extensions: " + error);

View File

@ -3,6 +3,7 @@ import {app} from "electron";
export class Arguments {
static readonly DEV_TOOLS = ["t", "dev-tools"];
static readonly DEV_TOOLS_GDB = ["gdb"];
static readonly DEBUG = ["d", "debug"];
static readonly DISABLE_ANIMATION = ["a", "disable-animation"];
static readonly SERVER_URL = ["u", "server-url"];

View File

@ -10,7 +10,7 @@ set(CMAKE_VERBOSE_MAKEFILE ON)
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")
set(NODEJS_VERSION "v5.0.6")
find_package(NodeJS REQUIRED)

View File

@ -86,14 +86,14 @@ ssize_t AudioOutputSource::enqueue_samples(const std::shared_ptr<tc::audio::Samp
this->sample_buffers.clear();
break;
case overflow_strategy::discard_buffer_half:
this->sample_buffers.erase(this->sample_buffers.begin(), this->sample_buffers.begin() + this->sample_buffers.size() / 2);
this->sample_buffers.erase(this->sample_buffers.begin(), this->sample_buffers.begin() + (int) (this->sample_buffers.size() / 2));
break;
case overflow_strategy::ignore:
break;
}
}
this->sample_buffers.push_back(std::move(buf));
this->sample_buffers.push_back(buf);
this->buffered_samples += buf->sample_size;
}

View File

@ -12,7 +12,7 @@ AudioResampler::AudioResampler(size_t irate, size_t orate, size_t channels) : _i
this->soxr_handle = soxr_create(this->_input_rate, this->_output_rate, (unsigned) this->_channels, &error, nullptr, nullptr, nullptr);
if(!this->soxr_handle) {
log_error(category::audio, tr("Failed to create soxr resampler: {}"), error);
log_error(category::audio, tr("Failed to create soxr resampler: {}. Input: {}; Output: {}; Channels: {}"), error, this->_input_rate, this->_output_rate, this->_channels);
}
}
}

View File

@ -34,6 +34,9 @@ namespace tc {
virtual bool valid() = 0;
virtual void finalize() = 0;
virtual void reset_encoder() = 0;
virtual void reset_decoder() = 0;
/**
* @return number of bytes written on success
*/

View File

@ -1,4 +1,5 @@
#include "OpusConverter.h"
#include "../../logger.h"
using namespace std;
using namespace tc::audio::codec;
@ -11,27 +12,57 @@ bool OpusConverter::valid() {
}
bool OpusConverter::initialize(std::string &error, int application_type) {
int error_id = 0;
lock_guard lock(this->coder_lock);
this->_application_type = application_type;
this->encoder = opus_encoder_create((opus_int32) this->_sample_rate, (int) this->_channels, application_type, &error_id);
if(!this->encoder || error_id) {
error = "failed to create encoder (" + to_string(error_id) + ")";
if(!this->_initialize_encoder(error))
return false;
}
this->decoder = opus_decoder_create((opus_int32) this->_sample_rate, (int) this->_channels, &error_id);
if(!this->encoder || error_id) {
opus_encoder_destroy(this->encoder);
this->encoder = nullptr;
error = "failed to create decoder (" + to_string(error_id) + ")";
if(!this->_initialize_decoder(error)) {
this->reset_encoder();
return false;
}
return true;
}
void OpusConverter::reset_encoder() {
lock_guard lock(this->coder_lock);
string error;
bool flag_error = false;
if(!(flag_error |= !this->_finalize_encoder(error))) {
error = "finalize failed (" + error + ")";
}
if(!flag_error && !(flag_error |= !this->_initialize_encoder(error))) {
error = "initialize failed (" + error + ")";
}
if(flag_error)
log_warn(category::audio, tr("Failed to reset opus encoder: {}"), error);
}
void OpusConverter::reset_decoder() {
lock_guard lock(this->coder_lock);
string error;
bool flag_error = false;
if(!(flag_error |= !this->_finalize_decoder(error))) {
error = "finalize failed (" + error + ")";
}
if(!flag_error && !(flag_error |= !this->_initialize_decoder(error))) {
error = "initialize failed (" + error + ")";
}
if(flag_error)
log_warn(category::audio, tr("Failed to reset opus decoder: {}"), error);
}
void OpusConverter::finalize() {
lock_guard lock(this->coder_lock);
@ -78,3 +109,45 @@ size_t OpusConverter::expected_encoded_length(size_t sample_count) {
//TODO calculate stuff
return 512;
}
bool OpusConverter::_initialize_decoder(std::string &error) {
if(!this->_finalize_decoder(error))
return false;
int error_id = 0;
this->decoder = opus_decoder_create((opus_int32) this->_sample_rate, (int) this->_channels, &error_id);
if(!this->encoder || error_id) {
error = "failed to create decoder (" + to_string(error_id) + ")";
return false;
}
return true;
}
bool OpusConverter::_initialize_encoder(std::string &error) {
if(!this->_finalize_encoder(error))
return false;
int error_id = 0;
this->encoder = opus_encoder_create((opus_int32) this->_sample_rate, (int) this->_channels, this->_application_type, &error_id);
if(!this->encoder || error_id) {
error = "failed to create encoder (" + to_string(error_id) + ")";
return false;
}
return true;
}
bool OpusConverter::_finalize_decoder(std::string &) {
if(this->decoder) {
opus_decoder_destroy(this->decoder);
this->decoder = nullptr;
}
return true;
}
bool OpusConverter::_finalize_encoder(std::string &) {
if(this->encoder) {
opus_encoder_destroy(this->encoder);
this->encoder = nullptr;
}
return true;
}

View File

@ -17,6 +17,9 @@ namespace tc {
bool initialize(std::string& /* error */, int /* application type */);
void finalize() override;
void reset_encoder() override;
void reset_decoder() override;
ssize_t encode(std::string & /* error */, const void * /* source */, void * /* target */, size_t /* target size */) override;
ssize_t decode(std::string & /* error */, const void */* source */, size_t /* source size */, void *pVoid1) override;
@ -27,6 +30,13 @@ namespace tc {
std::mutex coder_lock;
OpusDecoder* decoder = nullptr;
OpusEncoder* encoder = nullptr;
int _application_type = 0;
bool _finalize_encoder(std::string& /* error */);
bool _finalize_decoder(std::string& /* error */);
bool _initialize_encoder(std::string& /* error */);
bool _initialize_decoder(std::string& /* error */);
};
}
}

View File

@ -418,8 +418,6 @@ NAN_METHOD(ServerConnection::send_command) {
}
this->protocol_handler->send_command(cmd);
auto end = chrono::system_clock::now();
log_trace(category::general, "Needed {}ms for command building & sending", chrono::duration_cast<chrono::milliseconds>(end - begin).count());
log_trace(category::general, "Command: {}", cmd.build());
}
NAN_METHOD(ServerConnection::_send_voice_data) {
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->send_voice_data(info);

View File

@ -17,11 +17,14 @@ VoiceSender::~VoiceSender() {
this->clear_buffer(); /* buffer might be accessed within encode_raw_frame, but this could not be trigered while this will be deallocated! */
}
bool VoiceSender::initialize_codec(std::string& error, connection::codec::value codec, size_t channels, size_t rate) {
bool VoiceSender::initialize_codec(std::string& error, connection::codec::value codec, size_t channels, size_t rate, bool reset_decoder) {
auto& data = this->codec[codec];
bool new_allocated = !data;
if(new_allocated) data = make_unique<AudioCodec>();
data->successfully_initialized = false;
if(new_allocated) {
data = make_unique<AudioCodec>();
data->packet_counter = 0;
data->last_packet = chrono::system_clock::now();
}
auto info = codec::get_info(codec);
if(!info || !info->supported) {
@ -34,7 +37,10 @@ bool VoiceSender::initialize_codec(std::string& error, connection::codec::value
data->converter = info->new_converter(error);
if(!data->converter)
return false;
} else if(reset_decoder) {
data->converter->reset_encoder();
}
if(!data->resampler || data->resampler->input_rate() != rate)
data->resampler = make_shared<AudioResampler>(rate, data->converter->sample_rate(), data->converter->channels());
if(!data->resampler->valid()) {
@ -42,7 +48,6 @@ bool VoiceSender::initialize_codec(std::string& error, connection::codec::value
return false;
}
data->successfully_initialized = true;
return true;
}
@ -52,6 +57,8 @@ void VoiceSender::send_data(const void *data, size_t samples, size_t rate, size_
log_warn(category::voice_connection, tr("Dropping raw audio frame because of an invalid handle."));
return;
}
lock.unlock();
auto frame = make_unique<AudioFrame>();
frame->sample_rate = rate;
@ -59,23 +66,35 @@ void VoiceSender::send_data(const void *data, size_t samples, size_t rate, size_
frame->buffer = pipes::buffer{(void*) data, samples * channels * 4};
frame->timestamp = chrono::system_clock::now();
/*
{
lock_guard buffer_lock(this->raw_audio_buffer_lock);
this->raw_audio_buffers.push_back(move(frame));
}
audio::encode_event_loop->schedule(static_pointer_cast<event::EventEntry>(this->_ref.lock()));
*/
lock.unlock();
encode_raw_frame(frame);
}
void VoiceSender::send_stop() {
lock_guard lock(this->_execute_lock);
unique_lock lock(this->_execute_lock);
if(!this->handle) {
log_warn(category::voice_connection, tr("Dropping audio end frame because of an invalid handle."));
return;
}
lock.unlock();
auto server = this->handle->handle();
server->send_voice_data(nullptr, 0, this->_current_codec, false);
auto frame = make_unique<AudioFrame>();
frame->sample_rate = 0;
frame->channels = 0;
frame->buffer = pipes::buffer{nullptr, 0};
frame->timestamp = chrono::system_clock::now();
{
lock_guard buffer_lock(this->raw_audio_buffer_lock);
this->raw_audio_buffers.push_back(move(frame));
}
audio::encode_event_loop->schedule(static_pointer_cast<event::EventEntry>(this->_ref.lock()));
}
void VoiceSender::finalize() {
@ -114,15 +133,42 @@ void VoiceSender::encode_raw_frame(const std::unique_ptr<AudioFrame> &frame) {
auto codec = this->_current_codec;
auto& codec_data = this->codec[codec];
string error;
bool flag_head = true, flag_reset = true;
if(codec_data) {
if(codec_data->last_packet + chrono::seconds(1) < frame->timestamp)
codec_data->packet_counter = 0;
if(!this->initialize_codec(error, codec, frame->channels, frame->sample_rate)) {
flag_head = codec_data->packet_counter < 5;
flag_reset = codec_data->packet_counter == 0;
codec_data->packet_counter++;
codec_data->last_packet = frame->timestamp;
}
if(frame->channels == 0 || frame->sample_rate == 0 || frame->buffer.empty()) {
lock_guard lock(this->_execute_lock);
if(!this->handle) {
log_warn(category::voice_connection, tr("Dropping audio end because of an invalid handle."));
return;
}
if(codec_data)
codec_data->packet_counter = 0;
auto server = this->handle->handle();
server->send_voice_data(this->_buffer, 0, codec, flag_head);
return;
}
string error;
if(flag_reset) {
log_trace(category::voice_connection, tr("Resetting encoder for voice sender"));
}
if(!this->initialize_codec(error, codec, frame->channels, frame->sample_rate, flag_reset)) {
log_error(category::voice_connection, tr("Failed to initialize codec: {}"), error);
return;
}
/* TODO: May test for channel and sample rate? */
this->ensure_buffer(codec_data->resampler->estimated_output_size(frame->buffer.length()));
auto resampled_samples = codec_data->resampler->process(this->_buffer, frame->buffer.data_ptr(), frame->buffer.length() / frame->channels / 4);
if(resampled_samples <= 0) {
@ -144,6 +190,6 @@ void VoiceSender::encode_raw_frame(const std::unique_ptr<AudioFrame> &frame) {
}
auto server = this->handle->handle();
server->send_voice_data(this->_buffer, encoded_bytes, codec, false);
server->send_voice_data(this->_buffer, encoded_bytes, codec, flag_head);
}
}

View File

@ -36,13 +36,15 @@ namespace tc {
VoiceConnection* handle;
struct AudioCodec {
bool successfully_initialized;
size_t packet_counter = 0;
std::chrono::system_clock::time_point last_packet;
std::shared_ptr<audio::codec::Converter> converter;
std::shared_ptr<audio::AudioResampler> resampler;
};
std::array<std::unique_ptr<AudioCodec>, codec::MAX + 1> codec{nullptr};
bool initialize_codec(std::string&, codec::value /* codec */, size_t /* channels */, size_t /* source sample rate */);
bool initialize_codec(std::string&, codec::value /* codec */, size_t /* channels */, size_t /* source sample rate */, bool /* reset decoder */);
codec::value _current_codec = codec::OPUS_VOICE;

View File

@ -246,7 +246,7 @@ void VoiceClient::finalize_js_object() {
}
#define target_buffer_length 16384
void VoiceClient::process_packet(uint16_t packet_id, const pipes::buffer_view& buffer, codec::value codec) {
void VoiceClient::process_packet(uint16_t packet_id, const pipes::buffer_view& buffer, codec::value codec, bool head) {
if(this->_volume == 0)
return;
@ -255,17 +255,12 @@ void VoiceClient::process_packet(uint16_t packet_id, const pipes::buffer_view& b
return;
}
if(buffer.empty()) {
log_trace(category::voice_connection, tr("Stopping replay for client {}. Empty buffer!"), this->_client_id);
this->set_state(state::stopping);
return;
}
auto encoded_buffer = make_unique<EncodedBuffer>();
encoded_buffer->packet_id = packet_id;
encoded_buffer->codec = codec;
encoded_buffer->receive_timestamp = chrono::system_clock::now();
encoded_buffer->buffer = buffer.own_buffer();
encoded_buffer->head = head;
{
lock_guard lock(this->audio_decode_queue_lock);
@ -315,13 +310,13 @@ void VoiceClient::process_encoded_buffer(const std::unique_ptr<EncodedBuffer> &b
auto& codec_data = this->codec[buffer->codec];
auto info = codec::get_info(buffer->codec);
if(!info || !info->supported) {
log_warn(category::voice_connection, tr("Received voice packet from client {}, but we dont support it ({})"), this->_client_id, buffer->codec);
return;
}
if(!codec_data) {
auto info = codec::get_info(buffer->codec);
if(!info || !info->supported) {
log_warn(category::voice_connection, tr("Received voice packet from client {}, but we dont support it ({})"), this->_client_id, buffer->codec);
return;
}
auto instance = make_unique<AudioCodec>();
instance->successfully_initialized = false;
@ -357,6 +352,21 @@ void VoiceClient::process_encoded_buffer(const std::unique_ptr<EncodedBuffer> &b
} else {
diff = buffer->packet_id - codec_data->last_packet_id;
}
if(codec_data->last_packet_timestamp + chrono::seconds(1) < buffer->receive_timestamp)
diff = 0xFFFF;
const auto old_packet_id = codec_data->last_packet_id;
codec_data->last_packet_timestamp = buffer->receive_timestamp;
codec_data->last_packet_id = buffer->packet_id;
if(buffer->buffer.empty()) {
/* lets playpack the last samples and we're done */
this->set_state(state::stopping);
log_trace(category::voice_connection, tr("Stopping replay for client {}. Empty buffer!"), this->_client_id);
return;
}
if(diff == 0) {
//Duplicated packets
log_warn(category::audio, tr("Received voice packet with the same ID then the last one. Dropping packet."));
@ -366,20 +376,19 @@ void VoiceClient::process_encoded_buffer(const std::unique_ptr<EncodedBuffer> &b
if(diff <= MAX_LOST_PACKETS) {
if(diff > 0) {
log_debug(category::voice_connection, tr("Client {} dropped one or more audio packets. Old packet id: {}, New packet id: {}, Diff: {}"), this->_client_id, codec_data->last_packet_id, buffer->packet_id, diff);
log_debug(category::voice_connection, tr("Client {} dropped one or more audio packets. Old packet id: {}, New packet id: {}, Diff: {}"), this->_client_id, old_packet_id, buffer->packet_id, diff);
auto status = codec_data->converter->decode_lost(error, diff);
if(status < 0)
log_warn(category::voice_connection, tr("Failed to decode (skip) dropped packets. Return code {} => {}"), status, error);
}
} else {
log_debug(category::voice_connection, tr("Client {} reinitialized decoder. Old packet id: {}, New packet id: {}"), this->_client_id, codec_data->last_packet_id, buffer->packet_id);
codec_data->converter = info->new_converter(error);
log_debug(category::voice_connection, tr("Client {} resetted decoder. Old packet id: {}, New packet id: {}, diff: {}"), this->_client_id, old_packet_id, buffer->packet_id, diff);
codec_data->converter->reset_decoder();
if(!codec_data->converter) {
log_warn(category::voice_connection, tr("Failed to initialize new codec decoder {} for client {}: {}"), buffer->codec, this->_client_id, error);
log_warn(category::voice_connection, tr("Failed to reset codec decoder {} for client {}: {}"), buffer->codec, this->_client_id, error);
return;
}
}
codec_data->last_packet_id = buffer->packet_id;
char target_buffer[target_buffer_length];
if(target_buffer_length < codec_data->converter->expected_decoded_length(buffer->buffer.data_ptr(), buffer->buffer.length())) {

View File

@ -75,7 +75,7 @@ namespace tc {
inline std::shared_ptr<VoiceClient> ref() { return this->_ref.lock(); }
void process_packet(uint16_t packet_id, const pipes::buffer_view& /* buffer */, codec::value /* codec */);
void process_packet(uint16_t packet_id, const pipes::buffer_view& /* buffer */, codec::value /* codec */, bool /* head */);
inline float get_volume() { return this->_volume; }
inline void set_volume(float value) { this->_volume = value; }
@ -90,6 +90,8 @@ namespace tc {
private:
struct AudioCodec {
uint16_t last_packet_id = 0;
std::chrono::system_clock::time_point last_packet_timestamp;
bool successfully_initialized;
std::shared_ptr<audio::codec::Converter> converter;
std::shared_ptr<audio::AudioResampler> resampler;
@ -120,6 +122,7 @@ namespace tc {
}
struct EncodedBuffer {
bool head;
uint16_t packet_id;
pipes::buffer buffer;
codec::value codec;

View File

@ -296,7 +296,7 @@ void VoiceConnection::process_packet(const std::shared_ptr<ts::protocol::ServerP
auto packet_id = be2le16(&packet->data()[0]);
auto client_id = be2le16(&packet->data()[2]);
auto codec_id = (uint8_t) packet->data()[4];
//container->flag_head = packet->hasFlag(PacketFlag::Compressed);
auto flag_head = packet->hasFlag(PacketFlag::Compressed);
//container->voice_data = packet->data().length() > 5 ? packet->data().range(5) : pipes::buffer{};
auto client = this->find_client(client_id);
@ -306,9 +306,9 @@ void VoiceConnection::process_packet(const std::shared_ptr<ts::protocol::ServerP
}
if(packet->data().length() > 5)
client->process_packet(packet_id, packet->data().range(5), (codec::value) codec_id);
client->process_packet(packet_id, packet->data().range(5), (codec::value) codec_id, flag_head);
else
client->process_packet(packet_id, pipes::buffer_view{nullptr, 0}, (codec::value) codec_id);
client->process_packet(packet_id, pipes::buffer_view{nullptr, 0}, (codec::value) codec_id, flag_head);
} else {
//TODO implement whisper
}

View File

@ -1,6 +1,6 @@
/// <reference path="../../exports/exports.d.ts" />
module.paths.push("../../build/linux_amd64");
module.paths.push("../../build/linux_x64");
import * as fs from "fs";
import * as handle from "teaclient_connection";
@ -115,74 +115,15 @@ class Bot {
}
}
/*
const bot_list = [];
for(let index = 0; index < 20; index++) {
const bot = new Bot();
bot_list.push(bot);
bot.connect();
}
*/
/*
const run = async () => {
const connection = handle.spawn_server_connection();
connection.connect({
timeout: 5000,
remote_port: port,
remote_host: host,
callback: error => {
if(error == 0) {
connection.send_command("clientinit", [
{
"client_key_offset": 2030434,
//"client_version": "1.0.0",
//"client_platform": "nodejs/linux",
"client_version": "3.1.8 [Build: 1516614607]",
"client_platform": "Windows",
"client_version_sign": "gDEgQf/BiOQZdAheKccM1XWcMUj2OUQqt75oFuvF2c0MQMXyv88cZQdUuckKbcBRp7RpmLInto4PIgd7mPO7BQ==",
"client_nickname": "TeaClient Native Module Test",
"client_input_hardware":true,
"client_output_hardware":true,
"client_default_channel":"",
"client_default_channel_password":"",
"client_server_password":"",
"client_meta_data":"",
"client_nickname_phonetic":"",
"client_default_token":"",
"hwid":"123,456123123123",
return_code:91
}
], []);
} else {
console.log("Bot connect failed: %o (%s) ", error, connection.error_message(error));
}
},
identity_key: "MG4DAgeAAgEgAiBC9JsqB1am6vowj2obomMyxm1GLk8qyRoxpBkAdiVYxwIgWksaSk7eyVQovZwPZBuiYHARz/xQD5zBUBK6e63V7hICIQCZ2glHe3kV62iIRKpkV2lzZGZtfBPRMbwIcU9aE1EVsg==",
teamspeak: true
});
await new Promise(resolve => {
connection.callback_command = command => {
console.log("Having: %s", command);
if(command === "channellistfinished") {
connection.disconnect("XXXX", () => {
console.log("disconnected!");
resolve();
});
}
};
setTimeout(resolve, 5000);
});
};
(async () => {
while(true) {
await run();
}
})();
*/
import * as net from "net";
import * as tls from "tls";
import * as https from "https";
@ -202,3 +143,4 @@ const run = async () => {
}
};
setInterval(run, 10);
*/

View File

@ -20,18 +20,18 @@
"author": "TeaSpeak (WolverinDEV)",
"license": "ISC",
"devDependencies": {
"@types/ejs": "^2.6.0",
"@types/electron-packager": "^12.0.0",
"@types/fs-extra": "^5.0.4",
"@types/jquery": "^3.3.11",
"@types/request": "^2.47.1",
"@types/request-promise": "^4.1.42",
"@types/tar-stream": "^1.6.0",
"@types/ejs": "^2.6.3",
"@types/electron-packager": "^14.0.0",
"@types/fs-extra": "^8.0.0",
"@types/jquery": "^3.3.30",
"@types/request": "^2.48.1",
"@types/request-promise": "^4.1.44",
"@types/tar-stream": "^1.6.1",
"asar": "^2.0.1",
"cmake-js": "^4.0.1",
"ejs": "^2.6.1",
"nodemon": "^1.18.6",
"electron-packager": "^12.2.0"
"ejs": "^2.6.2",
"electron-packager": "^14.0.0",
"nodemon": "^1.19.1"
},
"dependencies": {
"@types/minimist": "^1.2.0",
@ -41,29 +41,29 @@
"assert-plus": "^1.0.0",
"aws-sign2": "^0.7.0",
"aws4": "^1.8.0",
"electron": "4.0.5",
"electron-rebuild": "^1.8.2",
"electron": "^5.0.6",
"electron-rebuild": "^1.8.5",
"extend": "^3.0.2",
"extsprintf": "^1.4.0",
"fs-extra": "^7.0.0",
"fs-extra": "^8.1.0",
"http-signature": "^1.2.0",
"jquery": "^3.3.1",
"jquery": "^3.4.1",
"json-stringify-safe": "^5.0.1",
"jsprim": "^2.0.0",
"jsrender": "^0.9.91",
"jsrender": "^1.0.3",
"nan": "^2.14.0",
"node-ssh": "^6.0.0",
"only": "0.0.2",
"psl": "^1.1.29",
"pure-uuid": "^1.5.4",
"psl": "^1.1.33",
"pure-uuid": "^1.5.7",
"request": "^2.47.1",
"request-progress": "^3.0.0",
"request-promise": "^4.2.2",
"request-promise": "^4.2.4",
"safe-buffer": "^5.1.2",
"safer-buffer": "^2.1.2",
"sshpk": "^1.14.2",
"tar-stream": "^1.6.2",
"tough-cookie": "^2.4.3"
"sshpk": "^1.16.1",
"tar-stream": "^2.1.0",
"tough-cookie": "^3.0.1"
},
"overrides": {
"os": {