Adjustments for the new client
This commit is contained in:
parent
ef7e5d5f66
commit
ea375bc07e
@ -1,7 +1,6 @@
|
||||
// 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;
|
||||
@ -9,6 +8,7 @@ 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";
|
||||
import {open_preview} from "./url-preview";
|
||||
|
||||
async function execute_app() {
|
||||
/* legacy, will be removed soon */
|
||||
@ -153,20 +153,6 @@ async function execute_app() {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {BrowserWindow, Menu, MenuItem, MessageBoxOptions, app, dialog} from "electron";
|
||||
import * as electron from "electron";
|
||||
import * as winmgr from "./window";
|
||||
import * as path from "path";
|
||||
|
||||
export let prevent_instant_close: boolean = true;
|
||||
export function set_prevent_instant_close(flag: boolean) {
|
||||
@ -13,93 +14,12 @@ 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.items[0].visible = false;
|
||||
}
|
||||
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");
|
||||
@ -112,17 +32,15 @@ function spawn_main_window(entry_point: string) {
|
||||
nodeIntegrationInWorker: true,
|
||||
nodeIntegration: true
|
||||
},
|
||||
icon: path.join(__dirname, "..", "..", "resources", "logo.ico")
|
||||
});
|
||||
|
||||
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', () => {
|
||||
require("./url-preview").close();
|
||||
main_window = null;
|
||||
prevent_instant_close = false;
|
||||
});
|
||||
@ -145,21 +63,8 @@ function spawn_main_window(entry_point: string) {
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
@ -245,6 +150,10 @@ export function execute() {
|
||||
|
||||
Menu.setApplicationMenu(null);
|
||||
init_listener();
|
||||
|
||||
console.log("Setting up render backend");
|
||||
require("./render-backend");
|
||||
|
||||
console.log("Spawn loading screen");
|
||||
loader.ui.execute_loader().then(async (entry_point: string) => {
|
||||
/* test if the updater may have an update found */
|
||||
|
22
modules/core/render-backend/index.ts
Normal file
22
modules/core/render-backend/index.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import "./menu";
|
||||
|
||||
import * as electron from "electron";
|
||||
import ipcMain = electron.ipcMain;
|
||||
import BrowserWindow = electron.BrowserWindow;
|
||||
|
||||
import {open as open_changelog} from "../app-updater/changelog";
|
||||
import * as updater from "../app-updater";
|
||||
|
||||
ipcMain.on('basic-action', (event, action, ...args: any[]) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
|
||||
if(action === "open-changelog") {
|
||||
open_changelog();
|
||||
} else if(action === "check-native-update") {
|
||||
updater.selected_channel().then(channel => updater.execute_graphical(channel, true));
|
||||
} else if(action === "open-dev-tools") {
|
||||
window.webContents.openDevTools();
|
||||
} else if(action === "reload-window") {
|
||||
window.reload();
|
||||
}
|
||||
});
|
34
modules/core/render-backend/menu.ts
Normal file
34
modules/core/render-backend/menu.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import * as electron from "electron";
|
||||
import ipcMain = electron.ipcMain;
|
||||
import BrowserWindow = electron.BrowserWindow;
|
||||
|
||||
ipcMain.on('top-menu', (event, menu_template: electron.MenuItemConstructorOptions[]) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
|
||||
const process_template = (item: electron.MenuItemConstructorOptions) => {
|
||||
if(typeof(item.icon) === "string" && item.icon.startsWith("data:"))
|
||||
item.icon = electron.nativeImage.createFromDataURL(item.icon);
|
||||
|
||||
item.click = () => window.webContents.send('top-menu', item.id);
|
||||
for(const i of item.submenu as electron.MenuItemConstructorOptions[] || []) {
|
||||
process_template(i);
|
||||
}
|
||||
};
|
||||
|
||||
for(const m of menu_template)
|
||||
process_template(m);
|
||||
|
||||
try {
|
||||
const menu = new electron.Menu();
|
||||
for(const m of menu_template) {
|
||||
try {
|
||||
menu.append(new electron.MenuItem(m));
|
||||
} catch(error) {
|
||||
console.error("Failed to build menu entry: %o\nSource: %o", error, m);
|
||||
}
|
||||
}
|
||||
window.setMenu(menu_template.length == 0 ? undefined : menu);
|
||||
} catch(error) {
|
||||
console.error("Failed to set window menu: %o", error);
|
||||
}
|
||||
});
|
@ -1,138 +0,0 @@
|
||||
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 {main_window} from "../main_window";
|
||||
import * as winmgr from "../window";
|
||||
|
||||
export interface UserData {
|
||||
session_id: string;
|
||||
username: string;
|
||||
|
||||
application_data: string;
|
||||
application_data_sign: string;
|
||||
}
|
||||
|
||||
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,
|
||||
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"));
|
||||
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);
|
||||
});
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
html {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.inner {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.inner-container {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
position: absolute;
|
||||
top: calc(50vh - 200px);
|
||||
left: calc(50vw - 200px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-family: Helvetica, serif;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.13);
|
||||
padding: 30px 0px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.box h1 {
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.box input {
|
||||
display: block;
|
||||
width: 300px;
|
||||
margin: 20px auto;
|
||||
padding: 15px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
color: #fff;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.box input:focus, .box input:active, .box button:focus, .box button:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.box button {
|
||||
background: #742ECC;
|
||||
border: 0;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
font-size: 20px;
|
||||
width: 330px;
|
||||
margin: 20px auto;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.box button:disabled {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.box button:active {
|
||||
background: #27ae60;
|
||||
}
|
||||
|
||||
.box p {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.box p span {
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.box .error {
|
||||
color: darkred;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#login {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#success {
|
||||
margin-top: 50px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=index.css.map */
|
@ -1 +0,0 @@
|
||||
{"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"}
|
@ -1,28 +0,0 @@
|
||||
<!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>
|
@ -1,84 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
import {UserData} from "../index";
|
||||
|
||||
(window as any).$ = 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
86
modules/core/url-preview/html/index.css
Normal file
86
modules/core/url-preview/html/index.css
Normal file
@ -0,0 +1,86 @@
|
||||
#nav-body-ctrls {
|
||||
background-color: #2a2a2a;
|
||||
padding: 20px;
|
||||
font-family: arial
|
||||
}
|
||||
|
||||
#nav-body-tabs {
|
||||
background: linear-gradient(#2a2a2a 75%, #404040);
|
||||
height: 36px;
|
||||
font-family: arial
|
||||
}
|
||||
|
||||
#nav-body-views {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.nav-icons {
|
||||
fill: #fcfcfc !important
|
||||
}
|
||||
|
||||
.nav-icons:hover {
|
||||
fill: #c2c2c2 !important
|
||||
}
|
||||
|
||||
#nav-ctrls-back, #nav-ctrls-forward, #nav-ctrls-reload {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
margin-right: 10px
|
||||
}
|
||||
|
||||
#nav-ctrls-url {
|
||||
box-shadow: 0 0;
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
height: 30px !important;
|
||||
margin-left: 8px;
|
||||
font-size: 11pt;
|
||||
outline: none;
|
||||
padding-left: 10px;
|
||||
color: #b7b7b7;
|
||||
background-color: #404040
|
||||
}
|
||||
|
||||
#nav-ctrls-url:focus {
|
||||
color: #fcfcfc;
|
||||
box-shadow: 0 0 5px #3d3d3d;
|
||||
}
|
||||
|
||||
#nav-tabs-add {
|
||||
margin: 5px
|
||||
}
|
||||
|
||||
.nav-tabs-tab {
|
||||
border-radius: 2px;
|
||||
height: 35px
|
||||
}
|
||||
|
||||
.nav-tabs-tab.active {
|
||||
background: #404040
|
||||
}
|
||||
|
||||
.nav-tabs-favicon {
|
||||
margin: 6px
|
||||
}
|
||||
|
||||
.nav-tabs-title {
|
||||
padding-left: 5px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
color: #fcfcfc
|
||||
}
|
||||
|
||||
.nav-tabs-title:hover {
|
||||
color: #c2c2c2
|
||||
}
|
||||
|
||||
.nav-tabs-close {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 6px;
|
||||
margin-left: 2px
|
||||
}
|
||||
|
||||
.nav-tabs-close:hover {
|
||||
fill: #dc143c !important
|
||||
}
|
34
modules/core/url-preview/html/index.html
Normal file
34
modules/core/url-preview/html/index.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>TeaClient - URL preview</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="./index.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="nav-body-ctrls">
|
||||
<!-- address -->
|
||||
</div>
|
||||
<div id="nav-body-tabs">
|
||||
<!-- tabs -->
|
||||
</div>
|
||||
<div id="nav-body-views">
|
||||
<!-- view -->
|
||||
</div>
|
||||
<script>
|
||||
let exports = {};
|
||||
</script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
78
modules/core/url-preview/html/index.ts
Normal file
78
modules/core/url-preview/html/index.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import * as electron from "electron";
|
||||
import * as path from "path";
|
||||
|
||||
interface Options {
|
||||
showBackButton: boolean,
|
||||
showForwardButton: boolean,
|
||||
showReloadButton: boolean,
|
||||
showUrlBar: boolean,
|
||||
showAddTabButton: boolean,
|
||||
closableTabs: boolean,
|
||||
verticalTabs: boolean,
|
||||
defaultFavicons: boolean,
|
||||
newTabCallback: (url: string, options: any) => any,
|
||||
changeTabCallback: () => any,
|
||||
newTabParams: any
|
||||
}
|
||||
|
||||
interface NewTabOptions {
|
||||
id: string,
|
||||
node: boolean,
|
||||
readonlyUrl: boolean,
|
||||
contextMenu: boolean,
|
||||
webviewAttributes: any,
|
||||
icon: "clean" | "default" | string,
|
||||
title: "default",
|
||||
close: boolean
|
||||
}
|
||||
|
||||
const enav = new (require('electron-navigation'))({
|
||||
closableTabs: true,
|
||||
showAddTabButton: false,
|
||||
defaultFavicons: true,
|
||||
|
||||
changeTabCallback: new_tab => {
|
||||
if(new_tab === undefined)
|
||||
window.close();
|
||||
}
|
||||
} as Options);
|
||||
|
||||
/* Required here: https://github.com/simply-coded/electron-navigation/blob/master/index.js#L364 */
|
||||
enav.executeJavaScript = () => {}; /* just to suppress an error cause by the API */
|
||||
|
||||
let _id_counter = 0;
|
||||
const execute_preview = (url: string) => {
|
||||
const id = "preview_" + (++_id_counter);
|
||||
const tab: HTMLElement & { executeJavaScript(js: string) : Promise<any> } = enav.newTab(url, {
|
||||
id: id,
|
||||
contextMenu: false,
|
||||
readonlyUrl: true,
|
||||
icon: "default",
|
||||
webviewAttributes: {
|
||||
'preload': path.join(__dirname, "inject.js")
|
||||
}
|
||||
} as NewTabOptions);
|
||||
|
||||
/* we only want to preload our script once */
|
||||
const show_preview = () => {
|
||||
tab.removeEventListener("dom-ready", show_preview);
|
||||
tab.removeAttribute("preload");
|
||||
|
||||
tab.executeJavaScript('__teaclient_preview_notice()').catch((error) => console.log("Failed to show TeaClient overlay! Error: %o", error));
|
||||
};
|
||||
|
||||
tab.addEventListener("dom-ready", show_preview);
|
||||
|
||||
tab.addEventListener('did-fail-load', (res: any) => {
|
||||
console.error("Side load failed: %o", res);
|
||||
if (res.errorCode != -3) {
|
||||
res.target.executeJavaScript('__teaclient_preview_error("' + res.errorCode + '", "' + encodeURIComponent(res.errorDescription) + '", "' + encodeURIComponent(res.validatedURL) + '")').catch(error => {
|
||||
console.warn("Failed to show error page: %o", error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
tab.addEventListener('close', () => enav.closeTab(id));
|
||||
};
|
||||
|
||||
electron.ipcRenderer.on('preview', (event, url) => execute_preview(url));
|
118
modules/core/url-preview/html/inject.ts
Normal file
118
modules/core/url-preview/html/inject.ts
Normal file
@ -0,0 +1,118 @@
|
||||
declare let __teaclient_preview_notice: () => any;
|
||||
declare let __teaclient_preview_error;
|
||||
|
||||
const electron = require("electron");
|
||||
const log_prefix = "[TeaSpeak::Preview] ";
|
||||
|
||||
const html_overlay =
|
||||
"<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>";
|
||||
|
||||
let _close_overlay: () => void;
|
||||
let _inject_overlay = () => {
|
||||
const element = document.createElement("div");
|
||||
element.id = "TeaClient-Overlay-Container";
|
||||
document.body.append(element);
|
||||
element.innerHTML = html_overlay;
|
||||
|
||||
{
|
||||
_close_overlay = () => {
|
||||
console.trace(log_prefix + "Closing preview notice");
|
||||
element.remove();
|
||||
};
|
||||
|
||||
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 = _close_overlay;
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
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");
|
||||
electron.remote.shell.openExternal(location.href, {
|
||||
activate: true
|
||||
}).catch(error => {
|
||||
console.warn(log_prefix + "Failed to open URL in browser window: %o", error);
|
||||
}).then(() => {
|
||||
window.close();
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* Put this into the global scope. But we dont leek some nodejs stuff! */
|
||||
console.log(log_prefix + "Script loaded waiting to be called!");
|
||||
__teaclient_preview_notice = () => {
|
||||
if(_inject_overlay) {
|
||||
console.log(log_prefix + "TeaClient overlay called. Showing overlay.");
|
||||
_inject_overlay();
|
||||
} else {
|
||||
console.warn(log_prefix + "TeaClient overlay called, but overlay method undefined. May an load error occured?");
|
||||
}
|
||||
};
|
||||
|
||||
const html_error = (error_code, error_desc, url) =>
|
||||
"<div style='background-color: whitesmoke; padding: 40px; margin: 20px; font-family: consolas,serif;'>" +
|
||||
"<h2 align=center>Oops, this page failed to load correctly.</h2>" +
|
||||
"<p align=center><i>ERROR [ " + error_code + ", " + error_desc + " ]</i></p>" +
|
||||
'<br/><hr/>' +
|
||||
'<h4>Try this</h4>' +
|
||||
'<li type=circle>Check your spelling - <b>"' + url + '".</b></li><br/>' +
|
||||
'<li type=circle><a href="javascript:location.reload();">Refresh</a> the page.</li><br/>' +
|
||||
'<li type=circle>Perform a <a href=javascript:location.href="https://www.google.com/search?q=' + url + '">search</a> instead.</li><br/>' +
|
||||
"</div>";
|
||||
|
||||
__teaclient_preview_error = (error_code, error_desc, url) => {
|
||||
document.body.innerHTML = html_error(decodeURIComponent(error_code), decodeURIComponent(error_desc), decodeURIComponent(url));
|
||||
_inject_overlay = undefined;
|
||||
if(_close_overlay) _close_overlay();
|
||||
};
|
@ -1,36 +1,85 @@
|
||||
import * as electron from "electron";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as winmgr from "../window";
|
||||
|
||||
let global_window: electron.BrowserWindow;
|
||||
let global_window_promise: Promise<void>;
|
||||
|
||||
export async function close() {
|
||||
while(global_window_promise) {
|
||||
try {
|
||||
await global_window_promise;
|
||||
break;
|
||||
} catch(error) {} /* error will be already logged */
|
||||
}
|
||||
if(global_window) {
|
||||
global_window.close();
|
||||
global_window = undefined;
|
||||
global_window_promise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function open_preview(url: string) {
|
||||
console.log("Open URL as preview: %s", url);
|
||||
const window = new electron.BrowserWindow({
|
||||
while(global_window_promise) {
|
||||
try {
|
||||
await global_window_promise;
|
||||
break;
|
||||
} catch(error) {} /* error will be already logged */
|
||||
}
|
||||
if(!global_window) {
|
||||
global_window_promise = (async () => {
|
||||
global_window = new electron.BrowserWindow({
|
||||
webPreferences: {
|
||||
webSecurity: true,
|
||||
nodeIntegration: false,
|
||||
nodeIntegrationInWorker: false,
|
||||
allowRunningInsecureContent: false,
|
||||
nodeIntegration: true,
|
||||
webviewTag: true
|
||||
},
|
||||
skipTaskbar: true,
|
||||
center: true,
|
||||
show: false,
|
||||
});
|
||||
await winmgr.apply_bounds('url-preview', window);
|
||||
winmgr.track_bounds('url-preview', window);
|
||||
window.setMenu(null);
|
||||
|
||||
window.loadURL(url).then(() => {
|
||||
//window.webContents.openDevTools();
|
||||
global_window.setMenuBarVisibility(false);
|
||||
global_window.setMenu(null);
|
||||
global_window.loadFile(path.join(__dirname, "html", "index.html")).then(() => {
|
||||
//global_window.webContents.openDevTools();
|
||||
});
|
||||
global_window.on('close', event => {
|
||||
global_window = undefined;
|
||||
});
|
||||
|
||||
//FIXME try catch?
|
||||
const inject_file = path.join(path.dirname(module.filename), "inject.js");
|
||||
try {
|
||||
await winmgr.apply_bounds('url-preview', global_window);
|
||||
winmgr.track_bounds('url-preview', global_window);
|
||||
|
||||
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);
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject("timeout"), 5000);
|
||||
global_window.on('ready-to-show', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} catch(error) {
|
||||
console.warn("Failed to initialize preview window. Dont show preview! Error: %o", error);
|
||||
throw "failed to initialize";
|
||||
}
|
||||
|
||||
global_window.show();
|
||||
})();
|
||||
try {
|
||||
await global_window_promise;
|
||||
} catch(error) {
|
||||
console.log("Failed to create preview window! Error: %o", error);
|
||||
try {
|
||||
global_window.close();
|
||||
} finally {
|
||||
global_window = undefined;
|
||||
}
|
||||
global_window_promise = undefined;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
global_window.webContents.send('preview', url);
|
||||
if(!global_window.isFocused())
|
||||
global_window.focus();
|
||||
}
|
||||
|
||||
electron.ipcMain.on('preview-action', (event, args) => {
|
||||
|
@ -1,78 +0,0 @@
|
||||
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
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
/// <reference path="../imports/imports_shared.d.ts" />
|
||||
window["require_setup"](module);
|
||||
|
||||
import {audio as naudio} from "teaclient_connection";
|
||||
import {audio, tr} from "../imports/imports_shared";
|
||||
|
||||
export namespace _audio.recorder {
|
||||
import InputDevice = audio.recorder.InputDevice;
|
||||
@ -20,8 +20,9 @@ export namespace _audio.recorder {
|
||||
default_input: e.input_default,
|
||||
supported: e.input_supported,
|
||||
name: e.name,
|
||||
driver: e.driver,
|
||||
sample_rate: 44100, /* TODO! */
|
||||
device_index: e.device_index
|
||||
device_index: e.device_index,
|
||||
} as NativeDevice
|
||||
}));
|
||||
}
|
||||
@ -67,7 +68,6 @@ export namespace _audio.recorder {
|
||||
get(): any {
|
||||
return this._callback_level;
|
||||
}, set(v: any): void {
|
||||
console.log("SET CALLBACK LEVEL! %o", v);
|
||||
if(v === this._callback_level)
|
||||
return;
|
||||
|
||||
@ -272,9 +272,25 @@ export namespace _audio.recorder {
|
||||
|
||||
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! */
|
||||
await new Promise((resolve, reject) => {
|
||||
this.handle.set_device(device ? device.device_index : -1, flag => {
|
||||
if(typeof(flag) === "boolean" && flag)
|
||||
resolve();
|
||||
else
|
||||
reject("failed to set device" + (typeof(flag) === "string" ? (": " + flag) : ""));
|
||||
});
|
||||
});
|
||||
if(!device) return;
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
this.handle.start(flag => {
|
||||
if(flag)
|
||||
resolve();
|
||||
else
|
||||
reject("start failed");
|
||||
});
|
||||
});
|
||||
} catch(error) {
|
||||
console.warn(tr("Failed to start playback on new input device (%o)"), error);
|
||||
throw error;
|
||||
@ -345,7 +361,7 @@ export namespace _audio.recorder {
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
async start(): Promise<audio.recorder.InputStartResult> {
|
||||
try {
|
||||
await this.stop();
|
||||
} catch(error) {
|
||||
@ -368,10 +384,18 @@ export namespace _audio.recorder {
|
||||
};
|
||||
}
|
||||
|
||||
this.handle.start();
|
||||
await new Promise((resolve, reject) => {
|
||||
this.handle.start(flag => {
|
||||
if(flag)
|
||||
resolve();
|
||||
else
|
||||
reject("start failed");
|
||||
});
|
||||
});
|
||||
for(const filter of this.filters)
|
||||
if(filter.is_enabled())
|
||||
filter.initialize();
|
||||
return audio.recorder.InputStartResult.EOK;
|
||||
} catch(error) {
|
||||
this._current_state = audio.recorder.InputState.PAUSED;
|
||||
throw error;
|
||||
@ -386,6 +410,93 @@ export namespace _audio.recorder {
|
||||
this.callback_end();
|
||||
this._current_state = audio.recorder.InputState.PAUSED;
|
||||
}
|
||||
|
||||
get_volume(): number {
|
||||
return this.handle.get_volume();
|
||||
}
|
||||
|
||||
set_volume(volume: number) {
|
||||
this.handle.set_volume(volume);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create_levelmeter(device: InputDevice) : Promise<audio.recorder.LevelMeter> {
|
||||
const meter = new NativeLevelmenter(device as any);
|
||||
await meter.initialize();
|
||||
return meter;
|
||||
}
|
||||
|
||||
class NativeLevelmenter implements audio.recorder.LevelMenter {
|
||||
readonly _device: NativeDevice;
|
||||
|
||||
private _callback: (num: number) => any;
|
||||
private _recorder: naudio.record.AudioRecorder;
|
||||
private _consumer: naudio.record.AudioConsumer;
|
||||
private _filter: naudio.record.ThresholdConsumeFilter;
|
||||
|
||||
constructor(device: NativeDevice) {
|
||||
this._device = device;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
this._recorder = naudio.record.create_recorder();
|
||||
this._consumer = this._recorder.create_consumer();
|
||||
|
||||
this._filter = this._consumer.create_filter_threshold(.5);
|
||||
this._filter.set_attack_smooth(.75);
|
||||
this._filter.set_release_smooth(.75);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
this._recorder.set_device(this._device.device_index, flag => {
|
||||
if(typeof(flag) === "boolean" && flag)
|
||||
resolve();
|
||||
else
|
||||
reject("initialize failed" + (typeof(flag) === "string" ? (": " + flag) : ""));
|
||||
});
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
this._recorder.start(flag => {
|
||||
if(flag)
|
||||
resolve();
|
||||
else
|
||||
reject("start failed");
|
||||
});
|
||||
});
|
||||
} catch(error) {
|
||||
if(typeof(error) === "string")
|
||||
throw error;
|
||||
console.warn(tr("Failed to initialize levelmeter for device %o: %o"), this._device, error);
|
||||
throw "initialize failed (lookup console)";
|
||||
}
|
||||
|
||||
/* references this variable, needs a destory() call, else memory leak */
|
||||
this._filter.set_analyze_filter(value => {
|
||||
(this._callback || (() => {}))(value);
|
||||
});
|
||||
}
|
||||
|
||||
destory() {
|
||||
if(this._filter) {
|
||||
this._filter.set_analyze_filter(undefined);
|
||||
this._consumer.unregister_filter(this._filter);
|
||||
}
|
||||
if(this._consumer)
|
||||
this._recorder.delete_consumer(this._consumer);
|
||||
this._recorder.stop();
|
||||
this._recorder.set_device(-1, () => {}); /* -1 := No device */
|
||||
this._recorder = undefined;
|
||||
this._consumer = undefined;
|
||||
this._filter = undefined;
|
||||
}
|
||||
|
||||
device(): audio.recorder.InputDevice {
|
||||
return this._device;
|
||||
}
|
||||
|
||||
set_observer(callback: (value: number) => any) {
|
||||
this._callback = callback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,8 @@ namespace _transfer {
|
||||
status: 200,
|
||||
statusText: "success",
|
||||
headers: {
|
||||
"X-media-bytes": base64ArrayBuffer(buffer)
|
||||
"X-media-bytes": base64_encode_ab(buffer)
|
||||
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
@ -284,6 +284,12 @@ export namespace _connection {
|
||||
});
|
||||
return this._command_handler_default.proxy_command_promise(promise, options);
|
||||
}
|
||||
|
||||
ping(): { native: number; javascript?: number } {
|
||||
return {
|
||||
native: this._native_handle ? (this._native_handle.current_ping() / 1000) : -2
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -299,5 +305,12 @@ export namespace _connection {
|
||||
console.log("Spawning native connection");
|
||||
return new native.ServerConnection(handle); /* will be overridden by the client */
|
||||
}
|
||||
|
||||
export function destroy_server_connection(handle: connection.AbstractServerConnection) {
|
||||
if(!(handle instanceof native.ServerConnection))
|
||||
throw "invalid handle";
|
||||
//TODO: Here!
|
||||
console.log("Call to destroy a server connection");
|
||||
}
|
||||
}
|
||||
Object.assign(window["connection"] || (window["connection"] = {}), _connection);
|
@ -1,3 +1,5 @@
|
||||
import {class_to_image} from "./icon-helper";
|
||||
|
||||
window["require_setup"](module);
|
||||
|
||||
import * as electron from "electron";
|
||||
@ -5,14 +7,11 @@ 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() {
|
||||
@ -21,72 +20,22 @@ class ElectronContextMenu implements contextmenu.ContextMenuProvider {
|
||||
this._current_menu.closePopup();
|
||||
this._current_menu = undefined;
|
||||
|
||||
for(const listener of this._close_listeners)
|
||||
for(const listener of this._close_listeners) {
|
||||
if(listener) {
|
||||
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 {
|
||||
@ -107,7 +56,7 @@ class ElectronContextMenu implements contextmenu.ContextMenuProvider {
|
||||
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),
|
||||
icon: class_to_image(entry.icon_class),
|
||||
visible: entry.visible,
|
||||
enabled: !entry.disabled
|
||||
});
|
||||
@ -128,7 +77,7 @@ class ElectronContextMenu implements contextmenu.ContextMenuProvider {
|
||||
type: "checkbox",
|
||||
checked: !!entry.checkbox_checked,
|
||||
click: click_callback,
|
||||
icon: this.class_to_image(entry.icon_class),
|
||||
icon: class_to_image(entry.icon_class),
|
||||
visible: entry.visible,
|
||||
enabled: !entry.disabled
|
||||
});
|
||||
@ -146,7 +95,7 @@ class ElectronContextMenu implements contextmenu.ContextMenuProvider {
|
||||
type: "submenu",
|
||||
submenu: sub_menu,
|
||||
click: click_callback,
|
||||
icon: this.class_to_image(entry.icon_class),
|
||||
icon: class_to_image(entry.icon_class),
|
||||
visible: entry.visible,
|
||||
enabled: !entry.disabled
|
||||
});
|
||||
|
63
modules/renderer/icon-helper.ts
Normal file
63
modules/renderer/icon-helper.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import * as electron from "electron";
|
||||
import NativeImage = electron.NativeImage;
|
||||
|
||||
let _div: JQuery;
|
||||
let _icon_mash_url: string;
|
||||
let _icon_mask_img: NativeImage;
|
||||
let _cache_klass_map: {[key: string]: NativeImage};
|
||||
|
||||
export function class_to_image(klass: string) : NativeImage {
|
||||
if(!klass || !_icon_mask_img || !_cache_klass_map)
|
||||
return undefined;
|
||||
|
||||
if(_cache_klass_map[klass])
|
||||
return _cache_klass_map[klass];
|
||||
|
||||
_div[0].classList.value = 'icon ' + klass;
|
||||
const data = window.getComputedStyle(_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 _cache_klass_map[klass] = _icon_mask_img.crop({
|
||||
height: 16,
|
||||
width: 16,
|
||||
x: offset_x == 0 ? 0 : -offset_x,
|
||||
y: offset_y == 0 ? 0 : -offset_y
|
||||
});
|
||||
}
|
||||
|
||||
export async function initialize() {
|
||||
if(!_div) {
|
||||
_div = $.spawn("div");
|
||||
_div.css('display', 'none');
|
||||
_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);
|
||||
|
||||
_cache_klass_map = {};
|
||||
_icon_mash_url = canvas.toDataURL();
|
||||
_icon_mask_img = electron.remote.nativeImage.createFromDataURL(_icon_mash_url);
|
||||
}
|
||||
|
||||
export function finalize() {
|
||||
_icon_mask_img = undefined;
|
||||
_icon_mash_url = undefined;
|
||||
_cache_klass_map = undefined;
|
||||
}
|
1065
modules/renderer/imports/.copy_imports_shared.d.ts
vendored
1065
modules/renderer/imports/.copy_imports_shared.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@ -170,17 +170,15 @@ const module_loader_setup = async () => {
|
||||
window["require_setup"] = _mod => {
|
||||
if(!_mod || !_mod.paths) return;
|
||||
|
||||
console.dir(_mod);
|
||||
|
||||
_mod.paths.push(...native_paths);
|
||||
const org_req = _mod.__proto__.require;
|
||||
const original_require = _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.require = (path: string) => {
|
||||
if(path.endsWith("imports/imports_shared")) {
|
||||
console.log("Proxy require for %s. Using 'window' as result.", path);
|
||||
return window;
|
||||
}
|
||||
return original_require.apply(_mod, [path]);
|
||||
};
|
||||
_mod.proxied = true;
|
||||
}
|
||||
@ -202,16 +200,24 @@ const load_modules = async () => {
|
||||
console.log("Loading native extensions...");
|
||||
try {
|
||||
try {
|
||||
require("./ppt");
|
||||
require("./version");
|
||||
} catch(error) {
|
||||
console.error("Failed to load ppt");
|
||||
console.error("Failed to load version extension");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
require("./version");
|
||||
const helper = require("./icon-helper");
|
||||
await helper.initialize();
|
||||
} catch(error) {
|
||||
console.error("Failed to load version extension");
|
||||
console.error("Failed to load the icon helper extension");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
require("./ppt");
|
||||
} catch(error) {
|
||||
console.error("Failed to load ppt");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
@ -236,6 +242,13 @@ const load_modules = async () => {
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
require("./menu");
|
||||
} catch(error) {
|
||||
console.error("Failed to load menu extension");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
require("./context-menu");
|
||||
} catch(error) {
|
||||
|
251
modules/renderer/menu.ts
Normal file
251
modules/renderer/menu.ts
Normal file
@ -0,0 +1,251 @@
|
||||
import {class_to_image} from "./icon-helper";
|
||||
|
||||
window["require_setup"](module);
|
||||
|
||||
import * as electron from "electron";
|
||||
import {top_menu as dtop_menu, Icon} from "./imports/imports_shared";
|
||||
|
||||
namespace _top_menu {
|
||||
import ipcRenderer = electron.ipcRenderer;
|
||||
namespace native {
|
||||
import ipcRenderer = electron.ipcRenderer;
|
||||
let _item_index = 1;
|
||||
|
||||
abstract class NativeMenuBase {
|
||||
protected _handle: NativeMenuBar;
|
||||
protected _click: () => any;
|
||||
id: string;
|
||||
|
||||
protected constructor(handle: NativeMenuBar, id?: string) {
|
||||
this._handle = handle;
|
||||
this.id = id || ("item_" + (_item_index++));
|
||||
}
|
||||
|
||||
abstract build() : electron.MenuItemConstructorOptions;
|
||||
abstract items(): (dtop_menu.MenuItem | dtop_menu.HRItem)[];
|
||||
|
||||
trigger_click() {
|
||||
if(this._click)
|
||||
this._click();
|
||||
}
|
||||
}
|
||||
|
||||
class NativeMenuItem extends NativeMenuBase implements dtop_menu.MenuItem {
|
||||
private _items: (NativeMenuItem | NativeHrItem)[] = [];
|
||||
private _label: string;
|
||||
private _enabled: boolean = true;
|
||||
private _visible: boolean = true;
|
||||
|
||||
private _icon_data: string;
|
||||
|
||||
constructor(handle: NativeMenuBar) {
|
||||
super(handle);
|
||||
|
||||
}
|
||||
|
||||
append_hr(): dtop_menu.HRItem {
|
||||
const item = new NativeHrItem(this._handle);
|
||||
this._items.push(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
append_item(label: string): dtop_menu.MenuItem {
|
||||
const item = new NativeMenuItem(this._handle);
|
||||
item.label(label);
|
||||
this._items.push(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
click(callback: () => any): this {
|
||||
this._click = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
delete_item(item: dtop_menu.MenuItem | dtop_menu.HRItem) {
|
||||
const i_index = this._items.indexOf(item as any);
|
||||
if(i_index < 0) return;
|
||||
this._items.splice(i_index, 1);
|
||||
}
|
||||
|
||||
disabled(value?: boolean): boolean {
|
||||
if(typeof(value) === "boolean")
|
||||
this._enabled = !value;
|
||||
return !this._enabled;
|
||||
}
|
||||
|
||||
icon(klass?: string | Promise<Icon> | Icon): string {
|
||||
if(typeof(klass) === "string") {
|
||||
const buffer = class_to_image(klass);
|
||||
if(buffer)
|
||||
this._icon_data = buffer.toDataURL();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
items(): (dtop_menu.MenuItem | dtop_menu.HRItem)[] {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
label(value?: string): string {
|
||||
if(typeof(value) === "string")
|
||||
this._label = value;
|
||||
return this._label;
|
||||
}
|
||||
|
||||
visible(value?: boolean): boolean {
|
||||
if(typeof(value) === "boolean")
|
||||
this._visible = value;
|
||||
return this._visible;
|
||||
}
|
||||
|
||||
build(): Electron.MenuItemConstructorOptions {
|
||||
return {
|
||||
id: this.id,
|
||||
|
||||
label: this._label || "",
|
||||
|
||||
submenu: this._items.length > 0 ? this._items.map(e => e.build()) : undefined,
|
||||
enabled: this._enabled,
|
||||
visible: this._visible,
|
||||
|
||||
icon: this._icon_data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NativeHrItem extends NativeMenuBase implements dtop_menu.HRItem {
|
||||
constructor(handle: NativeMenuBar) {
|
||||
super(handle);
|
||||
}
|
||||
|
||||
build(): Electron.MenuItemConstructorOptions {
|
||||
return {
|
||||
type: 'separator',
|
||||
id: this.id
|
||||
}
|
||||
}
|
||||
|
||||
items(): (dtop_menu.MenuItem | dtop_menu.HRItem)[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function is_similar_deep(a, b) {
|
||||
if(typeof(a) !== typeof(b))
|
||||
return false;
|
||||
if(typeof(a) !== "object")
|
||||
return a === b;
|
||||
|
||||
const aProps = Object.keys(a);
|
||||
const bProps = Object.keys(b);
|
||||
|
||||
if (aProps.length != bProps.length)
|
||||
return false;
|
||||
|
||||
for (let i = 0; i < aProps.length; i++) {
|
||||
const propName = aProps[i];
|
||||
|
||||
if(!is_similar_deep(a[propName], b[propName]))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
export class NativeMenuBar implements dtop_menu.MenuBarDriver {
|
||||
private static _instance: NativeMenuBar;
|
||||
|
||||
private menu: electron.Menu;
|
||||
private _items: NativeMenuItem[] = [];
|
||||
private _current_menu: electron.MenuItemConstructorOptions[];
|
||||
|
||||
public static instance() : NativeMenuBar {
|
||||
if(!this._instance)
|
||||
this._instance = new NativeMenuBar();
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
append_item(label: string): dtop_menu.MenuItem {
|
||||
const item = new NativeMenuItem(this);
|
||||
item.label(label);
|
||||
this._items.push(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
delete_item(item: dtop_menu.MenuItem) {
|
||||
const i_index = this._items.indexOf(item as any);
|
||||
if(i_index < 0) return;
|
||||
this._items.splice(i_index, 1);
|
||||
}
|
||||
|
||||
flush_changes() {
|
||||
const target_menu = this.build_menu();
|
||||
if(is_similar_deep(target_menu, this._current_menu))
|
||||
return;
|
||||
|
||||
this._current_menu = target_menu;
|
||||
ipcRenderer.send('top-menu', target_menu);
|
||||
}
|
||||
|
||||
private build_menu() : electron.MenuItemConstructorOptions[] {
|
||||
return this._items.map(e => e.build());
|
||||
}
|
||||
|
||||
items(): dtop_menu.MenuItem[] {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.menu = new electron.remote.Menu();
|
||||
ipcRenderer.on('top-menu', (event, clicked_item) => {
|
||||
console.log("Item %o clicked", clicked_item);
|
||||
const check_item = (item: NativeMenuBase) => {
|
||||
if(item.id == clicked_item) {
|
||||
item.trigger_click();
|
||||
return true;
|
||||
}
|
||||
for(const child of item.items())
|
||||
if(check_item(child as NativeMenuBase))
|
||||
return true;
|
||||
};
|
||||
|
||||
for(const item of this._items)
|
||||
if(check_item(item))
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Global variable
|
||||
// @ts-ignore
|
||||
top_menu.set_driver(native.NativeMenuBar.instance());
|
||||
|
||||
|
||||
const call_basic_action = (name: string, ...args: any[]) => ipcRenderer.send('basic-action', name, ...args);
|
||||
top_menu.native_actions = {
|
||||
open_change_log() {
|
||||
call_basic_action("open-changelog");
|
||||
},
|
||||
|
||||
check_native_update() {
|
||||
call_basic_action("check-native-update");
|
||||
},
|
||||
|
||||
quit() {
|
||||
call_basic_action("quit");
|
||||
},
|
||||
|
||||
open_dev_tools() {
|
||||
call_basic_action("open-dev-tools");
|
||||
},
|
||||
|
||||
reload_page() {
|
||||
call_basic_action("reload-window")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export {};
|
@ -92,7 +92,7 @@ namespace _ppt {
|
||||
current_state.code = event.key_code;
|
||||
|
||||
for(const hook of key_hooks) {
|
||||
if(hook.key_code != event.key_code) continue;
|
||||
if(hook.key_code && 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;
|
||||
|
@ -1,5 +1,6 @@
|
||||
set(MODULE_NAME "teaclient_connection")
|
||||
|
||||
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -static-libasan -lasan -lubsan")
|
||||
set(SOURCE_FILES
|
||||
src/logger.cpp
|
||||
src/EventLoop.cpp
|
||||
|
10
native/serverconnection/exports/exports.d.ts
vendored
10
native/serverconnection/exports/exports.d.ts
vendored
@ -69,6 +69,9 @@ declare module "teaclient_connection" {
|
||||
send_command(command: string, arguments: any[], switches: string[]);
|
||||
send_voice_data(buffer: Uint8Array, codec_id: number, header: boolean);
|
||||
send_voice_data_raw(buffer: Float32Array, channels: number, sample_rate: number, header: boolean);
|
||||
|
||||
/* ping in microseconds */
|
||||
current_ping() : number;
|
||||
}
|
||||
|
||||
export function spawn_server_connection() : NativeServerConnection;
|
||||
@ -223,12 +226,15 @@ declare module "teaclient_connection" {
|
||||
|
||||
export interface AudioRecorder {
|
||||
get_device() : number;
|
||||
set_device(device: number); /* Recorder needs to be started afterwards */
|
||||
set_device(device: number, callback: (flag: boolean | string) => void); /* Recorder needs to be started afterwards */
|
||||
|
||||
start();
|
||||
start(callback: (flag: boolean) => void);
|
||||
started() : boolean;
|
||||
stop();
|
||||
|
||||
get_volume() : number;
|
||||
set_volume(volume: number);
|
||||
|
||||
create_consumer() : AudioConsumer;
|
||||
get_consumers() : AudioConsumer[];
|
||||
delete_consumer(consumer: AudioConsumer);
|
||||
|
@ -21,10 +21,10 @@ AudioConsumer::AudioConsumer(tc::audio::AudioInput *handle, size_t channel_count
|
||||
|
||||
void AudioConsumer::handle_framed_data(const void *buffer, size_t samples) {
|
||||
unique_lock read_callback_lock(this->on_read_lock);
|
||||
if(!this->on_read)
|
||||
return;
|
||||
auto function = this->on_read; /* copy */
|
||||
read_callback_lock.unlock();
|
||||
if(!function)
|
||||
return;
|
||||
|
||||
function(buffer, samples);
|
||||
}
|
||||
@ -52,8 +52,10 @@ bool AudioInput::open_device(std::string& error, PaDeviceIndex index) {
|
||||
|
||||
this->close_device();
|
||||
this->_current_device_index = index;
|
||||
this->_current_device = Pa_GetDeviceInfo(index);
|
||||
if(index == paNoDevice)
|
||||
return true;
|
||||
|
||||
this->_current_device = Pa_GetDeviceInfo(index);
|
||||
if(!this->_current_device) {
|
||||
this->_current_device_index = paNoDevice;
|
||||
error = "failed to get device info";
|
||||
@ -141,9 +143,22 @@ int AudioInput::_audio_callback(const void *a, void *b, unsigned long c, const P
|
||||
int AudioInput::audio_callback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags) {
|
||||
if (!input) /* hmmm.. suspicious */
|
||||
return 0;
|
||||
|
||||
if(this->_volume != 1) {
|
||||
auto ptr = (float*) input;
|
||||
auto left = frameCount * this->_channel_count;
|
||||
while(left-- > 0)
|
||||
*(ptr++) *= this->_volume;
|
||||
}
|
||||
|
||||
auto begin = chrono::system_clock::now();
|
||||
for(const auto& consumer : this->consumers()) {
|
||||
consumer->process_data(input, frameCount);
|
||||
}
|
||||
|
||||
auto end = chrono::system_clock::now();
|
||||
auto ms = chrono::duration_cast<chrono::milliseconds>(end - begin).count();
|
||||
if(ms > 5) {
|
||||
log_warn(category::audio, tr("Processing of audio input needed {}ms. This could be an issue!"), chrono::duration_cast<chrono::milliseconds>(end - begin).count());
|
||||
}
|
||||
return 0;
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
#include <iostream>
|
||||
#include <functional>
|
||||
#include <portaudio.h>
|
||||
#include <misc/spin_lock.h>
|
||||
#include "AudioSamples.h"
|
||||
|
||||
namespace tc {
|
||||
@ -23,7 +24,7 @@ namespace tc {
|
||||
|
||||
size_t const frame_size = 0;
|
||||
|
||||
std::mutex on_read_lock; /* locked to access the function */
|
||||
spin_lock on_read_lock; /* locked to access the function */
|
||||
std::function<void(const void* /* buffer */, size_t /* samples */)> on_read;
|
||||
private:
|
||||
AudioConsumer(AudioInput* handle, size_t channel_count, size_t sample_rate, size_t frame_size);
|
||||
|
@ -86,7 +86,7 @@ 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() + (int) (this->sample_buffers.size() / 2));
|
||||
this->sample_buffers.erase(this->sample_buffers.begin(), this->sample_buffers.begin() + (int) ceil(this->sample_buffers.size() / 2));
|
||||
break;
|
||||
case overflow_strategy::ignore:
|
||||
break;
|
||||
|
@ -36,8 +36,10 @@ void Reframer::process(const void *source, size_t samples) {
|
||||
}
|
||||
}
|
||||
|
||||
auto _on_frame = this->on_frame;
|
||||
while(samples > this->_frame_size) {
|
||||
this->on_frame(source);
|
||||
if(_on_frame)
|
||||
_on_frame(source);
|
||||
samples -= this->_frame_size;
|
||||
source = (char*) source + this->_frame_size * this->_channels * 4;
|
||||
}
|
||||
|
@ -40,7 +40,9 @@ AudioConsumerWrapper::AudioConsumerWrapper(AudioRecorderWrapper* h, const std::s
|
||||
handle->on_read = [&](const void* buffer, size_t length){ this->process_data(buffer, length); };
|
||||
}
|
||||
|
||||
//this->_recorder->js_ref(); /* FML FIXME: Mem leak! (In general the consumer live is related to the recorder handle) */
|
||||
#ifdef DO_DEADLOCK_REF
|
||||
this->_recorder->js_ref(); /* FML Mem leak! (In general the consumer live is related to the recorder handle, but for nodejs testing we want to keep this reference ) */
|
||||
#endif
|
||||
}
|
||||
|
||||
AudioConsumerWrapper::~AudioConsumerWrapper() {
|
||||
@ -53,8 +55,10 @@ AudioConsumerWrapper::~AudioConsumerWrapper() {
|
||||
this->_handle = nullptr;
|
||||
}
|
||||
|
||||
#ifdef DO_DEADLOCK_REF
|
||||
if(this->_recorder)
|
||||
this->_recorder->js_unref();
|
||||
#endif
|
||||
}
|
||||
|
||||
void AudioConsumerWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
|
||||
|
@ -77,7 +77,7 @@ namespace tc {
|
||||
void do_wrap(const v8::Local<v8::Object>& /* object */);
|
||||
|
||||
void unbind(); /* called with execute_lock locked */
|
||||
void process_data(const void* /* buffer */, size_t /* samples */); /* TODO: Lock the execute_lock! */
|
||||
void process_data(const void* /* buffer */, size_t /* samples */);
|
||||
|
||||
struct DataEntry {
|
||||
void* buffer = nullptr;
|
||||
@ -96,7 +96,6 @@ namespace tc {
|
||||
Nan::callback_t<> _call_ended;
|
||||
Nan::callback_t<> _call_started;
|
||||
/*
|
||||
*
|
||||
callback_data: (buffer: Float32Array) => any;
|
||||
callback_ended: () => any;
|
||||
*/
|
||||
|
@ -68,6 +68,8 @@ AudioFilterWrapper::~AudioFilterWrapper() {
|
||||
auto threshold_filter = dynamic_pointer_cast<filter::ThresholdFilter>(this->_filter);
|
||||
if(threshold_filter)
|
||||
threshold_filter->on_analyze = nullptr;
|
||||
|
||||
this->_callback_analyzed.Reset();
|
||||
}
|
||||
|
||||
void AudioFilterWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
|
||||
|
@ -34,6 +34,9 @@ NAN_MODULE_INIT(AudioRecorderWrapper::Init) {
|
||||
Nan::SetPrototypeMethod(klass, "started", AudioRecorderWrapper::_started);
|
||||
Nan::SetPrototypeMethod(klass, "stop", AudioRecorderWrapper::_stop);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_volume", AudioRecorderWrapper::_get_volume);
|
||||
Nan::SetPrototypeMethod(klass, "set_volume", AudioRecorderWrapper::_set_volume);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_consumers", AudioRecorderWrapper::_get_consumers);
|
||||
Nan::SetPrototypeMethod(klass, "create_consumer", AudioRecorderWrapper::_create_consumer);
|
||||
Nan::SetPrototypeMethod(klass, "delete_consumer", AudioRecorderWrapper::_delete_consumer);
|
||||
@ -126,28 +129,69 @@ NAN_METHOD(AudioRecorderWrapper::_set_device) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
auto input = handle->_input;
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
if(info.Length() != 2 || !info[0]->IsNumber() || !info[1]->IsFunction()) {
|
||||
Nan::ThrowError("invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
auto device_id = info[0]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
|
||||
unique_ptr<Nan::Persistent<v8::Function>> _callback = make_unique<Nan::Persistent<v8::Function>>(info[1].As<v8::Function>());
|
||||
unique_ptr<Nan::Persistent<v8::Object>> _recorder = make_unique<Nan::Persistent<v8::Object>>(info.Holder());
|
||||
|
||||
auto _async_callback = Nan::async_callback([call = std::move(_callback), recorder = move(_recorder)](bool result, std::string error) mutable {
|
||||
Nan::HandleScope scope;
|
||||
auto callback_function = call->Get(Nan::GetCurrentContext()->GetIsolate());
|
||||
|
||||
v8::Local<v8::Value> argv[1];
|
||||
if(result)
|
||||
argv[0] = v8::Boolean::New(Nan::GetCurrentContext()->GetIsolate(), result);
|
||||
else
|
||||
argv[0] = Nan::NewOneByteString((uint8_t*) error.data(), error.length()).ToLocalChecked();
|
||||
callback_function->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, argv);
|
||||
|
||||
recorder->Reset();
|
||||
call->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
std::thread([_async_callback, input, device_id]{
|
||||
string error;
|
||||
if(!input->open_device(error, device_id)) {
|
||||
Nan::ThrowError(Nan::New<v8::String>("failed to open device (" + error + ")").ToLocalChecked());
|
||||
return;
|
||||
}
|
||||
auto flag = input->open_device(error, device_id);
|
||||
_async_callback(std::forward<bool>(flag), std::forward<std::string>(error));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::_start) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
auto input = handle->_input;
|
||||
|
||||
if(!input->record()) {
|
||||
Nan::ThrowError("failed to start");
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("missing callback");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!info[0]->IsFunction()) {
|
||||
Nan::ThrowError("not a function");
|
||||
return;
|
||||
}
|
||||
|
||||
unique_ptr<Nan::Persistent<v8::Function>> _callback = make_unique<Nan::Persistent<v8::Function>>(info[0].As<v8::Function>());
|
||||
unique_ptr<Nan::Persistent<v8::Object>> _recorder = make_unique<Nan::Persistent<v8::Object>>(info.Holder());
|
||||
|
||||
auto _async_callback = Nan::async_callback([call = std::move(_callback), recorder = move(_recorder)](bool result) mutable {
|
||||
Nan::HandleScope scope;
|
||||
auto callback_function = call->Get(Nan::GetCurrentContext()->GetIsolate());
|
||||
|
||||
v8::Local<v8::Value> argv[1];
|
||||
argv[0] = v8::Boolean::New(Nan::GetCurrentContext()->GetIsolate(), result);
|
||||
callback_function->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, argv);
|
||||
|
||||
recorder->Reset();
|
||||
call->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
auto input = handle->_input;
|
||||
std::thread([_async_callback, input]{
|
||||
_async_callback(input->record());
|
||||
}).detach();
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::_started) {
|
||||
@ -204,3 +248,19 @@ NAN_METHOD(AudioRecorderWrapper::_delete_consumer) {
|
||||
auto consumer = ObjectWrap::Unwrap<AudioConsumerWrapper>(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked());
|
||||
handle->delete_consumer(consumer);
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::_set_volume) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
handle->_input->set_volume(info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::_get_volume) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
info.GetReturnValue().Set(handle->_input->volume());
|
||||
}
|
@ -38,6 +38,9 @@ namespace tc {
|
||||
static NAN_METHOD(_get_consumers);
|
||||
static NAN_METHOD(_delete_consumer);
|
||||
|
||||
static NAN_METHOD(_set_volume);
|
||||
static NAN_METHOD(_get_volume);
|
||||
|
||||
std::shared_ptr<AudioConsumerWrapper> create_consumer();
|
||||
void delete_consumer(const AudioConsumerWrapper*);
|
||||
|
||||
|
@ -444,7 +444,7 @@ bool ProtocolHandler::create_datagram_packets(std::vector<pipes::buffer> &result
|
||||
} else {
|
||||
packet->applyPacketId(this->_packet_id_manager);
|
||||
}
|
||||
log_trace(category::connection, tr("Packet {} got packet id {}"), packet->type().name(), packet->packetId());
|
||||
//log_trace(category::connection, tr("Packet {} got packet id {}"), packet->type().name(), packet->packetId());
|
||||
}
|
||||
if(!this->crypt_handler.progressPacketOut(packet.get(), error, false)) {
|
||||
log_error(category::connection, tr("Failed to encrypt packet: {}"), error);
|
||||
|
@ -75,6 +75,8 @@ namespace tc {
|
||||
|
||||
ecc_key& get_identity_key() { return this->crypto.identity; }
|
||||
|
||||
inline std::chrono::microseconds current_ping() { return this->ping.value; }
|
||||
|
||||
connection_state::value connection_state = connection_state::INITIALIZING;
|
||||
server_type::value server_type = server_type::TEASPEAK;
|
||||
private:
|
||||
|
@ -60,29 +60,6 @@ void ProtocolHandler::handlePacketCommand(const std::shared_ptr<ts::protocol::Se
|
||||
|
||||
void ProtocolHandler::handlePacketVoice(const std::shared_ptr<ts::protocol::ServerPacket> &packet) {
|
||||
this->handle->voice_connection->process_packet(packet);
|
||||
|
||||
/*
|
||||
if(packet->type() == PacketTypeInfo::Voice) {
|
||||
if(packet->data().length() < 5) {
|
||||
//TODO log invalid voice packet
|
||||
return;
|
||||
}
|
||||
auto container = make_unique<ServerConnection::VoicePacket>();
|
||||
container->packet_id = be2le16(&packet->data()[0]);
|
||||
container->client_id = be2le16(&packet->data()[2]);
|
||||
container->codec_id = (uint8_t) packet->data()[4];
|
||||
container->flag_head = packet->hasFlag(PacketFlag::Compressed);
|
||||
container->voice_data = packet->data().length() > 5 ? packet->data().range(5) : pipes::buffer{};
|
||||
|
||||
{
|
||||
lock_guard lock(this->handle->pending_voice_lock);
|
||||
this->handle->pending_voice.push_back(move(container));
|
||||
}
|
||||
this->handle->execute_pending_voice.call(true);
|
||||
} else {
|
||||
//TODO implement whisper
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
void ProtocolHandler::handlePacketPing(const std::shared_ptr<ts::protocol::ServerPacket> &packet) {
|
||||
|
@ -58,6 +58,7 @@ NAN_MODULE_INIT(ServerConnection::Init) {
|
||||
Nan::SetPrototypeMethod(klass, "send_command", ServerConnection::_send_command);
|
||||
Nan::SetPrototypeMethod(klass, "send_voice_data", ServerConnection::_send_voice_data);
|
||||
Nan::SetPrototypeMethod(klass, "send_voice_data_raw", ServerConnection::_send_voice_data_raw);
|
||||
Nan::SetPrototypeMethod(klass, "current_ping", ServerConnection::_current_ping);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
@ -642,3 +643,12 @@ void ServerConnection::_execute_callback_disconnect(const std::string &reason) {
|
||||
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, arguments);
|
||||
|
||||
}
|
||||
|
||||
NAN_METHOD(ServerConnection::_current_ping) {
|
||||
auto connection = ObjectWrap::Unwrap<ServerConnection>(info.Holder());
|
||||
auto& phandler = connection->protocol_handler;
|
||||
if(phandler)
|
||||
info.GetReturnValue().Set((uint32_t) chrono::floor<microseconds>(phandler->current_ping()).count());
|
||||
else
|
||||
info.GetReturnValue().Set(-1);
|
||||
}
|
@ -85,6 +85,7 @@ namespace tc {
|
||||
static NAN_METHOD(_send_voice_data);
|
||||
static NAN_METHOD(_send_voice_data_raw);
|
||||
static NAN_METHOD(_error_message);
|
||||
static NAN_METHOD(_current_ping);
|
||||
|
||||
std::unique_ptr<Nan::Callback> callback_connect;
|
||||
std::unique_ptr<Nan::Callback> callback_disconnect;
|
||||
|
@ -208,12 +208,27 @@ VoiceClientWrap::~VoiceClientWrap() {}
|
||||
|
||||
VoiceClient::VoiceClient(const std::shared_ptr<VoiceConnection>&, uint16_t client_id) : _client_id(client_id) {
|
||||
this->output_source = global_audio_output->create_source();
|
||||
this->output_source->overflow_strategy = audio::overflow_strategy::discard_buffer_half;
|
||||
this->output_source->max_latency = (size_t) ceil(this->output_source->sample_rate * 0.12);
|
||||
this->output_source->overflow_strategy = audio::overflow_strategy::ignore;
|
||||
this->output_source->max_latency = (size_t) ceil(this->output_source->sample_rate * 1);
|
||||
this->output_source->min_buffer = (size_t) ceil(this->output_source->sample_rate * 0.025);
|
||||
|
||||
this->output_source->on_underflow = [&]{
|
||||
if(this->_state == state::stopping)
|
||||
this->set_state(state::stopped);
|
||||
else if(this->_state != state::stopped) {
|
||||
if(this->_last_received_packet + chrono::seconds(1) < chrono::system_clock::now()) {
|
||||
this->set_state(state::stopped);
|
||||
log_warn(category::audio, tr("Client {} has a audio buffer underflow and not received any data for one second. Stopping replay."), this->_client_id);
|
||||
} else {
|
||||
if(this->_state != state::buffering) {
|
||||
log_warn(category::audio, tr("Client {} has a audio buffer underflow. Buffer again."), this->_client_id);
|
||||
this->set_state(state::buffering);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
this->output_source->on_overflow = [&](size_t count){
|
||||
log_warn(category::audio, tr("Client {} has a audio buffer overflow of {}."), this->_client_id, count);
|
||||
};
|
||||
}
|
||||
|
||||
@ -265,6 +280,7 @@ void VoiceClient::process_packet(uint16_t packet_id, const pipes::buffer_view& b
|
||||
encoded_buffer->buffer = buffer.own_buffer();
|
||||
encoded_buffer->head = head;
|
||||
|
||||
this->_last_received_packet = encoded_buffer->receive_timestamp;
|
||||
{
|
||||
lock_guard lock(this->audio_decode_queue_lock);
|
||||
this->audio_decode_queue.push_back(move(encoded_buffer));
|
||||
@ -356,7 +372,7 @@ void VoiceClient::process_encoded_buffer(const std::unique_ptr<EncodedBuffer> &b
|
||||
diff = buffer->packet_id - codec_data->last_packet_id;
|
||||
}
|
||||
|
||||
if(codec_data->last_packet_timestamp + chrono::seconds(1) < buffer->receive_timestamp)
|
||||
if(codec_data->last_packet_timestamp + chrono::seconds(1) < buffer->receive_timestamp || this->_state >= state::stopping)
|
||||
diff = 0xFFFF;
|
||||
|
||||
const auto old_packet_id = codec_data->last_packet_id;
|
||||
@ -427,6 +443,7 @@ void VoiceClient::process_encoded_buffer(const std::unique_ptr<EncodedBuffer> &b
|
||||
}
|
||||
|
||||
auto enqueued = this->output_source->enqueue_samples(target_buffer, resampled_samples);
|
||||
if(enqueued != resampled_samples)
|
||||
log_warn(category::voice_connection, tr("Failed to enqueue all samples for client {}. Enqueued {} of {}"), this->_client_id, enqueued, resampled_samples);
|
||||
this->set_state(state::playing);
|
||||
//cout << "Enqueued " << enqueued << " samples" << endl;
|
||||
}
|
@ -112,6 +112,7 @@ namespace tc {
|
||||
uint16_t _client_id;
|
||||
float _volume = 1.f;
|
||||
|
||||
std::chrono::system_clock::time_point _last_received_packet;
|
||||
state::value _state = state::stopped;
|
||||
inline void set_state(state::value value) {
|
||||
if(value == this->_state)
|
||||
|
@ -299,6 +299,7 @@ void VoiceConnection::process_packet(const std::shared_ptr<ts::protocol::ServerP
|
||||
auto flag_head = packet->has_flag(PacketFlag::Compressed);
|
||||
//container->voice_data = packet->data().length() > 5 ? packet->data().range(5) : pipes::buffer{};
|
||||
|
||||
//log_info(category::voice_connection, tr("Received voice packet from {}. Packet ID: {}"), client_id, packet_id);
|
||||
auto client = this->find_client(client_id);
|
||||
if(!client) {
|
||||
log_warn(category::voice_connection, tr("Received voice packet from unknown client {}. Dropping packet!"), client_id);
|
||||
|
@ -1,7 +1,12 @@
|
||||
/// <reference path="../../exports/exports.d.ts" />
|
||||
console.log("HELLO WORLD");
|
||||
module.paths.push("../../build/linux_x64");
|
||||
module.paths.push("../../build/win32_64");
|
||||
|
||||
//LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libasan.so.5
|
||||
const os = require('os');
|
||||
//process.dlopen(module, '/usr/lib/x86_64-linux-gnu/libasan.so.5',
|
||||
// os.constants.dlopen.RTLD_NOW);
|
||||
import * as fs from "fs";
|
||||
|
||||
const original_require = require;
|
||||
@ -118,8 +123,8 @@ const do_connect = () => {
|
||||
timeout: 5000,
|
||||
remote_port: 9987,
|
||||
//remote_host: "188.40.240.20", /* twerion */
|
||||
remote_host: "localhost",
|
||||
//remote_host: "ts.teaspeak.de",
|
||||
//remote_host: "localhost",
|
||||
remote_host: "ts.teaspeak.de",
|
||||
//remote_host: "51.68.181.92",
|
||||
//remote_host: "94.130.236.135",
|
||||
//remote_host: "54.36.232.11", /* the beast */
|
||||
@ -160,11 +165,14 @@ const do_connect = () => {
|
||||
console.log(">> Audio end");
|
||||
};
|
||||
|
||||
connection._voice_connection.set_audio_source(consumer);
|
||||
/*
|
||||
consumer.callback_data = buffer => {
|
||||
console.log("Sending voice data");
|
||||
connection.send_voice_data_raw(buffer, consumer.channels, consumer.sample_rate, true);
|
||||
//stream.write_data_rated(buffer.buffer, true, consumer.sample_rate);
|
||||
};
|
||||
*/
|
||||
}
|
||||
},
|
||||
|
||||
@ -173,12 +181,15 @@ const do_connect = () => {
|
||||
});
|
||||
|
||||
connection.callback_voice_data = (buffer, client_id, codec_id, flag_head, packet_id) => {
|
||||
console.log("Having data!");
|
||||
connection.send_voice_data(buffer, codec_id, flag_head);
|
||||
}
|
||||
};
|
||||
|
||||
connection.callback_command = (command, arguments1, switches) => {
|
||||
console.log("Command: %s", command);
|
||||
}
|
||||
|
||||
connection._voice_connection.register_client(7);
|
||||
};
|
||||
do_connect();
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
"crash_handler": "electron . crash-handler",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "electron --js-flags='--expose-gc' --debug --dev-tools --disable-hardware-acceleration .",
|
||||
"start-d": "electron . --debug -t --gdb -su http://dev.clientapi.teaspeak.de/",
|
||||
"start-d": "electron . --disable-hardware-acceleration --debug -t -su http://dev.clientapi.teaspeak.de/",
|
||||
"start-d1": "electron . --disable-hardware-acceleration --debug -t --gdb -su http://clientapi.teaspeak.de/ --updater-ui-loader_type=0",
|
||||
"start-n": "electron . -t --disable-hardware-acceleration --no-single-instance -u=https://clientapi.teaspeak.de/ -d --updater-ui-loader_type=0",
|
||||
"start-01": "electron . --updater-channel=test -u=http://dev.clientapi.teaspeak.de/ -d --updater-ui-loader_type=0 --updater-local-version=1.0.1",
|
||||
@ -48,6 +48,7 @@
|
||||
"aws4": "^1.8.0",
|
||||
"electron": "5.0.6",
|
||||
"electron-installer-windows": "^1.1.1",
|
||||
"electron-navigation": "^1.5.8",
|
||||
"electron-rebuild": "^1.8.5",
|
||||
"electron-winstaller": "^2.7.0",
|
||||
"electron-wix-msi": "^2.1.1",
|
||||
|
Loading…
Reference in New Issue
Block a user