A mass of updates (Better client communication and speaker selection)
parent
a24ee83716
commit
bc4d274705
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$UI_BASE_PATH = "ui-files/";
|
$UI_BASE_PATH = "ui-files/";
|
||||||
|
$UI_RAW_BASE_PATH = "ui-files/raw/";
|
||||||
$CLIENT_BASE_PATH = "files/";
|
$CLIENT_BASE_PATH = "files/";
|
||||||
|
|
||||||
if(!isset($_SERVER['REQUEST_METHOD'])) {
|
if(!isset($_SERVER['REQUEST_METHOD'])) {
|
||||||
|
@ -46,10 +47,7 @@
|
||||||
include $name;
|
include $name;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$file = fopen($name, "r") or die(json_encode([
|
$file = fopen($name, "r") or error_exit("missing file \"" . $name . "\".");
|
||||||
"success" => false,
|
|
||||||
"error" => "missing file (" . $name . ")"
|
|
||||||
]));
|
|
||||||
|
|
||||||
echo (fread($file, filesize($name)));
|
echo (fread($file, filesize($name)));
|
||||||
fclose($file);
|
fclose($file);
|
||||||
|
@ -64,7 +62,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function handle_develop_web_request() {
|
function handle_develop_web_request() {
|
||||||
global $UI_BASE_PATH;
|
global $UI_RAW_BASE_PATH;
|
||||||
|
|
||||||
if(isset($_GET) && isset($_GET["type"])) {
|
if(isset($_GET) && isset($_GET["type"])) {
|
||||||
if($_GET["type"] === "files") {
|
if($_GET["type"] === "files") {
|
||||||
|
@ -73,7 +71,7 @@
|
||||||
/* header("mode: develop"); */
|
/* header("mode: develop"); */
|
||||||
|
|
||||||
echo ("type\thash\tpath\tname\n");
|
echo ("type\thash\tpath\tname\n");
|
||||||
foreach (list_dir($UI_BASE_PATH) as $file) {
|
foreach (list_dir($UI_RAW_BASE_PATH) as $file) {
|
||||||
$type_idx = strrpos($file, ".");
|
$type_idx = strrpos($file, ".");
|
||||||
$type = substr($file, $type_idx + 1);
|
$type = substr($file, $type_idx + 1);
|
||||||
if($type == "php") $type = "html";
|
if($type == "php") $type = "html";
|
||||||
|
@ -85,13 +83,13 @@
|
||||||
$name_idx = strrpos($name, ".");
|
$name_idx = strrpos($name, ".");
|
||||||
$name = substr($name, 0, $name_idx);
|
$name = substr($name, 0, $name_idx);
|
||||||
|
|
||||||
echo $type . "\t" . sha1_file($UI_BASE_PATH . $file) . "\t" . $path . "\t" . $name . "." . $type . "\n";
|
echo $type . "\t" . sha1_file($UI_RAW_BASE_PATH . $file) . "\t" . $path . "\t" . $name . "." . $type . "\n";
|
||||||
}
|
}
|
||||||
die;
|
die;
|
||||||
} else if($_GET["type"] === "file") {
|
} else if($_GET["type"] === "file") {
|
||||||
header("Content-Type: text/plain");
|
header("Content-Type: text/plain");
|
||||||
|
|
||||||
$path = realpath($UI_BASE_PATH . $_GET["path"]);
|
$path = realpath($UI_RAW_BASE_PATH . $_GET["path"]);
|
||||||
$name = $_GET["name"];
|
$name = $_GET["name"];
|
||||||
if($path === False || strpos($path, realpath(".")) === False || strpos($name, "/") !== False) error_exit("Invalid file");
|
if($path === False || strpos($path, realpath(".")) === False || strpos($name, "/") !== False) error_exit("Invalid file");
|
||||||
|
|
||||||
|
@ -132,10 +130,64 @@
|
||||||
header("Content-Transfer-Encoding: Binary");
|
header("Content-Transfer-Encoding: Binary");
|
||||||
header("Content-Length:".filesize($path . $platform->update));
|
header("Content-Length:".filesize($path . $platform->update));
|
||||||
header("Content-Disposition: attachment; filename=update.tar.gz");
|
header("Content-Disposition: attachment; filename=update.tar.gz");
|
||||||
|
header("info-version: 1");
|
||||||
readfile($path . $platform->update);
|
readfile($path . $platform->update);
|
||||||
die();
|
die();
|
||||||
}
|
}
|
||||||
error_exit("Missing platform, arch or file");
|
error_exit("Missing platform, arch or file");
|
||||||
|
} else if ($_GET["type"] == "ui-info") {
|
||||||
|
global $UI_BASE_PATH;
|
||||||
|
|
||||||
|
$version_info = file_get_contents($UI_BASE_PATH . "info.json");
|
||||||
|
if($version_info === false) $version_info = array();
|
||||||
|
else $version_info = json_decode($version_info, true);
|
||||||
|
|
||||||
|
$info = array();
|
||||||
|
$info["success"] = true;
|
||||||
|
$info["versions"] = array();
|
||||||
|
|
||||||
|
foreach($version_info as $channel => $data) {
|
||||||
|
if(!isset($data["latest"])) continue;
|
||||||
|
|
||||||
|
$channel_info = [
|
||||||
|
"timestamp" => $data["latest"]["timestamp"],
|
||||||
|
"version" => $data["latest"]["version"],
|
||||||
|
"git-ref" => $data["latest"]["git-ref"],
|
||||||
|
"channel" => $channel
|
||||||
|
];
|
||||||
|
array_push($info["versions"], $channel_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode($info));
|
||||||
|
} else if ($_GET["type"] == "ui-download") {
|
||||||
|
global $UI_BASE_PATH;
|
||||||
|
|
||||||
|
if(!isset($_GET["git-ref"]) || !isset($_GET["channel"]) || !isset($_GET["version"]))
|
||||||
|
error_exit("missing required parameters");
|
||||||
|
|
||||||
|
$version_info = file_get_contents($UI_BASE_PATH . "info.json");
|
||||||
|
if($version_info === false) $version_info = array();
|
||||||
|
else $version_info = json_decode($version_info, true);
|
||||||
|
|
||||||
|
if(!isset($version_info[$_GET["channel"]]))
|
||||||
|
error_exit("missing channel");
|
||||||
|
|
||||||
|
foreach ($version_info[$_GET["channel"]]["history"] as $entry) {
|
||||||
|
if($entry["version"] == $_GET["version"] && $entry["git-ref"] == $_GET["git-ref"]) {
|
||||||
|
header("Cache-Control: public"); // needed for internet explorer
|
||||||
|
header("Content-Type: application/binary");
|
||||||
|
header("Content-Transfer-Encoding: Binary");
|
||||||
|
header("Content-Disposition: attachment; filename=ui.tar.gz");
|
||||||
|
header("info-version: 1");
|
||||||
|
$read = readfile($entry["file"]);
|
||||||
|
header("Content-Length:" . $read);
|
||||||
|
|
||||||
|
if($read === false) error_exit("internal error: Failed to read file!");
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_exit("missing version");
|
||||||
}
|
}
|
||||||
} else if($_POST["type"] == "deploy-build") {
|
} else if($_POST["type"] == "deploy-build") {
|
||||||
global $CLIENT_BASE_PATH;
|
global $CLIENT_BASE_PATH;
|
||||||
|
@ -158,7 +210,7 @@
|
||||||
if($_FILES["installer"]["error"] !== UPLOAD_ERR_OK) error_exit("Upload for installer failed!");
|
if($_FILES["installer"]["error"] !== UPLOAD_ERR_OK) error_exit("Upload for installer failed!");
|
||||||
|
|
||||||
$json_version = json_decode($_POST["version"], true);
|
$json_version = json_decode($_POST["version"], true);
|
||||||
$version = $json_version["major"] . "." . $json_version["minor"] . "." . $json_version["patch"] . ($json_version["build"] > 0 ? $json_version["build"] : "");
|
$version = $json_version["major"] . "." . $json_version["minor"] . "." . $json_version["patch"] . ($json_version["build"] > 0 ? "-" . $json_version["build"] : "");
|
||||||
$path = $CLIENT_BASE_PATH . DIRECTORY_SEPARATOR . $_POST["channel"] . DIRECTORY_SEPARATOR . $version . DIRECTORY_SEPARATOR;
|
$path = $CLIENT_BASE_PATH . DIRECTORY_SEPARATOR . $_POST["channel"] . DIRECTORY_SEPARATOR . $version . DIRECTORY_SEPARATOR;
|
||||||
exec("mkdir -p " . $path);
|
exec("mkdir -p " . $path);
|
||||||
//mkdir($path, 777, true);
|
//mkdir($path, 777, true);
|
||||||
|
@ -216,6 +268,55 @@
|
||||||
move_uploaded_file($_FILES["installer"]["tmp_name"],$path . $filename_install);
|
move_uploaded_file($_FILES["installer"]["tmp_name"],$path . $filename_install);
|
||||||
move_uploaded_file($_FILES["update"]["tmp_name"],$path . $filename_update);
|
move_uploaded_file($_FILES["update"]["tmp_name"],$path . $filename_update);
|
||||||
|
|
||||||
|
die(json_encode([
|
||||||
|
"success" => true
|
||||||
|
]));
|
||||||
|
} else if($_POST["type"] == "deploy-ui-build") {
|
||||||
|
global $UI_BASE_PATH;
|
||||||
|
|
||||||
|
if(!isset($_POST["secret"]) || !isset($_POST["channel"]) || !isset($_POST["version"]) || !isset($_POST["git_ref"]))
|
||||||
|
error_exit("Missing required information!");
|
||||||
|
|
||||||
|
$path = $UI_BASE_PATH . DIRECTORY_SEPARATOR;
|
||||||
|
$channeled_path = $UI_BASE_PATH . DIRECTORY_SEPARATOR . $_POST["channel"];
|
||||||
|
$filename = "TeaClientUI-" . $_POST["version"] . "_" . $_POST["git_ref"] . ".tar.gz";
|
||||||
|
exec("mkdir -p " . $path);
|
||||||
|
exec("mkdir -p " . $channeled_path);
|
||||||
|
|
||||||
|
{
|
||||||
|
$require_secret = file_get_contents(".deploy_secret");
|
||||||
|
if($require_secret === false || strlen($require_secret) == 0) error_exit("Server missing secret!");
|
||||||
|
|
||||||
|
error_log($_POST["secret"]);
|
||||||
|
error_log(trim($require_secret));
|
||||||
|
if(!is_string($_POST["secret"])) error_exit("Invalid secret!");
|
||||||
|
if(strcmp(trim($require_secret), trim($_POST["secret"])) !== 0)
|
||||||
|
error_exit("Secret does not match!");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
$info = file_get_contents($path . "info.json");
|
||||||
|
if($info === false) $info = array();
|
||||||
|
else $info = json_decode($info, true);
|
||||||
|
|
||||||
|
$channel_info = &$info[$_POST["channel"]];
|
||||||
|
if(!$channel_info) $channel_info = array();
|
||||||
|
|
||||||
|
$entry = [
|
||||||
|
"timestamp" => time(),
|
||||||
|
"file" => $channeled_path . DIRECTORY_SEPARATOR . $filename,
|
||||||
|
"version" => $_POST["version"],
|
||||||
|
"git-ref" => $_POST["git_ref"]
|
||||||
|
];
|
||||||
|
|
||||||
|
$channel_info["latest"] = $entry;
|
||||||
|
if(!$channel_info["history"]) $channel_info["history"] = array();
|
||||||
|
array_push($channel_info["history"], $entry);
|
||||||
|
|
||||||
|
file_put_contents($path . "info.json", json_encode($info));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
move_uploaded_file($_FILES["file"]["tmp_name"],$channeled_path . DIRECTORY_SEPARATOR . $filename);
|
||||||
die(json_encode([
|
die(json_encode([
|
||||||
"success" => true
|
"success" => true
|
||||||
]));
|
]));
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
/* native functions declaration */
|
|
||||||
|
|
||||||
import * as updater from "updater/updater";
|
|
||||||
declare function displayCriticalError(message: string);
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
declare namespace native {
|
||||||
|
function client_version(): Promise<string>;
|
||||||
|
}
|
||||||
|
declare namespace forum {
|
||||||
|
interface UserData {
|
||||||
|
session_id: string;
|
||||||
|
username: string;
|
||||||
|
application_data: string;
|
||||||
|
application_data_sign: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
declare namespace audio.player {
|
||||||
|
interface Device {
|
||||||
|
device_id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
function initialized(): boolean;
|
||||||
|
function context(): AudioContext;
|
||||||
|
function destination(): AudioNode;
|
||||||
|
function on_ready(cb: () => any): void;
|
||||||
|
function initialize(): boolean;
|
||||||
|
function available_devices(): Promise<Device[]>;
|
||||||
|
function set_device(device_id?: string): Promise<void>;
|
||||||
|
function current_device(device_id?: string): Device;
|
||||||
|
}
|
||||||
|
declare function getUserMediaFunction(): (settings: any, success: any, fail: any) => void;
|
|
@ -1,9 +0,0 @@
|
||||||
export interface UserData {
|
|
||||||
session_id: string;
|
|
||||||
username: string;
|
|
||||||
application_data: string;
|
|
||||||
application_data_sign: string;
|
|
||||||
}
|
|
||||||
export declare function open_login(enforce?: boolean): Promise<UserData>;
|
|
||||||
export declare function current_data(): UserData | undefined;
|
|
||||||
export declare function logout(): void;
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {UserData} from "../app-definitions/teaforo/manager";
|
/// <reference path="../app-definitions/native_api.d.ts" />
|
||||||
|
|
||||||
const ipc = require("electron").ipcRenderer;
|
const ipc = require("electron").ipcRenderer;
|
||||||
let callback_listener: (() => any)[] = [];
|
let callback_listener: (() => any)[] = [];
|
||||||
|
|
||||||
ipc.on('teaforo-update', (event, data: UserData) => {
|
ipc.on('teaforo-update', (event, data: forum.UserData) => {
|
||||||
console.log("Got data update: %o", data);
|
console.log("Got data update: %o", data);
|
||||||
forumIdentity = data ? new TeaForumIdentity(data.application_data, data.application_data_sign) : undefined;
|
forumIdentity = data ? new TeaForumIdentity(data.application_data, data.application_data_sign) : undefined;
|
||||||
try {
|
try {
|
||||||
|
|
15
files.php
15
files.php
|
@ -97,6 +97,15 @@
|
||||||
],
|
],
|
||||||
|
|
||||||
/* web specs */
|
/* web specs */
|
||||||
|
[
|
||||||
|
"web-only" => true,
|
||||||
|
"type" => "js",
|
||||||
|
"search-pattern" => "/.*\.js$/",
|
||||||
|
"build-target" => "dev",
|
||||||
|
|
||||||
|
"path" => "js/",
|
||||||
|
"local-path" => "./web/js/"
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"web-only" => true,
|
"web-only" => true,
|
||||||
"type" => "css",
|
"type" => "css",
|
||||||
|
@ -250,7 +259,7 @@
|
||||||
$environment = "web/dev-environment";
|
$environment = "web/dev-environment";
|
||||||
} else if ($_SERVER["argv"][2] == "client") {
|
} else if ($_SERVER["argv"][2] == "client") {
|
||||||
$flagset = 0b10;
|
$flagset = 0b10;
|
||||||
$environment = "client-api/environment/ui-files";
|
$environment = "client-api/environment/ui-files/raw";
|
||||||
} else {
|
} else {
|
||||||
error_log("Invalid type!");
|
error_log("Invalid type!");
|
||||||
goto help;
|
goto help;
|
||||||
|
@ -262,7 +271,7 @@
|
||||||
$environment = "web/rel-environment";
|
$environment = "web/rel-environment";
|
||||||
} else if ($_SERVER["argv"][2] == "client") {
|
} else if ($_SERVER["argv"][2] == "client") {
|
||||||
$flagset = 0b10;
|
$flagset = 0b10;
|
||||||
$environment = "client-api/environment/ui-files";
|
$environment = "client-api/environment/ui-files/raw";
|
||||||
} else {
|
} else {
|
||||||
error_log("Invalid type!");
|
error_log("Invalid type!");
|
||||||
goto help;
|
goto help;
|
||||||
|
@ -318,7 +327,7 @@
|
||||||
if(!is_dir("versions/stable"))
|
if(!is_dir("versions/stable"))
|
||||||
exec($command = "mkdir -p versions/beta", $output, $state); if($state) goto handle_error;
|
exec($command = "mkdir -p versions/beta", $output, $state); if($state) goto handle_error;
|
||||||
|
|
||||||
exec($command = "ln -s ../api.php ./", $output, $state);
|
exec($command = "ln -s ../api.php ./", $output, $state); $state = 0; //Dont handle an error here!
|
||||||
if($state) goto handle_error;
|
if($state) goto handle_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
<link rel="stylesheet" href="css/modals.css" type="text/css">
|
<link rel="stylesheet" href="css/modals.css" type="text/css">
|
||||||
<link rel="stylesheet" href="css/modal-banlist.css" type="text/css">
|
<link rel="stylesheet" href="css/modal-banlist.css" type="text/css">
|
||||||
<link rel="stylesheet" href="css/modal-bancreate.css" type="text/css">
|
<link rel="stylesheet" href="css/modal-bancreate.css" type="text/css">
|
||||||
|
<link rel="stylesheet" href="css/modal-settings.css" type="text/css">
|
||||||
<link rel="stylesheet" href="css/loader.css" type="text/css">
|
<link rel="stylesheet" href="css/loader.css" type="text/css">
|
||||||
<link rel="stylesheet" href="css/music/info_plate.css" type="text/css">
|
<link rel="stylesheet" href="css/music/info_plate.css" type="text/css">
|
||||||
<link rel="stylesheet" href="css/frame/SelectInfo.css" type="text/css">
|
<link rel="stylesheet" href="css/frame/SelectInfo.css" type="text/css">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Welcome here! This repository is created with two reasons:\n 1. People can bring their own ideas and follow their implementation\n 2. People can see TeaSpeak Web client progress and avoid creating repetitive issues all the time.",
|
"description": "Welcome here! This repository is created with two reasons:\n 1. People can bring their own ideas and follow their implementation\n 2. People can see TeaSpeak Web client progress and avoid creating repetitive issues all the time.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"directories": {},
|
"directories": {},
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
TMP_FILE_NAME="TeaSpeakUI.tar.gz"
|
||||||
|
TMP_DIR_NAME="tmp"
|
||||||
|
|
||||||
|
BASEDIR=$(dirname "$0")
|
||||||
|
cd "$BASEDIR/../"
|
||||||
|
|
||||||
|
if [ "$#" -ne 2 ]; then
|
||||||
|
echo "Illegal number of parameters"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d client-api/environment/ui-files/ ]; then
|
||||||
|
echo "Missing UI Files"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${teaclient_deploy_secret}" == "" ]; then
|
||||||
|
echo "Missing deploy secret!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -e "${TMP_FILE_NAME}" ]; then
|
||||||
|
echo "Temp file already exists!"
|
||||||
|
echo "Deleting it!"
|
||||||
|
rm ${TMP_FILE_NAME}
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to delete file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
GIT_HASH=$(git rev-parse --verify --short HEAD)
|
||||||
|
APPLICATION_VERSION=$(cat package.json | python -c "import sys, json; print(json.load(sys.stdin)['version'])")
|
||||||
|
echo "Git hash ${GIT_HASH} on version ${APPLICATION_VERSION} on channel $2"
|
||||||
|
|
||||||
|
#Packaging the app
|
||||||
|
cd client-api/environment/ui-files/
|
||||||
|
if [ -e ${TMP_DIR_NAME} ]; then
|
||||||
|
rm -r ${TMP_DIR_NAME}
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to remove temporary directory!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
cp -rL raw ${TMP_DIR_NAME}
|
||||||
|
|
||||||
|
for file in $(find ${TMP_DIR_NAME} -name '*.php'); do
|
||||||
|
echo "Evaluating php file $file"
|
||||||
|
RESULT=$(php "${file}" 2> /dev/null)
|
||||||
|
CODE=$?
|
||||||
|
if [ ${CODE} -ne 0 ]; then
|
||||||
|
echo "Failed to evaluate php file $file!"
|
||||||
|
echo "Return code $CODE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${RESULT}" > "${file::-4}.html"
|
||||||
|
done
|
||||||
|
|
||||||
|
cd ${TMP_DIR_NAME}
|
||||||
|
tar chvzf ${TMP_FILE_NAME} *
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to pack file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mv ${TMP_FILE_NAME} ../../../../
|
||||||
|
cd ../
|
||||||
|
rm -r ${TMP_DIR_NAME}
|
||||||
|
cd ../../../
|
||||||
|
|
||||||
|
RESP=$(curl \
|
||||||
|
-k \
|
||||||
|
-X POST \
|
||||||
|
-F "type=deploy-ui-build" \
|
||||||
|
-F "channel=$2" \
|
||||||
|
-F "version=$APPLICATION_VERSION" \
|
||||||
|
-F "git_ref=$GIT_HASH" \
|
||||||
|
-F "secret=${teaclient_deploy_secret}" \
|
||||||
|
-F "file=@`pwd`/TeaSpeakUI.tar.gz" \
|
||||||
|
$1
|
||||||
|
)
|
||||||
|
echo "$RESP"
|
||||||
|
SUCCESS=$(echo ${RESP} | python -c "import sys, json; print(json.load(sys.stdin)['success'])")
|
||||||
|
|
||||||
|
if [ ! "${SUCCESS}" == "True" ]; then
|
||||||
|
ERROR=$(echo ${RESP} | python -c "import sys, json; print(json.load(sys.stdin)['error'])" 2>/dev/null)
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
ERROR=$(echo ${RESP} | python -c "import sys, json; print(json.load(sys.stdin)['msg'])" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
echo "Failed to deploy build!"
|
||||||
|
echo "${ERROR}"
|
||||||
|
|
||||||
|
rm ${TMP_FILE_NAME}
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Build deployed!"
|
|
@ -0,0 +1,100 @@
|
||||||
|
.modal .settings_audio {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
margin: 3px;
|
||||||
|
> div {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group_box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-device {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
a {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-device-error {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
vertical-align: center;
|
||||||
|
border: darkred 2px solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #be00006b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-device-select {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-left: 5px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-vad-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
input {
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-vad {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-vad-impl {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-vad-impl-entry {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
interface Window {
|
||||||
|
displayCriticalError: typeof displayCriticalError;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare function displayCriticalError(message: string);
|
|
@ -29,12 +29,12 @@ abstract class BasicCodec implements Codec {
|
||||||
channelCount: number = 1;
|
channelCount: number = 1;
|
||||||
samplesPerUnit: number = 960;
|
samplesPerUnit: number = 960;
|
||||||
|
|
||||||
constructor(codecSampleRate: number) {
|
protected constructor(codecSampleRate: number) {
|
||||||
this.channelCount = 1;
|
this.channelCount = 1;
|
||||||
this.samplesPerUnit = 960;
|
this.samplesPerUnit = 960;
|
||||||
this._audioContext = new (window.webkitOfflineAudioContext || window.OfflineAudioContext)(AudioController.globalContext.destination.channelCount, 1024,AudioController.globalContext.sampleRate );
|
this._audioContext = new (window.webkitOfflineAudioContext || window.OfflineAudioContext)(audio.player.destination().channelCount, 1024, audio.player.context().sampleRate);
|
||||||
this._codecSampleRate = codecSampleRate;
|
this._codecSampleRate = codecSampleRate;
|
||||||
this._decodeResampler = new AudioResampler(AudioController.globalContext.sampleRate);
|
this._decodeResampler = new AudioResampler(audio.player.context().sampleRate);
|
||||||
this._encodeResampler = new AudioResampler(codecSampleRate);
|
this._encodeResampler = new AudioResampler(codecSampleRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,6 @@ class CodecWrapper extends BasicCodec {
|
||||||
this._workerListener.push({
|
this._workerListener.push({
|
||||||
token: token,
|
token: token,
|
||||||
resolve: data => {
|
resolve: data => {
|
||||||
console.log("Init result: %o", data);
|
|
||||||
this._initialized = data["success"] == true;
|
this._initialized = data["success"] == true;
|
||||||
if(data["success"] == true)
|
if(data["success"] == true)
|
||||||
resolve();
|
resolve();
|
||||||
|
@ -144,7 +143,7 @@ class CodecWrapper extends BasicCodec {
|
||||||
|
|
||||||
private sendWorkerMessage(message: any, transfare?: any[]) {
|
private sendWorkerMessage(message: any, transfare?: any[]) {
|
||||||
message["timestamp"] = Date.now();
|
message["timestamp"] = Date.now();
|
||||||
this._worker.postMessage(message, transfare);
|
this._worker.postMessage(message, transfare as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWorkerMessage(message: any) {
|
private onWorkerMessage(message: any) {
|
||||||
|
|
|
@ -329,11 +329,17 @@ class HandshakeHandler {
|
||||||
}).then(() => this.handshake_finished()); //TODO handle error
|
}).then(() => this.handshake_finished()); //TODO handle error
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handshake_finished(version?: string) {
|
private handshake_finished(version?: string) {
|
||||||
if(window.require && !version) {
|
if(native_client && window["native"] && native.client_version && !version) {
|
||||||
version = "?.?.?"; //FIXME findout version!
|
native.client_version()
|
||||||
|
.then( this.handshake_finished.bind(this))
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Failed to get version:");
|
||||||
|
console.error(error);
|
||||||
|
this.handshake_finished("?.?.?");
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
//TODO variables!
|
//TODO variables!
|
||||||
client_nickname: this.name ? this.name : this.identity.name(),
|
client_nickname: this.name ? this.name : this.identity.name(),
|
||||||
|
@ -344,7 +350,7 @@ class HandshakeHandler {
|
||||||
client_browser_engine: navigator.product
|
client_browser_engine: navigator.product
|
||||||
};
|
};
|
||||||
|
|
||||||
if(window.require) {
|
if(version) {
|
||||||
data.client_version = "TeaClient ";
|
data.client_version = "TeaClient ";
|
||||||
data.client_version += " " + version;
|
data.client_version += " " + version;
|
||||||
|
|
||||||
|
|
|
@ -52,21 +52,21 @@ namespace app {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadScripts(paths: (string | string[])[]) : {path: string, promise: Promise<Boolean>}[] {
|
function load_scripts(paths: (string | string[])[]) : {path: string, promise: Promise<Boolean>}[] {
|
||||||
let result = [];
|
let result = [];
|
||||||
for(let path of paths)
|
for(let path of paths)
|
||||||
result.push({path: path, promise: loadScript(path)});
|
result.push({path: path, promise: load_script(path)});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadScript(path: string | string[]) : Promise<Boolean> {
|
function load_script(path: string | string[]) : Promise<Boolean> {
|
||||||
if(Array.isArray(path)) { //Having fallbacks
|
if(Array.isArray(path)) { //Having fallbacks
|
||||||
return new Promise<Boolean>((resolve, reject) => {
|
return new Promise<Boolean>((resolve, reject) => {
|
||||||
loadScript(path[0]).then(resolve).catch(error => {
|
load_script(path[0]).then(resolve).catch(error => {
|
||||||
if(path.length >= 2) {
|
if(path.length >= 2) {
|
||||||
loadScript(path.slice(1)).then(resolve).catch(() => reject("could not load file " + formatPath(path)));
|
load_script(path.slice(1)).then(resolve).catch(() => reject("could not load file " + formatPath(path)));
|
||||||
} else {
|
} else {
|
||||||
reject("could not load file (event fallback's)");
|
reject("could not load file");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -99,7 +99,7 @@ function formatPath(path: string | string[]) {
|
||||||
function loadRelease() {
|
function loadRelease() {
|
||||||
app.type = app.Type.RELEASE;
|
app.type = app.Type.RELEASE;
|
||||||
console.log("Load for release!");
|
console.log("Load for release!");
|
||||||
awaitLoad(loadScripts([
|
awaitLoad(load_scripts([
|
||||||
//Load general API's
|
//Load general API's
|
||||||
["wasm/TeaWeb-Identity.js"],
|
["wasm/TeaWeb-Identity.js"],
|
||||||
["js/client.min.js", "js/client.js"]
|
["js/client.min.js", "js/client.js"]
|
||||||
|
@ -110,11 +110,19 @@ function loadRelease() {
|
||||||
console.error("Could not load " + error.path);
|
console.error("Could not load " + error.path);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Only possible for developers! **/
|
/** Only possible for developers! **/
|
||||||
function loadDebug() {
|
function loadDebug() {
|
||||||
app.type = app.Type.DEBUG;
|
app.type = app.Type.DEBUG;
|
||||||
console.log("Load for debug!");
|
console.log("Load for debug!");
|
||||||
|
|
||||||
|
let custom_scripts: string[] | string[][] = [];
|
||||||
|
|
||||||
|
if(!window.require) {
|
||||||
|
console.log("Adding browser audio player");
|
||||||
|
custom_scripts.push(["js/audio/AudioPlayer.js"]);
|
||||||
|
}
|
||||||
|
|
||||||
load_wait_scripts([
|
load_wait_scripts([
|
||||||
["wasm/TeaWeb-Identity.js"],
|
["wasm/TeaWeb-Identity.js"],
|
||||||
|
|
||||||
|
@ -171,7 +179,9 @@ function loadDebug() {
|
||||||
"js/FileManager.js",
|
"js/FileManager.js",
|
||||||
"js/client.js",
|
"js/client.js",
|
||||||
"js/chat.js",
|
"js/chat.js",
|
||||||
"js/Identity.js"
|
"js/Identity.js",
|
||||||
|
|
||||||
|
...custom_scripts
|
||||||
]).then(() => load_wait_scripts([
|
]).then(() => load_wait_scripts([
|
||||||
"js/codec/CodecWrapper.js"
|
"js/codec/CodecWrapper.js"
|
||||||
])).then(() => load_wait_scripts([
|
])).then(() => load_wait_scripts([
|
||||||
|
@ -208,7 +218,7 @@ function awaitLoad(promises: {path: string, promise: Promise<Boolean>}[]) : Prom
|
||||||
}
|
}
|
||||||
|
|
||||||
function load_wait_scripts(paths: (string | string[])[]) : Promise<void> {
|
function load_wait_scripts(paths: (string | string[])[]) : Promise<void> {
|
||||||
return awaitLoad(loadScripts(paths));
|
return awaitLoad(load_scripts(paths));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -293,7 +303,7 @@ function loadSide() {
|
||||||
["https://webrtc.github.io/adapter/adapter-latest.js"]
|
["https://webrtc.github.io/adapter/adapter-latest.js"]
|
||||||
])).then(() => {
|
])).then(() => {
|
||||||
//Load the teaweb scripts
|
//Load the teaweb scripts
|
||||||
loadScript("js/proto.js").then(loadDebug).catch(loadRelease);
|
load_script("js/proto.js").then(loadDebug).catch(loadRelease);
|
||||||
//Load the teaweb templates
|
//Load the teaweb templates
|
||||||
loadTemplates();
|
loadTemplates();
|
||||||
});
|
});
|
||||||
|
@ -345,7 +355,7 @@ if(typeof Module === "undefined")
|
||||||
app.initialize();
|
app.initialize();
|
||||||
app.loadedListener.push(fadeoutLoader);
|
app.loadedListener.push(fadeoutLoader);
|
||||||
|
|
||||||
if(!window.displayCriticalError) {
|
if(!window.displayCriticalError) { /* Declare this function here only because its required before load */
|
||||||
window.displayCriticalError = function(message: string) {
|
window.displayCriticalError = function(message: string) {
|
||||||
if(typeof(createErrorModal) !== 'undefined') {
|
if(typeof(createErrorModal) !== 'undefined') {
|
||||||
createErrorModal("A critical error occurred while loading the page!", message, {closeable: false}).open();
|
createErrorModal("A critical error occurred while loading the page!", message, {closeable: false}).open();
|
||||||
|
|
|
@ -138,7 +138,7 @@ function main() {
|
||||||
|
|
||||||
$("#music-test").replaceWith(tag);
|
$("#music-test").replaceWith(tag);
|
||||||
|
|
||||||
//Modals.spawnSettingsModal();
|
Modals.spawnSettingsModal();
|
||||||
/*
|
/*
|
||||||
Modals.spawnYesNo("Are your sure?", "Do you really want to exit?", flag => {
|
Modals.spawnYesNo("Are your sure?", "Do you really want to exit?", flag => {
|
||||||
console.log("Response: " + flag);
|
console.log("Response: " + flag);
|
||||||
|
@ -154,13 +154,17 @@ function main() {
|
||||||
app.loadedListener.push(() => {
|
app.loadedListener.push(() => {
|
||||||
try {
|
try {
|
||||||
main();
|
main();
|
||||||
if(!AudioController.initialized()) {
|
if(!audio.player.initialized()) {
|
||||||
log.info(LogCategory.VOICE, "Initialize audio controller later!");
|
log.info(LogCategory.VOICE, "Initialize audio controller later!");
|
||||||
$(document).one('click', event => AudioController.initializeFromGesture());
|
if(!audio.player.initializeFromGesture) {
|
||||||
|
console.error("Missing audio.player.initializeFromGesture");
|
||||||
|
} else
|
||||||
|
$(document).one('click', event => audio.player.initializeFromGesture());
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
|
console.error(ex.stack);
|
||||||
if(ex instanceof ReferenceError || ex instanceof TypeError)
|
if(ex instanceof ReferenceError || ex instanceof TypeError)
|
||||||
ex = ex.message + ":<br>" + ex.stack;
|
ex = ex.name + ": " + ex.message;
|
||||||
displayCriticalError("Failed to invoke main function:<br>" + ex);
|
displayCriticalError("Failed to invoke main function:<br>" + ex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
/// <reference path="../../voice/AudioController.ts" />
|
/// <reference path="../../voice/AudioController.ts" />
|
||||||
|
|
||||||
namespace Modals {
|
namespace Modals {
|
||||||
|
import set = Reflect.set;
|
||||||
|
|
||||||
export function spawnSettingsModal() {
|
export function spawnSettingsModal() {
|
||||||
let modal;
|
let modal;
|
||||||
modal = createModal({
|
modal = createModal({
|
||||||
|
@ -35,87 +37,190 @@ namespace Modals {
|
||||||
|
|
||||||
function initialiseSettingListeners(modal: Modal, tag: JQuery) {
|
function initialiseSettingListeners(modal: Modal, tag: JQuery) {
|
||||||
//Voice
|
//Voice
|
||||||
initialiseVoiceListeners(modal, tag.find(".settings_voice"));
|
initialiseVoiceListeners(modal, tag.find(".settings_audio"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function initialiseVoiceListeners(modal: Modal, tag: JQuery) {
|
function initialiseVoiceListeners(modal: Modal, tag: JQuery) {
|
||||||
let currentVAD = settings.global("vad_type");
|
let currentVAD = settings.global("vad_type", "ppt");
|
||||||
|
|
||||||
tag.find("input[type=radio][name=\"vad_type\"]").change(function (this: HTMLButtonElement) {
|
{ //Initialized voice activation detection
|
||||||
tag.find(".vad_settings .vad_type").text($(this).attr("display"));
|
const vad_tag = tag.find(".settings-vad-container");
|
||||||
tag.find(".vad_settings .vad_type_settings").hide();
|
|
||||||
tag.find(".vad_settings .vad_type_" + this.value).show();
|
|
||||||
settings.changeGlobal("vad_type", this.value);
|
|
||||||
globalClient.voiceConnection.voiceRecorder.reinitialiseVAD();
|
|
||||||
|
|
||||||
switch (this.value) {
|
vad_tag.find('input[type=radio]').on('change', event => {
|
||||||
case "ppt":
|
const select = event.currentTarget as HTMLSelectElement;
|
||||||
let keyCode: number = parseInt(settings.global("vad_ppt_key", JQuery.Key.T.toString()));
|
{
|
||||||
tag.find(".vat_ppt_key").text(String.fromCharCode(keyCode));
|
vad_tag.find(".settings-vad-impl-entry").hide();
|
||||||
break;
|
vad_tag.find(".setting-vad-" + select.value).show();
|
||||||
case "vad":
|
}
|
||||||
let slider = tag.find(".vad_vad_slider");
|
{
|
||||||
let vad: VoiceActivityDetectorVAD = globalClient.voiceConnection.voiceRecorder.getVADHandler() as VoiceActivityDetectorVAD;
|
settings.changeGlobal("vad_type", select.value);
|
||||||
slider.val(vad.percentageThreshold);
|
globalClient.voiceConnection.voiceRecorder.reinitialiseVAD();
|
||||||
slider.trigger("change");
|
}
|
||||||
globalClient.voiceConnection.voiceRecorder.update(true);
|
|
||||||
vad.percentage_listener = per => {
|
switch (select.value) {
|
||||||
tag.find(".vad_vad_bar_filler")
|
case "ppt":
|
||||||
.css("width", per + "%");
|
let keyCode: number = parseInt(settings.global("vad_ppt_key", JQuery.Key.T.toString()));
|
||||||
};
|
vad_tag.find(".vat_ppt_key").text(String.fromCharCode(keyCode));
|
||||||
break;
|
break;
|
||||||
|
case "vad":
|
||||||
|
let slider = vad_tag.find(".vad_vad_slider");
|
||||||
|
let vad: VoiceActivityDetectorVAD = globalClient.voiceConnection.voiceRecorder.getVADHandler() as VoiceActivityDetectorVAD;
|
||||||
|
slider.val(vad.percentageThreshold);
|
||||||
|
slider.trigger("change");
|
||||||
|
globalClient.voiceConnection.voiceRecorder.update(true);
|
||||||
|
vad.percentage_listener = per => {
|
||||||
|
vad_tag.find(".vad_vad_bar_filler")
|
||||||
|
.css("width", per + "%");
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
{ //Initialized push to talk
|
||||||
|
vad_tag.find(".vat_ppt_key").click(function () {
|
||||||
|
let modal = createModal({
|
||||||
|
body: "",
|
||||||
|
header: () => {
|
||||||
|
let head = $.spawn("div");
|
||||||
|
head.text("Type the key you wish");
|
||||||
|
head.css("background-color", "blue");
|
||||||
|
return head;
|
||||||
|
},
|
||||||
|
footer: ""
|
||||||
|
});
|
||||||
|
$(document).one("keypress", function (e) {
|
||||||
|
console.log("Got key " + e.keyCode);
|
||||||
|
modal.close();
|
||||||
|
settings.changeGlobal("vad_ppt_key", e.keyCode.toString());
|
||||||
|
globalClient.voiceConnection.voiceRecorder.reinitialiseVAD();
|
||||||
|
vad_tag.find(".vat_ppt_key").text(String.fromCharCode(e.keyCode));
|
||||||
|
});
|
||||||
|
modal.open();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if(!currentVAD)
|
{ //Initialized voice activation detection
|
||||||
currentVAD = "ppt";
|
let slider = vad_tag.find(".vad_vad_slider");
|
||||||
let elm = tag.find("input[type=radio][name=\"vad_type\"][value=\"" + currentVAD + "\"]");
|
slider.on("input change", () => {
|
||||||
elm.attr("checked", "true");
|
settings.changeGlobal("vad_threshold", slider.val().toString());
|
||||||
|
let vad = globalClient.voiceConnection.voiceRecorder.getVADHandler();
|
||||||
|
if(vad instanceof VoiceActivityDetectorVAD)
|
||||||
|
vad.percentageThreshold = slider.val() as number;
|
||||||
|
vad_tag.find(".vad_vad_slider_value").text(slider.val().toString());
|
||||||
|
});
|
||||||
|
modal.properties.registerCloseListener(() => {
|
||||||
|
let vad = globalClient.voiceConnection.voiceRecorder.getVADHandler();
|
||||||
|
if(vad instanceof VoiceActivityDetectorVAD)
|
||||||
|
vad.percentage_listener = undefined;
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
tag.find(".vat_ppt_key").click(function () {
|
let target_tag = vad_tag.find('input[type=radio][name="vad_type"][value="' + currentVAD + '"]');
|
||||||
let modal = createModal({
|
if(target_tag.length == 0) {
|
||||||
body: "",
|
console.warn("Failed to find tag for " + currentVAD + ". Using latest tag!");
|
||||||
header: () => {
|
target_tag = vad_tag.find('input[type=radio][name="vad_type"]').last();
|
||||||
let head = $.spawn("div");
|
}
|
||||||
head.text("Type the key you wish");
|
target_tag.prop("checked", true);
|
||||||
head.css("background-color", "blue");
|
setTimeout(() => target_tag.trigger('change'), 0);
|
||||||
return head;
|
}
|
||||||
},
|
|
||||||
footer: ""
|
{ //Initialize microphone
|
||||||
|
|
||||||
|
const setting_tag = tag.find(".settings-microphone");
|
||||||
|
const tag_select = setting_tag.find(".audio-select-microphone");
|
||||||
|
console.log(setting_tag);
|
||||||
|
console.log(setting_tag.find(".settings-device-error"));
|
||||||
|
console.log(setting_tag.find(".settings-device-error").html());
|
||||||
|
|
||||||
|
{ //List devices
|
||||||
|
$.spawn("option")
|
||||||
|
.attr("device-id", "")
|
||||||
|
.attr("device-group", "")
|
||||||
|
.text("No device")
|
||||||
|
.appendTo(tag_select);
|
||||||
|
|
||||||
|
navigator.mediaDevices.enumerateDevices().then(devices => {
|
||||||
|
const active_device = globalClient.voiceConnection.voiceRecorder.device_id();
|
||||||
|
|
||||||
|
for(const device of devices) {
|
||||||
|
console.debug("Got device %s (%s): %s", device.deviceId, device.kind, device.label);
|
||||||
|
if(device.kind !== 'audioinput') continue;
|
||||||
|
|
||||||
|
$.spawn("option")
|
||||||
|
.attr("device-id", device.deviceId)
|
||||||
|
.attr("device-group", device.groupId)
|
||||||
|
.text(device.label)
|
||||||
|
.prop("selected", device.deviceId == active_device)
|
||||||
|
.appendTo(tag_select);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("Could not enumerate over devices!");
|
||||||
|
console.error(error);
|
||||||
|
setting_tag.find(".settings-device-error")
|
||||||
|
.text("Could not get device list!")
|
||||||
|
.css("display", "block");
|
||||||
|
});
|
||||||
|
|
||||||
|
if(tag_select.find("option:selected").length == 0)
|
||||||
|
tag_select.find("option").prop("selected", true);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
tag_select.on('change', event => {
|
||||||
|
let selected_tag = tag_select.find("option:selected");
|
||||||
|
let deviceId = selected_tag.attr("device-id");
|
||||||
|
let groupId = selected_tag.attr("device-group");
|
||||||
|
console.log("Selected microphone device: id: %o group: %o", deviceId, groupId);
|
||||||
|
globalClient.voiceConnection.voiceRecorder.change_device(deviceId, groupId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{ //Initialize speaker
|
||||||
|
const setting_tag = tag.find(".settings-speaker");
|
||||||
|
const tag_select = setting_tag.find(".audio-select-speaker");
|
||||||
|
const active_device = audio.player.current_device();
|
||||||
|
|
||||||
|
audio.player.available_devices().then(devices => {
|
||||||
|
for(const device of devices) {
|
||||||
|
$.spawn("option")
|
||||||
|
.attr("device-id", device.device_id)
|
||||||
|
.text(device.name)
|
||||||
|
.prop("selected", device.device_id == active_device.device_id)
|
||||||
|
.appendTo(tag_select);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("Could not enumerate over devices!");
|
||||||
|
console.error(error);
|
||||||
|
setting_tag.find(".settings-device-error")
|
||||||
|
.text("Could not get device list!")
|
||||||
|
.css("display", "block");
|
||||||
});
|
});
|
||||||
$(document).one("keypress", function (e) {
|
|
||||||
console.log("Got key " + e.keyCode);
|
|
||||||
modal.close();
|
|
||||||
settings.changeGlobal("vad_ppt_key", e.keyCode.toString());
|
|
||||||
globalClient.voiceConnection.voiceRecorder.reinitialiseVAD();
|
|
||||||
tag.find(".vat_ppt_key").text(String.fromCharCode(e.keyCode));
|
|
||||||
});
|
|
||||||
modal.open();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
//VAD VAD
|
if(tag_select.find("option:selected").length == 0)
|
||||||
let slider = tag.find(".vad_vad_slider");
|
tag_select.find("option").prop("selected", true);
|
||||||
slider.on("input change", () => {
|
|
||||||
settings.changeGlobal("vad_threshold", slider.val().toString());
|
|
||||||
let vad = globalClient.voiceConnection.voiceRecorder.getVADHandler();
|
|
||||||
if(vad instanceof VoiceActivityDetectorVAD)
|
|
||||||
vad.percentageThreshold = slider.val() as number;
|
|
||||||
tag.find(".vad_vad_slider_value").text(slider.val().toString());
|
|
||||||
});
|
|
||||||
modal.properties.registerCloseListener(() => {
|
|
||||||
let vad = globalClient.voiceConnection.voiceRecorder.getVADHandler();
|
|
||||||
if(vad instanceof VoiceActivityDetectorVAD)
|
|
||||||
vad.percentage_listener = undefined;
|
|
||||||
|
|
||||||
});
|
{
|
||||||
|
const error_tag = setting_tag.find(".settings-device-error");
|
||||||
|
tag_select.on('change', event => {
|
||||||
//Trigger radio button select for VAD setting setup
|
let selected_tag = tag_select.find("option:selected");
|
||||||
elm.trigger("change");
|
let deviceId = selected_tag.attr("device-id");
|
||||||
|
console.log("Selected speaker device: id: %o", deviceId);
|
||||||
|
audio.player.set_device(deviceId).then(() => error_tag.css("display", "none")).catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
error_tag
|
||||||
|
.text("Failed to change device!")
|
||||||
|
.css("display", "block");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Initialise microphones
|
//Initialise microphones
|
||||||
|
/*
|
||||||
let select_microphone = tag.find(".voice_microphone_select");
|
let select_microphone = tag.find(".voice_microphone_select");
|
||||||
let select_error = tag.find(".voice_microphone_select_error");
|
let select_error = tag.find(".voice_microphone_select_error");
|
||||||
|
|
||||||
|
@ -148,6 +253,7 @@ namespace Modals {
|
||||||
console.log("Selected microphone device: id: %o group: %o", deviceId, groupId);
|
console.log("Selected microphone device: id: %o group: %o", deviceId, groupId);
|
||||||
globalClient.voiceConnection.voiceRecorder.change_device(deviceId, groupId);
|
globalClient.voiceConnection.voiceRecorder.change_device(deviceId, groupId);
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
//Initialise speakers
|
//Initialise speakers
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/// <reference path="../../exports/audio/AudioPlayer.d.ts" />
|
||||||
|
|
||||||
enum PlayerState {
|
enum PlayerState {
|
||||||
PREBUFFERING,
|
PREBUFFERING,
|
||||||
PLAYING,
|
PLAYING,
|
||||||
|
@ -6,73 +8,15 @@ enum PlayerState {
|
||||||
STOPPED
|
STOPPED
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Navigator {
|
|
||||||
mozGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void;
|
|
||||||
webkitGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AudioController {
|
class AudioController {
|
||||||
private static getUserMediaFunction() {
|
|
||||||
if(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)
|
|
||||||
return (settings, success, fail) => { navigator.mediaDevices.getUserMedia(settings).then(success).catch(fail); };
|
|
||||||
return navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
|
|
||||||
}
|
|
||||||
public static userMedia = AudioController.getUserMediaFunction();
|
|
||||||
private static _globalContext: AudioContext;
|
|
||||||
private static _globalContextPromise: Promise<void>;
|
|
||||||
private static _audioInstances: AudioController[] = [];
|
private static _audioInstances: AudioController[] = [];
|
||||||
private static _initialized_listener: (() => any)[] = [];
|
|
||||||
private static _globalReplayScheduler: NodeJS.Timer;
|
private static _globalReplayScheduler: NodeJS.Timer;
|
||||||
private static _timeIndex: number = 0;
|
private static _timeIndex: number = 0;
|
||||||
private static _audioDestinationStream: MediaStream;
|
private static _audioDestinationStream: MediaStream;
|
||||||
|
|
||||||
static get globalContext() : AudioContext {
|
|
||||||
if(this._globalContext && this._globalContext.state != "suspended") return this._globalContext;
|
|
||||||
|
|
||||||
if(!this._globalContext)
|
|
||||||
this._globalContext = new (window.webkitAudioContext || window.AudioContext)();
|
|
||||||
if(this._globalContext.state == "suspended") {
|
|
||||||
if(!this._globalContextPromise) {
|
|
||||||
(this._globalContextPromise = this._globalContext.resume()).then(() => {
|
|
||||||
this.fire_initialized();
|
|
||||||
}).catch(error => {
|
|
||||||
displayCriticalError("Failed to initialize global audio context! (" + error + ")");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this._globalContext.resume(); //We already have our listener
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this._globalContext.state == "running") {
|
|
||||||
this.fire_initialized();
|
|
||||||
return this._globalContext;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static fire_initialized() {
|
|
||||||
while(this._initialized_listener.length > 0)
|
|
||||||
this._initialized_listener.pop_front()();
|
|
||||||
}
|
|
||||||
|
|
||||||
static initialized() : boolean {
|
|
||||||
return (this.globalContext || {state: ""}).state === "running";
|
|
||||||
}
|
|
||||||
|
|
||||||
static on_initialized(callback: () => any) {
|
|
||||||
if(this.globalContext)
|
|
||||||
callback();
|
|
||||||
else
|
|
||||||
this._initialized_listener.push(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
static initializeFromGesture() {
|
|
||||||
AudioController.globalContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
static initializeAudioController() {
|
static initializeAudioController() {
|
||||||
AudioController.globalContext; //Just test here
|
if(!audio.player.initialize())
|
||||||
|
console.warn("Failed to initialize audio controller!");
|
||||||
//this._globalReplayScheduler = setInterval(() => { AudioController.invokeNextReplay(); }, 20); //Fix me
|
//this._globalReplayScheduler = setInterval(() => { AudioController.invokeNextReplay(); }, 20); //Fix me
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +86,7 @@ class AudioController {
|
||||||
onSilence: () => void;
|
onSilence: () => void;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
AudioController.on_initialized(() => this.speakerContext = AudioController.globalContext);
|
audio.player.on_ready(() => this.speakerContext = audio.player.context());
|
||||||
|
|
||||||
this.onSpeaking = function () { };
|
this.onSpeaking = function () { };
|
||||||
this.onSilence = function () { };
|
this.onSilence = function () { };
|
||||||
|
@ -204,6 +148,10 @@ class AudioController {
|
||||||
private playQueue() {
|
private playQueue() {
|
||||||
let buffer: AudioBuffer;
|
let buffer: AudioBuffer;
|
||||||
while(buffer = this.audioCache.pop_front()) {
|
while(buffer = this.audioCache.pop_front()) {
|
||||||
|
if(this.playingAudioCache.length >= this._latencyBufferLength * 1.5 + 3) {
|
||||||
|
console.log("Dropping buffer because playing queue grows to much");
|
||||||
|
continue; /* drop the data (we're behind) */
|
||||||
|
}
|
||||||
if(this._timeIndex < this.speakerContext.currentTime) this._timeIndex = this.speakerContext.currentTime;
|
if(this._timeIndex < this.speakerContext.currentTime) this._timeIndex = this.speakerContext.currentTime;
|
||||||
|
|
||||||
let player = this.speakerContext.createBufferSource();
|
let player = this.speakerContext.createBufferSource();
|
||||||
|
@ -212,7 +160,7 @@ class AudioController {
|
||||||
player.onended = () => this.removeNode(player);
|
player.onended = () => this.removeNode(player);
|
||||||
this.playingAudioCache.push(player);
|
this.playingAudioCache.push(player);
|
||||||
|
|
||||||
player.connect(AudioController.globalContext.destination);
|
player.connect(audio.player.destination());
|
||||||
player.start(this._timeIndex);
|
player.start(this._timeIndex);
|
||||||
this._timeIndex += buffer.duration;
|
this._timeIndex += buffer.duration;
|
||||||
}
|
}
|
||||||
|
@ -275,3 +223,9 @@ class AudioController {
|
||||||
return this._codecCache[codec];
|
return this._codecCache[codec];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUserMediaFunction() {
|
||||||
|
if(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)
|
||||||
|
return (settings, success, fail) => { navigator.mediaDevices.getUserMedia(settings).then(success).catch(fail); };
|
||||||
|
return navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ class AudioResampler {
|
||||||
targetSampleRate: number;
|
targetSampleRate: number;
|
||||||
private _use_promise: boolean;
|
private _use_promise: boolean;
|
||||||
|
|
||||||
constructor(targetSampleRate: number = 44100){
|
constructor(targetSampleRate: number){
|
||||||
this.targetSampleRate = targetSampleRate;
|
this.targetSampleRate = targetSampleRate;
|
||||||
if(this.targetSampleRate < 3000 || this.targetSampleRate > 384000) throw "The target sample rate is outside the range [3000, 384000].";
|
if(this.targetSampleRate < 3000 || this.targetSampleRate > 384000) throw "The target sample rate is outside the range [3000, 384000].";
|
||||||
}
|
}
|
||||||
|
@ -10,11 +10,11 @@ class AudioResampler {
|
||||||
resample(buffer: AudioBuffer) : Promise<AudioBuffer> {
|
resample(buffer: AudioBuffer) : Promise<AudioBuffer> {
|
||||||
if(!buffer) {
|
if(!buffer) {
|
||||||
console.warn("Received empty buffer as input! Returning empty output!");
|
console.warn("Received empty buffer as input! Returning empty output!");
|
||||||
return new Promise<AudioBuffer>(resolve => resolve(undefined));
|
return Promise.resolve(buffer);
|
||||||
}
|
}
|
||||||
//console.log("Encode from %i to %i", buffer.sampleRate, this.targetSampleRate);
|
//console.log("Encode from %i to %i", buffer.sampleRate, this.targetSampleRate);
|
||||||
if(buffer.sampleRate == this.targetSampleRate)
|
if(buffer.sampleRate == this.targetSampleRate)
|
||||||
return new Promise<AudioBuffer>(resolve => resolve(buffer));
|
return Promise.resolve(buffer);
|
||||||
|
|
||||||
let context;
|
let context;
|
||||||
context = new (window.webkitOfflineAudioContext || window.OfflineAudioContext)(buffer.numberOfChannels, Math.ceil(buffer.length * this.targetSampleRate / buffer.sampleRate), this.targetSampleRate);
|
context = new (window.webkitOfflineAudioContext || window.OfflineAudioContext)(buffer.numberOfChannels, Math.ceil(buffer.length * this.targetSampleRate / buffer.sampleRate), this.targetSampleRate);
|
||||||
|
|
|
@ -146,7 +146,7 @@ class VoiceConnection {
|
||||||
this.voiceRecorder.on_start = this.handleVoiceStarted.bind(this);
|
this.voiceRecorder.on_start = this.handleVoiceStarted.bind(this);
|
||||||
this.voiceRecorder.reinitialiseVAD();
|
this.voiceRecorder.reinitialiseVAD();
|
||||||
|
|
||||||
AudioController.on_initialized(() => {
|
audio.player.on_ready(() => {
|
||||||
log.info(LogCategory.VOICE, "Initializing voice handler after AudioController has been initialized!");
|
log.info(LogCategory.VOICE, "Initializing voice handler after AudioController has been initialized!");
|
||||||
this.codec_pool[4].initialize(2);
|
this.codec_pool[4].initialize(2);
|
||||||
this.codec_pool[5].initialize(2);
|
this.codec_pool[5].initialize(2);
|
||||||
|
@ -193,7 +193,7 @@ class VoiceConnection {
|
||||||
stream.disconnect();
|
stream.disconnect();
|
||||||
|
|
||||||
if(!this.local_audio_stream)
|
if(!this.local_audio_stream)
|
||||||
this.local_audio_stream = AudioController.globalContext.createMediaStreamDestination();
|
this.local_audio_stream = audio.player.context().createMediaStreamDestination();
|
||||||
stream.connect(this.local_audio_stream);
|
stream.connect(this.local_audio_stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,8 +65,8 @@ class VoiceRecorder {
|
||||||
this._deviceId = settings.global("microphone_device_id", "default");
|
this._deviceId = settings.global("microphone_device_id", "default");
|
||||||
this._deviceGroup = settings.global("microphone_device_group", "default");
|
this._deviceGroup = settings.global("microphone_device_group", "default");
|
||||||
|
|
||||||
AudioController.on_initialized(() => {
|
audio.player.on_ready(() => {
|
||||||
this.audioContext = AudioController.globalContext;
|
this.audioContext = audio.player.context();
|
||||||
this.processor = this.audioContext.createScriptProcessor(VoiceRecorder.BUFFER_SIZE, VoiceRecorder.CHANNELS, VoiceRecorder.CHANNELS);
|
this.processor = this.audioContext.createScriptProcessor(VoiceRecorder.BUFFER_SIZE, VoiceRecorder.CHANNELS, VoiceRecorder.CHANNELS);
|
||||||
|
|
||||||
const empty_buffer = this.audioContext.createBuffer(VoiceRecorder.CHANNELS, VoiceRecorder.BUFFER_SIZE, 48000);
|
const empty_buffer = this.audioContext.createBuffer(VoiceRecorder.CHANNELS, VoiceRecorder.BUFFER_SIZE, 48000);
|
||||||
|
@ -102,7 +102,7 @@ class VoiceRecorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
available() : boolean {
|
available() : boolean {
|
||||||
return !!AudioController.userMedia;
|
return !!getUserMediaFunction() && !!getUserMediaFunction();
|
||||||
}
|
}
|
||||||
|
|
||||||
recording() : boolean {
|
recording() : boolean {
|
||||||
|
@ -184,7 +184,8 @@ class VoiceRecorder {
|
||||||
console.log("[VoiceRecorder] Start recording! (Device: %o | Group: %o)", device, groupId);
|
console.log("[VoiceRecorder] Start recording! (Device: %o | Group: %o)", device, groupId);
|
||||||
this._recording = true;
|
this._recording = true;
|
||||||
|
|
||||||
AudioController.userMedia({
|
//FIXME Implement that here for thew client as well
|
||||||
|
getUserMediaFunction()({
|
||||||
audio: {
|
audio: {
|
||||||
deviceId: device,
|
deviceId: device,
|
||||||
groupId: groupId
|
groupId: groupId
|
||||||
|
@ -257,7 +258,7 @@ class VoiceActivityDetectorVAD extends VoiceActivityDetector {
|
||||||
percentage_listener: (per: number) => void = ($) => {};
|
percentage_listener: (per: number) => void = ($) => {};
|
||||||
|
|
||||||
initialise() {
|
initialise() {
|
||||||
this.analyzer = AudioController.globalContext.createAnalyser();
|
this.analyzer = audio.player.context().createAnalyser();
|
||||||
this.analyzer.smoothingTimeConstant = 1; //TODO test
|
this.analyzer.smoothingTimeConstant = 1; //TODO test
|
||||||
this.buffer = new Uint8Array(this.analyzer.fftSize);
|
this.buffer = new Uint8Array(this.analyzer.fftSize);
|
||||||
return super.initialise();
|
return super.initialise();
|
||||||
|
|
|
@ -95,7 +95,6 @@ class OpusWorker implements CodecWorker {
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
return "invalid result on decode (" + result + ")";
|
return "invalid result on decode (" + result + ")";
|
||||||
}
|
}
|
||||||
console.log("Result: %o | Channel count %o", result, this.channelCount);
|
|
||||||
return Module.HEAPF32.slice(this.decodeBuffer.byteOffset / 4, (this.decodeBuffer.byteOffset / 4) + (result * this.channelCount));
|
return Module.HEAPF32.slice(this.decodeBuffer.byteOffset / 4, (this.decodeBuffer.byteOffset / 4) + (result * this.channelCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -564,47 +564,65 @@
|
||||||
<x-content>Didnt setuped yet!</x-content>
|
<x-content>Didnt setuped yet!</x-content>
|
||||||
</x-entry>
|
</x-entry>
|
||||||
<x-entry>
|
<x-entry>
|
||||||
<x-tag>Voice</x-tag>
|
<x-tag>Audio</x-tag>
|
||||||
<x-content>
|
<x-content>
|
||||||
<div class="settings_voice align_column">
|
<div class="settings_audio">
|
||||||
<div style="justify-content: right">
|
<div class="group_box">
|
||||||
<a style="margin-left: 20px">Microphone:</a>
|
<div class="header">Microphone</div>
|
||||||
<select class="voice_microphone_select"></select>
|
<div class="content settings-microphone">
|
||||||
<hr>
|
<div class="settings-device settings-device-microphone">
|
||||||
</div>
|
<div class="settings-device-error"></div>
|
||||||
<div style="display: flex; flex-direction: row; width: 100%; justify-content: space-evenly"><div style="vertical-align: center; margin: 20px; min-width: 175px">
|
<div class="settings-device-select">
|
||||||
<a>Voice Activity Detection</a>
|
<a>Device:</a>
|
||||||
<div>
|
<div><select class="audio-select-microphone"></select></div>
|
||||||
<fieldset class="GroupBox">
|
|
||||||
<div><input type="radio" name="vad_type" value="pt" display="Always active"> Always active</div>
|
|
||||||
<div><input type="radio" name="vad_type" value="vad" display="Voice activity detection"> Voice activity detection</div>
|
|
||||||
<div><input type="radio" name="vad_type" value="ppt" display="Push to talk"> Push to talk</div>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="border-left:1px solid #000;height: auto;"></div>
|
|
||||||
<div style="flex-direction: column; align-content: stretch; vertical-align: center; margin: 20px;">
|
|
||||||
<div class="vad_settings">
|
|
||||||
<div style="font-size: 14px; text-align: center"><a class="vad_type">Type[Unknown]</a> settings</div>
|
|
||||||
<div class="vad_type_settings vad_type_pt">There are no setting entries for an <b>always</b> online voice detection.</div>
|
|
||||||
<div class="vad_type_settings vad_type_ppt">
|
|
||||||
<a>Push to talk key:</a>
|
|
||||||
<button class="vat_ppt_key">Uninitialised</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="vad_type_settings vad_type_vad">
|
</div>
|
||||||
<div>Voice activity threshold (<a class="vad_vad_slider_value">20</a>%)</div>
|
<div class="settings-vad-container">
|
||||||
<div class="vad_vad_threshold_selector">
|
<div class="group_box">
|
||||||
<div class="vad_vad_bar">
|
<div class="header">Voice Activity Detection</div>
|
||||||
<div style="width: 100%; height: 100%; position: absolute">
|
<div class="content">
|
||||||
<div class="vad_vad_bar_filler"></div>
|
<fieldset>
|
||||||
|
<div><input type="radio" name="vad_type" value="pt" display="Always active"> Always active</div>
|
||||||
|
<div><input type="radio" name="vad_type" value="vad" display="Voice activity detection"> Voice activity detection</div>
|
||||||
|
<div><input type="radio" name="vad_type" value="ppt" display="Push to talk"> Push to talk</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-vad-impl">
|
||||||
|
<div class="settings-vad-impl-entry setting-vad-pt">
|
||||||
|
There are no setting entries for an <b>always</b> online voice detection.
|
||||||
|
</div>
|
||||||
|
<div class="settings-vad-impl-entry setting-vad-ppt">
|
||||||
|
<a>Push to talk key:</a>
|
||||||
|
<button class="vat_ppt_key">Uninitialised</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-vad-impl-entry setting-vad-vad">
|
||||||
|
<div>Voice activity threshold (<a class="vad_vad_slider_value">20</a>%)</div>
|
||||||
|
<div class="vad_vad_threshold_selector">
|
||||||
|
<div class="vad_vad_bar">
|
||||||
|
<div style="width: 100%; height: 100%; position: absolute">
|
||||||
|
<div class="vad_vad_bar_filler"></div>
|
||||||
|
</div>
|
||||||
|
<input type="range" min="0" max="100" value="50" class="vad_vad_slider">
|
||||||
</div>
|
</div>
|
||||||
<input type="range" min="0" max="100" value="50" class="vad_vad_slider">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="group_box">
|
||||||
|
<div class="header">Speaker</div>
|
||||||
|
<div class="content settings-speaker">
|
||||||
|
<div class="settings-device settings-device-speaker">
|
||||||
|
<div class="settings-device-error"></div>
|
||||||
|
<div class="settings-device-select">
|
||||||
|
<a>Device:</a>
|
||||||
|
<div><select class="audio-select-speaker"></select></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-content>
|
</x-content>
|
||||||
</x-entry>
|
</x-entry>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"module": "none",
|
"module": "commonjs",
|
||||||
"sourceMap": true
|
"sourceMap": true
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
interface Navigator {
|
||||||
|
mozGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void;
|
||||||
|
webkitGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace audio.player {
|
||||||
|
let _globalContext: AudioContext;
|
||||||
|
let _globalContextPromise: Promise<void>;
|
||||||
|
let _initialized_listener: (() => any)[] = [];
|
||||||
|
|
||||||
|
export interface Device {
|
||||||
|
device_id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initialize() : boolean {
|
||||||
|
context();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initialized() : boolean {
|
||||||
|
return !!_globalContext && _globalContext.state === 'running';
|
||||||
|
}
|
||||||
|
|
||||||
|
function fire_initialized() {
|
||||||
|
console.log("Fire initialized: %o", _initialized_listener);
|
||||||
|
while(_initialized_listener.length > 0)
|
||||||
|
_initialized_listener.pop_front()();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function context() : AudioContext {
|
||||||
|
if(_globalContext && _globalContext.state != "suspended") return _globalContext;
|
||||||
|
|
||||||
|
if(!_globalContext)
|
||||||
|
_globalContext = new (window.webkitAudioContext || window.AudioContext)();
|
||||||
|
if(_globalContext.state == "suspended") {
|
||||||
|
if(!_globalContextPromise) {
|
||||||
|
(_globalContextPromise = _globalContext.resume()).then(() => {
|
||||||
|
fire_initialized();
|
||||||
|
}).catch(error => {
|
||||||
|
displayCriticalError("Failed to initialize global audio context! (" + error + ")");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_globalContext.resume(); //We already have our listener
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_globalContext.state == "running") {
|
||||||
|
fire_initialized();
|
||||||
|
return _globalContext;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function destination() : AudioNode {
|
||||||
|
return context().destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function on_ready(cb: () => any) {
|
||||||
|
if(initialized())
|
||||||
|
cb();
|
||||||
|
else
|
||||||
|
_initialized_listener.push(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WEB_DEVICE: Device = {device_id: "", name: "default playback"};
|
||||||
|
|
||||||
|
export function available_devices() : Promise<Device[]> {
|
||||||
|
return Promise.resolve([WEB_DEVICE])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function set_device(device_id: string) : Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function current_device() : Device {
|
||||||
|
return WEB_DEVICE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initializeFromGesture() {
|
||||||
|
context();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"listFiles": true,
|
||||||
|
"module": "system",
|
||||||
|
"target": "es6",
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"allowJs": false,
|
||||||
|
"checkJs": false,
|
||||||
|
|
||||||
|
"outFile": "declarations/web_api"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"js/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue