Implemented native opus codec transfer
This commit is contained in:
parent
1dbd201267
commit
986f84f726
8 changed files with 154 additions and 63 deletions
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
$localhost = true;
|
$localhost = true;
|
||||||
}
|
}
|
||||||
|
$localhost |= gethostname() == "WolverinDEV";
|
||||||
if (!$localhost || $testXF) {
|
if (!$localhost || $testXF) {
|
||||||
//redirectOnInvalidSession();
|
//redirectOnInvalidSession();
|
||||||
}
|
}
|
||||||
|
@ -54,6 +55,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
spawnProperty('connect_default_host', $localhost ? "localhost" : "ts.TeaSpeak.de");
|
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_data', $_COOKIE[$GLOBALS["COOKIE_NAME_USER_DATA"]]);
|
||||||
spawnProperty('forum_user_sign', $_COOKIE[$GLOBALS["COOKIE_NAME_USER_SIGN"]]);
|
spawnProperty('forum_user_sign', $_COOKIE[$GLOBALS["COOKIE_NAME_USER_SIGN"]]);
|
||||||
?>
|
?>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
enum IdentitifyType {
|
enum IdentitifyType {
|
||||||
TEAFORO,
|
TEAFORO,
|
||||||
TEAMSPEAK
|
TEAMSPEAK,
|
||||||
|
NICKNAME
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace TSIdentityHelper {
|
namespace TSIdentityHelper {
|
||||||
|
@ -72,6 +73,33 @@ interface Identity {
|
||||||
valid() : boolean;
|
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 {
|
class TeamSpeakIdentity implements Identity {
|
||||||
private handle: any;
|
private handle: any;
|
||||||
private _name: string;
|
private _name: string;
|
||||||
|
|
|
@ -293,11 +293,17 @@ class HandshakeHandler {
|
||||||
data.publicKey = (this.identity as TeamSpeakIdentity).publicKey();
|
data.publicKey = (this.identity as TeamSpeakIdentity).publicKey();
|
||||||
} else if(this.identity.type() == IdentitifyType.TEAFORO) {
|
} else if(this.identity.type() == IdentitifyType.TEAFORO) {
|
||||||
data.data = (this.identity as TeaForumIdentity).identityDataJson;
|
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 => {
|
this.connection.sendCommand("handshakebegin", data).catch(error => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
//TODO here
|
//TODO here
|
||||||
|
}).then(() => {
|
||||||
|
if(this.identity.type() == IdentitifyType.NICKNAME) {
|
||||||
|
this.handshake_finished();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,8 +313,18 @@ class HandshakeHandler {
|
||||||
proof = (this.identity as TeamSpeakIdentity).signMessage(json[0]["message"]);
|
proof = (this.identity as TeamSpeakIdentity).signMessage(json[0]["message"]);
|
||||||
} else if(this.identity.type() == IdentitifyType.TEAFORO) {
|
} else if(this.identity.type() == IdentitifyType.TEAFORO) {
|
||||||
proof = (this.identity as TeaForumIdentity).identitySign;
|
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("handshakeindentityproof", {proof: proof}).then(() => {
|
||||||
|
this.handshake_finished();
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("Got login error");
|
||||||
|
console.log(error);
|
||||||
|
}); //TODO handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
private handshake_finished() {
|
||||||
this.connection.sendCommand("clientinit", {
|
this.connection.sendCommand("clientinit", {
|
||||||
//TODO variables!
|
//TODO variables!
|
||||||
client_nickname: this.name ? this.name : this.identity.name(),
|
client_nickname: this.name ? this.name : this.identity.name(),
|
||||||
|
@ -316,10 +332,6 @@ class HandshakeHandler {
|
||||||
client_version: navigator.userAgent,
|
client_version: navigator.userAgent,
|
||||||
//client_browser_engine: navigator.product
|
//client_browser_engine: navigator.product
|
||||||
});
|
});
|
||||||
}).catch(error => {
|
|
||||||
console.error("Got login error");
|
|
||||||
console.log(error);
|
|
||||||
}); //TODO handle error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ namespace Modals {
|
||||||
|
|
||||||
let field_nickname = tag.find(".connect_nickname");
|
let field_nickname = tag.find(".connect_nickname");
|
||||||
let nickname = field_nickname.val().toString();
|
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(flag_address) {
|
||||||
if(field_address.hasClass("invalid_input"))
|
if(field_address.hasClass("invalid_input"))
|
||||||
|
@ -61,6 +61,7 @@ namespace Modals {
|
||||||
tag.find(".identity_select").val(IdentitifyType[def_connect_type ? def_connect_type : settings.global("connect_identity_type", IdentitifyType.TEAFORO)]);
|
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
|
setTimeout(() => tag.find(".identity_select").trigger('change'), 0); //For some reason could not be run instantly
|
||||||
|
|
||||||
|
{
|
||||||
tag.find(".identity_file").change(function (this: HTMLInputElement) {
|
tag.find(".identity_file").change(function (this: HTMLInputElement) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function() {
|
reader.onload = function() {
|
||||||
|
@ -97,11 +98,32 @@ namespace Modals {
|
||||||
updateFields();
|
updateFields();
|
||||||
});
|
});
|
||||||
tag.find(".identity_string").val(settings.global("connect_identity_teamspeak_identity", ""));
|
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'); });
|
tag.find(".identity_config_" + IdentitifyType[IdentitifyType.TEAMSPEAK]).on('shown', ev => {
|
||||||
|
tag.find(".identity_string").trigger('change');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
if(!forumIdentity)
|
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]).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.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
|
//connect_address
|
||||||
return tag;
|
return tag;
|
||||||
|
|
|
@ -101,12 +101,20 @@ class CodecPool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum VoiceConnectionType {
|
||||||
|
JS_ENCODE,
|
||||||
|
NATIVE_ENCODE
|
||||||
|
}
|
||||||
|
|
||||||
class VoiceConnection {
|
class VoiceConnection {
|
||||||
client: TSClient;
|
client: TSClient;
|
||||||
rtcPeerConnection: RTCPeerConnection;
|
rtcPeerConnection: RTCPeerConnection;
|
||||||
dataChannel: RTCDataChannel;
|
dataChannel: RTCDataChannel;
|
||||||
|
|
||||||
voiceRecorder: VoiceRecorder;
|
voiceRecorder: VoiceRecorder;
|
||||||
|
type: VoiceConnectionType = VoiceConnectionType.NATIVE_ENCODE;
|
||||||
|
|
||||||
|
local_audio_stream: any;
|
||||||
|
|
||||||
private codec_pool: CodecPool[] = [
|
private codec_pool: CodecPool[] = [
|
||||||
new CodecPool(this,0,"Spex A", undefined), //Spex
|
new CodecPool(this,0,"Spex A", undefined), //Spex
|
||||||
|
@ -124,13 +132,23 @@ class VoiceConnection {
|
||||||
constructor(client) {
|
constructor(client) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.voiceRecorder = new VoiceRecorder(this);
|
this.voiceRecorder = new VoiceRecorder(this);
|
||||||
|
if(this.type != VoiceConnectionType.NATIVE_ENCODE) {
|
||||||
this.voiceRecorder.on_data = this.handleVoiceData.bind(this);
|
this.voiceRecorder.on_data = this.handleVoiceData.bind(this);
|
||||||
|
}
|
||||||
this.voiceRecorder.on_end = this.handleVoiceEnded.bind(this);
|
this.voiceRecorder.on_end = this.handleVoiceEnded.bind(this);
|
||||||
this.voiceRecorder.reinitialiseVAD();
|
this.voiceRecorder.reinitialiseVAD();
|
||||||
|
|
||||||
AudioController.on_initialized(() => {
|
AudioController.on_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);
|
||||||
|
|
||||||
|
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);
|
this.send_task = setInterval(this.sendNextVoicePacket.bind(this), 20);
|
||||||
|
@ -180,6 +198,7 @@ class VoiceConnection {
|
||||||
createSession() {
|
createSession() {
|
||||||
this._ice_use_cache = true;
|
this._ice_use_cache = true;
|
||||||
|
|
||||||
|
|
||||||
let config: RTCConfiguration = {};
|
let config: RTCConfiguration = {};
|
||||||
config.iceServers = [];
|
config.iceServers = [];
|
||||||
config.iceServers.push({ urls: 'stun:stun.l.google.com:19302' });
|
config.iceServers.push({ urls: 'stun:stun.l.google.com:19302' });
|
||||||
|
@ -192,10 +211,14 @@ class VoiceConnection {
|
||||||
this.dataChannel.binaryType = "arraybuffer";
|
this.dataChannel.binaryType = "arraybuffer";
|
||||||
|
|
||||||
let sdpConstraints : RTCOfferOptions = {};
|
let sdpConstraints : RTCOfferOptions = {};
|
||||||
sdpConstraints.offerToReceiveAudio = 0;
|
sdpConstraints.offerToReceiveAudio = this.type == VoiceConnectionType.NATIVE_ENCODE ? 1 : 0;
|
||||||
sdpConstraints.offerToReceiveVideo = 0;
|
sdpConstraints.offerToReceiveVideo = 0;
|
||||||
|
|
||||||
this.rtcPeerConnection.onicecandidate = this.onIceCandidate.bind(this);
|
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), () => {
|
this.rtcPeerConnection.createOffer(this.onOfferCreated.bind(this), () => {
|
||||||
console.error("Could not create ice offer!");
|
console.error("Could not create ice offer!");
|
||||||
}, sdpConstraints);
|
}, sdpConstraints);
|
||||||
|
@ -317,6 +340,10 @@ class VoiceConnection {
|
||||||
.then(encoder => encoder.encodeSamples(this.client.getClient().getAudioController().codecCache(4), data));
|
.then(encoder => encoder.encodeSamples(this.client.getClient().getAudioController().codecCache(4), data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audio_destination() : AudioNode {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private handleVoiceEnded() {
|
private handleVoiceEnded() {
|
||||||
if(!this.voiceRecorder) return;
|
if(!this.voiceRecorder) return;
|
||||||
if(!this.client.connected) return;
|
if(!this.client.connected) return;
|
||||||
|
@ -324,6 +351,6 @@ class VoiceConnection {
|
||||||
console.log("Voice ended");
|
console.log("Voice ended");
|
||||||
this.client.getClient().speaking = false;
|
this.client.getClient().speaking = false;
|
||||||
if(this.dataChannel)
|
if(this.dataChannel)
|
||||||
this.sendVoicePacket(new Uint8Array(0), 4); //TODO Use channel codec!
|
this.sendVoicePacket(new Uint8Array(0), 5); //TODO Use channel codec!
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -29,7 +29,7 @@ class VoiceRecorder {
|
||||||
private static readonly BUFFER_SIZE = 1024 * 4;
|
private static readonly BUFFER_SIZE = 1024 * 4;
|
||||||
|
|
||||||
handle: VoiceConnection;
|
handle: VoiceConnection;
|
||||||
on_data: (data: AudioBuffer, head: boolean) => void = (data) => {};
|
on_data: (data: AudioBuffer, head: boolean) => void = undefined;
|
||||||
on_end: () => void = () => {};
|
on_end: () => void = () => {};
|
||||||
|
|
||||||
private _recording: boolean = false;
|
private _recording: boolean = false;
|
||||||
|
@ -38,8 +38,8 @@ class VoiceRecorder {
|
||||||
private mediaStream: MediaStream = undefined;
|
private mediaStream: MediaStream = undefined;
|
||||||
|
|
||||||
private audioContext: AudioContext;
|
private audioContext: AudioContext;
|
||||||
private processor: any;
|
private processor: ScriptProcessorNode;
|
||||||
private mute: GainNode;
|
get_output_stream() : ScriptProcessorNode { return this.processor; }
|
||||||
|
|
||||||
private vadHandler: VoiceActivityDetector;
|
private vadHandler: VoiceActivityDetector;
|
||||||
private _chunkCount: number = 0;
|
private _chunkCount: number = 0;
|
||||||
|
@ -58,20 +58,20 @@ class VoiceRecorder {
|
||||||
this.processor = this.audioContext.createScriptProcessor(VoiceRecorder.BUFFER_SIZE, VoiceRecorder.CHANNELS, VoiceRecorder.CHANNELS);
|
this.processor = this.audioContext.createScriptProcessor(VoiceRecorder.BUFFER_SIZE, VoiceRecorder.CHANNELS, VoiceRecorder.CHANNELS);
|
||||||
|
|
||||||
this.processor.addEventListener('audioprocess', ev => {
|
this.processor.addEventListener('audioprocess', ev => {
|
||||||
if(this.microphoneStream && this.vadHandler.shouldRecord(ev.inputBuffer))
|
if(this.microphoneStream && this.vadHandler.shouldRecord(ev.inputBuffer)) {
|
||||||
|
if(this.on_data)
|
||||||
this.on_data(ev.inputBuffer, this._chunkCount++ == 0);
|
this.on_data(ev.inputBuffer, this._chunkCount++ == 0);
|
||||||
else {
|
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();
|
if(this._chunkCount != 0) this.on_end();
|
||||||
this._chunkCount = 0
|
this._chunkCount = 0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.processor.connect(this.audioContext.destination);
|
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)
|
if(this.vadHandler)
|
||||||
this.vadHandler.initialise();
|
this.vadHandler.initialise();
|
||||||
this.on_microphone(this.mediaStream);
|
this.on_microphone(this.mediaStream);
|
||||||
|
@ -92,10 +92,6 @@ class VoiceRecorder {
|
||||||
return this.mediaStream;
|
return this.mediaStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDestinationContext() : AudioNode {
|
|
||||||
return this.mute;
|
|
||||||
}
|
|
||||||
|
|
||||||
getMicrophoneStream() : MediaStreamAudioSourceNode {
|
getMicrophoneStream() : MediaStreamAudioSourceNode {
|
||||||
return this.microphoneStream;
|
return this.microphoneStream;
|
||||||
}
|
}
|
||||||
|
|
|
@ -437,6 +437,7 @@
|
||||||
<select class="identity_select">
|
<select class="identity_select">
|
||||||
<option name="identity_type" value="TEAFORO">Forum Account</option>
|
<option name="identity_type" value="TEAFORO">Forum Account</option>
|
||||||
<option name="identity_type" value="TEAMSPEAK">TeamSpeak</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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -450,6 +451,9 @@
|
||||||
<div class="identity_config_TEAFORO identity_config">
|
<div class="identity_config_TEAFORO identity_config">
|
||||||
You're using your forum account as verification
|
You're using your forum account as verification
|
||||||
</div>
|
</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 style="background-color: red; border-radius: 1px; display: none" class="error_message"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
2
vendor/bbcode
vendored
2
vendor/bbcode
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit 67a1519a7254f2db9706f13184c0c38f331464b0
|
Subproject commit 5fe4c1d9dedf52484b036575d4b068cde8858fb6
|
Loading…
Add table
Reference in a new issue