Implemented native opus codec transfer

This commit is contained in:
WolverinDEV 2018-09-21 23:25:03 +02:00
parent 1dbd201267
commit 986f84f726
8 changed files with 154 additions and 63 deletions

View file

@ -19,6 +19,7 @@
$localhost = true;
}
$localhost |= gethostname() == "WolverinDEV";
if (!$localhost || $testXF) {
//redirectOnInvalidSession();
}
@ -54,6 +55,7 @@
}
spawnProperty('connect_default_host', $localhost ? "localhost" : "ts.TeaSpeak.de");
spawnProperty('localhost_debug', $localhost ? "true" : "false");
spawnProperty('forum_user_data', $_COOKIE[$GLOBALS["COOKIE_NAME_USER_DATA"]]);
spawnProperty('forum_user_sign', $_COOKIE[$GLOBALS["COOKIE_NAME_USER_SIGN"]]);
?>

View file

@ -1,6 +1,7 @@
enum IdentitifyType {
TEAFORO,
TEAMSPEAK
TEAMSPEAK,
NICKNAME
}
namespace TSIdentityHelper {
@ -72,6 +73,33 @@ interface Identity {
valid() : boolean;
}
class NameIdentity implements Identity {
private _name: string;
constructor(name: string) {
this._name = name;
}
set_name(name: string) { this._name = name; }
name(): string {
return this._name;
}
uid(): string {
return btoa(this._name); //FIXME hash!
}
type(): IdentitifyType {
return IdentitifyType.NICKNAME;
}
valid(): boolean {
return this._name != undefined && this._name != "";
}
}
class TeamSpeakIdentity implements Identity {
private handle: any;
private _name: string;

View file

@ -293,11 +293,17 @@ class HandshakeHandler {
data.publicKey = (this.identity as TeamSpeakIdentity).publicKey();
} else if(this.identity.type() == IdentitifyType.TEAFORO) {
data.data = (this.identity as TeaForumIdentity).identityDataJson;
} else if(this.identity.type() == IdentitifyType.NICKNAME) {
data["client_nickname"] = this.identity.name();
}
this.connection.sendCommand("handshakebegin", data).catch(error => {
console.log(error);
//TODO here
}).then(() => {
if(this.identity.type() == IdentitifyType.NICKNAME) {
this.handshake_finished();
}
});
}
@ -307,20 +313,26 @@ class HandshakeHandler {
proof = (this.identity as TeamSpeakIdentity).signMessage(json[0]["message"]);
} else if(this.identity.type() == IdentitifyType.TEAFORO) {
proof = (this.identity as TeaForumIdentity).identitySign;
} else if(this.identity.type() == IdentitifyType.NICKNAME) {
//FIXME handle error this should never happen!
}
this.connection.sendCommand("handshakeindentityproof", {proof: proof}).then(() => {
this.connection.sendCommand("clientinit", {
//TODO variables!
client_nickname: this.name ? this.name : this.identity.name(),
client_platform: navigator.platform,
client_version: navigator.userAgent,
//client_browser_engine: navigator.product
});
this.handshake_finished();
}).catch(error => {
console.error("Got login error");
console.log(error);
}); //TODO handle error
}
private handshake_finished() {
this.connection.sendCommand("clientinit", {
//TODO variables!
client_nickname: this.name ? this.name : this.identity.name(),
client_platform: navigator.platform,
client_version: navigator.userAgent,
//client_browser_engine: navigator.product
});
}
}
class ConnectionCommandHandler {

View file

@ -23,7 +23,7 @@ namespace Modals {
let field_nickname = tag.find(".connect_nickname");
let nickname = field_nickname.val().toString();
let flag_nickname = nickname.length == 0 || nickname.length >= 3 && nickname.length <= 32;
let flag_nickname = (nickname.length == 0 && connectIdentity && connectIdentity.name().length > 0) || nickname.length >= 3 && nickname.length <= 32;
if(flag_address) {
if(field_address.hasClass("invalid_input"))
@ -61,47 +61,69 @@ namespace Modals {
tag.find(".identity_select").val(IdentitifyType[def_connect_type ? def_connect_type : settings.global("connect_identity_type", IdentitifyType.TEAFORO)]);
setTimeout(() => tag.find(".identity_select").trigger('change'), 0); //For some reason could not be run instantly
tag.find(".identity_file").change(function (this: HTMLInputElement) {
const reader = new FileReader();
reader.onload = function() {
connectIdentity = TSIdentityHelper.loadIdentityFromFileContains(reader.result);
{
tag.find(".identity_file").change(function (this: HTMLInputElement) {
const reader = new FileReader();
reader.onload = function() {
connectIdentity = TSIdentityHelper.loadIdentityFromFileContains(reader.result);
console.log(connectIdentity.uid());
if(!connectIdentity) tag.find(".error_message").text("Could not read identity! " + TSIdentityHelper.last_error());
else {
tag.find(".identity_string").val((connectIdentity as TeamSpeakIdentity).exported());
settings.changeGlobal("connect_identity_teamspeak_identity", (connectIdentity as TeamSpeakIdentity).exported());
console.log(connectIdentity.uid());
if(!connectIdentity) tag.find(".error_message").text("Could not read identity! " + TSIdentityHelper.last_error());
else {
tag.find(".identity_string").val((connectIdentity as TeamSpeakIdentity).exported());
settings.changeGlobal("connect_identity_teamspeak_identity", (connectIdentity as TeamSpeakIdentity).exported());
}
(!!connectIdentity ? tag.hide : tag.show).apply(tag.find(".error_message"));
updateFields();
};
reader.onerror = ev => {
tag.find(".error_message").text("Could not read identity file!").show();
updateFields();
};
reader.readAsText(this.files[0]);
});
tag.find(".identity_string").on('change', function (this: HTMLInputElement) {
if(this.value.length == 0){
tag.find(".error_message").text("Please select an identity!");
connectIdentity = undefined;
} else {
connectIdentity = TSIdentityHelper.loadIdentity(this.value);
if(!connectIdentity) tag.find(".error_message").text("Could not parse identity! " + TSIdentityHelper.last_error());
else settings.changeGlobal("connect_identity_teamspeak_identity", this.value);
}
(!!connectIdentity ? tag.hide : tag.show).apply(tag.find(".error_message"));
tag.find(".identity_file").val("");
updateFields();
};
reader.onerror = ev => {
tag.find(".error_message").text("Could not read identity file!").show();
});
tag.find(".identity_string").val(settings.global("connect_identity_teamspeak_identity", ""));
tag.find(".identity_config_" + IdentitifyType[IdentitifyType.TEAMSPEAK]).on('shown', ev => {
tag.find(".identity_string").trigger('change');
});
}
{
if(!forumIdentity)
tag.find(".identity_config_" + IdentitifyType[IdentitifyType.TEAFORO]).html("You cant use your TeaSpeak forum account.<br>You're not connected!");
tag.find(".identity_config_" + IdentitifyType[IdentitifyType.TEAFORO]).on('shown', ev => {
connectIdentity = forumIdentity;
updateFields();
};
reader.readAsText(this.files[0]);
});
});
}
tag.find(".identity_string").on('change', function (this: HTMLInputElement) {
if(this.value.length == 0){
tag.find(".error_message").text("Please select an identity!");
connectIdentity = undefined;
} else {
connectIdentity = TSIdentityHelper.loadIdentity(this.value);
if(!connectIdentity) tag.find(".error_message").text("Could not parse identity! " + TSIdentityHelper.last_error());
else settings.changeGlobal("connect_identity_teamspeak_identity", this.value);
}
(!!connectIdentity ? tag.hide : tag.show).apply(tag.find(".error_message"));
tag.find(".identity_file").val("");
updateFields();
});
tag.find(".identity_string").val(settings.global("connect_identity_teamspeak_identity", ""));
tag.find(".identity_config_" + IdentitifyType[IdentitifyType.TEAMSPEAK]).on('shown', ev => { tag.find(".identity_string").trigger('change'); });
if(!forumIdentity)
tag.find(".identity_config_" + IdentitifyType[IdentitifyType.TEAFORO]).html("You cant use your TeaSpeak forum account.<br>You're not connected!");
tag.find(".identity_config_" + IdentitifyType[IdentitifyType.TEAFORO]).on('shown', ev => { connectIdentity = forumIdentity; updateFields(); });
{
tag.find(".identity_config_" + IdentitifyType[IdentitifyType.NICKNAME]).on('shown', ev => {
connectIdentity = new NameIdentity(tag.find(".connect_nickname").val() as string);
updateFields();
});
tag.find(".connect_nickname").on("keyup", () => {
if(connectIdentity instanceof NameIdentity)
connectIdentity.set_name(tag.find(".connect_nickname").val() as string);
});
if(!settings.static("localhost_debug", false))
tag.find(".identity_select option[value=" + IdentitifyType[IdentitifyType.NICKNAME] + "]").remove();
}
//connect_address
return tag;

View file

@ -101,12 +101,20 @@ class CodecPool {
}
}
enum VoiceConnectionType {
JS_ENCODE,
NATIVE_ENCODE
}
class VoiceConnection {
client: TSClient;
rtcPeerConnection: RTCPeerConnection;
dataChannel: RTCDataChannel;
voiceRecorder: VoiceRecorder;
type: VoiceConnectionType = VoiceConnectionType.NATIVE_ENCODE;
local_audio_stream: any;
private codec_pool: CodecPool[] = [
new CodecPool(this,0,"Spex A", undefined), //Spex
@ -124,13 +132,23 @@ class VoiceConnection {
constructor(client) {
this.client = client;
this.voiceRecorder = new VoiceRecorder(this);
this.voiceRecorder.on_data = this.handleVoiceData.bind(this);
if(this.type != VoiceConnectionType.NATIVE_ENCODE) {
this.voiceRecorder.on_data = this.handleVoiceData.bind(this);
}
this.voiceRecorder.on_end = this.handleVoiceEnded.bind(this);
this.voiceRecorder.reinitialiseVAD();
AudioController.on_initialized(() => {
this.codec_pool[4].initialize(2);
this.codec_pool[5].initialize(2);
if(this.type == VoiceConnectionType.NATIVE_ENCODE) {
let stream = this.voiceRecorder.get_output_stream();
stream.disconnect();
this.local_audio_stream = AudioController.globalContext.createMediaStreamDestination();
stream.connect(this.local_audio_stream);
}
});
this.send_task = setInterval(this.sendNextVoicePacket.bind(this), 20);
@ -180,6 +198,7 @@ class VoiceConnection {
createSession() {
this._ice_use_cache = true;
let config: RTCConfiguration = {};
config.iceServers = [];
config.iceServers.push({ urls: 'stun:stun.l.google.com:19302' });
@ -192,10 +211,14 @@ class VoiceConnection {
this.dataChannel.binaryType = "arraybuffer";
let sdpConstraints : RTCOfferOptions = {};
sdpConstraints.offerToReceiveAudio = 0;
sdpConstraints.offerToReceiveAudio = this.type == VoiceConnectionType.NATIVE_ENCODE ? 1 : 0;
sdpConstraints.offerToReceiveVideo = 0;
this.rtcPeerConnection.onicecandidate = this.onIceCandidate.bind(this);
if(this.local_audio_stream) { //May a typecheck?
this.rtcPeerConnection.addStream(this.local_audio_stream.stream);
}
this.rtcPeerConnection.createOffer(this.onOfferCreated.bind(this), () => {
console.error("Could not create ice offer!");
}, sdpConstraints);
@ -317,6 +340,10 @@ class VoiceConnection {
.then(encoder => encoder.encodeSamples(this.client.getClient().getAudioController().codecCache(4), data));
}
audio_destination() : AudioNode {
return undefined;
}
private handleVoiceEnded() {
if(!this.voiceRecorder) return;
if(!this.client.connected) return;
@ -324,6 +351,6 @@ class VoiceConnection {
console.log("Voice ended");
this.client.getClient().speaking = false;
if(this.dataChannel)
this.sendVoicePacket(new Uint8Array(0), 4); //TODO Use channel codec!
this.sendVoicePacket(new Uint8Array(0), 5); //TODO Use channel codec!
}
}

View file

@ -29,7 +29,7 @@ class VoiceRecorder {
private static readonly BUFFER_SIZE = 1024 * 4;
handle: VoiceConnection;
on_data: (data: AudioBuffer, head: boolean) => void = (data) => {};
on_data: (data: AudioBuffer, head: boolean) => void = undefined;
on_end: () => void = () => {};
private _recording: boolean = false;
@ -38,8 +38,8 @@ class VoiceRecorder {
private mediaStream: MediaStream = undefined;
private audioContext: AudioContext;
private processor: any;
private mute: GainNode;
private processor: ScriptProcessorNode;
get_output_stream() : ScriptProcessorNode { return this.processor; }
private vadHandler: VoiceActivityDetector;
private _chunkCount: number = 0;
@ -58,20 +58,20 @@ class VoiceRecorder {
this.processor = this.audioContext.createScriptProcessor(VoiceRecorder.BUFFER_SIZE, VoiceRecorder.CHANNELS, VoiceRecorder.CHANNELS);
this.processor.addEventListener('audioprocess', ev => {
if(this.microphoneStream && this.vadHandler.shouldRecord(ev.inputBuffer))
this.on_data(ev.inputBuffer, this._chunkCount++ == 0);
else {
if(this.microphoneStream && this.vadHandler.shouldRecord(ev.inputBuffer)) {
if(this.on_data)
this.on_data(ev.inputBuffer, this._chunkCount++ == 0);
else {
for(let channel = 0; channel < ev.inputBuffer.numberOfChannels; channel++)
ev.outputBuffer.copyToChannel(ev.inputBuffer.getChannelData(channel), channel);
}
} else {
if(this._chunkCount != 0) this.on_end();
this._chunkCount = 0
}
});
this.processor.connect(this.audioContext.destination);
//Not needed but make sure we have data for the preprocessor
this.mute = this.audioContext.createGain();
this.mute.gain.setValueAtTime(0, 0);
this.mute.connect(this.audioContext.destination);
if(this.vadHandler)
this.vadHandler.initialise();
this.on_microphone(this.mediaStream);
@ -92,10 +92,6 @@ class VoiceRecorder {
return this.mediaStream;
}
getDestinationContext() : AudioNode {
return this.mute;
}
getMicrophoneStream() : MediaStreamAudioSourceNode {
return this.microphoneStream;
}

View file

@ -437,6 +437,7 @@
<select class="identity_select">
<option name="identity_type" value="TEAFORO">Forum Account</option>
<option name="identity_type" value="TEAMSPEAK">TeamSpeak</option>
<option name="identity_type" value="NICKNAME">Nickname (Debug purposes only!)</option> <!-- Only available on localhost for debug -->
</select>
</div>
<hr>
@ -450,6 +451,9 @@
<div class="identity_config_TEAFORO identity_config">
You're using your forum account as verification
</div>
<div class="identity_config_NICKNAME identity_config">
This is just for debug and uses the name as unique identifier
</div>
<div style="background-color: red; border-radius: 1px; display: none" class="error_message"></div>
</div>

2
vendor/bbcode vendored

@ -1 +1 @@
Subproject commit 67a1519a7254f2db9706f13184c0c38f331464b0
Subproject commit 5fe4c1d9dedf52484b036575d4b068cde8858fb6