Update to 1.4.11
This commit is contained in:
parent
a8b1299c4e
commit
4a49d36ece
2
github
2
github
@ -1 +1 @@
|
|||||||
Subproject commit 27579459d13317200cb9d49f2908b92c22031b11
|
Subproject commit 4fa1ab237cd12b53de46fe82d31c942513c619bd
|
@ -122,5 +122,5 @@ function deploy_client() {
|
|||||||
#install_npm
|
#install_npm
|
||||||
#compile_scripts
|
#compile_scripts
|
||||||
#compile_native
|
#compile_native
|
||||||
package_client
|
#package_client
|
||||||
#deploy_client
|
deploy_client
|
||||||
|
@ -2,8 +2,8 @@ import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
|||||||
|
|
||||||
import * as electron from "electron";
|
import * as electron from "electron";
|
||||||
import {tr, tra} from "tc-shared/i18n/localize";
|
import {tr, tra} from "tc-shared/i18n/localize";
|
||||||
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
|
||||||
import {handle_connect_request} from "tc-shared/main";
|
import {handle_connect_request} from "tc-shared/main";
|
||||||
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
|
|
||||||
electron.ipcRenderer.on('connect', (event, url) => handle_native_connect_request(url));
|
electron.ipcRenderer.on('connect', (event, url) => handle_native_connect_request(url));
|
||||||
|
|
||||||
@ -25,8 +25,9 @@ function handle_native_connect_request(url_string: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let connection = server_connections.active_connection();
|
let connection = server_connections.active_connection();
|
||||||
if(connection.connected)
|
if(connection.connected) {
|
||||||
connection = server_connections.spawn_server_connection();
|
connection = server_connections.spawn_server_connection();
|
||||||
|
}
|
||||||
|
|
||||||
handle_connect_request({
|
handle_connect_request({
|
||||||
address: url.host + ":" + (url.searchParams.has("port") ? url.searchParams.get("port") : "9987"),
|
address: url.host + ":" + (url.searchParams.has("port") ? url.searchParams.get("port") : "9987"),
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import {Settings, settings} from "tc-shared/settings";
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
|
||||||
import {tr} from "tc-shared/i18n/localize";
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
import {Arguments, process_args} from "../shared/process-arguments";
|
import {Arguments, process_args} from "../shared/process-arguments";
|
||||||
import {remote} from "electron";
|
import {remote} from "electron";
|
||||||
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
|
|
||||||
window.onbeforeunload = event => {
|
window.onbeforeunload = event => {
|
||||||
if(settings.static(Settings.KEY_DISABLE_UNLOAD_DIALOG))
|
if(settings.static(Settings.KEY_DISABLE_UNLOAD_DIALOG))
|
||||||
|
175
modules/renderer/WindowsTrayHandler.ts
Normal file
175
modules/renderer/WindowsTrayHandler.ts
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import * as loader from "tc-loader";
|
||||||
|
import {Stage} from "tc-loader";
|
||||||
|
|
||||||
|
import {MenuItemConstructorOptions, NativeImage, remote, Tray} from "electron";
|
||||||
|
import {clientIconClassToImage} from "./IconHelper";
|
||||||
|
import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
|
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
||||||
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
|
import {global_client_actions} from "tc-shared/events/GlobalEvents";
|
||||||
|
|
||||||
|
const kTrayGlobalUniqueId = "9ccaf91c-a54f-45e0-b061-c50c9f7864ca";
|
||||||
|
|
||||||
|
let tray: Tray;
|
||||||
|
let eventListener = [];
|
||||||
|
let defaultIcon: NativeImage;
|
||||||
|
async function initializeTray() {
|
||||||
|
defaultIcon = clientIconClassToImage(ClientIcon.TeaspeakLogo);
|
||||||
|
defaultIcon = remote.nativeImage.createFromBuffer(defaultIcon.toPNG());
|
||||||
|
|
||||||
|
tray = new remote.Tray(defaultIcon);
|
||||||
|
tray.setTitle("TeaSpeak - Client");
|
||||||
|
tray.on("double-click", () => remote.getCurrentWindow().show());
|
||||||
|
|
||||||
|
server_connections.events().on("notify_active_handler_changed", event => initializeConnection(event.newHandler));
|
||||||
|
initializeConnection(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeConnection(connection: ConnectionHandler) {
|
||||||
|
eventListener.forEach(callback => callback());
|
||||||
|
eventListener = [];
|
||||||
|
|
||||||
|
let showClientStatus = connection?.connection_state === ConnectionState.CONNECTED;
|
||||||
|
let clientStatusIcon: ClientIcon = connection?.getClient().getStatusIcon();
|
||||||
|
|
||||||
|
const updateTray = () => {
|
||||||
|
if(showClientStatus) {
|
||||||
|
let icon = clientIconClassToImage(clientStatusIcon);
|
||||||
|
icon = remote.nativeImage.createFromBuffer(icon.toPNG());
|
||||||
|
tray.setImage(icon);
|
||||||
|
tray.setToolTip("TeaSpeak - Client\nConnected to " + connection.channelTree.server.properties.virtualserver_name);
|
||||||
|
} else {
|
||||||
|
tray.setImage(defaultIcon);
|
||||||
|
tray.setToolTip("TeaSpeak - Client");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateContextMenu = () => {
|
||||||
|
let items: MenuItemConstructorOptions[] = [];
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
{ label: tr("Show TeaClient"), type: "normal", icon: defaultIcon, click: () => remote.getCurrentWindow().show() },
|
||||||
|
{ label: "seperator", type: "separator" },
|
||||||
|
);
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
{
|
||||||
|
label: tr("Connect to server"),
|
||||||
|
type: "normal",
|
||||||
|
icon: clientIconClassToImage(ClientIcon.Connect),
|
||||||
|
click: () => {
|
||||||
|
global_client_actions.fire("action_open_window_connect", { newTab: connection?.connected });
|
||||||
|
remote.getCurrentWindow().show();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: tr("Disconnect from current server"),
|
||||||
|
type: "normal",
|
||||||
|
icon: clientIconClassToImage(ClientIcon.Disconnect),
|
||||||
|
click: () => connection.disconnectFromServer(),
|
||||||
|
enabled: connection?.connected
|
||||||
|
},
|
||||||
|
{ label: "seperator", type: "separator" },
|
||||||
|
)
|
||||||
|
|
||||||
|
if(connection) {
|
||||||
|
if(connection.isMicrophoneDisabled()) {
|
||||||
|
items.push({
|
||||||
|
label: tr("Enable microphone"),
|
||||||
|
type: "normal",
|
||||||
|
icon: clientIconClassToImage(ClientIcon.ActivateMicrophone),
|
||||||
|
checked: true,
|
||||||
|
click: () => connection.setMicrophoneMuted(false)
|
||||||
|
});
|
||||||
|
} else if(connection.isMicrophoneMuted()) {
|
||||||
|
items.push({
|
||||||
|
label: tr("Unmute microphone"),
|
||||||
|
type: "normal",
|
||||||
|
icon: clientIconClassToImage(ClientIcon.InputMuted),
|
||||||
|
checked: true,
|
||||||
|
click: () => {
|
||||||
|
connection.setMicrophoneMuted(false);
|
||||||
|
connection.acquireInputHardware().then(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
items.push({
|
||||||
|
label: tr("Mute microphone"),
|
||||||
|
type: "normal",
|
||||||
|
icon: clientIconClassToImage(ClientIcon.InputMuted),
|
||||||
|
checked: false,
|
||||||
|
click: () => connection.setMicrophoneMuted(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(connection.isSpeakerMuted()) {
|
||||||
|
items.push({
|
||||||
|
label: tr("Unmute speaker/headphones"),
|
||||||
|
type: "normal",
|
||||||
|
icon: clientIconClassToImage(ClientIcon.OutputMuted),
|
||||||
|
checked: true,
|
||||||
|
click: () => connection.setSpeakerMuted(false)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
items.push({
|
||||||
|
label: tr("Mute speaker/headphones"),
|
||||||
|
type: "normal",
|
||||||
|
icon: clientIconClassToImage(ClientIcon.OutputMuted),
|
||||||
|
checked: true,
|
||||||
|
click: () => connection.setSpeakerMuted(false)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
{ label: "seperator", type: "separator" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
{ label: tr("Quit"), type: "normal", icon: clientIconClassToImage(ClientIcon.CloseButton), click: () => remote.getCurrentWindow().close() }
|
||||||
|
);
|
||||||
|
|
||||||
|
tray.setContextMenu(remote.Menu.buildFromTemplate(items));
|
||||||
|
};
|
||||||
|
|
||||||
|
if(connection) {
|
||||||
|
eventListener.push(connection.channelTree.server.events.on("notify_properties_updated", event => {
|
||||||
|
if("virtualserver_name" in event.updated_properties) {
|
||||||
|
updateTray();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
eventListener.push(connection.events().on("notify_connection_state_changed", event => {
|
||||||
|
showClientStatus = event.new_state === ConnectionState.CONNECTED;
|
||||||
|
updateTray();
|
||||||
|
updateContextMenu();
|
||||||
|
}));
|
||||||
|
|
||||||
|
eventListener.push(connection.getClient().events.on("notify_status_icon_changed", event => {
|
||||||
|
clientStatusIcon = event.newIcon;
|
||||||
|
updateTray();
|
||||||
|
}));
|
||||||
|
|
||||||
|
eventListener.push(connection.events().on("notify_state_updated", event => {
|
||||||
|
switch (event.state) {
|
||||||
|
case "away":
|
||||||
|
case "microphone":
|
||||||
|
case "speaker":
|
||||||
|
updateContextMenu();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContextMenu();
|
||||||
|
updateTray();
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
name: "tray bar",
|
||||||
|
function: initializeTray,
|
||||||
|
priority: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("unload", () => tray.destroy());
|
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AbstractInput,
|
AbstractInput,
|
||||||
|
FilterMode,
|
||||||
InputConsumer,
|
InputConsumer,
|
||||||
InputConsumerType,
|
InputConsumerType,
|
||||||
InputEvents,
|
InputEvents,
|
||||||
@ -14,6 +15,7 @@ import {Filter, FilterType, FilterTypeClass} from "tc-shared/voice/Filter";
|
|||||||
import {NativeFilter, NStateFilter, NThresholdFilter, NVoiceLevelFilter} from "./AudioFilter";
|
import {NativeFilter, NStateFilter, NThresholdFilter, NVoiceLevelFilter} from "./AudioFilter";
|
||||||
import {IDevice} from "tc-shared/audio/recorder";
|
import {IDevice} from "tc-shared/audio/recorder";
|
||||||
import {LogCategory, logWarn} from "tc-shared/log";
|
import {LogCategory, logWarn} from "tc-shared/log";
|
||||||
|
import NativeFilterMode = audio.record.FilterMode;
|
||||||
|
|
||||||
export class NativeInput implements AbstractInput {
|
export class NativeInput implements AbstractInput {
|
||||||
readonly events: Registry<InputEvents>;
|
readonly events: Registry<InputEvents>;
|
||||||
@ -90,8 +92,9 @@ export class NativeInput implements AbstractInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setDeviceId(device: string | undefined): Promise<void> {
|
async setDeviceId(device: string | undefined): Promise<void> {
|
||||||
if(this.deviceId === device)
|
if(this.deviceId === device) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.deviceId = device;
|
this.deviceId = device;
|
||||||
await this.stop();
|
await this.stop();
|
||||||
@ -160,8 +163,10 @@ export class NativeInput implements AbstractInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setConsumer(consumer: InputConsumer): Promise<void> {
|
async setConsumer(consumer: InputConsumer): Promise<void> {
|
||||||
if(typeof(consumer) !== "undefined")
|
if(typeof(consumer) !== "undefined") {
|
||||||
throw "we only support native consumers!"; // TODO: May create a general wrapper?
|
throw "we only support native consumers!"; // TODO: May create a general wrapper?
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,6 +177,40 @@ export class NativeInput implements AbstractInput {
|
|||||||
getVolume(): number {
|
getVolume(): number {
|
||||||
return this.nativeHandle.get_volume();
|
return this.nativeHandle.get_volume();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFilterMode(): FilterMode {
|
||||||
|
const mode = this.nativeConsumer.get_filter_mode();
|
||||||
|
switch (mode) {
|
||||||
|
case NativeFilterMode.Block:
|
||||||
|
return FilterMode.Block;
|
||||||
|
|
||||||
|
case NativeFilterMode.Bypass:
|
||||||
|
return FilterMode.Bypass;
|
||||||
|
|
||||||
|
case NativeFilterMode.Filter:
|
||||||
|
default:
|
||||||
|
return FilterMode.Filter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilterMode(mode: FilterMode) {
|
||||||
|
let nativeMode: NativeFilterMode;
|
||||||
|
switch (mode) {
|
||||||
|
case FilterMode.Filter:
|
||||||
|
nativeMode = NativeFilterMode.Filter;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FilterMode.Bypass:
|
||||||
|
nativeMode = NativeFilterMode.Bypass;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FilterMode.Block:
|
||||||
|
nativeMode = NativeFilterMode.Block;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nativeConsumer.set_filter_mode(nativeMode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NativeLevelMeter implements LevelMeter {
|
export class NativeLevelMeter implements LevelMeter {
|
||||||
@ -236,11 +275,11 @@ export class NativeLevelMeter implements LevelMeter {
|
|||||||
this._filter = undefined;
|
this._filter = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
device(): IDevice {
|
getDevice(): IDevice {
|
||||||
return this._device;
|
return this._device;
|
||||||
}
|
}
|
||||||
|
|
||||||
set_observer(callback: (value: number) => any) {
|
setObserver(callback: (value: number) => any) {
|
||||||
this._callback = callback;
|
this._callback = callback;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -17,12 +17,12 @@ import {
|
|||||||
} from "tc-native/connection";
|
} from "tc-native/connection";
|
||||||
import {ConnectionCommandHandler} from "tc-shared/connection/CommandHandler";
|
import {ConnectionCommandHandler} from "tc-shared/connection/CommandHandler";
|
||||||
import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
|
import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
|
||||||
import {ServerAddress} from "tc-shared/ui/server";
|
|
||||||
import {TeaSpeakHandshakeHandler} from "tc-shared/profiles/identities/TeamSpeakIdentity";
|
import {TeaSpeakHandshakeHandler} from "tc-shared/profiles/identities/TeamSpeakIdentity";
|
||||||
import {VoiceConnection} from "./VoiceConnection";
|
import {NativeVoiceConnectionWrapper} from "./VoiceConnection";
|
||||||
import {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection";
|
import {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection";
|
||||||
import {LogCategory, logDebug, logWarn} from "tc-shared/log";
|
import {LogCategory, logDebug, logWarn} from "tc-shared/log";
|
||||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||||
|
import {ServerAddress} from "tc-shared/tree/Server";
|
||||||
|
|
||||||
interface ErrorCodeListener {
|
interface ErrorCodeListener {
|
||||||
callback: (result: CommandResult) => void;
|
callback: (result: CommandResult) => void;
|
||||||
@ -166,7 +166,7 @@ class ErrorCommandHandler extends AbstractCommandHandler {
|
|||||||
|
|
||||||
export class ServerConnection extends AbstractServerConnection {
|
export class ServerConnection extends AbstractServerConnection {
|
||||||
private _native_handle: NativeServerConnection;
|
private _native_handle: NativeServerConnection;
|
||||||
private readonly _voice_connection: VoiceConnection;
|
private readonly _voice_connection: NativeVoiceConnectionWrapper;
|
||||||
|
|
||||||
private _do_teamspeak: boolean;
|
private _do_teamspeak: boolean;
|
||||||
|
|
||||||
@ -204,10 +204,9 @@ export class ServerConnection extends AbstractServerConnection {
|
|||||||
arguments: args
|
arguments: args
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
this._voice_connection = new VoiceConnection(this, this._native_handle._voice_connection);
|
this._voice_connection = new NativeVoiceConnectionWrapper(this, this._native_handle._voice_connection);
|
||||||
|
|
||||||
this.command_helper.initialize();
|
this.command_helper.initialize();
|
||||||
this._voice_connection.setup();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
native_handle() : NativeServerConnection {
|
native_handle() : NativeServerConnection {
|
||||||
|
@ -1,84 +1,173 @@
|
|||||||
import {ServerConnection} from "./ServerConnection";
|
|
||||||
import {NativeVoiceConnection} from "tc-native/connection";
|
|
||||||
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
|
||||||
import {tr} from "tc-shared/i18n/localize";
|
|
||||||
import {LogCategory} from "tc-shared/log";
|
|
||||||
import * as log from "tc-shared/log";
|
|
||||||
import {
|
import {
|
||||||
AbstractVoiceConnection,
|
AbstractVoiceConnection,
|
||||||
LatencySettings,
|
VoiceConnectionStatus,
|
||||||
VoiceClient,
|
WhisperSessionInitializer
|
||||||
VoiceConnectionStatus
|
|
||||||
} from "tc-shared/connection/VoiceConnection";
|
} from "tc-shared/connection/VoiceConnection";
|
||||||
|
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
||||||
|
import {NativeVoiceClient, NativeVoiceConnection, PlayerState} from "tc-native/connection";
|
||||||
|
import {ServerConnection} from "./ServerConnection";
|
||||||
|
import {VoiceClient} from "tc-shared/voice/VoiceClient";
|
||||||
|
import {WhisperSession, WhisperTarget} from "tc-shared/voice/VoiceWhisper";
|
||||||
import {NativeInput} from "../audio/AudioRecorder";
|
import {NativeInput} from "../audio/AudioRecorder";
|
||||||
|
import {ConnectionState} from "tc-shared/ConnectionHandler";
|
||||||
|
import {VoicePlayerEvents, VoicePlayerLatencySettings, VoicePlayerState} from "tc-shared/voice/VoicePlayer";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {LogCategory, logInfo, logWarn} from "tc-shared/log";
|
||||||
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
|
|
||||||
export class VoiceConnection extends AbstractVoiceConnection {
|
export class NativeVoiceConnectionWrapper extends AbstractVoiceConnection {
|
||||||
readonly connection: ServerConnection;
|
private readonly serverConnectionStateChangedListener;
|
||||||
readonly handle: NativeVoiceConnection;
|
private readonly native: NativeVoiceConnection;
|
||||||
|
|
||||||
private _audio_source: RecorderProfile;
|
private localAudioStarted = false;
|
||||||
|
private connectionState: VoiceConnectionStatus;
|
||||||
|
private currentRecorder: RecorderProfile;
|
||||||
|
|
||||||
|
private registeredVoiceClients: {[key: number]: NativeVoiceClientWrapper} = {};
|
||||||
|
|
||||||
|
private currentlyReplayingAudio = false;
|
||||||
|
private readonly voiceClientStateChangedEventListener;
|
||||||
|
|
||||||
constructor(connection: ServerConnection, voice: NativeVoiceConnection) {
|
constructor(connection: ServerConnection, voice: NativeVoiceConnection) {
|
||||||
super(connection);
|
super(connection);
|
||||||
this.connection = connection;
|
this.native = voice;
|
||||||
this.handle = voice;
|
|
||||||
|
this.serverConnectionStateChangedListener = () => {
|
||||||
|
if(this.connection.getConnectionState() === ConnectionState.CONNECTED) {
|
||||||
|
this.setConnectionState(VoiceConnectionStatus.Connected);
|
||||||
|
} else {
|
||||||
|
this.setConnectionState(VoiceConnectionStatus.Disconnected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connection.events.on("notify_connection_state_changed", this.serverConnectionStateChangedListener);
|
||||||
|
this.connectionState = VoiceConnectionStatus.Disconnected;
|
||||||
|
|
||||||
|
this.voiceClientStateChangedEventListener = this.handleVoiceClientStateChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
setup() { }
|
destroy() {
|
||||||
|
this.connection.events.off("notify_connection_state_changed", this.serverConnectionStateChangedListener);
|
||||||
|
}
|
||||||
|
|
||||||
async acquire_voice_recorder(recorder: RecorderProfile | undefined, enforce?: boolean) {
|
getConnectionState(): VoiceConnectionStatus {
|
||||||
if(this._audio_source === recorder && !enforce)
|
return this.connectionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFailedMessage(): string {
|
||||||
|
/* the native voice connection can't fail */
|
||||||
|
return "this message should never appear";
|
||||||
|
}
|
||||||
|
|
||||||
|
private setConnectionState(state: VoiceConnectionStatus) {
|
||||||
|
if(this.connectionState === state) {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(this._audio_source)
|
|
||||||
await this._audio_source.unmount();
|
|
||||||
|
|
||||||
if(recorder) {
|
|
||||||
if(!(recorder.input instanceof NativeInput))
|
|
||||||
throw "Recorder input must be an instance of NativeInput!";
|
|
||||||
await recorder.unmount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleVoiceEnded();
|
const oldState = this.connectionState;
|
||||||
this._audio_source = recorder;
|
this.connectionState = state;
|
||||||
|
this.events.fire("notify_connection_status_changed", { oldStatus: oldState, newStatus: state });
|
||||||
|
}
|
||||||
|
|
||||||
if(recorder) {
|
encodingSupported(codec: number): boolean {
|
||||||
recorder.current_handler = this.connection.client;
|
return this.native.encoding_supported(codec);
|
||||||
|
}
|
||||||
|
|
||||||
recorder.callback_unmount = () => {
|
decodingSupported(codec: number): boolean {
|
||||||
this._audio_source = undefined;
|
return this.native.decoding_supported(codec);
|
||||||
this.handle.set_audio_source(undefined);
|
}
|
||||||
this.connection.client.update_voice_status(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
recorder.callback_start = this.on_voice_started.bind(this);
|
async acquireVoiceRecorder(recorder: RecorderProfile | undefined): Promise<void> {
|
||||||
recorder.callback_stop = this.handleVoiceEnded.bind(this);
|
if(this.currentRecorder === recorder) {
|
||||||
|
return;
|
||||||
(recorder as any).callback_support_change = () => {
|
|
||||||
this.connection.client.update_voice_status(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handle.set_audio_source((recorder.input as NativeInput).getNativeConsumer());
|
|
||||||
}
|
}
|
||||||
this.connection.client.update_voice_status(undefined);
|
|
||||||
|
if(this.currentRecorder) {
|
||||||
|
await this.currentRecorder.unmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleVoiceEndEvent();
|
||||||
|
|
||||||
|
this.currentRecorder = recorder;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(recorder) {
|
||||||
|
if(!(recorder.input instanceof NativeInput)) {
|
||||||
|
this.currentRecorder = undefined;
|
||||||
|
throw "Recorder input must be an instance of NativeInput!";
|
||||||
|
}
|
||||||
|
|
||||||
|
await recorder.unmount();
|
||||||
|
|
||||||
|
recorder.current_handler = this.connection.client;
|
||||||
|
|
||||||
|
recorder.callback_unmount = () => {
|
||||||
|
this.currentRecorder = undefined;
|
||||||
|
this.native.set_audio_source(undefined);
|
||||||
|
this.handleVoiceEndEvent();
|
||||||
|
};
|
||||||
|
|
||||||
|
recorder.callback_start = this.handleVoiceStartEvent.bind(this);
|
||||||
|
recorder.callback_stop = this.handleVoiceEndEvent.bind(this);
|
||||||
|
|
||||||
|
this.native.set_audio_source(recorder.input.getNativeConsumer());
|
||||||
|
}
|
||||||
|
} catch(error) {
|
||||||
|
this.currentRecorder = undefined;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
this.events.fire("notify_recorder_changed", {})
|
||||||
}
|
}
|
||||||
|
|
||||||
voice_playback_support() : boolean {
|
voiceRecorder(): RecorderProfile {
|
||||||
return this.connection.connected();
|
return this.currentRecorder;
|
||||||
}
|
}
|
||||||
|
|
||||||
voice_send_support() : boolean {
|
getEncoderCodec(): number {
|
||||||
return this.connection.connected();
|
return this.native.get_encoder_codec();
|
||||||
}
|
}
|
||||||
|
|
||||||
private current_channel_codec() : number {
|
setEncoderCodec(codec: number) {
|
||||||
|
this.native.set_encoder_codec(codec);
|
||||||
|
}
|
||||||
|
|
||||||
|
isReplayingVoice(): boolean {
|
||||||
|
return this.currentlyReplayingAudio;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setReplayingVoice(status: boolean) {
|
||||||
|
if(status === this.currentlyReplayingAudio) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.currentlyReplayingAudio = status;
|
||||||
|
this.events.fire("notify_voice_replay_state_change", { replaying: status });
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleVoiceClientStateChange() {
|
||||||
|
this.setReplayingVoice(this.availableVoiceClients().findIndex(client => client.getState() === VoicePlayerState.PLAYING || client.getState() === VoicePlayerState.BUFFERING) !== -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleVoiceStartEvent() {
|
||||||
const chandler = this.connection.client;
|
const chandler = this.connection.client;
|
||||||
return (chandler.getClient().currentChannel() || {properties: { channel_codec: 4}}).properties.channel_codec;
|
if(chandler.isMicrophoneMuted()) {
|
||||||
|
logWarn(LogCategory.VOICE, tr("Received local voice started event, even thou we're muted!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.native.enable_voice_send(true);
|
||||||
|
this.localAudioStarted = true;
|
||||||
|
logInfo(LogCategory.VOICE, tr("Local voice started"));
|
||||||
|
|
||||||
|
const ch = chandler.getClient();
|
||||||
|
if(ch) ch.speaking = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleVoiceEnded() {
|
private handleVoiceEndEvent() {
|
||||||
|
this.native.enable_voice_send(false);
|
||||||
|
|
||||||
const chandler = this.connection.client;
|
const chandler = this.connection.client;
|
||||||
chandler.getClient().speaking = false;
|
const ch = chandler.getClient();
|
||||||
|
if(ch) ch.speaking = false;
|
||||||
|
|
||||||
if(!chandler.connected)
|
if(!chandler.connected)
|
||||||
return false;
|
return false;
|
||||||
@ -86,97 +175,148 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
|||||||
if(chandler.isMicrophoneMuted())
|
if(chandler.isMicrophoneMuted())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
console.log(tr("Local voice ended"));
|
logInfo(LogCategory.VOICE, tr("Local voice ended"));
|
||||||
//TODO Send end? (Or is this already an automated thing?)
|
this.localAudioStarted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private on_voice_started() {
|
availableVoiceClients(): NativeVoiceClientWrapper[] {
|
||||||
const chandler = this.connection.client;
|
return Object.keys(this.registeredVoiceClients).map(clientId => this.registeredVoiceClients[clientId]);
|
||||||
if(chandler.isMicrophoneMuted()) {
|
|
||||||
/* evil hack due to the settings :D */
|
|
||||||
log.warn(LogCategory.VOICE, tr("Received local voice started event, even thou we're muted! Do not send any voice."));
|
|
||||||
if(this.handle) {
|
|
||||||
this.handle.enable_voice_send(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(LogCategory.VOICE, tr("Local voice started (Native)"));
|
|
||||||
this.handle.enable_voice_send(true);
|
|
||||||
|
|
||||||
const ch = chandler.getClient();
|
|
||||||
if(ch) ch.speaking = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getConnectionState(): VoiceConnectionStatus {
|
registerVoiceClient(clientId: number) {
|
||||||
return VoiceConnectionStatus.Connected;
|
const client = new NativeVoiceClientWrapper(this.native.register_client(clientId));
|
||||||
|
client.events.on("notify_state_changed", this.voiceClientStateChangedEventListener);
|
||||||
|
this.registeredVoiceClients[clientId] = client;
|
||||||
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
voice_recorder(): RecorderProfile {
|
unregisterVoiceClient(client: VoiceClient) {
|
||||||
return this._audio_source;
|
if(!(client instanceof NativeVoiceClientWrapper))
|
||||||
|
throw "invalid client type";
|
||||||
|
|
||||||
|
delete this.registeredVoiceClients[client.getClientId()];
|
||||||
|
this.native.unregister_client(client.getClientId());
|
||||||
|
client.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
available_clients(): VoiceClient[] {
|
stopAllVoiceReplays() {
|
||||||
return this.handle.available_clients().map(e => Object.assign(e, {
|
this.availableVoiceClients().forEach(client => client.abortReplay());
|
||||||
support_latency_settings() { return true; },
|
|
||||||
reset_latency_settings() {
|
|
||||||
const stream = this.get_stream();
|
|
||||||
stream.set_buffer_latency(0.080);
|
|
||||||
stream.set_buffer_max_latency(0.5);
|
|
||||||
return this.latency_settings();
|
|
||||||
},
|
|
||||||
latency_settings(settings?: LatencySettings) : LatencySettings {
|
|
||||||
const stream = this.get_stream();
|
|
||||||
if(typeof settings !== "undefined") {
|
|
||||||
stream.set_buffer_latency(settings.min_buffer / 1000);
|
|
||||||
stream.set_buffer_max_latency(settings.max_buffer / 100);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
max_buffer: Math.floor(stream.get_buffer_max_latency() * 1000),
|
|
||||||
min_buffer: Math.floor(stream.get_buffer_latency() * 1000)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
support_flush() { return true; },
|
|
||||||
flush() {
|
|
||||||
const stream = this.get_stream();
|
|
||||||
stream.flush_buffer();
|
|
||||||
}
|
|
||||||
}) as any); /* cast to any because of: Type 'import("/mnt/d/TeaSpeak/client_linux/client/imports/shared-app/connection/ConnectionBase").voice.PlayerState' is not assignable to type 'import("tc-native/connection").PlayerState' */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
find_client(client_id: number) : VoiceClient | undefined {
|
/* whisper API */
|
||||||
for(const client of this.available_clients())
|
getWhisperSessionInitializer(): WhisperSessionInitializer | undefined {
|
||||||
if(client.client_id === client_id)
|
|
||||||
return client;
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
unregister_client(client: VoiceClient): Promise<void> {
|
getWhisperSessions(): WhisperSession[] {
|
||||||
this.handle.unregister_client(client.client_id);
|
return [];
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
register_client(client_id: number): VoiceClient {
|
getWhisperTarget(): WhisperTarget | undefined {
|
||||||
const client = this.handle.register_client(client_id);
|
return undefined;
|
||||||
const c = this.find_client(client_id);
|
|
||||||
c.reset_latency_settings();
|
|
||||||
return c;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
decoding_supported(codec: number): boolean {
|
setWhisperSessionInitializer(initializer: WhisperSessionInitializer | undefined) {
|
||||||
return this.handle.decoding_supported(codec);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
encoding_supported(codec: number): boolean {
|
startWhisper(target: WhisperTarget): Promise<void> {
|
||||||
return this.handle.encoding_supported(codec);
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
get_encoder_codec(): number {
|
dropWhisperSession(session: WhisperSession) {
|
||||||
return this.handle.get_encoder_codec();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set_encoder_codec(codec: number) {
|
stopWhisper() {
|
||||||
return this.handle.set_encoder_codec(codec);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NativeVoiceClientWrapper implements VoiceClient {
|
||||||
|
private readonly native: NativeVoiceClient;
|
||||||
|
readonly events: Registry<VoicePlayerEvents>;
|
||||||
|
private playerState: VoicePlayerState;
|
||||||
|
|
||||||
|
constructor(native: NativeVoiceClient) {
|
||||||
|
this.events = new Registry<VoicePlayerEvents>();
|
||||||
|
this.native = native;
|
||||||
|
this.playerState = VoicePlayerState.STOPPED;
|
||||||
|
|
||||||
|
this.native.callback_state_changed = state => {
|
||||||
|
switch (state) {
|
||||||
|
case PlayerState.BUFFERING:
|
||||||
|
this.setState(VoicePlayerState.BUFFERING);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerState.PLAYING:
|
||||||
|
this.setState(VoicePlayerState.PLAYING);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerState.STOPPED:
|
||||||
|
this.setState(VoicePlayerState.STOPPED);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerState.STOPPING:
|
||||||
|
this.setState(VoicePlayerState.STOPPING);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.events.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
abortReplay() {
|
||||||
|
this.native.abort_replay();
|
||||||
|
}
|
||||||
|
|
||||||
|
flushBuffer() {
|
||||||
|
this.native.get_stream().flush_buffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
getClientId(): number {
|
||||||
|
return this.native.client_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getState(): VoicePlayerState {
|
||||||
|
return this.playerState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setState(state: VoicePlayerState) {
|
||||||
|
if(this.playerState === state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldState = this.playerState;
|
||||||
|
this.playerState = state;
|
||||||
|
this.events.fire("notify_state_changed", { oldState: oldState, newState: state });
|
||||||
|
}
|
||||||
|
|
||||||
|
setVolume(volume: number) {
|
||||||
|
this.native.set_volume(volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
getVolume(): number {
|
||||||
|
return this.native.get_volume();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetLatencySettings() {
|
||||||
|
const stream = this.native.get_stream();
|
||||||
|
stream.set_buffer_latency(0.080);
|
||||||
|
stream.set_buffer_max_latency(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLatencySettings(settings: VoicePlayerLatencySettings) {
|
||||||
|
const stream = this.native.get_stream();
|
||||||
|
stream.set_buffer_latency(settings.minBufferTime / 1000);
|
||||||
|
stream.set_buffer_max_latency(settings.maxBufferTime / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLatencySettings(): Readonly<VoicePlayerLatencySettings> {
|
||||||
|
const stream = this.native.get_stream();
|
||||||
|
|
||||||
|
return {
|
||||||
|
maxBufferTime: stream.get_buffer_max_latency() * 1000,
|
||||||
|
minBufferTime: stream.get_buffer_latency() * 1000
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
180
modules/renderer/connection/VoiceConnection.ts.old
Normal file
180
modules/renderer/connection/VoiceConnection.ts.old
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import {ServerConnection} from "./ServerConnection";
|
||||||
|
import {NativeVoiceConnection} from "tc-native/connection";
|
||||||
|
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
||||||
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
import * as log from "tc-shared/log";
|
||||||
|
import {
|
||||||
|
AbstractVoiceConnection,
|
||||||
|
VoiceConnectionStatus
|
||||||
|
} from "tc-shared/connection/VoiceConnection";
|
||||||
|
import {NativeInput} from "../audio/AudioRecorder";
|
||||||
|
|
||||||
|
export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
|
readonly connection: ServerConnection;
|
||||||
|
readonly handle: NativeVoiceConnection;
|
||||||
|
|
||||||
|
private _audio_source: RecorderProfile;
|
||||||
|
|
||||||
|
constructor(connection: ServerConnection, voice: NativeVoiceConnection) {
|
||||||
|
super(connection);
|
||||||
|
this.connection = connection;
|
||||||
|
this.handle = voice;
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() { }
|
||||||
|
|
||||||
|
async acquire_voice_recorder(recorder: RecorderProfile | undefined, enforce?: boolean) {
|
||||||
|
if(this._audio_source === recorder && !enforce)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(this._audio_source)
|
||||||
|
await this._audio_source.unmount();
|
||||||
|
|
||||||
|
if(recorder) {
|
||||||
|
if(!(recorder.input instanceof NativeInput))
|
||||||
|
throw "Recorder input must be an instance of NativeInput!";
|
||||||
|
await recorder.unmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleVoiceEnded();
|
||||||
|
this._audio_source = recorder;
|
||||||
|
|
||||||
|
if(recorder) {
|
||||||
|
recorder.current_handler = this.connection.client;
|
||||||
|
|
||||||
|
recorder.callback_unmount = () => {
|
||||||
|
this._audio_source = undefined;
|
||||||
|
this.handle.set_audio_source(undefined);
|
||||||
|
this.connection.client.update_voice_status(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
recorder.callback_start = this.on_voice_started.bind(this);
|
||||||
|
recorder.callback_stop = this.handleVoiceEnded.bind(this);
|
||||||
|
|
||||||
|
(recorder as any).callback_support_change = () => {
|
||||||
|
this.connection.client.update_voice_status(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handle.set_audio_source((recorder.input as NativeInput).getNativeConsumer());
|
||||||
|
}
|
||||||
|
this.connection.client.update_voice_status(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
voice_playback_support() : boolean {
|
||||||
|
return this.connection.connected();
|
||||||
|
}
|
||||||
|
|
||||||
|
voice_send_support() : boolean {
|
||||||
|
return this.connection.connected();
|
||||||
|
}
|
||||||
|
|
||||||
|
private current_channel_codec() : number {
|
||||||
|
const chandler = this.connection.client;
|
||||||
|
return (chandler.getClient().currentChannel() || {properties: { channel_codec: 4}}).properties.channel_codec;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleVoiceEnded() {
|
||||||
|
const chandler = this.connection.client;
|
||||||
|
chandler.getClient().speaking = false;
|
||||||
|
|
||||||
|
if(!chandler.connected)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if(chandler.isMicrophoneMuted())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
console.log(tr("Local voice ended"));
|
||||||
|
//TODO Send end? (Or is this already an automated thing?)
|
||||||
|
}
|
||||||
|
|
||||||
|
private on_voice_started() {
|
||||||
|
const chandler = this.connection.client;
|
||||||
|
if(chandler.isMicrophoneMuted()) {
|
||||||
|
/* evil hack due to the settings :D */
|
||||||
|
log.warn(LogCategory.VOICE, tr("Received local voice started event, even thou we're muted! Do not send any voice."));
|
||||||
|
if(this.handle) {
|
||||||
|
this.handle.enable_voice_send(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(LogCategory.VOICE, tr("Local voice started (Native)"));
|
||||||
|
this.handle.enable_voice_send(true);
|
||||||
|
|
||||||
|
const ch = chandler.getClient();
|
||||||
|
if(ch) ch.speaking = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getConnectionState(): VoiceConnectionStatus {
|
||||||
|
return VoiceConnectionStatus.Connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
voice_recorder(): RecorderProfile {
|
||||||
|
return this._audio_source;
|
||||||
|
}
|
||||||
|
|
||||||
|
available_clients(): VoiceClient[] {
|
||||||
|
return this.handle.available_clients().map(e => Object.assign(e, {
|
||||||
|
support_latency_settings() { return true; },
|
||||||
|
reset_latency_settings() {
|
||||||
|
const stream = this.get_stream();
|
||||||
|
stream.set_buffer_latency(0.080);
|
||||||
|
stream.set_buffer_max_latency(0.5);
|
||||||
|
return this.latency_settings();
|
||||||
|
},
|
||||||
|
latency_settings(settings?: LatencySettings) : LatencySettings {
|
||||||
|
const stream = this.get_stream();
|
||||||
|
if(typeof settings !== "undefined") {
|
||||||
|
stream.set_buffer_latency(settings.min_buffer / 1000);
|
||||||
|
stream.set_buffer_max_latency(settings.max_buffer / 100);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
max_buffer: Math.floor(stream.get_buffer_max_latency() * 1000),
|
||||||
|
min_buffer: Math.floor(stream.get_buffer_latency() * 1000)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
support_flush() { return true; },
|
||||||
|
flush() {
|
||||||
|
const stream = this.get_stream();
|
||||||
|
stream.flush_buffer();
|
||||||
|
}
|
||||||
|
}) as any); /* cast to any because of: Type 'import("/mnt/d/TeaSpeak/client_linux/client/imports/shared-app/connection/ConnectionBase").voice.PlayerState' is not assignable to type 'import("tc-native/connection").PlayerState' */
|
||||||
|
}
|
||||||
|
|
||||||
|
find_client(client_id: number) : VoiceClient | undefined {
|
||||||
|
for(const client of this.available_clients())
|
||||||
|
if(client.client_id === client_id)
|
||||||
|
return client;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
unregister_client(client: VoiceClient): Promise<void> {
|
||||||
|
this.handle.unregister_client(client.client_id);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
register_client(client_id: number): VoiceClient {
|
||||||
|
const client = this.handle.register_client(client_id);
|
||||||
|
const c = this.find_client(client_id);
|
||||||
|
c.reset_latency_settings();
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
decoding_supported(codec: number): boolean {
|
||||||
|
return this.handle.decoding_supported(codec);
|
||||||
|
}
|
||||||
|
|
||||||
|
encoding_supported(codec: number): boolean {
|
||||||
|
return this.handle.encoding_supported(codec);
|
||||||
|
}
|
||||||
|
|
||||||
|
get_encoder_codec(): number {
|
||||||
|
return this.handle.get_encoder_codec();
|
||||||
|
}
|
||||||
|
|
||||||
|
set_encoder_codec(codec: number) {
|
||||||
|
return this.handle.set_encoder_codec(codec);
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,9 @@
|
|||||||
import {ServerAddress} from "tc-shared/ui/server";
|
|
||||||
import * as loader from "tc-loader";
|
import * as loader from "tc-loader";
|
||||||
|
|
||||||
import {AddressTarget, ResolveOptions} from "tc-shared/dns";
|
import {AddressTarget, ResolveOptions} from "tc-shared/dns";
|
||||||
import * as dns_handler from "tc-native/dns";
|
import * as dns_handler from "tc-native/dns";
|
||||||
|
import {ServerAddress} from "tc-shared/tree/Server";
|
||||||
|
|
||||||
export function supported() { return true; }
|
|
||||||
export async function resolve_address(address: ServerAddress, _options?: ResolveOptions) : Promise<AddressTarget> {
|
export async function resolve_address(address: ServerAddress, _options?: ResolveOptions) : Promise<AddressTarget> {
|
||||||
/* backwards compatibility */
|
/* backwards compatibility */
|
||||||
if(typeof(address) === "string") {
|
if(typeof(address) === "string") {
|
||||||
@ -26,6 +25,7 @@ export async function resolve_address(address: ServerAddress, _options?: Resolve
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
export function supported() { return true; }
|
||||||
|
|
||||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
name: "Native DNS initialized",
|
name: "Native DNS initialized",
|
||||||
|
@ -165,6 +165,7 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
|||||||
await import("./hooks/ChangeLogClient");
|
await import("./hooks/ChangeLogClient");
|
||||||
|
|
||||||
await import("./UnloadHandler");
|
await import("./UnloadHandler");
|
||||||
|
await import("./WindowsTrayHandler");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
window.displayCriticalError("Failed to load native extensions: " + error);
|
window.displayCriticalError("Failed to load native extensions: " + error);
|
||||||
|
9
native/serverconnection/exports/exports.d.ts
vendored
9
native/serverconnection/exports/exports.d.ts
vendored
@ -184,6 +184,12 @@ declare module "tc-native/connection" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export namespace record {
|
export namespace record {
|
||||||
|
enum FilterMode {
|
||||||
|
Bypass,
|
||||||
|
Filter,
|
||||||
|
Block
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConsumeFilter {
|
export interface ConsumeFilter {
|
||||||
get_name() : string;
|
get_name() : string;
|
||||||
}
|
}
|
||||||
@ -229,6 +235,9 @@ declare module "tc-native/connection" {
|
|||||||
create_filter_threshold(threshold: number) : ThresholdConsumeFilter;
|
create_filter_threshold(threshold: number) : ThresholdConsumeFilter;
|
||||||
create_filter_state() : StateConsumeFilter;
|
create_filter_state() : StateConsumeFilter;
|
||||||
|
|
||||||
|
set_filter_mode(mode: FilterMode);
|
||||||
|
get_filter_mode() : FilterMode;
|
||||||
|
|
||||||
callback_data: (buffer: Float32Array) => any;
|
callback_data: (buffer: Float32Array) => any;
|
||||||
callback_ended: () => any;
|
callback_ended: () => any;
|
||||||
callback_started: () => any;
|
callback_started: () => any;
|
||||||
|
@ -24,6 +24,9 @@ NAN_MODULE_INIT(AudioConsumerWrapper::Init) {
|
|||||||
Nan::SetPrototypeMethod(klass, "create_filter_threshold", AudioConsumerWrapper::_create_filter_threshold);
|
Nan::SetPrototypeMethod(klass, "create_filter_threshold", AudioConsumerWrapper::_create_filter_threshold);
|
||||||
Nan::SetPrototypeMethod(klass, "create_filter_state", AudioConsumerWrapper::_create_filter_state);
|
Nan::SetPrototypeMethod(klass, "create_filter_state", AudioConsumerWrapper::_create_filter_state);
|
||||||
|
|
||||||
|
Nan::SetPrototypeMethod(klass, "get_filter_mode", AudioConsumerWrapper::_get_filter_mode);
|
||||||
|
Nan::SetPrototypeMethod(klass, "set_filter_mode", AudioConsumerWrapper::_set_filter_mode);
|
||||||
|
|
||||||
constructor_template().Reset(klass);
|
constructor_template().Reset(klass);
|
||||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||||
}
|
}
|
||||||
@ -92,7 +95,7 @@ void AudioConsumerWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
|
|||||||
|
|
||||||
v8::Local<v8::Value> argv[1];
|
v8::Local<v8::Value> argv[1];
|
||||||
argv[0] = js_fbuffer;
|
argv[0] = js_fbuffer;
|
||||||
callback_function.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, argv);
|
(void) callback_function.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, argv);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -103,7 +106,7 @@ void AudioConsumerWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
|
|||||||
v8::Local<v8::Value> callback_function = Nan::Get(handle, Nan::New<v8::String>("callback_ended").ToLocalChecked()).FromMaybe(v8::Local<v8::Value>{});
|
v8::Local<v8::Value> callback_function = Nan::Get(handle, Nan::New<v8::String>("callback_ended").ToLocalChecked()).FromMaybe(v8::Local<v8::Value>{});
|
||||||
if(callback_function.IsEmpty() || callback_function->IsNullOrUndefined() || !callback_function->IsFunction())
|
if(callback_function.IsEmpty() || callback_function->IsNullOrUndefined() || !callback_function->IsFunction())
|
||||||
return;
|
return;
|
||||||
callback_function.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
|
(void) callback_function.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
|
||||||
});
|
});
|
||||||
|
|
||||||
this->_call_started = Nan::async_callback([&]{
|
this->_call_started = Nan::async_callback([&]{
|
||||||
@ -113,7 +116,7 @@ void AudioConsumerWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
|
|||||||
v8::Local<v8::Value> callback_function = Nan::Get(handle, Nan::New<v8::String>("callback_started").ToLocalChecked()).FromMaybe(v8::Local<v8::Value>{});
|
v8::Local<v8::Value> callback_function = Nan::Get(handle, Nan::New<v8::String>("callback_started").ToLocalChecked()).FromMaybe(v8::Local<v8::Value>{});
|
||||||
if(callback_function.IsEmpty() || callback_function->IsNullOrUndefined() || !callback_function->IsFunction())
|
if(callback_function.IsEmpty() || callback_function->IsNullOrUndefined() || !callback_function->IsFunction())
|
||||||
return;
|
return;
|
||||||
callback_function.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
|
(void) callback_function.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
|
||||||
});
|
});
|
||||||
|
|
||||||
Nan::Set(this->handle(), Nan::New<v8::String>("frame_size").ToLocalChecked(), Nan::New<v8::Number>((uint32_t) this->_handle->frame_size));
|
Nan::Set(this->handle(), Nan::New<v8::String>("frame_size").ToLocalChecked(), Nan::New<v8::Number>((uint32_t) this->_handle->frame_size));
|
||||||
@ -131,32 +134,43 @@ void AudioConsumerWrapper::unbind() {
|
|||||||
void AudioConsumerWrapper::process_data(const void *buffer, size_t samples) {
|
void AudioConsumerWrapper::process_data(const void *buffer, size_t samples) {
|
||||||
lock_guard lock(this->execute_lock);
|
lock_guard lock(this->execute_lock);
|
||||||
|
|
||||||
auto filters = this->filters();
|
bool should_process{true};
|
||||||
for(const auto& filter : filters) {
|
if(this->filter_mode_ == FilterMode::FILTER) {
|
||||||
auto _filter = filter->filter();
|
auto filters = this->filters();
|
||||||
if(!_filter) continue;
|
for(const auto& filter : filters) {
|
||||||
|
auto _filter = filter->filter();
|
||||||
|
if(!_filter) continue;
|
||||||
|
|
||||||
if(_filter->frame_size() != samples) {
|
if(_filter->frame_size() != samples) {
|
||||||
cerr << "Tried to use a filter, but frame size does not match!" << endl;
|
cerr << "Tried to use a filter, but frame size does not match!" << endl;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if(!_filter->process(buffer)) {
|
if(!_filter->process(buffer)) {
|
||||||
if(!this->last_consumed) {
|
should_process = false;
|
||||||
this->last_consumed = true;
|
break;
|
||||||
this->_call_ended();
|
}
|
||||||
unique_lock native_read_lock(this->native_read_callback_lock);
|
}
|
||||||
if(this->native_read_callback) {
|
} else if(this->filter_mode_ != FilterMode::BYPASS) {
|
||||||
auto callback = this->native_read_callback; /* copy */
|
should_process = false;
|
||||||
native_read_lock.unlock();
|
|
||||||
callback(nullptr, 0); /* notify end */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this->last_consumed)
|
if(!should_process) {
|
||||||
this->_call_started();
|
if(!this->last_consumed) {
|
||||||
|
this->last_consumed = true;
|
||||||
|
this->_call_ended();
|
||||||
|
unique_lock native_read_lock(this->native_read_callback_lock);
|
||||||
|
if(this->native_read_callback) {
|
||||||
|
auto callback = this->native_read_callback; /* copy */
|
||||||
|
native_read_lock.unlock();
|
||||||
|
callback(nullptr, 0); /* notify end */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this->last_consumed) {
|
||||||
|
this->_call_started();
|
||||||
|
}
|
||||||
this->last_consumed = false;
|
this->last_consumed = false;
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -177,7 +191,7 @@ void AudioConsumerWrapper::process_data(const void *buffer, size_t samples) {
|
|||||||
buf->sample_count = samples;
|
buf->sample_count = samples;
|
||||||
|
|
||||||
{
|
{
|
||||||
lock_guard lock(this->_data_lock);
|
lock_guard data_lock{this->_data_lock};
|
||||||
this->_data_entries.push_back(move(buf));
|
this->_data_entries.push_back(move(buf));
|
||||||
}
|
}
|
||||||
this->_call_data();
|
this->_call_data();
|
||||||
@ -199,8 +213,8 @@ std::shared_ptr<AudioFilterWrapper> AudioConsumerWrapper::create_filter(const st
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
lock_guard lock(this->_filters_lock);
|
lock_guard lock(this->filter_mutex_);
|
||||||
this->_filters.push_back(result);
|
this->filter_.push_back(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -209,20 +223,22 @@ std::shared_ptr<AudioFilterWrapper> AudioConsumerWrapper::create_filter(const st
|
|||||||
void AudioConsumerWrapper::delete_filter(const AudioFilterWrapper* filter) {
|
void AudioConsumerWrapper::delete_filter(const AudioFilterWrapper* filter) {
|
||||||
shared_ptr<AudioFilterWrapper> handle; /* need to keep the handle 'till everything has been finished */
|
shared_ptr<AudioFilterWrapper> handle; /* need to keep the handle 'till everything has been finished */
|
||||||
{
|
{
|
||||||
lock_guard lock(this->_filters_lock);
|
lock_guard lock(this->filter_mutex_);
|
||||||
for(auto& c : this->_filters) {
|
for(auto& c : this->filter_) {
|
||||||
if(&*c == filter) {
|
if(&*c == filter) {
|
||||||
handle = c;
|
handle = c;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!handle)
|
if(!handle) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto it = find(this->_filters.begin(), this->_filters.end(), handle);
|
auto it = find(this->filter_.begin(), this->filter_.end(), handle);
|
||||||
if(it != this->_filters.end())
|
if(it != this->filter_.end()) {
|
||||||
this->_filters.erase(it);
|
this->filter_.erase(it);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,4 +335,21 @@ NAN_METHOD(AudioConsumerWrapper::_create_filter_state) {
|
|||||||
|
|
||||||
auto object = handle->create_filter("state", filter);
|
auto object = handle->create_filter("state", filter);
|
||||||
info.GetReturnValue().Set(object->handle());
|
info.GetReturnValue().Set(object->handle());
|
||||||
|
}
|
||||||
|
|
||||||
|
NAN_METHOD(AudioConsumerWrapper::_get_filter_mode) {
|
||||||
|
auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder());
|
||||||
|
info.GetReturnValue().Set((int) handle->filter_mode_);
|
||||||
|
}
|
||||||
|
|
||||||
|
NAN_METHOD(AudioConsumerWrapper::_set_filter_mode) {
|
||||||
|
auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder());
|
||||||
|
|
||||||
|
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||||
|
Nan::ThrowError("invalid argument");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto value = info[0].As<v8::Number>()->ToInteger()->Value();
|
||||||
|
handle->filter_mode_ = (FilterMode) value;
|
||||||
}
|
}
|
@ -17,15 +17,12 @@ namespace tc {
|
|||||||
namespace recorder {
|
namespace recorder {
|
||||||
class AudioFilterWrapper;
|
class AudioFilterWrapper;
|
||||||
class AudioRecorderWrapper;
|
class AudioRecorderWrapper;
|
||||||
/*
|
|
||||||
get_filters() : ConsumeFilter[];
|
|
||||||
|
|
||||||
register_filter(filter: ConsumeFilter);
|
enum FilterMode {
|
||||||
unregister_filter(filter: ConsumeFilter);
|
BYPASS,
|
||||||
|
FILTER,
|
||||||
create_filter_vad() : VADConsumeFilter;
|
BLOCK
|
||||||
create_filter_threshold() : ThresholdConsumeFilter;
|
};
|
||||||
*/
|
|
||||||
|
|
||||||
class AudioConsumerWrapper : public Nan::ObjectWrap {
|
class AudioConsumerWrapper : public Nan::ObjectWrap {
|
||||||
friend class AudioRecorderWrapper;
|
friend class AudioRecorderWrapper;
|
||||||
@ -52,14 +49,19 @@ namespace tc {
|
|||||||
static NAN_METHOD(_create_filter_threshold);
|
static NAN_METHOD(_create_filter_threshold);
|
||||||
static NAN_METHOD(_create_filter_state);
|
static NAN_METHOD(_create_filter_state);
|
||||||
|
|
||||||
|
static NAN_METHOD(_get_filter_mode);
|
||||||
|
static NAN_METHOD(_set_filter_mode);
|
||||||
|
|
||||||
std::shared_ptr<AudioFilterWrapper> create_filter(const std::string& /* name */, const std::shared_ptr<filter::Filter>& /* filter impl */);
|
std::shared_ptr<AudioFilterWrapper> create_filter(const std::string& /* name */, const std::shared_ptr<filter::Filter>& /* filter impl */);
|
||||||
void delete_filter(const AudioFilterWrapper*);
|
void delete_filter(const AudioFilterWrapper*);
|
||||||
|
|
||||||
inline std::deque<std::shared_ptr<AudioFilterWrapper>> filters() {
|
inline std::deque<std::shared_ptr<AudioFilterWrapper>> filters() {
|
||||||
std::lock_guard lock(this->_filters_lock);
|
std::lock_guard lock(this->filter_mutex_);
|
||||||
return this->_filters;
|
return this->filter_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline FilterMode filter_mode() const { return this->filter_mode_; }
|
||||||
|
|
||||||
inline std::shared_ptr<AudioConsumer> native_consumer() { return this->_handle; }
|
inline std::shared_ptr<AudioConsumer> native_consumer() { return this->_handle; }
|
||||||
|
|
||||||
std::mutex native_read_callback_lock;
|
std::mutex native_read_callback_lock;
|
||||||
@ -70,8 +72,9 @@ namespace tc {
|
|||||||
std::mutex execute_lock;
|
std::mutex execute_lock;
|
||||||
std::shared_ptr<AudioConsumer> _handle;
|
std::shared_ptr<AudioConsumer> _handle;
|
||||||
|
|
||||||
std::mutex _filters_lock;
|
std::mutex filter_mutex_;
|
||||||
std::deque<std::shared_ptr<AudioFilterWrapper>> _filters;
|
std::deque<std::shared_ptr<AudioFilterWrapper>> filter_;
|
||||||
|
FilterMode filter_mode_{FilterMode::FILTER};
|
||||||
bool last_consumed = false;
|
bool last_consumed = false;
|
||||||
|
|
||||||
void do_wrap(const v8::Local<v8::Object>& /* object */);
|
void do_wrap(const v8::Local<v8::Object>& /* object */);
|
||||||
@ -95,10 +98,6 @@ namespace tc {
|
|||||||
Nan::callback_t<> _call_data;
|
Nan::callback_t<> _call_data;
|
||||||
Nan::callback_t<> _call_ended;
|
Nan::callback_t<> _call_ended;
|
||||||
Nan::callback_t<> _call_started;
|
Nan::callback_t<> _call_started;
|
||||||
/*
|
|
||||||
callback_data: (buffer: Float32Array) => any;
|
|
||||||
callback_ended: () => any;
|
|
||||||
*/
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,8 +66,9 @@ AudioFilterWrapper::~AudioFilterWrapper() {
|
|||||||
log_free("AudioFilterWrapper", this);
|
log_free("AudioFilterWrapper", this);
|
||||||
|
|
||||||
auto threshold_filter = dynamic_pointer_cast<filter::ThresholdFilter>(this->_filter);
|
auto threshold_filter = dynamic_pointer_cast<filter::ThresholdFilter>(this->_filter);
|
||||||
if(threshold_filter)
|
if(threshold_filter) {
|
||||||
threshold_filter->on_analyze = nullptr;
|
threshold_filter->on_analyze = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
this->_callback_analyzed.Reset();
|
this->_callback_analyzed.Reset();
|
||||||
}
|
}
|
||||||
@ -284,7 +285,6 @@ NAN_METHOD(AudioFilterWrapper::_set_analyze_filter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
NAN_METHOD(AudioFilterWrapper::_is_consuming) {
|
NAN_METHOD(AudioFilterWrapper::_is_consuming) {
|
||||||
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
||||||
if(!handle->_filter) {
|
if(!handle->_filter) {
|
||||||
|
@ -27,7 +27,7 @@ namespace tc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AudioFilterWrapper(const std::string& name, const std::shared_ptr<filter::Filter>& /* handle */);
|
AudioFilterWrapper(const std::string& name, const std::shared_ptr<filter::Filter>& /* handle */);
|
||||||
virtual ~AudioFilterWrapper();
|
~AudioFilterWrapper() override;
|
||||||
|
|
||||||
static NAN_METHOD(_get_name);
|
static NAN_METHOD(_get_name);
|
||||||
|
|
||||||
|
@ -181,6 +181,16 @@ NAN_MODULE_INIT(init) {
|
|||||||
audio::recorder::AudioRecorderWrapper::Init(namespace_record);
|
audio::recorder::AudioRecorderWrapper::Init(namespace_record);
|
||||||
audio::recorder::AudioConsumerWrapper::Init(namespace_record);
|
audio::recorder::AudioConsumerWrapper::Init(namespace_record);
|
||||||
audio::recorder::AudioFilterWrapper::Init(namespace_record);
|
audio::recorder::AudioFilterWrapper::Init(namespace_record);
|
||||||
|
|
||||||
|
{
|
||||||
|
auto enum_object = Nan::New<v8::Object>();
|
||||||
|
ENUM_SET(enum_object, "Bypass", audio::recorder::FilterMode::BYPASS);
|
||||||
|
ENUM_SET(enum_object, "Filter", audio::recorder::FilterMode::FILTER);
|
||||||
|
ENUM_SET(enum_object, "Block", audio::recorder::BLOCK);
|
||||||
|
|
||||||
|
Nan::DefineOwnProperty(namespace_record, Nan::New<v8::String>("FilterMode").ToLocalChecked(), enum_object, v8::DontDelete);
|
||||||
|
}
|
||||||
|
|
||||||
Nan::Set(namespace_audio, Nan::New<v8::String>("record").ToLocalChecked(), namespace_record);
|
Nan::Set(namespace_audio, Nan::New<v8::String>("record").ToLocalChecked(), namespace_record);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
@ -84,7 +84,7 @@ namespace tc::connection {
|
|||||||
void process_packet(uint16_t packet_id, const pipes::buffer_view& /* buffer */, codec::value /* codec */, bool /* head */);
|
void process_packet(uint16_t packet_id, const pipes::buffer_view& /* buffer */, codec::value /* codec */, bool /* head */);
|
||||||
void execute_tick();
|
void execute_tick();
|
||||||
|
|
||||||
inline float get_volume() { return this->volume_; }
|
inline float get_volume() const { return this->volume_; }
|
||||||
inline void set_volume(float value) { this->volume_ = value; }
|
inline void set_volume(float value) { this->volume_ = value; }
|
||||||
|
|
||||||
inline state::value state() { return this->state_; }
|
inline state::value state() { return this->state_; }
|
||||||
@ -121,7 +121,7 @@ namespace tc::connection {
|
|||||||
uint16_t last_packet_id{0xFFFF}; /* the first packet id is 0 so one packet before is 0xFFFF */
|
uint16_t last_packet_id{0xFFFF}; /* the first packet id is 0 so one packet before is 0xFFFF */
|
||||||
std::chrono::system_clock::time_point last_packet_timestamp;
|
std::chrono::system_clock::time_point last_packet_timestamp;
|
||||||
|
|
||||||
inline std::chrono::system_clock::time_point stream_timeout() {
|
inline std::chrono::system_clock::time_point stream_timeout() const {
|
||||||
return this->last_packet_timestamp + std::chrono::milliseconds{1000};
|
return this->last_packet_timestamp + std::chrono::milliseconds{1000};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TeaClient",
|
"name": "TeaClient",
|
||||||
"version": "1.4.10",
|
"version": "1.4.11",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user