A lot of updates
This commit is contained in:
@@ -1,152 +0,0 @@
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <condition_variable>
|
||||
#include <cassert>
|
||||
#include "EventLoop.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::event;
|
||||
|
||||
EventExecutor::EventExecutor(const std::string& thread_prefix) : thread_prefix(thread_prefix) {}
|
||||
EventExecutor::~EventExecutor() {
|
||||
unique_lock lock(this->lock);
|
||||
this->_shutdown(lock);
|
||||
this->_reset_events(lock);
|
||||
}
|
||||
|
||||
bool EventExecutor::initialize(int threads) {
|
||||
unique_lock lock(this->lock);
|
||||
this->_shutdown(lock);
|
||||
if(!lock.owns_lock())
|
||||
lock.lock();
|
||||
|
||||
this->should_shutdown = false;
|
||||
while(threads-- > 0) {
|
||||
this->threads.emplace_back(&EventExecutor::_executor, this);
|
||||
|
||||
#ifndef WIN32
|
||||
{
|
||||
auto handle = this->threads.back().native_handle();
|
||||
auto name = this->thread_prefix + to_string(this->threads.size());
|
||||
pthread_setname_np(handle, name.c_str());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void EventExecutor::shutdown() {
|
||||
unique_lock lock(this->lock);
|
||||
this->_shutdown(lock);
|
||||
}
|
||||
|
||||
bool EventExecutor::schedule(const std::shared_ptr<tc::event::EventEntry> &entry) {
|
||||
unique_lock lock(this->lock);
|
||||
if(!entry || entry->_event_ptr)
|
||||
return true; /* already scheduled */
|
||||
|
||||
auto linked_entry = new LinkedEntry{};
|
||||
linked_entry->entry = entry;
|
||||
linked_entry->next = nullptr;
|
||||
linked_entry->previous = this->tail;
|
||||
linked_entry->scheduled = std::chrono::system_clock::now();
|
||||
|
||||
if(this->tail) {
|
||||
this->tail->next = linked_entry;
|
||||
this->tail = linked_entry;
|
||||
} else {
|
||||
this->head = linked_entry;
|
||||
this->tail = linked_entry;
|
||||
}
|
||||
this->condition.notify_one();
|
||||
|
||||
entry->_event_ptr = linked_entry;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EventExecutor::cancel(const std::shared_ptr<tc::event::EventEntry> &entry) {
|
||||
unique_lock lock(this->lock);
|
||||
if(!entry || !entry->_event_ptr)
|
||||
return false;
|
||||
|
||||
auto linked_entry = (LinkedEntry*) entry->_event_ptr;
|
||||
this->head = linked_entry->next;
|
||||
if(this->head) {
|
||||
assert(linked_entry == this->head->previous);
|
||||
this->head->previous = nullptr;
|
||||
} else {
|
||||
assert(linked_entry == this->tail);
|
||||
this->tail = nullptr;
|
||||
}
|
||||
|
||||
delete linked_entry;
|
||||
entry->_event_ptr = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
void EventExecutor::_shutdown(std::unique_lock<std::mutex> &lock) {
|
||||
if(!lock.owns_lock())
|
||||
lock.lock();
|
||||
|
||||
this->should_shutdown = true;
|
||||
this->condition.notify_all();
|
||||
lock.unlock();
|
||||
for(auto& thread : this->threads)
|
||||
thread.join(); /* TODO: Timeout? */
|
||||
lock.lock();
|
||||
this->should_shutdown = false;
|
||||
}
|
||||
|
||||
void EventExecutor::_reset_events(std::unique_lock<std::mutex> &lock) {
|
||||
if(!lock.owns_lock())
|
||||
lock.lock();
|
||||
auto entry = this->head;
|
||||
while(entry) {
|
||||
auto next = entry->next;
|
||||
delete entry;
|
||||
entry = next;
|
||||
}
|
||||
this->head = nullptr;
|
||||
this->tail = nullptr;
|
||||
}
|
||||
|
||||
void EventExecutor::_executor(tc::event::EventExecutor *loop) {
|
||||
while(true) {
|
||||
unique_lock lock(loop->lock);
|
||||
loop->condition.wait(lock, [&] { return loop->should_shutdown || loop->head != nullptr; });
|
||||
if(loop->should_shutdown)
|
||||
break;
|
||||
|
||||
if(!loop->head)
|
||||
continue;
|
||||
|
||||
auto linked_entry = loop->head;
|
||||
loop->head = linked_entry->next;
|
||||
if(loop->head) {
|
||||
assert(linked_entry == loop->head->previous);
|
||||
loop->head->previous = nullptr;
|
||||
} else {
|
||||
assert(linked_entry == loop->tail);
|
||||
loop->tail = nullptr;
|
||||
}
|
||||
|
||||
auto event_handler = linked_entry->entry.lock();
|
||||
assert(event_handler->_event_ptr == linked_entry);
|
||||
event_handler->_event_ptr = nullptr;
|
||||
lock.unlock();
|
||||
|
||||
if(event_handler) {
|
||||
if(event_handler->single_thread_executed()) {
|
||||
auto execute_lock = event_handler->execute_lock(false);
|
||||
if(!execute_lock) {
|
||||
event_handler->event_execute_dropped(linked_entry->scheduled);
|
||||
} else {
|
||||
event_handler->event_execute(linked_entry->scheduled);
|
||||
}
|
||||
} else {
|
||||
event_handler->event_execute(linked_entry->scheduled);
|
||||
}
|
||||
}
|
||||
delete linked_entry;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <condition_variable>
|
||||
|
||||
namespace tc {
|
||||
namespace event {
|
||||
class EventExecutor;
|
||||
class EventEntry {
|
||||
friend class EventExecutor;
|
||||
public:
|
||||
virtual void event_execute(const std::chrono::system_clock::time_point& /* scheduled timestamp */) = 0;
|
||||
virtual void event_execute_dropped(const std::chrono::system_clock::time_point& /* scheduled timestamp */) {}
|
||||
|
||||
std::unique_lock<std::mutex> execute_lock(bool force) {
|
||||
if(force)
|
||||
return std::unique_lock<std::mutex>(this->_execute_mutex);
|
||||
else
|
||||
return std::unique_lock<std::mutex>(this->_execute_mutex, std::try_to_lock);
|
||||
}
|
||||
|
||||
inline bool single_thread_executed() const { return this->_single_thread; }
|
||||
inline void single_thread_executed(bool value) { this->_single_thread = value; }
|
||||
private:
|
||||
void* _event_ptr = nullptr;
|
||||
bool _single_thread = true; /* if its set to true there might are some dropped executes! */
|
||||
std::mutex _execute_mutex;
|
||||
};
|
||||
|
||||
class EventExecutor {
|
||||
public:
|
||||
explicit EventExecutor(const std::string& /* thread prefix */);
|
||||
virtual ~EventExecutor();
|
||||
|
||||
bool initialize(int /* num threads */);
|
||||
bool schedule(const std::shared_ptr<EventEntry>& /* entry */);
|
||||
bool cancel(const std::shared_ptr<EventEntry>& /* entry */); /* Note: Will not cancel already running executes */
|
||||
void shutdown();
|
||||
private:
|
||||
struct LinkedEntry {
|
||||
LinkedEntry* previous;
|
||||
LinkedEntry* next;
|
||||
|
||||
std::chrono::system_clock::time_point scheduled;
|
||||
std::weak_ptr<EventEntry> entry;
|
||||
};
|
||||
static void _executor(EventExecutor*);
|
||||
void _shutdown(std::unique_lock<std::mutex>&);
|
||||
void _reset_events(std::unique_lock<std::mutex>&);
|
||||
|
||||
bool should_shutdown = true;
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
std::mutex lock;
|
||||
std::condition_variable condition;
|
||||
|
||||
LinkedEntry* head = nullptr;
|
||||
LinkedEntry* tail = nullptr;
|
||||
|
||||
std::string thread_prefix;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
#include "AudioDevice.h"
|
||||
#include "../logger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc;
|
||||
using namespace tc::audio;
|
||||
|
||||
extern bool devices_cached(); /* if the result is false then the call to devices() may take a while */
|
||||
extern std::deque<std::shared_ptr<AudioDevice>> devices();
|
||||
|
||||
bool _devices_cached = false;
|
||||
mutex _audio_devices_lock;
|
||||
deque<shared_ptr<AudioDevice>> _audio_devices{};
|
||||
|
||||
bool audio::devices_cached() {
|
||||
return _devices_cached;
|
||||
}
|
||||
|
||||
void audio::clear_device_cache() {
|
||||
lock_guard lock(_audio_devices_lock);
|
||||
_audio_devices.clear();
|
||||
_devices_cached = false;
|
||||
}
|
||||
|
||||
deque<shared_ptr<AudioDevice>> audio::devices() {
|
||||
lock_guard lock(_audio_devices_lock);
|
||||
if(_devices_cached)
|
||||
return _audio_devices;
|
||||
|
||||
/* query devices */
|
||||
auto device_count = Pa_GetDeviceCount();
|
||||
if(device_count < 0) {
|
||||
log_error(category::audio, tr("Pa_GetDeviceCount() returned {}"), device_count);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto default_input_device = Pa_GetDefaultInputDevice();
|
||||
auto default_output_device = Pa_GetDefaultOutputDevice();
|
||||
|
||||
for(PaDeviceIndex device_index = 0; device_index < device_count; device_index++) {
|
||||
auto device_info = Pa_GetDeviceInfo(device_index);
|
||||
if(!device_info) {
|
||||
log_warn(category::audio, tr("Pa_GetDeviceInfo(...) failed for device {}"), device_index);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto device_host_info = Pa_GetHostApiInfo(device_info->hostApi);
|
||||
if(!device_host_info) {
|
||||
log_warn(category::audio, tr("Pa_GetHostApiInfo(...) failed for device {} with host api {}"), device_index, device_info->hostApi);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto info = make_shared<AudioDevice>();
|
||||
info->device_id = device_index;
|
||||
info->name = device_info->name;
|
||||
|
||||
info->max_inputs = device_info->maxInputChannels;
|
||||
info->input_supported = device_info->maxInputChannels > 0;
|
||||
|
||||
info->max_outputs = device_info->maxOutputChannels;
|
||||
info->output_supported = device_info->maxOutputChannels > 0;
|
||||
|
||||
info->is_default_input = device_index == default_input_device;
|
||||
info->is_default_output = device_index == default_output_device;
|
||||
|
||||
info->driver = device_host_info->name;
|
||||
info->is_default_driver_input = device_index == device_host_info->defaultInputDevice;
|
||||
info->is_default_driver_output = device_index == device_host_info->defaultOutputDevice;
|
||||
|
||||
PaStreamParameters test_parameters{};
|
||||
test_parameters.device = device_index;
|
||||
test_parameters.sampleFormat = paFloat32;
|
||||
test_parameters.suggestedLatency = 0; /* ignored by Pa_IsFormatSupported() */
|
||||
test_parameters.hostApiSpecificStreamInfo = nullptr;
|
||||
|
||||
/*
|
||||
if(info->input_supported) {
|
||||
test_parameters.channelCount = device_info->maxInputChannels;
|
||||
for(size_t index = 0; standard_sample_rates[index] > 0; index++) {
|
||||
auto rate = standard_sample_rates[index];
|
||||
auto err = Pa_IsFormatSupported(&test_parameters, nullptr, rate);
|
||||
if(err == paFormatIsSupported)
|
||||
info->supported_input_rates.push_back(rate);
|
||||
}
|
||||
}
|
||||
if(info->output_supported) {
|
||||
test_parameters.channelCount = device_info->maxOutputChannels;
|
||||
for(size_t index = 0; standard_sample_rates[index] > 0; index++) {
|
||||
auto rate = standard_sample_rates[index];
|
||||
auto err = Pa_IsFormatSupported(nullptr, &test_parameters, rate);
|
||||
if(err == paFormatIsSupported)
|
||||
info->supported_output_rates.push_back(rate);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
_audio_devices.push_back(info);
|
||||
}
|
||||
|
||||
_devices_cached = true;
|
||||
return _audio_devices;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include <functional>
|
||||
#include <portaudio.h>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
static constexpr double standard_sample_rates[] = {
|
||||
8000.0, 9600.0, 11025.0, 12000.0, 16000.0, 22050.0, 24000.0, 32000.0,
|
||||
44100.0, 48000.0, 88200.0, 96000.0, 192000.0, -1 /* negative terminated list */
|
||||
};
|
||||
|
||||
struct AudioDevice {
|
||||
PaDeviceIndex device_id;
|
||||
|
||||
bool is_default_output;
|
||||
bool is_default_input;
|
||||
|
||||
bool is_default_driver_output;
|
||||
bool is_default_driver_input;
|
||||
|
||||
bool input_supported;
|
||||
bool output_supported;
|
||||
std::string name;
|
||||
std::string driver;
|
||||
|
||||
/*
|
||||
std::vector<double> supported_input_rates;
|
||||
std::vector<double> supported_output_rates;
|
||||
*/
|
||||
|
||||
int max_inputs;
|
||||
int max_outputs;
|
||||
};
|
||||
|
||||
extern void clear_device_cache();
|
||||
extern bool devices_cached(); /* if the result is false then the call to devices() may take a while */
|
||||
extern std::deque<std::shared_ptr<AudioDevice>> devices();
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include "AudioInput.h"
|
||||
#include "AudioReframer.h"
|
||||
#include "../logger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc;
|
||||
using namespace tc::audio;
|
||||
|
||||
AudioConsumer::AudioConsumer(tc::audio::AudioInput *handle, size_t channel_count, size_t sample_rate, size_t frame_size) :
|
||||
handle(handle),
|
||||
channel_count(channel_count),
|
||||
sample_rate(sample_rate) ,
|
||||
frame_size(frame_size) {
|
||||
if(this->frame_size > 0) {
|
||||
this->reframer = make_unique<Reframer>(channel_count, frame_size);
|
||||
this->reframer->on_frame = [&](const void* buffer) { this->handle_framed_data(buffer, this->frame_size); };
|
||||
}
|
||||
}
|
||||
|
||||
void AudioConsumer::handle_framed_data(const void *buffer, size_t samples) {
|
||||
unique_lock read_callback_lock(this->on_read_lock);
|
||||
auto function = this->on_read; /* copy */
|
||||
read_callback_lock.unlock();
|
||||
if(!function)
|
||||
return;
|
||||
|
||||
function(buffer, samples);
|
||||
}
|
||||
|
||||
void AudioConsumer::process_data(const void *buffer, size_t samples) {
|
||||
if(this->reframer)
|
||||
this->reframer->process(buffer, samples);
|
||||
else
|
||||
this->handle_framed_data(buffer, samples);
|
||||
}
|
||||
|
||||
AudioInput::AudioInput(size_t channels, size_t rate) : _channel_count(channels), _sample_rate(rate) {}
|
||||
AudioInput::~AudioInput() {
|
||||
this->close_device();
|
||||
lock_guard lock(this->consumers_lock);
|
||||
for(const auto& consumer : this->_consumers)
|
||||
consumer->handle = nullptr;
|
||||
}
|
||||
|
||||
bool AudioInput::open_device(std::string& error, PaDeviceIndex index) {
|
||||
lock_guard lock(this->input_stream_lock);
|
||||
|
||||
if(index == this->_current_device_index)
|
||||
return true;
|
||||
|
||||
this->close_device();
|
||||
this->_current_device_index = index;
|
||||
if(index == paNoDevice)
|
||||
return true;
|
||||
|
||||
this->_current_device = Pa_GetDeviceInfo(index);
|
||||
if(!this->_current_device) {
|
||||
this->_current_device_index = paNoDevice;
|
||||
error = "failed to get device info";
|
||||
return false;
|
||||
}
|
||||
|
||||
PaStreamParameters parameters{};
|
||||
memset(¶meters, 0, sizeof(parameters));
|
||||
parameters.channelCount = (int) this->_channel_count;
|
||||
parameters.device = this->_current_device_index;
|
||||
parameters.sampleFormat = paFloat32;
|
||||
parameters.suggestedLatency = this->_current_device->defaultLowOutputLatency;
|
||||
|
||||
auto err = Pa_OpenStream(&this->input_stream, ¶meters, nullptr, (double) this->_sample_rate, paFramesPerBufferUnspecified, paClipOff, &AudioInput::_audio_callback, this);
|
||||
if(err != paNoError) {
|
||||
error = "Pa_OpenStream returned " + to_string(err);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AudioInput::record() {
|
||||
lock_guard lock(this->input_stream_lock);
|
||||
if(!this->input_stream)
|
||||
return false;
|
||||
if(Pa_IsStreamActive(this->input_stream))
|
||||
return true;
|
||||
|
||||
auto err = Pa_StartStream(this->input_stream);
|
||||
if(err != paNoError && err != paStreamIsNotStopped) {
|
||||
log_error(category::audio, tr("Pa_StartStream returned {}"), err);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AudioInput::recording() {
|
||||
lock_guard lock(this->input_stream_lock);
|
||||
return this->input_stream && Pa_IsStreamActive(this->input_stream);
|
||||
}
|
||||
|
||||
void AudioInput::stop() {
|
||||
lock_guard lock(this->input_stream_lock);
|
||||
if(this->input_stream) {
|
||||
Pa_AbortStream(this->input_stream);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioInput::close_device() {
|
||||
lock_guard lock(this->input_stream_lock);
|
||||
if(this->input_stream) {
|
||||
/* TODO: Test for errors */
|
||||
Pa_AbortStream(this->input_stream);
|
||||
Pa_CloseStream(this->input_stream);
|
||||
|
||||
this->input_stream = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<AudioConsumer> AudioInput::create_consumer(size_t frame_length) {
|
||||
auto result = shared_ptr<AudioConsumer>(new AudioConsumer(this, this->_channel_count, this->_sample_rate, frame_length));
|
||||
{
|
||||
lock_guard lock(this->consumers_lock);
|
||||
this->_consumers.push_back(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void AudioInput::delete_consumer(const std::shared_ptr<AudioConsumer> &source) {
|
||||
{
|
||||
lock_guard lock(this->consumers_lock);
|
||||
auto it = find(this->_consumers.begin(), this->_consumers.end(), source);
|
||||
if(it != this->_consumers.end())
|
||||
this->_consumers.erase(it);
|
||||
}
|
||||
|
||||
source->handle = nullptr;
|
||||
}
|
||||
|
||||
int AudioInput::_audio_callback(const void *a, void *b, unsigned long c, const PaStreamCallbackTimeInfo* d, PaStreamCallbackFlags e, void *_ptr_audio_output) {
|
||||
return reinterpret_cast<AudioInput*>(_ptr_audio_output)->audio_callback(a, b, c, d, e);
|
||||
}
|
||||
|
||||
int AudioInput::audio_callback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags) {
|
||||
if (!input) /* hmmm.. suspicious */
|
||||
return 0;
|
||||
|
||||
if(this->_volume != 1) {
|
||||
auto ptr = (float*) input;
|
||||
auto left = frameCount * this->_channel_count;
|
||||
while(left-- > 0)
|
||||
*(ptr++) *= this->_volume;
|
||||
}
|
||||
|
||||
auto begin = chrono::system_clock::now();
|
||||
for(const auto& consumer : this->consumers()) {
|
||||
consumer->process_data(input, frameCount);
|
||||
}
|
||||
auto end = chrono::system_clock::now();
|
||||
auto ms = chrono::duration_cast<chrono::milliseconds>(end - begin).count();
|
||||
if(ms > 5) {
|
||||
log_warn(category::audio, tr("Processing of audio input needed {}ms. This could be an issue!"), chrono::duration_cast<chrono::milliseconds>(end - begin).count());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include <functional>
|
||||
#include <portaudio.h>
|
||||
#include <misc/spin_lock.h>
|
||||
#include "AudioSamples.h"
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
class AudioInput;
|
||||
class Reframer;
|
||||
|
||||
class AudioConsumer {
|
||||
friend class AudioInput;
|
||||
public:
|
||||
AudioInput* handle;
|
||||
|
||||
size_t const channel_count = 0;
|
||||
size_t const sample_rate = 0;
|
||||
|
||||
size_t const frame_size = 0;
|
||||
|
||||
spin_lock on_read_lock; /* locked to access the function */
|
||||
std::function<void(const void* /* buffer */, size_t /* samples */)> on_read;
|
||||
private:
|
||||
AudioConsumer(AudioInput* handle, size_t channel_count, size_t sample_rate, size_t frame_size);
|
||||
|
||||
std::unique_ptr<Reframer> reframer;
|
||||
std::mutex buffer_lock;
|
||||
size_t buffered_samples = 0;
|
||||
std::deque<std::shared_ptr<SampleBuffer>> sample_buffers;
|
||||
|
||||
void process_data(const void* /* buffer */, size_t /* samples */);
|
||||
void handle_framed_data(const void* /* buffer */, size_t /* samples */);
|
||||
};
|
||||
|
||||
class AudioInput {
|
||||
public:
|
||||
AudioInput(size_t /* channels */, size_t /* rate */);
|
||||
virtual ~AudioInput();
|
||||
|
||||
bool open_device(std::string& /* error */, PaDeviceIndex);
|
||||
bool record();
|
||||
bool recording();
|
||||
void stop();
|
||||
void close_device();
|
||||
PaDeviceIndex current_device() { return this->_current_device_index; }
|
||||
|
||||
std::deque<std::shared_ptr<AudioConsumer>> consumers() {
|
||||
std::lock_guard lock(this->consumers_lock);
|
||||
return this->_consumers;
|
||||
}
|
||||
|
||||
std::shared_ptr<AudioConsumer> create_consumer(size_t /* frame size */);
|
||||
void delete_consumer(const std::shared_ptr<AudioConsumer>& /* source */);
|
||||
|
||||
inline size_t channel_count() { return this->_channel_count; }
|
||||
inline size_t sample_rate() { return this->_sample_rate; }
|
||||
|
||||
inline float volume() { return this->_volume; }
|
||||
inline void set_volume(float value) { this->_volume = value; }
|
||||
private:
|
||||
static int _audio_callback(const void *, void *, unsigned long, const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, void*);
|
||||
int audio_callback(const void *, void *, unsigned long, const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags);
|
||||
|
||||
size_t const _channel_count;
|
||||
size_t const _sample_rate;
|
||||
|
||||
std::mutex consumers_lock;
|
||||
std::deque<std::shared_ptr<AudioConsumer>> _consumers;
|
||||
|
||||
std::recursive_mutex input_stream_lock;
|
||||
const PaDeviceInfo* _current_device = nullptr;
|
||||
PaDeviceIndex _current_device_index = paNoDevice;
|
||||
PaStream* input_stream = nullptr;
|
||||
|
||||
float _volume = 1.f;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
#include <cassert>
|
||||
#include "AudioMerger.h"
|
||||
#include "../logger.h"
|
||||
#include <iostream>
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::audio;
|
||||
|
||||
/* technique based on http://www.vttoth.com/CMS/index.php/technical-notes/68 */
|
||||
inline float merge_ab(float a, float b) {
|
||||
/*
|
||||
* Form: A,B := [0;n]
|
||||
* Z = 2(A + B) - (2/n) * A * B - n
|
||||
*
|
||||
* For a range from 0 to 2: Z = 2(A + B) - AB - 2
|
||||
*/
|
||||
|
||||
a += 1;
|
||||
b += 1;
|
||||
|
||||
return (2 * (a + b) - a * b - 2) - 1;
|
||||
}
|
||||
|
||||
bool merge::merge_sources(void *_dest, void *_src_a, void *_src_b, size_t channels, size_t samples) {
|
||||
auto dest = (float*) _dest;
|
||||
auto src_a = (float*) _src_a;
|
||||
auto src_b = (float*) _src_b;
|
||||
|
||||
for(size_t index = 0; index < channels * samples; index++)
|
||||
dest[index] = merge_ab(src_a[index], src_b[index]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool merge::merge_n_sources(void *dest, void **srcs, size_t src_length, size_t channels, size_t samples) {
|
||||
assert(src_length > 0);
|
||||
|
||||
/* find the first non empty source */
|
||||
while(!srcs[0]) {
|
||||
srcs++;
|
||||
src_length--;
|
||||
|
||||
if(src_length == 0)
|
||||
return false;
|
||||
}
|
||||
memcpy(dest, srcs[0], channels * samples * 4);
|
||||
srcs++;
|
||||
src_length--;
|
||||
|
||||
while(src_length > 0) {
|
||||
/* only invoke is srcs is not null! */
|
||||
if(srcs[0] && !merge::merge_sources(dest, srcs[0], dest, channels, samples))
|
||||
return false;
|
||||
|
||||
srcs++;
|
||||
src_length--;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#define stack_buffer_length (1024 * 8)
|
||||
bool merge::merge_channels_interleaved(void *target, size_t target_channels, const void *src, size_t src_channels, size_t samples) {
|
||||
assert(src_channels == 1 || src_channels == 2);
|
||||
assert(target_channels == 1 || target_channels == 2);
|
||||
|
||||
if(src_channels == target_channels) {
|
||||
if(src == target)
|
||||
return true;
|
||||
memcpy(target, src, samples * src_channels * 4);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(src_channels == 1 && target_channels == 2) {
|
||||
auto source_array = (float*) src;
|
||||
auto target_array = (float*) target;
|
||||
|
||||
if(source_array == target_array) {
|
||||
if(stack_buffer_length < samples * src_channels) {
|
||||
log_error(category::audio, tr("channel merge failed due to large inputs"));
|
||||
return false;
|
||||
}
|
||||
float stack_buffer[stack_buffer_length];
|
||||
memcpy(stack_buffer, src, src_channels * samples * 4);
|
||||
source_array = stack_buffer;
|
||||
|
||||
while(samples-- > 0) {
|
||||
*(target_array++) = *source_array;
|
||||
*(target_array++) = *source_array;
|
||||
source_array++;
|
||||
}
|
||||
} else {
|
||||
while(samples-- > 0) {
|
||||
*(target_array++) = *source_array;
|
||||
*(target_array++) = *source_array;
|
||||
source_array++;
|
||||
}
|
||||
}
|
||||
} else if(src_channels == 2 && target_channels == 1) {
|
||||
auto source_array = (float*) src;
|
||||
auto target_array = (float*) target;
|
||||
|
||||
while(samples-- > 0) {
|
||||
*(target_array++) = merge_ab(*(source_array), *(source_array + 1));
|
||||
source_array += 2;
|
||||
}
|
||||
} else
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
namespace merge {
|
||||
/* the result buffer could be equal to one of the source buffers to prevent unnecessary allocations
|
||||
* Note: The sample order is irrelevant
|
||||
*/
|
||||
extern bool merge_sources(void* /* result */, void* /* source a */, void* /* source b */, size_t /* channels */, size_t /* samples */);
|
||||
|
||||
extern bool merge_n_sources(void* /* result */, void** /* sources */, size_t /* size_t sources count */, size_t /* channels */, size_t /* samples */);
|
||||
|
||||
extern bool merge_channels_interleaved(void* /* result */, size_t /* result channels */, const void* /* source */, size_t /* source channels */, size_t /* samples */);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,293 +0,0 @@
|
||||
#include "AudioOutput.h"
|
||||
#include "AudioMerger.h"
|
||||
#include "../logger.h"
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
using namespace std;
|
||||
using namespace tc;
|
||||
using namespace tc::audio;
|
||||
|
||||
void AudioOutputSource::clear() {
|
||||
lock_guard lock(this->buffer_lock);
|
||||
this->sample_buffers.clear();
|
||||
this->buffered_samples = 0;
|
||||
}
|
||||
|
||||
ssize_t AudioOutputSource::pop_samples(void *buffer, size_t samples) {
|
||||
auto sample_count = samples;
|
||||
|
||||
_retest:
|
||||
{
|
||||
lock_guard lock(this->buffer_lock);
|
||||
while(sample_count > 0 && !this->sample_buffers.empty()) {
|
||||
auto buf = this->sample_buffers[0];
|
||||
auto sc = min((size_t) (buf->sample_size - buf->sample_index), (size_t) sample_count);
|
||||
if(sc > 0 && buffer) { /* just to ensure */
|
||||
memcpy(buffer, (char *) buf->sample_data + this->channel_count * buf->sample_index * 4, sc * this->channel_count * 4);
|
||||
} else {
|
||||
#ifndef WIN32
|
||||
/* for my debugger */
|
||||
__asm__("nop");
|
||||
#endif
|
||||
}
|
||||
|
||||
sample_count -= sc;
|
||||
buf->sample_index += (uint16_t) sc;
|
||||
if(buf->sample_index == buf->sample_size)
|
||||
this->sample_buffers.pop_front();
|
||||
|
||||
if(buffer)
|
||||
buffer = (char*) buffer + sc * this->channel_count * 4;
|
||||
}
|
||||
this->buffered_samples -= samples - sample_count;
|
||||
}
|
||||
|
||||
if(sample_count > 0) {
|
||||
if(this->on_underflow) {
|
||||
if(this->on_underflow()) {
|
||||
goto _retest;
|
||||
}
|
||||
}
|
||||
|
||||
this->buffering = true;
|
||||
}
|
||||
|
||||
if(this->on_read)
|
||||
this->on_read();
|
||||
|
||||
return samples - sample_count; /* return the written samples */
|
||||
}
|
||||
|
||||
ssize_t AudioOutputSource::enqueue_samples(const void *buffer, size_t samples) {
|
||||
auto buf = SampleBuffer::allocate((uint8_t) this->channel_count, (uint16_t) samples);
|
||||
if(!buf)
|
||||
return -1;
|
||||
|
||||
buf->sample_index = 0;
|
||||
memcpy(buf->sample_data, buffer, this->channel_count * samples * 4);
|
||||
|
||||
return this->enqueue_samples(buf);
|
||||
}
|
||||
|
||||
ssize_t AudioOutputSource::enqueue_samples(const std::shared_ptr<tc::audio::SampleBuffer> &buf) {
|
||||
if(!buf) return 0;
|
||||
|
||||
{
|
||||
unique_lock lock(this->buffer_lock);
|
||||
if(this->max_latency > 0 && this->buffered_samples + buf->sample_size > this->max_latency) {
|
||||
/* overflow! */
|
||||
auto overflow_length = this->buffered_samples + buf->sample_size - this->max_latency;
|
||||
if(this->on_overflow) {
|
||||
lock.unlock();
|
||||
this->on_overflow(overflow_length);
|
||||
lock.lock();
|
||||
}
|
||||
|
||||
switch (this->overflow_strategy) {
|
||||
case overflow_strategy::discard_input:
|
||||
return -2;
|
||||
case overflow_strategy::discard_buffer_all:
|
||||
this->sample_buffers.clear();
|
||||
break;
|
||||
case overflow_strategy::discard_buffer_half:
|
||||
this->sample_buffers.erase(this->sample_buffers.begin(), this->sample_buffers.begin() + (int) ceil(this->sample_buffers.size() / 2));
|
||||
break;
|
||||
case overflow_strategy::ignore:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this->sample_buffers.push_back(buf);
|
||||
this->buffered_samples += buf->sample_size;
|
||||
}
|
||||
|
||||
return buf->sample_size;
|
||||
}
|
||||
|
||||
AudioOutput::AudioOutput(size_t channels, size_t rate) : _channel_count(channels), _sample_rate(rate) {}
|
||||
AudioOutput::~AudioOutput() {
|
||||
this->close_device();
|
||||
this->cleanup_buffers();
|
||||
}
|
||||
|
||||
std::shared_ptr<AudioOutputSource> AudioOutput::create_source() {
|
||||
auto result = shared_ptr<AudioOutputSource>(new AudioOutputSource(this, this->_channel_count, this->_sample_rate));
|
||||
{
|
||||
lock_guard lock(this->sources_lock);
|
||||
this->_sources.push_back(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void AudioOutput::delete_source(const std::shared_ptr<tc::audio::AudioOutputSource> &source) {
|
||||
{
|
||||
lock_guard lock(this->sources_lock);
|
||||
auto it = find(this->_sources.begin(), this->_sources.end(), source);
|
||||
if(it != this->_sources.end())
|
||||
this->_sources.erase(it);
|
||||
}
|
||||
|
||||
source->handle = nullptr;
|
||||
}
|
||||
|
||||
void AudioOutput::cleanup_buffers() {
|
||||
lock_guard buffer_lock(this->buffer_lock);
|
||||
if(this->source_buffer)
|
||||
free(this->source_buffer);
|
||||
if(this->source_merge_buffer)
|
||||
free(this->source_merge_buffer);
|
||||
|
||||
this->source_merge_buffer = nullptr;
|
||||
this->source_buffer = nullptr;
|
||||
this->source_merge_buffer_length = 0;
|
||||
this->source_buffer_length = 0;
|
||||
}
|
||||
|
||||
int AudioOutput::_audio_callback(const void *a, void *b, unsigned long c, const PaStreamCallbackTimeInfo* d, PaStreamCallbackFlags e, void *_ptr_audio_output) {
|
||||
return reinterpret_cast<AudioOutput*>(_ptr_audio_output)->audio_callback(a, b, c, d, e);
|
||||
}
|
||||
|
||||
int AudioOutput::audio_callback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags) {
|
||||
if(!output) /* hmmm.. suspicious */
|
||||
return 0;
|
||||
|
||||
lock_guard buffer_lock(this->buffer_lock);
|
||||
size_t buffer_length = frameCount * 4 * this->_channel_count;
|
||||
size_t sources = 0;
|
||||
size_t actual_sources = 0;
|
||||
|
||||
auto volume = this->_volume;
|
||||
{
|
||||
lock_guard lock(this->sources_lock);
|
||||
sources = this->_sources.size();
|
||||
actual_sources = sources;
|
||||
|
||||
if(sources > 0) {
|
||||
if(volume > 0) { /* allocate the required space */
|
||||
auto source_buffer_length = buffer_length * sources;
|
||||
auto source_merge_buffer_length = sizeof(void*) * sources;
|
||||
|
||||
if(this->source_buffer_length < source_buffer_length || !this->source_buffer) {
|
||||
if(this->source_buffer)
|
||||
free(this->source_buffer);
|
||||
this->source_buffer = malloc(source_buffer_length);
|
||||
this->source_buffer_length = source_buffer_length;
|
||||
}
|
||||
if(this->source_merge_buffer_length < source_merge_buffer_length || !this->source_merge_buffer) {
|
||||
if(this->source_merge_buffer)
|
||||
free(this->source_merge_buffer);
|
||||
this->source_merge_buffer = (void**) malloc(source_merge_buffer_length);
|
||||
this->source_merge_buffer_length = source_merge_buffer_length;
|
||||
}
|
||||
}
|
||||
|
||||
for(size_t index = 0; index < sources; index++) {
|
||||
auto& source = this->_sources[index];
|
||||
|
||||
{
|
||||
lock_guard lock(source->buffer_lock);
|
||||
if(source->buffering) {
|
||||
if(source->buffered_samples > source->min_buffer) {
|
||||
source->buffering = false;
|
||||
} else {
|
||||
this->source_merge_buffer[index] = nullptr;
|
||||
actual_sources--;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(volume > 0) {
|
||||
this->source_merge_buffer[index] = (char*) this->source_buffer + (buffer_length * index);
|
||||
auto written_frames = this->_sources[index]->pop_samples(this->source_merge_buffer[index], frameCount);
|
||||
if(written_frames != frameCount) {
|
||||
if(written_frames == 0) {
|
||||
this->source_merge_buffer[index] = nullptr;
|
||||
actual_sources--;
|
||||
} else {
|
||||
/* fill up the rest with silence (0) */
|
||||
auto written = written_frames * this->_channel_count * 4;
|
||||
memset((char*) this->source_merge_buffer[index] + written, 0, (frameCount - written_frames) * this->_channel_count * 4);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this->_sources[index]->pop_samples(nullptr, frameCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(actual_sources > 0 && volume > 0) {
|
||||
if(!merge::merge_n_sources(output, this->source_merge_buffer, sources, this->_channel_count, frameCount)) {
|
||||
log_warn(category::audio, tr("failed to merge buffers!"));
|
||||
}
|
||||
auto float_length = this->_channel_count * frameCount;
|
||||
auto data = (float*) output;
|
||||
while(float_length-- > 0)
|
||||
*data++ *= volume;
|
||||
|
||||
} else {
|
||||
memset(output, 0, this->_channel_count * 4 * frameCount);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool AudioOutput::open_device(std::string& error, PaDeviceIndex index) {
|
||||
lock_guard lock(this->output_stream_lock);
|
||||
|
||||
if(index == this->_current_device_index)
|
||||
return true;
|
||||
|
||||
this->close_device();
|
||||
this->_current_device_index = index;
|
||||
this->_current_device = Pa_GetDeviceInfo(index);
|
||||
|
||||
if(!this->_current_device) {
|
||||
this->_current_device_index = paNoDevice;
|
||||
error = "failed to get device info";
|
||||
return false;
|
||||
}
|
||||
|
||||
PaStreamParameters output_parameters{};
|
||||
memset(&output_parameters, 0, sizeof(output_parameters));
|
||||
output_parameters.channelCount = (int) this->_channel_count;
|
||||
output_parameters.device = this->_current_device_index;
|
||||
output_parameters.sampleFormat = paFloat32;
|
||||
output_parameters.suggestedLatency = this->_current_device->defaultLowOutputLatency;
|
||||
|
||||
auto err = Pa_OpenStream(&output_stream, nullptr, &output_parameters, (double) this->_sample_rate, paFramesPerBufferUnspecified, paClipOff, &AudioOutput::_audio_callback, this);
|
||||
if(err != paNoError) {
|
||||
error = "Pa_OpenStream returned " + to_string(err);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AudioOutput::playback() {
|
||||
lock_guard lock(this->output_stream_lock);
|
||||
if(!this->output_stream)
|
||||
return false;
|
||||
|
||||
auto err = Pa_StartStream(this->output_stream);
|
||||
if(err != paNoError && err != paStreamIsNotStopped) {
|
||||
log_error(category::audio, tr("Pa_StartStream returned {}"), err);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioOutput::close_device() {
|
||||
lock_guard lock(this->output_stream_lock);
|
||||
if(this->output_stream) {
|
||||
/* TODO: Test for errors */
|
||||
Pa_StopStream(this->output_stream);
|
||||
Pa_CloseStream(this->output_stream);
|
||||
|
||||
this->output_stream = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include <functional>
|
||||
#include <portaudio.h>
|
||||
#include "AudioSamples.h"
|
||||
|
||||
#ifdef WIN32
|
||||
#define ssize_t int64_t
|
||||
#endif
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
class AudioOutput;
|
||||
|
||||
namespace overflow_strategy {
|
||||
enum value {
|
||||
ignore,
|
||||
discard_buffer_all,
|
||||
discard_buffer_half,
|
||||
discard_input
|
||||
};
|
||||
}
|
||||
class AudioOutputSource {
|
||||
friend class AudioOutput;
|
||||
public:
|
||||
AudioOutput* handle;
|
||||
|
||||
size_t const channel_count = 0;
|
||||
size_t const sample_rate = 0;
|
||||
|
||||
bool buffering = true;
|
||||
size_t min_buffer = 0;
|
||||
|
||||
/* For stream set it to the max latency in samples you want.
|
||||
* Zero means unlimited
|
||||
*/
|
||||
size_t max_latency = 0;
|
||||
overflow_strategy::value overflow_strategy = overflow_strategy::discard_buffer_half;
|
||||
|
||||
/* if it returns true then the it means that the buffer has been refilled, we have to test again */
|
||||
std::function<bool()> on_underflow;
|
||||
std::function<void(size_t /* sample count */)> on_overflow;
|
||||
std::function<void()> on_read; /* will be invoked after sample read, e.g. for buffer fullup */
|
||||
|
||||
void clear();
|
||||
ssize_t pop_samples(void* /* output buffer */, size_t /* sample count */);
|
||||
ssize_t enqueue_samples(const void * /* input buffer */, size_t /* sample count */);
|
||||
ssize_t enqueue_samples(const std::shared_ptr<SampleBuffer>& /* buffer */);
|
||||
private:
|
||||
AudioOutputSource(AudioOutput* handle, size_t channel_count, size_t sample_rate) : handle(handle), channel_count(channel_count), sample_rate(sample_rate) {}
|
||||
|
||||
std::mutex buffer_lock;
|
||||
size_t buffered_samples = 0;
|
||||
std::deque<std::shared_ptr<SampleBuffer>> sample_buffers;
|
||||
};
|
||||
|
||||
class AudioOutput {
|
||||
public:
|
||||
AudioOutput(size_t /* channels */, size_t /* rate */);
|
||||
virtual ~AudioOutput();
|
||||
|
||||
bool open_device(std::string& /* error */, PaDeviceIndex);
|
||||
bool playback();
|
||||
void close_device();
|
||||
PaDeviceIndex current_device() { return this->_current_device_index; }
|
||||
|
||||
std::deque<std::shared_ptr<AudioOutputSource>> sources() {
|
||||
std::lock_guard lock(this->sources_lock);
|
||||
return this->_sources;
|
||||
}
|
||||
|
||||
std::shared_ptr<AudioOutputSource> create_source();
|
||||
void delete_source(const std::shared_ptr<AudioOutputSource>& /* source */);
|
||||
|
||||
inline size_t channel_count() { return this->_channel_count; }
|
||||
inline size_t sample_rate() { return this->_sample_rate; }
|
||||
|
||||
inline float volume() { return this->_volume; }
|
||||
inline void set_volume(float value) { this->_volume = value; }
|
||||
private:
|
||||
static int _audio_callback(const void *, void *, unsigned long, const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, void*);
|
||||
int audio_callback(const void *, void *, unsigned long, const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags);
|
||||
|
||||
size_t const _channel_count;
|
||||
size_t const _sample_rate;
|
||||
|
||||
std::mutex sources_lock;
|
||||
std::deque<std::shared_ptr<AudioOutputSource>> _sources;
|
||||
|
||||
std::recursive_mutex output_stream_lock;
|
||||
const PaDeviceInfo* _current_device = nullptr;
|
||||
PaDeviceIndex _current_device_index = paNoDevice;
|
||||
PaStream* output_stream = nullptr;
|
||||
|
||||
std::mutex buffer_lock; /* not required, but why not. Usually only used within audio_callback! */
|
||||
void* source_buffer = nullptr;
|
||||
void** source_merge_buffer = nullptr;
|
||||
|
||||
size_t source_buffer_length = 0;
|
||||
size_t source_merge_buffer_length = 0;
|
||||
void cleanup_buffers();
|
||||
|
||||
float _volume = 1.f;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include "AudioReframer.h"
|
||||
|
||||
using namespace tc::audio;
|
||||
|
||||
Reframer::Reframer(size_t channels, size_t frame_size) : _frame_size(frame_size), _channels(channels) {
|
||||
this->buffer = nullptr;
|
||||
this->_buffer_index = 0;
|
||||
}
|
||||
|
||||
Reframer::~Reframer() {
|
||||
if(this->buffer)
|
||||
free(this->buffer);
|
||||
}
|
||||
|
||||
void Reframer::process(const void *source, size_t samples) {
|
||||
if(!this->buffer)
|
||||
this->buffer = (float*) malloc(this->_channels * this->_frame_size * sizeof(float));
|
||||
assert(this->on_frame);
|
||||
|
||||
if(this->_buffer_index > 0) {
|
||||
if(this->_buffer_index + samples > this->_frame_size) {
|
||||
auto required = this->_frame_size - this->_buffer_index;
|
||||
auto length = required * this->_channels * 4;
|
||||
|
||||
memcpy((char*) this->buffer + this->_buffer_index * 4 * this->_channels, source, length);
|
||||
samples -= required;
|
||||
source = (char*) source + length;
|
||||
|
||||
this->on_frame(this->buffer);
|
||||
} else {
|
||||
memcpy((char*) this->buffer + this->_buffer_index * 4 * this->_channels, source, samples * this->_channels * 4);
|
||||
this->_buffer_index += samples;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto _on_frame = this->on_frame;
|
||||
while(samples > this->_frame_size) {
|
||||
if(_on_frame)
|
||||
_on_frame(source);
|
||||
samples -= this->_frame_size;
|
||||
source = (char*) source + this->_frame_size * this->_channels * 4;
|
||||
}
|
||||
if(samples > 0)
|
||||
memcpy((char*) this->buffer, source, samples * this->_channels * 4);
|
||||
this->_buffer_index = samples;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <cstdio>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
class Reframer {
|
||||
public:
|
||||
Reframer(size_t channels, size_t frame_size);
|
||||
virtual ~Reframer();
|
||||
|
||||
void process(const void* /* source */, size_t /* samples */);
|
||||
|
||||
inline size_t channels() { return this->_channels; }
|
||||
inline size_t frame_size() { return this->_frame_size; }
|
||||
|
||||
std::function<void(const void* /* buffer */)> on_frame;
|
||||
private:
|
||||
void* buffer;
|
||||
size_t _buffer_index;
|
||||
|
||||
size_t _channels;
|
||||
size_t _frame_size;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include "AudioResampler.h"
|
||||
#include "../logger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::audio;
|
||||
|
||||
AudioResampler::AudioResampler(size_t irate, size_t orate, size_t channels) : _input_rate(irate), _output_rate(orate), _channels(channels) {
|
||||
if(this->input_rate() != this->output_rate()) {
|
||||
soxr_error_t error;
|
||||
this->soxr_handle = soxr_create((double) this->_input_rate, (double) this->_output_rate, (unsigned) this->_channels, &error, nullptr, nullptr, nullptr);
|
||||
|
||||
if(!this->soxr_handle) {
|
||||
log_error(category::audio, tr("Failed to create soxr resampler: {}. Input: {}; Output: {}; Channels: {}"), error, this->_input_rate, this->_output_rate, this->_channels);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AudioResampler::~AudioResampler() {
|
||||
if(this->soxr_handle)
|
||||
soxr_delete(this->soxr_handle);
|
||||
}
|
||||
|
||||
ssize_t AudioResampler::process(void *output, const void *input, size_t input_length) {
|
||||
if(this->io_ratio() == 1) {
|
||||
if(input != output)
|
||||
memcpy(output, input, input_length * this->_channels * 4);
|
||||
|
||||
return input_length;
|
||||
}
|
||||
if(!this->soxr_handle)
|
||||
return -2;
|
||||
|
||||
size_t output_length = 0;
|
||||
auto error = soxr_process(this->soxr_handle, input, input_length, nullptr, output, this->estimated_output_size(input_length), &output_length);
|
||||
if(error) {
|
||||
log_error(category::audio, tr("Failed to process resample: {}"), error);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return output_length;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <cmath>
|
||||
#include <soxr.h>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
|
||||
#if defined(WIN32) && !defined(ssize_t)
|
||||
#define ssize_t int64_t
|
||||
#endif
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
class AudioResampler {
|
||||
public:
|
||||
AudioResampler(size_t /* input rate */, size_t /* output rate */, size_t /* channels */);
|
||||
virtual ~AudioResampler();
|
||||
|
||||
inline size_t channels() { return this->_channels; }
|
||||
inline size_t input_rate() { return this->_input_rate; }
|
||||
inline size_t output_rate() { return this->_output_rate; }
|
||||
|
||||
inline long double io_ratio() { return (long double) this->_output_rate / (long double) this->_input_rate; }
|
||||
inline size_t estimated_output_size(size_t input_length) {
|
||||
return (size_t) lroundl(this->io_ratio() * input_length) + 1;
|
||||
}
|
||||
|
||||
inline bool valid() { return this->io_ratio() == 1 || this->soxr_handle != nullptr; }
|
||||
|
||||
ssize_t process(void* /* output */, const void* /* input */, size_t /* input length */);
|
||||
private:
|
||||
size_t const _channels = 0;
|
||||
size_t const _input_rate = 0;
|
||||
size_t const _output_rate = 0;
|
||||
|
||||
soxr_t soxr_handle = nullptr;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
#include "AudioSamples.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc;
|
||||
using namespace tc::audio;
|
||||
|
||||
std::shared_ptr<SampleBuffer> SampleBuffer::allocate(uint8_t channels, uint16_t samples) {
|
||||
auto _buffer = (SampleBuffer*) malloc(SampleBuffer::HEAD_LENGTH + channels * samples * 4);
|
||||
if(!_buffer)
|
||||
return nullptr;
|
||||
|
||||
_buffer->sample_size = samples;
|
||||
_buffer->sample_index = 0;
|
||||
return shared_ptr<SampleBuffer>(_buffer, ::free);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
|
||||
#ifdef WIN32
|
||||
#pragma pack(push,1)
|
||||
#define __attribute__packed_1
|
||||
#else
|
||||
#define __attribute__packed_1 __attribute__((packed, aligned(1)))
|
||||
#endif
|
||||
/* Every sample is a float (4byte) */
|
||||
struct __attribute__packed_1 SampleBuffer {
|
||||
static constexpr size_t HEAD_LENGTH = 4;
|
||||
|
||||
uint16_t sample_size;
|
||||
uint16_t sample_index;
|
||||
|
||||
char sample_data[
|
||||
#ifndef WIN32
|
||||
0
|
||||
#else
|
||||
1 /* windows does not allow zero sized arrays */
|
||||
#endif
|
||||
];
|
||||
|
||||
static std::shared_ptr<SampleBuffer> allocate(uint8_t /* channels */, uint16_t /* samples */);
|
||||
};
|
||||
|
||||
#ifndef WIN32
|
||||
static_assert(sizeof(SampleBuffer) == 4, "Invalid SampleBuffer packaging!");
|
||||
#else
|
||||
#pragma pack(pop)
|
||||
static_assert(sizeof(SampleBuffer) == 5, "Invalid SampleBuffer packaging!");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
//
|
||||
// Created by wolverindev on 28.04.19.
|
||||
//
|
||||
|
||||
#include "Converter.h"
|
||||
|
||||
using namespace tc::audio::codec;
|
||||
|
||||
Converter::Converter(size_t c, size_t s, size_t f) : _channels(c), _sample_rate(s), _frame_size(f) {}
|
||||
Converter::~Converter() {}
|
||||
|
||||
bool type::supported(value type) {
|
||||
#ifdef HAVE_CODEC_OPUS
|
||||
if(type == type::opus)
|
||||
return true;
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_CODEC_SPEEX
|
||||
if(type == type::speex)
|
||||
return true;
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_CODEC_FLAC
|
||||
if(type == type::flac)
|
||||
return true;
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_CODEC_CELT
|
||||
if(type == type::celt)
|
||||
return true;
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#if !defined(ssize_t) && defined(WIN32)
|
||||
#define ssize_t int64_t
|
||||
#endif
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
namespace codec {
|
||||
namespace type {
|
||||
enum value {
|
||||
undefined,
|
||||
|
||||
/* supported */
|
||||
opus,
|
||||
speex,
|
||||
|
||||
/* unsupported */
|
||||
flac,
|
||||
celt
|
||||
};
|
||||
|
||||
extern bool supported(value);
|
||||
}
|
||||
|
||||
class Converter {
|
||||
public:
|
||||
Converter(size_t /* channels */, size_t /* sample rate */, size_t /* frame size */);
|
||||
virtual ~Converter();
|
||||
|
||||
/* initialize parameters depend on the codec */
|
||||
virtual bool valid() = 0;
|
||||
virtual void finalize() = 0;
|
||||
|
||||
virtual void reset_encoder() = 0;
|
||||
virtual void reset_decoder() = 0;
|
||||
|
||||
/**
|
||||
* @return number of bytes written on success
|
||||
*/
|
||||
virtual ssize_t encode(std::string& /* error */, const void* /* source */, void* /* destination */, size_t /* destination byte length */) = 0;
|
||||
|
||||
/**
|
||||
* @return number of samples on success
|
||||
*/
|
||||
virtual ssize_t decode(std::string& /* error */, const void* /* source */, size_t /* source byte length */, void* /* destination */) = 0;
|
||||
virtual ssize_t decode_lost(std::string& /* error */, size_t /* packets */) = 0;
|
||||
|
||||
virtual size_t expected_encoded_length(size_t /* sample count */) = 0;
|
||||
virtual size_t expected_decoded_length(const void* /* source */, size_t /* source byte length */) {
|
||||
return this->bytes_per_frame();
|
||||
}
|
||||
|
||||
inline size_t channels() { return this->_channels; }
|
||||
inline size_t sample_rate() { return this->_sample_rate; }
|
||||
inline size_t frame_size() { return this->_frame_size; }
|
||||
|
||||
inline size_t bytes_per_frame() { return this->_channels * this->_frame_size * 4; }
|
||||
protected:
|
||||
size_t _frame_size;
|
||||
size_t _channels;
|
||||
size_t _sample_rate;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
#include "OpusConverter.h"
|
||||
#include "../../logger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::audio::codec;
|
||||
|
||||
OpusConverter::OpusConverter(size_t c, size_t s, size_t f) : Converter(c, s, f) { }
|
||||
OpusConverter::~OpusConverter() {}
|
||||
|
||||
bool OpusConverter::valid() {
|
||||
return this->encoder && this->decoder;
|
||||
}
|
||||
|
||||
bool OpusConverter::initialize(std::string &error, int application_type) {
|
||||
lock_guard lock(this->coder_lock);
|
||||
this->_application_type = application_type;
|
||||
|
||||
if(!this->_initialize_encoder(error))
|
||||
return false;
|
||||
|
||||
if(!this->_initialize_decoder(error)) {
|
||||
this->reset_encoder();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpusConverter::reset_encoder() {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
|
||||
auto result = opus_encoder_ctl(this->encoder, OPUS_RESET_STATE);
|
||||
if(result != OPUS_OK)
|
||||
log_warn(category::audio, tr("Failed to reset opus encoder. Opus result: {}"), result);
|
||||
}
|
||||
|
||||
void OpusConverter::reset_decoder() {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
auto result = opus_decoder_ctl(this->decoder, OPUS_RESET_STATE);
|
||||
if(result != OPUS_OK)
|
||||
log_warn(category::audio, tr("Failed to reset opus decoder. Opus result: {}"), result);
|
||||
}
|
||||
|
||||
|
||||
void OpusConverter::finalize() {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
if(this->encoder) opus_encoder_destroy(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
|
||||
if(this->decoder) opus_decoder_destroy(this->decoder);
|
||||
this->decoder = nullptr;
|
||||
}
|
||||
|
||||
ssize_t OpusConverter::encode(std::string &error, const void *source, void *target, size_t target_length) {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
auto result = opus_encode_float(this->encoder, (float*) source, (int) this->_frame_size, (uint8_t*) target, (opus_int32) target_length);
|
||||
if(result < OPUS_OK) {
|
||||
error = to_string(result) + "|" + opus_strerror(result);
|
||||
return -1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ssize_t OpusConverter::decode(std::string &error, const void *source, size_t source_length, void *target) {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
auto result = opus_decode_float(this->decoder, (uint8_t*) source, (opus_int32) source_length, (float*) target, (int) this->_frame_size, 0);
|
||||
if(result < OPUS_OK) {
|
||||
error = to_string(result) + "|" + opus_strerror(result);
|
||||
return -1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ssize_t OpusConverter::decode_lost(std::string &error, size_t packets) {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
auto buffer = (float*) malloc(this->_frame_size * this->_channels * sizeof(float));
|
||||
while (packets-- > 0) {
|
||||
auto result = opus_decode_float(this->decoder, nullptr, 0, buffer, (int) this->_frame_size, false);
|
||||
if(result < OPUS_OK)
|
||||
log_warn(category::audio, tr("Opus decode lost resulted in error: {}"), result);
|
||||
}
|
||||
free(buffer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t OpusConverter::expected_encoded_length(size_t sample_count) {
|
||||
//TODO calculate stuff
|
||||
return 512;
|
||||
}
|
||||
|
||||
bool OpusConverter::_initialize_decoder(std::string &error) {
|
||||
if(!this->_finalize_decoder(error))
|
||||
return false;
|
||||
|
||||
int error_id = 0;
|
||||
this->decoder = opus_decoder_create((opus_int32) this->_sample_rate, (int) this->_channels, &error_id);
|
||||
if(!this->encoder || error_id) {
|
||||
error = "failed to create decoder (" + to_string(error_id) + ")";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpusConverter::_initialize_encoder(std::string &error) {
|
||||
if(!this->_finalize_encoder(error))
|
||||
return false;
|
||||
|
||||
int error_id = 0;
|
||||
this->encoder = opus_encoder_create((opus_int32) this->_sample_rate, (int) this->_channels, this->_application_type, &error_id);
|
||||
if(!this->encoder || error_id) {
|
||||
error = "failed to create encoder (" + to_string(error_id) + ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
error_id = opus_encoder_ctl(encoder, OPUS_SET_BITRATE(64000));
|
||||
if(error_id) {
|
||||
error = "failed to set bitrate (" + to_string(error_id) + ")";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpusConverter::_finalize_decoder(std::string &) {
|
||||
if(this->decoder) {
|
||||
opus_decoder_destroy(this->decoder);
|
||||
this->decoder = nullptr;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpusConverter::_finalize_encoder(std::string &) {
|
||||
if(this->encoder) {
|
||||
opus_encoder_destroy(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Converter.h"
|
||||
#include <opus/opus.h>
|
||||
#include <mutex>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
namespace codec {
|
||||
class OpusConverter : public Converter {
|
||||
public:
|
||||
OpusConverter(size_t /* channels */, size_t /* sample rate */, size_t /* frame size */);
|
||||
virtual ~OpusConverter();
|
||||
|
||||
bool valid() override;
|
||||
|
||||
bool initialize(std::string& /* error */, int /* application type */);
|
||||
void finalize() override;
|
||||
|
||||
void reset_encoder() override;
|
||||
void reset_decoder() override;
|
||||
|
||||
ssize_t encode(std::string & /* error */, const void * /* source */, void * /* target */, size_t /* target size */) override;
|
||||
ssize_t decode(std::string & /* error */, const void * /* source */, size_t /* source size */, void *pVoid1) override;
|
||||
|
||||
ssize_t decode_lost(std::string &string, size_t /* packets */) override;
|
||||
|
||||
size_t expected_encoded_length(size_t size) override;
|
||||
private:
|
||||
std::mutex coder_lock;
|
||||
OpusDecoder* decoder = nullptr;
|
||||
OpusEncoder* encoder = nullptr;
|
||||
|
||||
int _application_type = 0;
|
||||
|
||||
bool _finalize_encoder(std::string& /* error */);
|
||||
bool _finalize_decoder(std::string& /* error */);
|
||||
bool _initialize_encoder(std::string& /* error */);
|
||||
bool _initialize_decoder(std::string& /* error */);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
namespace filter {
|
||||
class Filter {
|
||||
public:
|
||||
Filter(size_t channel_count, size_t sample_rate, size_t frame_size) :
|
||||
_frame_size(frame_size), _sample_rate(sample_rate), _channels(channel_count) {}
|
||||
|
||||
virtual bool process(const void* /* buffer */) = 0;
|
||||
|
||||
inline size_t sample_rate() { return this->_sample_rate; }
|
||||
inline size_t channels() { return this->_channels; }
|
||||
inline size_t frame_size() { return this->_frame_size; }
|
||||
|
||||
protected:
|
||||
size_t _frame_size;
|
||||
size_t _sample_rate;
|
||||
size_t _channels;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#include "FilterState.h"
|
||||
#include "Filter.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::audio;
|
||||
using namespace tc::audio::filter;
|
||||
|
||||
StateFilter::StateFilter(size_t a, size_t b, size_t c) : Filter(a, b, c) {}
|
||||
StateFilter::~StateFilter() {}
|
||||
|
||||
bool StateFilter::initialize(std::string &) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StateFilter::process(const void *_buffer) {
|
||||
return !this->_consume;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Filter.h"
|
||||
#include <mutex>
|
||||
#include <fvad.h>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
namespace filter {
|
||||
class StateFilter : public Filter {
|
||||
public:
|
||||
StateFilter(size_t /* channel count */, size_t /* sample rate */, size_t /* frame size */);
|
||||
virtual ~StateFilter();
|
||||
|
||||
bool initialize(std::string& /* error */);
|
||||
bool process(const void* /* buffer */) override;
|
||||
|
||||
inline bool consumes_input() { return this->_consume; }
|
||||
inline void set_consume_input(bool state) { this->_consume = state; }
|
||||
private:
|
||||
bool _consume = false;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include "FilterThreshold.h"
|
||||
#include "../../logger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::audio;
|
||||
using namespace tc::audio::filter;
|
||||
|
||||
ThresholdFilter::ThresholdFilter(size_t a, size_t b, size_t c) : Filter(a, b, c) {}
|
||||
ThresholdFilter::~ThresholdFilter() {}
|
||||
|
||||
bool ThresholdFilter::initialize(std::string &, float val, size_t margin) {
|
||||
this->_threshold = val;
|
||||
this->_margin_frames = margin;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* technique based on http://www.vttoth.com/CMS/index.php/technical-notes/68 */
|
||||
inline float merge_ab(float a, float b, float n) {
|
||||
/*
|
||||
* Form: A,B := [0;n]
|
||||
* IF A <= n/2 AND B <= N/2
|
||||
* Z = (2 * A * B) / n
|
||||
* ELSE
|
||||
* Z = 2(A + B) - (2/n) * A * B - n
|
||||
*
|
||||
* For a range from 0 to 2: Z = 2(A + B) - AB - 2
|
||||
*/
|
||||
|
||||
auto half_n = n / 2;
|
||||
auto inv_half_n = 1 / half_n;
|
||||
|
||||
if(a < half_n && b < half_n)
|
||||
return inv_half_n * a * b;
|
||||
|
||||
return 2 * (a + b) - inv_half_n * a * b - n;
|
||||
}
|
||||
|
||||
bool ThresholdFilter::process(const void *_buffer) {
|
||||
float value = -1;
|
||||
auto analyze_callback = this->on_analyze;
|
||||
|
||||
for(size_t channel = 0; channel < this->_channels; channel++) {
|
||||
auto percentage = this->analyze(_buffer, channel);
|
||||
|
||||
if(channel == 0)
|
||||
value = (float) percentage;
|
||||
else
|
||||
value = merge_ab(value, (float) percentage, 100);
|
||||
}
|
||||
|
||||
auto last_level = this->_current_level;
|
||||
float smooth;
|
||||
if(this->_margin_processed_frames == 0) /* we're in release */
|
||||
smooth = this->_release_smooth;
|
||||
else
|
||||
smooth = this->_attack_smooth;
|
||||
|
||||
this->_current_level = last_level * smooth + value * (1 - smooth);
|
||||
//log_trace(category::audio, "Vad level: before: {}, edit: {}, now: {}, smooth: {}", last_level, value, this->_current_level, smooth);
|
||||
|
||||
if(analyze_callback)
|
||||
analyze_callback(this->_current_level);
|
||||
|
||||
if(this->_current_level >= this->_threshold) {
|
||||
this->_margin_processed_frames = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return this->_margin_processed_frames++ < this->_margin_frames;
|
||||
}
|
||||
|
||||
|
||||
long double ThresholdFilter::analyze(const void *_buffer, size_t channel) {
|
||||
/* equation taken from the web client */
|
||||
|
||||
auto buffer = (float*) _buffer;
|
||||
buffer += channel;
|
||||
|
||||
long double value = 0;
|
||||
auto sample = this->_frame_size;
|
||||
|
||||
while(sample-- > 0) {
|
||||
const auto num = floorl(*buffer * 127) / 127.f; /* to be like the web client */
|
||||
value += num * num;
|
||||
buffer += this->_channels;
|
||||
}
|
||||
|
||||
long double rms = sqrtl(value / (long double) this->_frame_size);
|
||||
auto db = (long double) 20 * (log(rms) / log(10));
|
||||
db = max((long double) -192, min(db, (long double) 0));
|
||||
|
||||
|
||||
float percentage = (float) (100 + (db * 1.92f));
|
||||
return max(0.f, min(percentage, 100.f));
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Filter.h"
|
||||
#include <mutex>
|
||||
#include <fvad.h>
|
||||
#include <functional>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
namespace filter {
|
||||
class ThresholdFilter : public Filter {
|
||||
public:
|
||||
ThresholdFilter(size_t /* channel count */, size_t /* sample rate */, size_t /* frame size */);
|
||||
virtual ~ThresholdFilter();
|
||||
|
||||
bool initialize(std::string& /* error */, float /* threshold */, size_t /* margin frames */);
|
||||
bool process(const void* /* buffer */) override;
|
||||
|
||||
long double analyze(const void* /* buffer */, size_t /* channel */);
|
||||
|
||||
inline float threshold() { return this->_threshold; }
|
||||
inline void set_threshold(float value) { this->_threshold = value; }
|
||||
|
||||
inline size_t margin_frames() { return this->_margin_frames; }
|
||||
inline void set_margin_frames(size_t value) { this->_margin_frames = value; }
|
||||
|
||||
inline void attack_smooth(float value) { this->_attack_smooth = value; }
|
||||
inline float attack_smooth() { return this->_attack_smooth; }
|
||||
|
||||
inline void release_smooth(float value) { this->_release_smooth = value; }
|
||||
inline float release_smooth() { return this->_release_smooth; }
|
||||
|
||||
std::function<void(float)> on_analyze;
|
||||
private:
|
||||
float _attack_smooth = 0;
|
||||
float _release_smooth = 0;
|
||||
float _current_level = 0;
|
||||
|
||||
float _threshold;
|
||||
|
||||
size_t _margin_frames = 0;
|
||||
size_t _margin_processed_frames = 0;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
#include <iostream>
|
||||
#include "FilterVad.h"
|
||||
#include "../AudioMerger.h"
|
||||
#include "../../logger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::audio;
|
||||
using namespace tc::audio::filter;
|
||||
|
||||
VadFilter::VadFilter(size_t channels, size_t rate, size_t frames) : Filter(channels, rate, frames) { }
|
||||
|
||||
VadFilter::~VadFilter() {
|
||||
this->cleanup_buffer();
|
||||
if(this->_vad_handle) {
|
||||
fvad_free(this->_vad_handle);
|
||||
this->_vad_handle = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void VadFilter::cleanup_buffer() {
|
||||
lock_guard lock(this->_buffer_lock);
|
||||
if(this->_buffer)
|
||||
free(this->_buffer);
|
||||
this->_buffer = nullptr;
|
||||
this->_buffer_size = 0;
|
||||
}
|
||||
|
||||
bool VadFilter::initialize(std::string &error, size_t mode, size_t margin) {
|
||||
|
||||
this->_vad_handle = fvad_new();
|
||||
if(!this->_vad_handle) {
|
||||
error = "failed to allocate handle";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(fvad_set_sample_rate(this->_vad_handle, (int) this->_sample_rate) != 0) {
|
||||
error = "invalid sample rate. Sample rate must be one of [8000, 16000, 32000 and 48000]";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(fvad_set_mode(this->_vad_handle, (int) mode) != 0) {
|
||||
error = "failed to set mode";
|
||||
return false;
|
||||
}
|
||||
|
||||
this->_mode = mode;
|
||||
this->_margin_frames = margin;
|
||||
if(this->_channels > 1) {
|
||||
this->ensure_buffer(this->_frame_size * this->_channels * 4); /* buffer to merge the channels into one channel */
|
||||
} else {
|
||||
this->ensure_buffer(this->_frame_size * 2);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VadFilter::process(const void *buffer) {
|
||||
if(!this->_vad_handle) {
|
||||
log_warn(category::audio, tr("Vad filter hasn't been initialized!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
lock_guard lock(this->_buffer_lock);
|
||||
if(this->_channels > 1) {
|
||||
if(!merge::merge_channels_interleaved(this->_buffer, 1, buffer, this->_channels, this->_frame_size)) {
|
||||
log_warn(category::audio, tr("Failed to merge channels"));
|
||||
return false;
|
||||
}
|
||||
buffer = this->_buffer;
|
||||
}
|
||||
|
||||
/* convert float32 samples to signed int16 */
|
||||
{
|
||||
auto target = (int16_t*) this->_buffer;
|
||||
auto source = (float*) buffer;
|
||||
auto sample = this->_frame_size;
|
||||
|
||||
float tmp;
|
||||
while(sample-- > 0) {
|
||||
tmp = *source++;
|
||||
tmp *= 32768;
|
||||
|
||||
if(tmp > 32767)
|
||||
tmp = 32767;
|
||||
|
||||
if(tmp < -32768)
|
||||
tmp = -32768;
|
||||
|
||||
*target++ = (int16_t) tmp;
|
||||
}
|
||||
}
|
||||
|
||||
auto result = fvad_process(this->_vad_handle, (int16_t*) this->_buffer, this->_frame_size);
|
||||
if(result == -1) {
|
||||
log_warn(category::audio, tr("Invalid frame length"));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto flag_vad = result == 1;
|
||||
|
||||
if(!flag_vad) {
|
||||
this->_margin_processed_frames++;
|
||||
return this->_margin_processed_frames <= this->_margin_frames;
|
||||
} else {
|
||||
this->_margin_processed_frames = 0;
|
||||
}
|
||||
return flag_vad;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Filter.h"
|
||||
#include <mutex>
|
||||
#include <fvad.h>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
namespace filter {
|
||||
class VadFilter : public Filter {
|
||||
public:
|
||||
VadFilter(size_t /* channel count */, size_t /* sample rate */, size_t /* frame size */);
|
||||
virtual ~VadFilter();
|
||||
|
||||
bool initialize(std::string& /* error */, size_t /* mode */, size_t /* margin frames */);
|
||||
bool process(const void* /* buffer */) override;
|
||||
|
||||
inline size_t margin_frames() { return this->_margin_frames; }
|
||||
inline void set_margin_frames(size_t value) { this->_margin_frames = value; }
|
||||
|
||||
inline size_t mode() { return this->_mode; }
|
||||
private:
|
||||
Fvad* _vad_handle = nullptr;
|
||||
|
||||
size_t _mode = 0;
|
||||
size_t _margin_frames = 0;
|
||||
size_t _margin_processed_frames = 0;
|
||||
|
||||
std::mutex _buffer_lock;
|
||||
void* _buffer = nullptr;
|
||||
size_t _buffer_size = 0;
|
||||
|
||||
void cleanup_buffer();
|
||||
inline void ensure_buffer(size_t length) {
|
||||
if(this->_buffer_size < length) {
|
||||
if(this->_buffer)
|
||||
free(this->_buffer);
|
||||
|
||||
this->_buffer_size = length;
|
||||
this->_buffer = malloc(this->_buffer_size);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
#include "AudioConsumer.h"
|
||||
#include "AudioRecorder.h"
|
||||
#include "AudioFilter.h"
|
||||
#include "../AudioInput.h"
|
||||
#include "../filter/Filter.h"
|
||||
#include "../filter/FilterVad.h"
|
||||
#include "../filter/FilterThreshold.h"
|
||||
#include "../filter/FilterState.h"
|
||||
#include "../../logger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::audio;
|
||||
using namespace tc::audio::recorder;
|
||||
|
||||
NAN_MODULE_INIT(AudioConsumerWrapper::Init) {
|
||||
auto klass = Nan::New<v8::FunctionTemplate>(AudioConsumerWrapper::NewInstance);
|
||||
klass->SetClassName(Nan::New("AudioConsumer").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_filters", AudioConsumerWrapper::_get_filters);
|
||||
Nan::SetPrototypeMethod(klass, "unregister_filter", AudioConsumerWrapper::_unregister_filter);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "create_filter_vad", AudioConsumerWrapper::_create_filter_vad);
|
||||
Nan::SetPrototypeMethod(klass, "create_filter_threshold", AudioConsumerWrapper::_create_filter_threshold);
|
||||
Nan::SetPrototypeMethod(klass, "create_filter_state", AudioConsumerWrapper::_create_filter_state);
|
||||
|
||||
constructor_template().Reset(klass);
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioConsumerWrapper::NewInstance) {
|
||||
if(!info.IsConstructCall())
|
||||
Nan::ThrowError("invalid invoke!");
|
||||
}
|
||||
|
||||
AudioConsumerWrapper::AudioConsumerWrapper(AudioRecorderWrapper* h, const std::shared_ptr<tc::audio::AudioConsumer> &handle) : _handle(handle), _recorder(h) {
|
||||
log_allocate("AudioConsumerWrapper", this);
|
||||
{
|
||||
lock_guard read_lock(handle->on_read_lock);
|
||||
handle->on_read = [&](const void* buffer, size_t length){ this->process_data(buffer, length); };
|
||||
}
|
||||
|
||||
#ifdef DO_DEADLOCK_REF
|
||||
this->_recorder->js_ref(); /* FML Mem leak! (In general the consumer live is related to the recorder handle, but for nodejs testing we want to keep this reference ) */
|
||||
#endif
|
||||
}
|
||||
|
||||
AudioConsumerWrapper::~AudioConsumerWrapper() {
|
||||
log_free("AudioConsumerWrapper", this);
|
||||
|
||||
lock_guard lock(this->execute_lock);
|
||||
this->unbind();
|
||||
if(this->_handle->handle) {
|
||||
this->_handle->handle->delete_consumer(this->_handle);
|
||||
this->_handle = nullptr;
|
||||
}
|
||||
|
||||
#ifdef DO_DEADLOCK_REF
|
||||
if(this->_recorder)
|
||||
this->_recorder->js_unref();
|
||||
#endif
|
||||
}
|
||||
|
||||
void AudioConsumerWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
|
||||
this->Wrap(obj);
|
||||
|
||||
this->_call_data = Nan::async_callback([&] {
|
||||
Nan::HandleScope scope;
|
||||
|
||||
auto handle = this->handle();
|
||||
v8::Local<v8::Value> callback_function = handle->Get(Nan::New<v8::String>("callback_data").ToLocalChecked());
|
||||
if(callback_function.IsEmpty() || callback_function->IsNullOrUndefined() || !callback_function->IsFunction()) {
|
||||
lock_guard lock(this->_data_lock);
|
||||
this->_data_entries.clear();
|
||||
}
|
||||
|
||||
std::unique_ptr<DataEntry> buffer;
|
||||
while(true) {
|
||||
{
|
||||
lock_guard lock(this->_data_lock);
|
||||
if(this->_data_entries.empty())
|
||||
break;
|
||||
buffer = move(this->_data_entries.front());
|
||||
this->_data_entries.pop_front();
|
||||
}
|
||||
|
||||
const auto byte_length = buffer->sample_count * this->_handle->channel_count * 4;
|
||||
auto js_buffer = v8::ArrayBuffer::New(Nan::GetCurrentContext()->GetIsolate(), byte_length);
|
||||
auto js_fbuffer = v8::Float32Array::New(js_buffer, 0, byte_length / 4);
|
||||
|
||||
memcpy(js_buffer->GetContents().Data(), buffer->buffer, byte_length);
|
||||
|
||||
v8::Local<v8::Value> argv[1];
|
||||
argv[0] = js_fbuffer;
|
||||
callback_function.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, argv);
|
||||
}
|
||||
});
|
||||
|
||||
this->_call_ended = Nan::async_callback([&]{
|
||||
Nan::HandleScope scope;
|
||||
|
||||
auto handle = this->handle();
|
||||
v8::Local<v8::Value> callback_function = handle->Get(Nan::New<v8::String>("callback_ended").ToLocalChecked());
|
||||
if(callback_function.IsEmpty() || callback_function->IsNullOrUndefined() || !callback_function->IsFunction())
|
||||
return;
|
||||
callback_function.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
|
||||
});
|
||||
|
||||
this->_call_started = Nan::async_callback([&]{
|
||||
Nan::HandleScope scope;
|
||||
|
||||
auto handle = this->handle();
|
||||
v8::Local<v8::Value> callback_function = handle->Get(Nan::New<v8::String>("callback_started").ToLocalChecked());
|
||||
if(callback_function.IsEmpty() || callback_function->IsNullOrUndefined() || !callback_function->IsFunction())
|
||||
return;
|
||||
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>(this->_handle->frame_size));
|
||||
Nan::Set(this->handle(), Nan::New<v8::String>("sample_rate").ToLocalChecked(), Nan::New<v8::Number>(this->_handle->sample_rate));
|
||||
Nan::Set(this->handle(), Nan::New<v8::String>("channels").ToLocalChecked(), Nan::New<v8::Number>(this->_handle->channel_count));
|
||||
}
|
||||
|
||||
void AudioConsumerWrapper::unbind() {
|
||||
if(this->_handle) {
|
||||
lock_guard lock(this->_handle->on_read_lock);
|
||||
this->_handle->on_read = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioConsumerWrapper::process_data(const void *buffer, size_t samples) {
|
||||
lock_guard lock(this->execute_lock);
|
||||
|
||||
auto filters = this->filters();
|
||||
for(const auto& filter : filters) {
|
||||
auto _filter = filter->filter();
|
||||
if(!_filter) continue;
|
||||
|
||||
if(_filter->frame_size() != samples) {
|
||||
cerr << "Tried to use a filter, but frame size does not match!" << endl;
|
||||
continue;
|
||||
}
|
||||
if(!_filter->process(buffer)) {
|
||||
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;
|
||||
|
||||
{
|
||||
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(buffer, samples);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto byte_length = samples * this->_handle->channel_count * 4;
|
||||
auto buf = make_unique<DataEntry>();
|
||||
buf->buffer = malloc(byte_length);
|
||||
memcpy(buf->buffer, buffer, byte_length);
|
||||
buf->sample_count = samples;
|
||||
|
||||
{
|
||||
lock_guard lock(this->_data_lock);
|
||||
this->_data_entries.push_back(move(buf));
|
||||
}
|
||||
this->_call_data();
|
||||
}
|
||||
|
||||
|
||||
|
||||
std::shared_ptr<AudioFilterWrapper> AudioConsumerWrapper::create_filter(const std::string& name, const std::shared_ptr<filter::Filter> &impl) {
|
||||
auto result = shared_ptr<AudioFilterWrapper>(new AudioFilterWrapper(name, impl), [](AudioFilterWrapper* ptr) {
|
||||
assert(v8::Isolate::GetCurrent());
|
||||
ptr->Unref();
|
||||
});
|
||||
|
||||
/* wrap into object */
|
||||
{
|
||||
auto js_object = Nan::NewInstance(Nan::New(AudioFilterWrapper::constructor()), 0, nullptr).ToLocalChecked();
|
||||
result->do_wrap(js_object);
|
||||
result->Ref();
|
||||
}
|
||||
|
||||
{
|
||||
lock_guard lock(this->_filters_lock);
|
||||
this->_filters.push_back(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void AudioConsumerWrapper::delete_filter(const AudioFilterWrapper* filter) {
|
||||
shared_ptr<AudioFilterWrapper> handle; /* need to keep the handle 'till everything has been finished */
|
||||
{
|
||||
lock_guard lock(this->_filters_lock);
|
||||
for(auto& c : this->_filters) {
|
||||
if(&*c == filter) {
|
||||
handle = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!handle)
|
||||
return;
|
||||
|
||||
{
|
||||
auto it = find(this->_filters.begin(), this->_filters.end(), handle);
|
||||
if(it != this->_filters.end())
|
||||
this->_filters.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
lock_guard lock(this->execute_lock); /* ensure that the filter isn't used right now */
|
||||
handle->_filter = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(AudioConsumerWrapper::_get_filters) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder());
|
||||
auto filters = handle->filters();
|
||||
|
||||
auto result = Nan::New<v8::Array>(filters.size());
|
||||
|
||||
for(size_t index = 0; index < filters.size(); index++)
|
||||
result->Set(index, filters[index]->handle());
|
||||
|
||||
info.GetReturnValue().Set(result);
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioConsumerWrapper::_unregister_filter) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder());
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsObject()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!Nan::New(AudioFilterWrapper::constructor_template())->HasInstance(info[0])) {
|
||||
Nan::ThrowError("invalid consumer");
|
||||
return;
|
||||
}
|
||||
|
||||
auto consumer = ObjectWrap::Unwrap<AudioFilterWrapper>(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked());
|
||||
handle->delete_filter(consumer);
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(AudioConsumerWrapper::_create_filter_vad) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder());
|
||||
auto consumer = handle->_handle;
|
||||
assert(consumer); /* should never be null! */
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
string error;
|
||||
auto filter = make_shared<filter::VadFilter>(consumer->channel_count,consumer->sample_rate,consumer->frame_size);
|
||||
if(!filter->initialize(error, info[0]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0), 2)) {
|
||||
Nan::ThrowError(Nan::New<v8::String>("failed to initialize filter (" + error + ")").ToLocalChecked());
|
||||
return;
|
||||
}
|
||||
|
||||
auto object = handle->create_filter("vad", filter);
|
||||
info.GetReturnValue().Set(object->handle());
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioConsumerWrapper::_create_filter_threshold) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder());
|
||||
auto consumer = handle->_handle;
|
||||
assert(consumer); /* should never be null! */
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
string error;
|
||||
auto filter = make_shared<filter::ThresholdFilter>(consumer->channel_count,consumer->sample_rate,consumer->frame_size);
|
||||
if(!filter->initialize(error, info[0]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0), 2)) {
|
||||
Nan::ThrowError(Nan::New<v8::String>("failed to initialize filter (" + error + ")").ToLocalChecked());
|
||||
return;
|
||||
}
|
||||
|
||||
auto object = handle->create_filter("threshold", filter);
|
||||
info.GetReturnValue().Set(object->handle());
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioConsumerWrapper::_create_filter_state) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioConsumerWrapper>(info.Holder());
|
||||
auto consumer = handle->_handle;
|
||||
assert(consumer); /* should never be null! */
|
||||
|
||||
string error;
|
||||
auto filter = make_shared<filter::StateFilter>(consumer->channel_count,consumer->sample_rate,consumer->frame_size);
|
||||
if(!filter->initialize(error)) {
|
||||
Nan::ThrowError(Nan::New<v8::String>("failed to initialize filter (" + error + ")").ToLocalChecked());
|
||||
return;
|
||||
}
|
||||
|
||||
auto object = handle->create_filter("state", filter);
|
||||
info.GetReturnValue().Set(object->handle());
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <nan.h>
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
#include <NanEventCallback.h>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
class AudioInput;
|
||||
class AudioConsumer;
|
||||
|
||||
namespace filter {
|
||||
class Filter;
|
||||
}
|
||||
|
||||
namespace recorder {
|
||||
class AudioFilterWrapper;
|
||||
class AudioRecorderWrapper;
|
||||
/*
|
||||
get_filters() : ConsumeFilter[];
|
||||
|
||||
register_filter(filter: ConsumeFilter);
|
||||
unregister_filter(filter: ConsumeFilter);
|
||||
|
||||
create_filter_vad() : VADConsumeFilter;
|
||||
create_filter_threshold() : ThresholdConsumeFilter;
|
||||
*/
|
||||
|
||||
class AudioConsumerWrapper : public Nan::ObjectWrap {
|
||||
friend class AudioRecorderWrapper;
|
||||
public:
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(NewInstance);
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
|
||||
static inline Nan::Persistent<v8::FunctionTemplate> & constructor_template() {
|
||||
static Nan::Persistent<v8::FunctionTemplate> my_constructor_template;
|
||||
return my_constructor_template;
|
||||
}
|
||||
|
||||
AudioConsumerWrapper(AudioRecorderWrapper*, const std::shared_ptr<AudioConsumer>& /* handle */);
|
||||
~AudioConsumerWrapper() override;
|
||||
|
||||
static NAN_METHOD(_get_filters);
|
||||
static NAN_METHOD(_unregister_filter);
|
||||
|
||||
static NAN_METHOD(_create_filter_vad);
|
||||
static NAN_METHOD(_create_filter_threshold);
|
||||
static NAN_METHOD(_create_filter_state);
|
||||
|
||||
std::shared_ptr<AudioFilterWrapper> create_filter(const std::string& /* name */, const std::shared_ptr<filter::Filter>& /* filter impl */);
|
||||
void delete_filter(const AudioFilterWrapper*);
|
||||
|
||||
inline std::deque<std::shared_ptr<AudioFilterWrapper>> filters() {
|
||||
std::lock_guard lock(this->_filters_lock);
|
||||
return this->_filters;
|
||||
}
|
||||
|
||||
inline std::shared_ptr<AudioConsumer> native_consumer() { return this->_handle; }
|
||||
|
||||
std::mutex native_read_callback_lock;
|
||||
std::function<void(const void * /* buffer */, size_t /* samples */)> native_read_callback;
|
||||
private:
|
||||
AudioRecorderWrapper* _recorder;
|
||||
|
||||
std::mutex execute_lock;
|
||||
std::shared_ptr<AudioConsumer> _handle;
|
||||
|
||||
std::mutex _filters_lock;
|
||||
std::deque<std::shared_ptr<AudioFilterWrapper>> _filters;
|
||||
bool last_consumed = false;
|
||||
|
||||
void do_wrap(const v8::Local<v8::Object>& /* object */);
|
||||
|
||||
void unbind(); /* called with execute_lock locked */
|
||||
void process_data(const void* /* buffer */, size_t /* samples */);
|
||||
|
||||
struct DataEntry {
|
||||
void* buffer = nullptr;
|
||||
size_t sample_count = 0;
|
||||
|
||||
~DataEntry() {
|
||||
if(buffer)
|
||||
free(buffer);
|
||||
}
|
||||
};
|
||||
|
||||
std::mutex _data_lock;
|
||||
std::deque<std::unique_ptr<DataEntry>> _data_entries;
|
||||
|
||||
Nan::callback_t<> _call_data;
|
||||
Nan::callback_t<> _call_ended;
|
||||
Nan::callback_t<> _call_started;
|
||||
/*
|
||||
callback_data: (buffer: Float32Array) => any;
|
||||
callback_ended: () => any;
|
||||
*/
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
#include "AudioFilter.h"
|
||||
#include "../filter/FilterVad.h"
|
||||
#include "../filter/FilterThreshold.h"
|
||||
#include "../filter/FilterState.h"
|
||||
#include "../../logger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::audio;
|
||||
using namespace tc::audio::recorder;
|
||||
|
||||
|
||||
NAN_MODULE_INIT(AudioFilterWrapper::Init) {
|
||||
auto klass = Nan::New<v8::FunctionTemplate>(AudioFilterWrapper::NewInstance);
|
||||
klass->SetClassName(Nan::New("AudioFilter").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_name", AudioFilterWrapper::_get_name);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_margin_frames", AudioFilterWrapper::_get_margin_frames);
|
||||
Nan::SetPrototypeMethod(klass, "set_margin_frames", AudioFilterWrapper::_set_margin_frames);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_level", AudioFilterWrapper::_get_level);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_threshold", AudioFilterWrapper::_get_threshold);
|
||||
Nan::SetPrototypeMethod(klass, "set_threshold", AudioFilterWrapper::_set_threshold);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_attack_smooth", AudioFilterWrapper::_get_attack_smooth);
|
||||
Nan::SetPrototypeMethod(klass, "set_attack_smooth", AudioFilterWrapper::_set_attack_smooth);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_release_smooth", AudioFilterWrapper::_get_release_smooth);
|
||||
Nan::SetPrototypeMethod(klass, "set_release_smooth", AudioFilterWrapper::_set_release_smooth);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "set_analyze_filter", AudioFilterWrapper::_set_analyze_filter);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "is_consuming", AudioFilterWrapper::_is_consuming);
|
||||
Nan::SetPrototypeMethod(klass, "set_consuming", AudioFilterWrapper::_set_consuming);
|
||||
|
||||
constructor_template().Reset(klass);
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioFilterWrapper::NewInstance) {
|
||||
if(!info.IsConstructCall())
|
||||
Nan::ThrowError("invalid invoke!");
|
||||
}
|
||||
|
||||
AudioFilterWrapper::AudioFilterWrapper(const std::string& name, const std::shared_ptr<tc::audio::filter::Filter> &filter) : _filter(filter), _name(name) {
|
||||
auto threshold_filter = dynamic_pointer_cast<filter::ThresholdFilter>(this->_filter);
|
||||
if(threshold_filter) {
|
||||
this->_call_analyzed = Nan::async_callback([&](float value) {
|
||||
Nan::HandleScope scope;
|
||||
|
||||
if(!this->_callback_analyzed.IsEmpty()) {
|
||||
auto cb = Nan::New<v8::Function>(this->_callback_analyzed);
|
||||
|
||||
v8::Local<v8::Value> argv[1];
|
||||
argv[0] = Nan::New<v8::Number>(value);
|
||||
|
||||
cb->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, argv);
|
||||
}
|
||||
});
|
||||
}
|
||||
log_allocate("AudioFilterWrapper", this);
|
||||
}
|
||||
AudioFilterWrapper::~AudioFilterWrapper() {
|
||||
log_free("AudioFilterWrapper", this);
|
||||
|
||||
auto threshold_filter = dynamic_pointer_cast<filter::ThresholdFilter>(this->_filter);
|
||||
if(threshold_filter)
|
||||
threshold_filter->on_analyze = nullptr;
|
||||
|
||||
this->_callback_analyzed.Reset();
|
||||
}
|
||||
|
||||
void AudioFilterWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
|
||||
this->Wrap(obj);
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioFilterWrapper::_get_name) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
||||
if(!handle->_filter) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set(Nan::New<v8::String>(handle->_name).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioFilterWrapper::_get_level) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
||||
if(!handle->_filter) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
auto filter = dynamic_pointer_cast<filter::VadFilter>(handle->_filter);
|
||||
if(!filter) {
|
||||
Nan::ThrowError("filter does not support this method");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set((int) filter->mode());
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(AudioFilterWrapper::_get_margin_frames) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
||||
if(!handle->_filter) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
|
||||
auto vad_filter = dynamic_pointer_cast<filter::VadFilter>(handle->_filter);
|
||||
auto threshold_filter = dynamic_pointer_cast<filter::ThresholdFilter>(handle->_filter);
|
||||
if(vad_filter) {
|
||||
info.GetReturnValue().Set((int) vad_filter->margin_frames());
|
||||
} else if(threshold_filter) {
|
||||
info.GetReturnValue().Set((int) threshold_filter->margin_frames());
|
||||
} else {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioFilterWrapper::_set_margin_frames) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
||||
if(!handle->_filter) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
auto vad_filter = dynamic_pointer_cast<filter::VadFilter>(handle->_filter);
|
||||
auto threshold_filter = dynamic_pointer_cast<filter::ThresholdFilter>(handle->_filter);
|
||||
if(vad_filter) {
|
||||
vad_filter->set_margin_frames(info[0]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
} else if(threshold_filter) {
|
||||
threshold_filter->set_margin_frames(info[0]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
} else {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(AudioFilterWrapper::_get_threshold) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
||||
if(!handle->_filter) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
auto filter = dynamic_pointer_cast<filter::ThresholdFilter>(handle->_filter);
|
||||
if(!filter) {
|
||||
Nan::ThrowError("filter does not support this method");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set((int) filter->threshold());
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(AudioFilterWrapper::_set_threshold) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
||||
if(!handle->_filter) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
auto filter = dynamic_pointer_cast<filter::ThresholdFilter>(handle->_filter);
|
||||
if(!filter) {
|
||||
Nan::ThrowError("filter does not support this method");
|
||||
return;
|
||||
}
|
||||
|
||||
filter->set_threshold(info[0]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioFilterWrapper::_get_attack_smooth) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
||||
if(!handle->_filter) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
auto filter = dynamic_pointer_cast<filter::ThresholdFilter>(handle->_filter);
|
||||
if(!filter) {
|
||||
Nan::ThrowError("filter does not support this method");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set((int) filter->attack_smooth());
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioFilterWrapper::_set_attack_smooth) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
||||
if(!handle->_filter) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
auto filter = dynamic_pointer_cast<filter::ThresholdFilter>(handle->_filter);
|
||||
if(!filter) {
|
||||
Nan::ThrowError("filter does not support this method");
|
||||
return;
|
||||
}
|
||||
|
||||
filter->attack_smooth(info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioFilterWrapper::_get_release_smooth) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
||||
if(!handle->_filter) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
auto filter = dynamic_pointer_cast<filter::ThresholdFilter>(handle->_filter);
|
||||
if(!filter) {
|
||||
Nan::ThrowError("filter does not support this method");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set((int) filter->release_smooth());
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioFilterWrapper::_set_release_smooth) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
||||
if(!handle->_filter) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
auto filter = dynamic_pointer_cast<filter::ThresholdFilter>(handle->_filter);
|
||||
if(!filter) {
|
||||
Nan::ThrowError("filter does not support this method");
|
||||
return;
|
||||
}
|
||||
|
||||
filter->release_smooth(info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioFilterWrapper::_set_analyze_filter) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
||||
if(!handle->_filter) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 1 || !(info[0]->IsFunction() || info[0]->IsNullOrUndefined())) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
auto filter = dynamic_pointer_cast<filter::ThresholdFilter>(handle->_filter);
|
||||
if(!filter) {
|
||||
Nan::ThrowError("filter does not support this method");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info[0]->IsNullOrUndefined()) {
|
||||
handle->_callback_analyzed.Reset();
|
||||
filter->on_analyze = nullptr;
|
||||
} else {
|
||||
handle->_callback_analyzed.Reset(info[0].As<v8::Function>());
|
||||
filter->on_analyze = [handle](float value){
|
||||
handle->_call_analyzed.call(std::forward<float>(value), true);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(AudioFilterWrapper::_is_consuming) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
||||
if(!handle->_filter) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
auto filter = dynamic_pointer_cast<filter::StateFilter>(handle->_filter);
|
||||
if(!filter) {
|
||||
Nan::ThrowError("filter does not support this method");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set(filter->consumes_input());
|
||||
}
|
||||
NAN_METHOD(AudioFilterWrapper::_set_consuming) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioFilterWrapper>(info.Holder());
|
||||
if(!handle->_filter) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsBoolean()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
auto filter = dynamic_pointer_cast<filter::StateFilter>(handle->_filter);
|
||||
if(!filter) {
|
||||
Nan::ThrowError("filter does not support this method");
|
||||
return;
|
||||
}
|
||||
|
||||
filter->set_consume_input(info[0]->BooleanValue(info.GetIsolate()));
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <nan.h>
|
||||
#include <NanEventCallback.h>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
namespace filter {
|
||||
class Filter;
|
||||
}
|
||||
|
||||
namespace recorder {
|
||||
class AudioConsumerWrapper;
|
||||
|
||||
class AudioFilterWrapper : public Nan::ObjectWrap {
|
||||
friend class AudioConsumerWrapper;
|
||||
public:
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(NewInstance);
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
static inline Nan::Persistent<v8::FunctionTemplate> & constructor_template() {
|
||||
static Nan::Persistent<v8::FunctionTemplate> my_constructor_template;
|
||||
return my_constructor_template;
|
||||
}
|
||||
|
||||
AudioFilterWrapper(const std::string& name, const std::shared_ptr<filter::Filter>& /* handle */);
|
||||
virtual ~AudioFilterWrapper();
|
||||
|
||||
static NAN_METHOD(_get_name);
|
||||
|
||||
/* VAD and Threshold */
|
||||
static NAN_METHOD(_get_margin_frames);
|
||||
static NAN_METHOD(_set_margin_frames);
|
||||
|
||||
/* VAD relevant */
|
||||
static NAN_METHOD(_get_level);
|
||||
|
||||
/* threshold filter relevant */
|
||||
static NAN_METHOD(_get_threshold);
|
||||
static NAN_METHOD(_set_threshold);
|
||||
|
||||
static NAN_METHOD(_get_attack_smooth);
|
||||
static NAN_METHOD(_set_attack_smooth);
|
||||
|
||||
static NAN_METHOD(_get_release_smooth);
|
||||
static NAN_METHOD(_set_release_smooth);
|
||||
|
||||
static NAN_METHOD(_set_analyze_filter);
|
||||
|
||||
/* consume filter */
|
||||
static NAN_METHOD(_is_consuming);
|
||||
static NAN_METHOD(_set_consuming);
|
||||
|
||||
inline std::shared_ptr<filter::Filter> filter() { return this->_filter; }
|
||||
private:
|
||||
std::shared_ptr<filter::Filter> _filter;
|
||||
std::string _name;
|
||||
|
||||
void do_wrap(const v8::Local<v8::Object>& /* object */);
|
||||
|
||||
Nan::callback_t<float> _call_analyzed;
|
||||
Nan::Persistent<v8::Function> _callback_analyzed;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
#include <cmath>
|
||||
#include "AudioOutputStream.h"
|
||||
#include "../AudioOutput.h"
|
||||
#include "../AudioResampler.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc;
|
||||
using namespace tc::audio;
|
||||
|
||||
NAN_MODULE_INIT(AudioOutputStreamWrapper::Init) {
|
||||
auto klass = Nan::New<v8::FunctionTemplate>(AudioOutputStreamWrapper::NewInstance);
|
||||
klass->SetClassName(Nan::New("AudioOutputStream").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "write_data", AudioOutputStreamWrapper::_write_data);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_buffer_latency", AudioOutputStreamWrapper::_get_buffer_latency);
|
||||
Nan::SetPrototypeMethod(klass, "set_buffer_latency", AudioOutputStreamWrapper::_set_buffer_latency);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_buffer_max_latency", AudioOutputStreamWrapper::_get_buffer_max_latency);
|
||||
Nan::SetPrototypeMethod(klass, "set_buffer_max_latency", AudioOutputStreamWrapper::_set_buffer_max_latency);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "write_data", AudioOutputStreamWrapper::_write_data);
|
||||
Nan::SetPrototypeMethod(klass, "write_data_rated", AudioOutputStreamWrapper::_write_data_rated);
|
||||
Nan::SetPrototypeMethod(klass, "deleted", AudioOutputStreamWrapper::_deleted);
|
||||
Nan::SetPrototypeMethod(klass, "delete", AudioOutputStreamWrapper::_delete);
|
||||
Nan::SetPrototypeMethod(klass, "clear", AudioOutputStreamWrapper::_clear);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioOutputStreamWrapper::NewInstance) {
|
||||
if(!info.IsConstructCall())
|
||||
Nan::ThrowError("invalid invoke!");
|
||||
}
|
||||
|
||||
|
||||
AudioOutputStreamWrapper::AudioOutputStreamWrapper(const std::shared_ptr<tc::audio::AudioOutputSource> &stream, bool owns) {
|
||||
this->_handle = stream;
|
||||
if(owns) {
|
||||
this->_own_handle = stream;
|
||||
}
|
||||
}
|
||||
|
||||
AudioOutputStreamWrapper::~AudioOutputStreamWrapper() {
|
||||
this->drop_stream();
|
||||
}
|
||||
|
||||
void AudioOutputStreamWrapper::drop_stream() {
|
||||
if(this->_own_handle) {
|
||||
auto handle = this->_own_handle->handle;
|
||||
if(handle) {
|
||||
handle->delete_source(this->_own_handle);
|
||||
}
|
||||
this->_own_handle->on_underflow = nullptr;
|
||||
this->_own_handle->on_overflow = nullptr;
|
||||
}
|
||||
|
||||
this->_handle.reset();
|
||||
this->_own_handle = nullptr;
|
||||
}
|
||||
|
||||
void AudioOutputStreamWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
|
||||
this->Wrap(obj);
|
||||
|
||||
auto handle = this->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
Nan::ForceSet(this->handle(), Nan::New<v8::String>("sample_rate").ToLocalChecked(), Nan::New<v8::Number>(handle->sample_rate), v8::ReadOnly);
|
||||
Nan::ForceSet(this->handle(), Nan::New<v8::String>("channels").ToLocalChecked(), Nan::New<v8::Number>(handle->channel_count), v8::ReadOnly);
|
||||
|
||||
if(this->_own_handle) {
|
||||
this->call_underflow = Nan::async_callback([&]{
|
||||
|
||||
Nan::HandleScope scope;
|
||||
auto handle = this->handle();
|
||||
auto callback = Nan::Get(handle, Nan::New<v8::String>("callback_underflow").ToLocalChecked()).ToLocalChecked();
|
||||
if(callback->IsFunction())
|
||||
callback.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
|
||||
});
|
||||
this->call_overflow = Nan::async_callback([&]{
|
||||
Nan::HandleScope scope;
|
||||
auto handle = this->handle();
|
||||
auto callback = Nan::Get(handle, Nan::New<v8::String>("callback_overflow").ToLocalChecked()).ToLocalChecked();
|
||||
if(callback->IsFunction())
|
||||
callback.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
|
||||
});
|
||||
|
||||
this->_own_handle->on_overflow = [&](size_t){ this->call_overflow(); };
|
||||
this->_own_handle->on_underflow = [&]{ this->call_underflow(); return false; };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(AudioOutputStreamWrapper::_clear) {
|
||||
auto client = ObjectWrap::Unwrap<AudioOutputStreamWrapper>(info.Holder());
|
||||
|
||||
auto handle = client->_own_handle;
|
||||
if(!handle) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
|
||||
handle->clear();
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioOutputStreamWrapper::_deleted) {
|
||||
auto client = ObjectWrap::Unwrap<AudioOutputStreamWrapper>(info.Holder());
|
||||
|
||||
info.GetReturnValue().Set(!client->_own_handle || !client->_own_handle->handle);
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioOutputStreamWrapper::_delete) {
|
||||
auto client = ObjectWrap::Unwrap<AudioOutputStreamWrapper>(info.Holder());
|
||||
client->drop_stream();
|
||||
}
|
||||
|
||||
ssize_t AudioOutputStreamWrapper::write_data(const std::shared_ptr<AudioOutputSource>& handle, void *source, size_t samples, bool interleaved) {
|
||||
ssize_t result = 0;
|
||||
if(interleaved) {
|
||||
result = handle->enqueue_samples(source, samples);
|
||||
} else {
|
||||
auto buffer = SampleBuffer::allocate(handle->channel_count, samples);
|
||||
auto src_buffer = (float*) source;
|
||||
auto target_buffer = (float*) buffer->sample_data;
|
||||
|
||||
while (samples-- > 0) {
|
||||
*target_buffer = *src_buffer;
|
||||
*(target_buffer + 1) = *(src_buffer + buffer->sample_size);
|
||||
|
||||
target_buffer += 2;
|
||||
src_buffer++;
|
||||
}
|
||||
|
||||
result = handle->enqueue_samples(buffer);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioOutputStreamWrapper::_write_data) {
|
||||
auto client = ObjectWrap::Unwrap<AudioOutputStreamWrapper>(info.Holder());
|
||||
|
||||
auto handle = client->_own_handle;
|
||||
if(!handle) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 2 || !info[0]->IsArrayBuffer() || !info[1]->IsBoolean()) {
|
||||
Nan::ThrowError("Invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
auto interleaved = info[1]->BooleanValue(info.GetIsolate());
|
||||
auto js_buffer = info[0].As<v8::ArrayBuffer>()->GetContents();
|
||||
|
||||
if(js_buffer.ByteLength() % (handle->channel_count * 4) != 0) {
|
||||
Nan::ThrowError("input buffer invalid size");
|
||||
return;
|
||||
}
|
||||
|
||||
auto samples = js_buffer.ByteLength() / handle->channel_count / 4;
|
||||
info.GetReturnValue().Set((int32_t) write_data(handle, js_buffer.Data(), samples, interleaved));
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioOutputStreamWrapper::_write_data_rated) {
|
||||
auto client = ObjectWrap::Unwrap<AudioOutputStreamWrapper>(info.Holder());
|
||||
|
||||
auto handle = client->_own_handle;
|
||||
if(!handle) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 3 || !info[0]->IsArrayBuffer() || !info[1]->IsBoolean() || !info[2]->IsNumber()) {
|
||||
Nan::ThrowError("Invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
auto sample_rate = info[2]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
auto interleaved = info[1]->BooleanValue(info.GetIsolate());
|
||||
auto js_buffer = info[0].As<v8::ArrayBuffer>()->GetContents();
|
||||
|
||||
auto samples = js_buffer.ByteLength() / handle->channel_count / 4;
|
||||
if(sample_rate == handle->sample_rate) {
|
||||
info.GetReturnValue().Set((int32_t) write_data(handle, js_buffer.Data(), samples, interleaved));
|
||||
} else {
|
||||
if(!client->_resampler || client->_resampler->input_rate() != sample_rate)
|
||||
client->_resampler = make_unique<AudioResampler>(sample_rate, handle->sample_rate, handle->channel_count);
|
||||
|
||||
if(!client->_resampler || !client->_resampler->valid()) {
|
||||
Nan::ThrowError("Resampling failed (invalid resampler)");
|
||||
return;
|
||||
}
|
||||
|
||||
ssize_t target_samples = client->_resampler->estimated_output_size(samples);
|
||||
auto buffer = SampleBuffer::allocate(handle->channel_count, max((size_t) samples, (size_t) target_samples));
|
||||
auto source_buffer = js_buffer.Data();
|
||||
if(!interleaved) {
|
||||
auto src_buffer = (float*) js_buffer.Data();
|
||||
auto target_buffer = (float*) buffer->sample_data;
|
||||
|
||||
auto samples_count = samples;
|
||||
while (samples_count-- > 0) {
|
||||
*target_buffer = *src_buffer;
|
||||
*(target_buffer + 1) = *(src_buffer + samples);
|
||||
|
||||
target_buffer += 2;
|
||||
src_buffer++;
|
||||
}
|
||||
source_buffer = buffer->sample_data;
|
||||
}
|
||||
|
||||
target_samples = client->_resampler->process(buffer->sample_data, source_buffer, samples);
|
||||
if(target_samples < 0) {
|
||||
Nan::ThrowError("Resampling failed");
|
||||
return;
|
||||
}
|
||||
|
||||
buffer->sample_index = 0;
|
||||
buffer->sample_size = target_samples;
|
||||
info.GetReturnValue().Set((int32_t) handle->enqueue_samples(buffer));
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioOutputStreamWrapper::_get_buffer_latency) {
|
||||
auto client = ObjectWrap::Unwrap<AudioOutputStreamWrapper>(info.Holder());
|
||||
|
||||
auto handle = client->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set((float) handle->min_buffer / (float) handle->sample_rate);
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioOutputStreamWrapper::_set_buffer_latency) {
|
||||
auto client = ObjectWrap::Unwrap<AudioOutputStreamWrapper>(info.Holder());
|
||||
|
||||
auto handle = client->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("Invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
handle->min_buffer = (size_t) ceil(handle->sample_rate * info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioOutputStreamWrapper::_get_buffer_max_latency) {
|
||||
auto client = ObjectWrap::Unwrap<AudioOutputStreamWrapper>(info.Holder());
|
||||
|
||||
auto handle = client->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set((float) handle->max_latency / (float) handle->sample_rate);
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioOutputStreamWrapper::_set_buffer_max_latency) {
|
||||
auto client = ObjectWrap::Unwrap<AudioOutputStreamWrapper>(info.Holder());
|
||||
|
||||
auto handle = client->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("Invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
handle->max_latency = (size_t) ceil(handle->sample_rate * info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <nan.h>
|
||||
#include <NanEventCallback.h>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
class AudioResampler;
|
||||
class AudioOutputSource;
|
||||
|
||||
class AudioOutputStreamWrapper : public Nan::ObjectWrap {
|
||||
public:
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(NewInstance);
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
|
||||
AudioOutputStreamWrapper(const std::shared_ptr<AudioOutputSource>& /* stream */, bool /* own */);
|
||||
virtual ~AudioOutputStreamWrapper();
|
||||
|
||||
void do_wrap(const v8::Local<v8::Object>&);
|
||||
void drop_stream();
|
||||
private:
|
||||
static ssize_t write_data(const std::shared_ptr<AudioOutputSource>&, void* source, size_t samples, bool interleaved);
|
||||
|
||||
/* general methods */
|
||||
static NAN_METHOD(_get_buffer_latency);
|
||||
static NAN_METHOD(_set_buffer_latency);
|
||||
static NAN_METHOD(_get_buffer_max_latency);
|
||||
static NAN_METHOD(_set_buffer_max_latency);
|
||||
|
||||
/* methods for owned streams only */
|
||||
static NAN_METHOD(_write_data);
|
||||
static NAN_METHOD(_write_data_rated);
|
||||
|
||||
static NAN_METHOD(_clear);
|
||||
static NAN_METHOD(_deleted);
|
||||
static NAN_METHOD(_delete);
|
||||
|
||||
std::unique_ptr<AudioResampler> _resampler;
|
||||
std::shared_ptr<AudioOutputSource> _own_handle;
|
||||
std::weak_ptr<AudioOutputSource> _handle;
|
||||
|
||||
Nan::callback_t<> call_underflow;
|
||||
Nan::callback_t<> call_overflow;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
#include <misc/base64.h>
|
||||
#include <misc/digest.h>
|
||||
#include "AudioPlayer.h"
|
||||
#include "../AudioOutput.h"
|
||||
#include "../AudioDevice.h"
|
||||
#include "AudioOutputStream.h"
|
||||
|
||||
using namespace tc;
|
||||
using namespace tc::audio;
|
||||
|
||||
extern tc::audio::AudioOutput* global_audio_output;
|
||||
|
||||
NAN_MODULE_INIT(player::init_js) {
|
||||
Nan::Set(target, Nan::New<v8::String>("current_device").ToLocalChecked(), Nan::GetFunction(Nan::New<v8::FunctionTemplate>(player::current_playback_device)).ToLocalChecked());
|
||||
Nan::Set(target, Nan::New<v8::String>("set_device").ToLocalChecked(), Nan::GetFunction(Nan::New<v8::FunctionTemplate>(player::set_playback_device)).ToLocalChecked());
|
||||
|
||||
Nan::Set(target, Nan::New<v8::String>("create_stream").ToLocalChecked(), Nan::GetFunction(Nan::New<v8::FunctionTemplate>(player::create_stream)).ToLocalChecked());
|
||||
|
||||
Nan::Set(target, Nan::New<v8::String>("get_master_volume").ToLocalChecked(), Nan::GetFunction(Nan::New<v8::FunctionTemplate>(player::get_master_volume)).ToLocalChecked());
|
||||
Nan::Set(target, Nan::New<v8::String>("set_master_volume").ToLocalChecked(), Nan::GetFunction(Nan::New<v8::FunctionTemplate>(player::set_master_volume)).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(audio::available_devices) {
|
||||
auto devices = audio::devices();
|
||||
auto result = Nan::New<v8::Array>(devices.size());
|
||||
|
||||
for(size_t index = 0; index < devices.size(); index++) {
|
||||
auto device_info = Nan::New<v8::Object>();
|
||||
auto device = devices[index];
|
||||
|
||||
Nan::Set(device_info, Nan::New<v8::String>("name").ToLocalChecked(), Nan::New<v8::String>(device->name).ToLocalChecked());
|
||||
Nan::Set(device_info, Nan::New<v8::String>("driver").ToLocalChecked(), Nan::New<v8::String>(device->driver).ToLocalChecked());
|
||||
Nan::Set(device_info, Nan::New<v8::String>("device_id").ToLocalChecked(), Nan::New<v8::String>(base64::encode(digest::sha1(device->name + device->driver))).ToLocalChecked());
|
||||
|
||||
Nan::Set(device_info, Nan::New<v8::String>("input_supported").ToLocalChecked(), Nan::New<v8::Boolean>(device->input_supported));
|
||||
Nan::Set(device_info, Nan::New<v8::String>("output_supported").ToLocalChecked(), Nan::New<v8::Boolean>(device->output_supported));
|
||||
|
||||
Nan::Set(device_info, Nan::New<v8::String>("input_default").ToLocalChecked(), Nan::New<v8::Boolean>(device->is_default_input));
|
||||
Nan::Set(device_info, Nan::New<v8::String>("output_default").ToLocalChecked(), Nan::New<v8::Boolean>(device->is_default_output));
|
||||
|
||||
Nan::Set(device_info, Nan::New<v8::String>("device_index").ToLocalChecked(), Nan::New<v8::Number>(device->device_id));
|
||||
|
||||
result->Set(index, device_info);
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set(result);
|
||||
}
|
||||
|
||||
NAN_METHOD(player::current_playback_device) {
|
||||
if(!global_audio_output) {
|
||||
info.GetReturnValue().Set(paNoDevice);
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set(global_audio_output->current_device());
|
||||
}
|
||||
|
||||
NAN_METHOD(player::set_playback_device) {
|
||||
if(!global_audio_output) {
|
||||
Nan::ThrowError("Global audio output hasn't been yet initialized!");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string error;
|
||||
if(!global_audio_output->open_device(error, info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0))) {
|
||||
Nan::ThrowError(Nan::New<v8::String>("failed to open device (" + error + ")").ToLocalChecked());
|
||||
return;
|
||||
}
|
||||
|
||||
if(!global_audio_output->playback()) {
|
||||
Nan::ThrowError("failed to open playback stream");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(player::create_stream) {
|
||||
if(!global_audio_output) {
|
||||
Nan::ThrowError("Global audio output hasn't been yet initialized!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto wrapper = new audio::AudioOutputStreamWrapper(global_audio_output->create_source(), true);
|
||||
auto object = Nan::NewInstance(Nan::New(audio::AudioOutputStreamWrapper::constructor()), 0, nullptr).ToLocalChecked();
|
||||
wrapper->do_wrap(object);
|
||||
info.GetReturnValue().Set(object);
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(player::get_master_volume) {
|
||||
if(!global_audio_output) {
|
||||
Nan::ThrowError("Global audio output hasn't been yet initialized!");
|
||||
return;
|
||||
}
|
||||
info.GetReturnValue().Set(global_audio_output->volume());
|
||||
}
|
||||
|
||||
NAN_METHOD(player::set_master_volume) {
|
||||
if(!global_audio_output) {
|
||||
Nan::ThrowError("Global audio output hasn't been yet initialized!");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("invalid arguments");
|
||||
return;
|
||||
}
|
||||
global_audio_output->set_volume(info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <nan.h>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
extern NAN_METHOD(available_devices);
|
||||
|
||||
namespace player {
|
||||
extern NAN_MODULE_INIT(init_js);
|
||||
|
||||
extern NAN_METHOD(current_playback_device);
|
||||
extern NAN_METHOD(set_playback_device);
|
||||
|
||||
extern NAN_METHOD(create_stream);
|
||||
|
||||
extern NAN_METHOD(get_master_volume);
|
||||
extern NAN_METHOD(set_master_volume);
|
||||
/*
|
||||
export function get_master_volume() : number;
|
||||
export function set_master_volume(volume: number);
|
||||
|
||||
export function set_device(device: AudioDevice) : Promise<void>;
|
||||
export function current_device() : AudioDevice;
|
||||
|
||||
export function available_devices() : AudioDevice[];
|
||||
|
||||
export function create_stream() : OwnedAudioOutputStream;
|
||||
export function delete_stream(stream: OwnedAudioOutputStream) : number;
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
#include <utility>
|
||||
|
||||
#include "AudioRecorder.h"
|
||||
#include "AudioConsumer.h"
|
||||
#include "../AudioInput.h"
|
||||
#include "../../logger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::audio;
|
||||
using namespace tc::audio::recorder;
|
||||
|
||||
NAN_MODULE_INIT(recorder::init_js) {
|
||||
Nan::Set(target, Nan::New<v8::String>("create_recorder").ToLocalChecked(), Nan::GetFunction(Nan::New<v8::FunctionTemplate>(recorder::create_recorder)).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(recorder::create_recorder) {
|
||||
auto input = make_shared<AudioInput>(2, 48000);
|
||||
auto wrapper = new AudioRecorderWrapper(input);
|
||||
auto js_object = Nan::NewInstance(Nan::New(AudioRecorderWrapper::constructor())).ToLocalChecked();
|
||||
wrapper->do_wrap(js_object);
|
||||
info.GetReturnValue().Set(js_object);
|
||||
}
|
||||
|
||||
|
||||
NAN_MODULE_INIT(AudioRecorderWrapper::Init) {
|
||||
auto klass = Nan::New<v8::FunctionTemplate>(AudioRecorderWrapper::NewInstance);
|
||||
klass->SetClassName(Nan::New("AudioRecorder").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_device", AudioRecorderWrapper::_get_device);
|
||||
Nan::SetPrototypeMethod(klass, "set_device", AudioRecorderWrapper::_set_device);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "start", AudioRecorderWrapper::_start);
|
||||
Nan::SetPrototypeMethod(klass, "started", AudioRecorderWrapper::_started);
|
||||
Nan::SetPrototypeMethod(klass, "stop", AudioRecorderWrapper::_stop);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_volume", AudioRecorderWrapper::_get_volume);
|
||||
Nan::SetPrototypeMethod(klass, "set_volume", AudioRecorderWrapper::_set_volume);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_consumers", AudioRecorderWrapper::_get_consumers);
|
||||
Nan::SetPrototypeMethod(klass, "create_consumer", AudioRecorderWrapper::_create_consumer);
|
||||
Nan::SetPrototypeMethod(klass, "delete_consumer", AudioRecorderWrapper::_delete_consumer);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::NewInstance) {
|
||||
if(!info.IsConstructCall())
|
||||
Nan::ThrowError("invalid invoke!");
|
||||
}
|
||||
|
||||
|
||||
AudioRecorderWrapper::AudioRecorderWrapper(std::shared_ptr<tc::audio::AudioInput> handle) : _input(std::move(handle)) {
|
||||
log_allocate("AudioRecorderWrapper", this);
|
||||
}
|
||||
AudioRecorderWrapper::~AudioRecorderWrapper() {
|
||||
if(this->_input) {
|
||||
this->_input->stop();
|
||||
this->_input->close_device();
|
||||
this->_input = nullptr;
|
||||
}
|
||||
{
|
||||
lock_guard lock(this->_consumer_lock);
|
||||
this->_consumers.clear();
|
||||
}
|
||||
log_free("AudioRecorderWrapper", this);
|
||||
}
|
||||
|
||||
std::shared_ptr<AudioConsumerWrapper> AudioRecorderWrapper::create_consumer() {
|
||||
auto result = shared_ptr<AudioConsumerWrapper>(new AudioConsumerWrapper(this, this->_input->create_consumer(960)), [](AudioConsumerWrapper* ptr) {
|
||||
assert(v8::Isolate::GetCurrent());
|
||||
ptr->Unref();
|
||||
});
|
||||
|
||||
/* wrap into object */
|
||||
{
|
||||
auto js_object = Nan::NewInstance(Nan::New(AudioConsumerWrapper::constructor()), 0, nullptr).ToLocalChecked();
|
||||
result->do_wrap(js_object);
|
||||
result->Ref();
|
||||
}
|
||||
|
||||
{
|
||||
lock_guard lock(this->_consumer_lock);
|
||||
this->_consumers.push_back(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void AudioRecorderWrapper::delete_consumer(const AudioConsumerWrapper* consumer) {
|
||||
shared_ptr<AudioConsumerWrapper> handle; /* need to keep the handle 'till everything has been finished */
|
||||
{
|
||||
lock_guard lock(this->_consumer_lock);
|
||||
for(auto& c : this->_consumers) {
|
||||
if(&*c == consumer) {
|
||||
handle = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!handle)
|
||||
return;
|
||||
|
||||
{
|
||||
auto it = find(this->_consumers.begin(), this->_consumers.end(), handle);
|
||||
if(it != this->_consumers.end())
|
||||
this->_consumers.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
lock_guard lock(handle->execute_lock); /* if we delete the consumer while executing strange stuff could happen */
|
||||
handle->unbind();
|
||||
this->_input->delete_consumer(handle->_handle);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioRecorderWrapper::do_wrap(const v8::Local<v8::Object> &obj) {
|
||||
this->Wrap(obj);
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::_get_device) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
auto input = handle->_input;
|
||||
|
||||
info.GetReturnValue().Set(input->current_device());
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::_set_device) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
auto input = handle->_input;
|
||||
|
||||
if(info.Length() != 2 || !info[0]->IsNumber() || !info[1]->IsFunction()) {
|
||||
Nan::ThrowError("invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
auto device_id = info[0]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
|
||||
unique_ptr<Nan::Persistent<v8::Function>> _callback = make_unique<Nan::Persistent<v8::Function>>(info[1].As<v8::Function>());
|
||||
unique_ptr<Nan::Persistent<v8::Object>> _recorder = make_unique<Nan::Persistent<v8::Object>>(info.Holder());
|
||||
|
||||
auto _async_callback = Nan::async_callback([call = std::move(_callback), recorder = move(_recorder)](bool result, std::string error) mutable {
|
||||
Nan::HandleScope scope;
|
||||
auto callback_function = call->Get(Nan::GetCurrentContext()->GetIsolate());
|
||||
|
||||
v8::Local<v8::Value> argv[1];
|
||||
if(result)
|
||||
argv[0] = v8::Boolean::New(Nan::GetCurrentContext()->GetIsolate(), result);
|
||||
else
|
||||
argv[0] = Nan::NewOneByteString((uint8_t*) error.data(), error.length()).ToLocalChecked();
|
||||
callback_function->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, argv);
|
||||
|
||||
recorder->Reset();
|
||||
call->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
std::thread([_async_callback, input, device_id]{
|
||||
string error;
|
||||
auto flag = input->open_device(error, device_id);
|
||||
_async_callback(std::forward<bool>(flag), std::forward<std::string>(error));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::_start) {
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("missing callback");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!info[0]->IsFunction()) {
|
||||
Nan::ThrowError("not a function");
|
||||
return;
|
||||
}
|
||||
|
||||
unique_ptr<Nan::Persistent<v8::Function>> _callback = make_unique<Nan::Persistent<v8::Function>>(info[0].As<v8::Function>());
|
||||
unique_ptr<Nan::Persistent<v8::Object>> _recorder = make_unique<Nan::Persistent<v8::Object>>(info.Holder());
|
||||
|
||||
auto _async_callback = Nan::async_callback([call = std::move(_callback), recorder = move(_recorder)](bool result) mutable {
|
||||
Nan::HandleScope scope;
|
||||
auto callback_function = call->Get(Nan::GetCurrentContext()->GetIsolate());
|
||||
|
||||
v8::Local<v8::Value> argv[1];
|
||||
argv[0] = v8::Boolean::New(Nan::GetCurrentContext()->GetIsolate(), result);
|
||||
callback_function->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, argv);
|
||||
|
||||
recorder->Reset();
|
||||
call->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
auto input = handle->_input;
|
||||
std::thread([_async_callback, input]{
|
||||
_async_callback(input->record());
|
||||
}).detach();
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::_started) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
auto input = handle->_input;
|
||||
|
||||
info.GetReturnValue().Set(input->recording());
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::_stop) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
auto input = handle->_input;
|
||||
|
||||
input->stop();
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::_create_consumer) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
auto consumer = handle->create_consumer();
|
||||
|
||||
if(!consumer) {
|
||||
Nan::ThrowError("failed to create consumer");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set(consumer->handle());
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::_get_consumers) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
auto consumers = handle->consumers();
|
||||
|
||||
auto result = Nan::New<v8::Array>(consumers.size());
|
||||
|
||||
for(size_t index = 0; index < consumers.size(); index++)
|
||||
result->Set(index, consumers[index]->handle());
|
||||
|
||||
info.GetReturnValue().Set(result);
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::_delete_consumer) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsObject()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!Nan::New(AudioConsumerWrapper::constructor_template())->HasInstance(info[0])) {
|
||||
Nan::ThrowError("invalid consumer");
|
||||
return;
|
||||
}
|
||||
|
||||
auto consumer = ObjectWrap::Unwrap<AudioConsumerWrapper>(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked());
|
||||
handle->delete_consumer(consumer);
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::_set_volume) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
handle->_input->set_volume(info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
}
|
||||
|
||||
NAN_METHOD(AudioRecorderWrapper::_get_volume) {
|
||||
auto handle = ObjectWrap::Unwrap<AudioRecorderWrapper>(info.Holder());
|
||||
info.GetReturnValue().Set(handle->_input->volume());
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <nan.h>
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
class AudioInput;
|
||||
|
||||
namespace recorder {
|
||||
class AudioConsumerWrapper;
|
||||
|
||||
extern NAN_MODULE_INIT(init_js);
|
||||
|
||||
extern NAN_METHOD(create_recorder);
|
||||
|
||||
class AudioRecorderWrapper : public Nan::ObjectWrap {
|
||||
public:
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(NewInstance);
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
|
||||
explicit AudioRecorderWrapper(std::shared_ptr<AudioInput> /* input */);
|
||||
~AudioRecorderWrapper() override;
|
||||
|
||||
static NAN_METHOD(_get_device);
|
||||
static NAN_METHOD(_set_device);
|
||||
|
||||
static NAN_METHOD(_start);
|
||||
static NAN_METHOD(_started);
|
||||
static NAN_METHOD(_stop);
|
||||
|
||||
static NAN_METHOD(_create_consumer);
|
||||
static NAN_METHOD(_get_consumers);
|
||||
static NAN_METHOD(_delete_consumer);
|
||||
|
||||
static NAN_METHOD(_set_volume);
|
||||
static NAN_METHOD(_get_volume);
|
||||
|
||||
std::shared_ptr<AudioConsumerWrapper> create_consumer();
|
||||
void delete_consumer(const AudioConsumerWrapper*);
|
||||
|
||||
inline std::deque<std::shared_ptr<AudioConsumerWrapper>> consumers() {
|
||||
std::lock_guard lock(this->_consumer_lock);
|
||||
return this->_consumers;
|
||||
}
|
||||
|
||||
void do_wrap(const v8::Local<v8::Object>& /* obj */);
|
||||
|
||||
inline void js_ref() { this->Ref(); }
|
||||
inline void js_unref() { this->Unref(); }
|
||||
private:
|
||||
std::shared_ptr<AudioInput> _input;
|
||||
|
||||
std::mutex _consumer_lock;
|
||||
std::deque<std::shared_ptr<AudioConsumerWrapper>> _consumers;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
#include <v8.h>
|
||||
#include <nan.h>
|
||||
#include <node.h>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <event2/thread.h>
|
||||
#include <misc/digest.h>
|
||||
|
||||
#include "logger.h"
|
||||
#include "NanException.h"
|
||||
#include "NanEventCallback.h"
|
||||
#include "connection/ServerConnection.h"
|
||||
#include "connection/audio/VoiceConnection.h"
|
||||
#include "connection/audio/VoiceClient.h"
|
||||
#include "connection/ft/FileTransferManager.h"
|
||||
#include "connection/ft/FileTransferObject.h"
|
||||
#include "audio/AudioOutput.h"
|
||||
#include "audio/AudioDevice.h"
|
||||
#include "audio/js/AudioOutputStream.h"
|
||||
#include "audio/js/AudioPlayer.h"
|
||||
#include "audio/js/AudioRecorder.h"
|
||||
#include "audio/js/AudioConsumer.h"
|
||||
#include "audio/js/AudioFilter.h"
|
||||
#include "connection/audio/AudioEventLoop.h"
|
||||
|
||||
#ifndef WIN32
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
#include <tomcrypt_misc.h>
|
||||
#include <tomcrypt.h>
|
||||
|
||||
#include <tommath.h>
|
||||
};
|
||||
|
||||
using namespace std;
|
||||
using namespace tc;
|
||||
using namespace tc::connection;
|
||||
using namespace tc::ft;
|
||||
|
||||
void testTomMath(){
|
||||
mp_int x{};
|
||||
mp_init(&x);
|
||||
mp_read_radix(&x, "2280880776330203449294339386427307168808659578661428574166839717243346815923951250209099128371839254311904649344289668000305972691071196233379180504231889", 10);
|
||||
|
||||
mp_int n{};
|
||||
mp_init(&n);
|
||||
mp_read_radix(&n, "436860662135489324843442078840868871476482593772359054106809367217662215065650065606351911592188139644751920724885335056877706082800496073391354240530016", 10);
|
||||
|
||||
mp_int exp{};
|
||||
mp_init(&exp);
|
||||
mp_2expt(&exp, 1000);
|
||||
|
||||
|
||||
mp_int r{};
|
||||
mp_init(&r);
|
||||
|
||||
if(mp_exptmod(&x, &exp, &n, &r) == CRYPT_OK) {
|
||||
log_warn(category::general, tr("TomCrypt check failed. Server connects main fail due to this mistake!"));
|
||||
//Nan::ThrowError("Tomcrypt library is too modern. Use an oder one!");
|
||||
//return;
|
||||
}
|
||||
//assert(mp_exptmod(&x, &exp, &n, &r) != CRYPT_OK); //if this method succeed than tommath failed. Unknown why but it is so
|
||||
|
||||
mp_clear_multi(&x, &n, &exp, &r, nullptr);
|
||||
}
|
||||
|
||||
tc::audio::AudioOutput* global_audio_output;
|
||||
#define ENUM_SET(object, key, value) \
|
||||
Nan::DefineOwnProperty(object, Nan::New<v8::String>(key).ToLocalChecked(), Nan::New<v8::Number>(value), v8::DontDelete); \
|
||||
Nan::Set(object, value, Nan::New<v8::String>(key).ToLocalChecked());
|
||||
|
||||
NAN_MODULE_INIT(init) {
|
||||
logger::initialize_node();
|
||||
#ifndef WIN32
|
||||
logger::info(category::general, tr("Hello World from C. PPID: {}, PID: {}"), getppid(), getpid());
|
||||
#else
|
||||
logger::info(category::general, tr("Hello World from C. PID: {}"), _getpid());
|
||||
#endif
|
||||
|
||||
/*
|
||||
{
|
||||
auto data = (uint8_t*) "Hello World";
|
||||
auto hash_result = digest::sha1((const char*) data, 11);
|
||||
if(hash_result.length() != 20)
|
||||
Nan::ThrowError("digest::sha1 test failed");
|
||||
log_error(category::connection, tr("Hash result: {}"), hash_result.length());
|
||||
}
|
||||
*/
|
||||
{
|
||||
auto data = (uint8_t*) "Hello World";
|
||||
auto hash_result = digest::sha1(std::string("Hello World"));
|
||||
if(hash_result.length() != 20)
|
||||
Nan::ThrowError("digest::sha1 test failed");
|
||||
}
|
||||
{
|
||||
auto data = (uint8_t*) "Hello World";
|
||||
|
||||
uint8_t result[SHA_DIGEST_LENGTH];
|
||||
digest::tomcrypt::sha1((char*) data, 11, result);
|
||||
auto hash_result = std::string((const char*) result, SHA_DIGEST_LENGTH);
|
||||
log_error(category::connection, tr("Hash result: {}"), hash_result.length());
|
||||
}
|
||||
|
||||
string error;
|
||||
//TODO here
|
||||
//PaJack_SetClientName("TeaClient");
|
||||
Pa_Initialize();
|
||||
|
||||
std::thread(audio::devices).detach(); /* cache the devices */
|
||||
|
||||
logger::info(category::general, "Loading crypt modules");
|
||||
std::string descriptors = "LTGE";
|
||||
{
|
||||
int crypt_init = false;
|
||||
for(const auto& c : descriptors)
|
||||
if((crypt_init = crypt_mp_init(&c) == CRYPT_OK))
|
||||
break;
|
||||
if(!crypt_init) {
|
||||
Nan::ThrowError("failed to init tomcrypt");
|
||||
return;
|
||||
}
|
||||
if(register_prng(&sprng_desc) == -1) {
|
||||
Nan::ThrowError("could not setup prng");
|
||||
return;
|
||||
}
|
||||
if (register_cipher(&rijndael_desc) == -1) {
|
||||
Nan::ThrowError("could not setup rijndael");
|
||||
return;
|
||||
}
|
||||
|
||||
testTomMath();
|
||||
}
|
||||
logger::info(category::general, "Crypt modules loaded");
|
||||
|
||||
#ifdef WIN32
|
||||
evthread_use_windows_threads();
|
||||
#else
|
||||
evthread_use_pthreads();
|
||||
#endif
|
||||
|
||||
tc::audio::init_event_loops();
|
||||
/* TODO: Test error codes and make the audi playback device configurable */
|
||||
global_audio_output = new tc::audio::AudioOutput(2, 48000); //48000 44100
|
||||
if(!global_audio_output->open_device(error, Pa_GetDefaultOutputDevice())) {
|
||||
logger::error(category::audio, "Failed to initialize default audio playback: {}", error);
|
||||
} else {
|
||||
if(!global_audio_output->playback()) {
|
||||
logger::error(category::audio, "Failed to start audio playback");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto namespace_audio = Nan::New<v8::Object>();
|
||||
Nan::Set(namespace_audio, Nan::New<v8::String>("available_devices").ToLocalChecked(), Nan::GetFunction(Nan::New<v8::FunctionTemplate>(audio::available_devices)).ToLocalChecked());
|
||||
{
|
||||
auto namespace_playback = Nan::New<v8::Object>();
|
||||
audio::player::init_js(namespace_playback);
|
||||
audio::AudioOutputStreamWrapper::Init(namespace_playback);
|
||||
Nan::Set(namespace_audio, Nan::New<v8::String>("playback").ToLocalChecked(), namespace_playback);
|
||||
}
|
||||
{
|
||||
auto namespace_record = Nan::New<v8::Object>();
|
||||
audio::recorder::init_js(namespace_record);
|
||||
audio::recorder::AudioRecorderWrapper::Init(namespace_record);
|
||||
audio::recorder::AudioConsumerWrapper::Init(namespace_record);
|
||||
audio::recorder::AudioFilterWrapper::Init(namespace_record);
|
||||
Nan::Set(namespace_audio, Nan::New<v8::String>("record").ToLocalChecked(), namespace_record);
|
||||
}
|
||||
Nan::Set(target, Nan::New<v8::String>("audio").ToLocalChecked(), namespace_audio);
|
||||
}
|
||||
|
||||
VoiceClientWrap::Init(target);
|
||||
VoiceConnectionWrap::Init(target);
|
||||
{
|
||||
auto enum_object = Nan::New<v8::Object>();
|
||||
ENUM_SET(enum_object, "BUFFERING", tc::connection::VoiceClient::state::buffering);
|
||||
ENUM_SET(enum_object, "PLAYING", tc::connection::VoiceClient::state::playing);
|
||||
ENUM_SET(enum_object, "STOPPING", tc::connection::VoiceClient::state::stopping);
|
||||
ENUM_SET(enum_object, "STOPPED", tc::connection::VoiceClient::state::stopped);
|
||||
Nan::DefineOwnProperty(target, Nan::New<v8::String>("PlayerState").ToLocalChecked(), enum_object, v8::DontDelete);
|
||||
}
|
||||
|
||||
transfer_manager = new tc::ft::FileTransferManager();
|
||||
transfer_manager->initialize();
|
||||
|
||||
ServerConnection::Init(target);
|
||||
Nan::Set(target, Nan::New<v8::String>("spawn_server_connection").ToLocalChecked(), Nan::GetFunction(Nan::New<v8::FunctionTemplate>(ServerConnection::new_instance)).ToLocalChecked());
|
||||
|
||||
/* ft namespace */
|
||||
{
|
||||
auto ft_namespace = Nan::New<v8::Object>();
|
||||
|
||||
TransferObjectWrap::Init(ft_namespace);
|
||||
Nan::Set(ft_namespace,
|
||||
Nan::New<v8::String>("upload_transfer_object_from_buffer").ToLocalChecked(),
|
||||
Nan::GetFunction(Nan::New<v8::FunctionTemplate>(TransferJSBufferSource::create_from_buffer)).ToLocalChecked()
|
||||
);
|
||||
Nan::Set(ft_namespace,
|
||||
Nan::New<v8::String>("download_transfer_object_from_buffer").ToLocalChecked(),
|
||||
Nan::GetFunction(Nan::New<v8::FunctionTemplate>(TransferJSBufferTarget::create_from_buffer)).ToLocalChecked()
|
||||
);
|
||||
Nan::Set(ft_namespace,
|
||||
Nan::New<v8::String>("upload_transfer_object_from_file").ToLocalChecked(),
|
||||
Nan::GetFunction(Nan::New<v8::FunctionTemplate>(TransferFileSource::create)).ToLocalChecked()
|
||||
);
|
||||
|
||||
//spawn_file_connection destroy_file_connection
|
||||
JSTransfer::Init(ft_namespace);
|
||||
Nan::Set(ft_namespace, Nan::New<v8::String>("spawn_connection").ToLocalChecked(), Nan::GetFunction(Nan::New<v8::FunctionTemplate>(JSTransfer::NewInstance)).ToLocalChecked());
|
||||
Nan::Set(ft_namespace, Nan::New<v8::String>("destroy_connection").ToLocalChecked(), Nan::GetFunction(Nan::New<v8::FunctionTemplate>(JSTransfer::destory_transfer)).ToLocalChecked());
|
||||
|
||||
Nan::Set(target, Nan::New<v8::String>("ft").ToLocalChecked(), ft_namespace);
|
||||
}
|
||||
|
||||
/* setup server types */
|
||||
{
|
||||
auto enum_object = Nan::New<v8::Object>();
|
||||
ENUM_SET(enum_object, "UNKNOWN", tc::connection::server_type::UNKNOWN);
|
||||
ENUM_SET(enum_object, "TEASPEAK", tc::connection::server_type::TEASPEAK);
|
||||
ENUM_SET(enum_object, "TEAMSPEAK", tc::connection::server_type::TEAMSPEAK);
|
||||
Nan::DefineOwnProperty(target, Nan::New<v8::String>("ServerType").ToLocalChecked(), enum_object, v8::DontDelete);
|
||||
}
|
||||
}
|
||||
|
||||
NODE_MODULE(MODULE_NAME, init)
|
||||
@@ -1,535 +0,0 @@
|
||||
#ifdef WIN32
|
||||
#include <WinSock2.h>
|
||||
#endif
|
||||
|
||||
#include "ProtocolHandler.h"
|
||||
#include "ServerConnection.h"
|
||||
#include "Socket.h"
|
||||
#include "../logger.h"
|
||||
#include <misc/base64.h>
|
||||
#include <misc/endianness.h>
|
||||
#include <protocol/buffers.h>
|
||||
#include <thread>
|
||||
#include <iostream>
|
||||
#include <protocol/Packet.h>
|
||||
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
using namespace tc::connection;
|
||||
using namespace ts::protocol;
|
||||
using namespace ts;
|
||||
|
||||
ProtocolHandler::ProtocolHandler(ServerConnection* handle) : handle(handle) {
|
||||
this->compression_handler.max_packet_size = 128 * 1024; /* max 128Kb */
|
||||
}
|
||||
|
||||
ProtocolHandler::~ProtocolHandler() {
|
||||
}
|
||||
|
||||
|
||||
void ProtocolHandler::reset() {
|
||||
this->server_type = server_type::UNKNOWN;
|
||||
this->disconnect_id++; /* we've been resetted any pending disconnects are not from interest anymore */
|
||||
this->client_id = 0;
|
||||
this->acknowledge_handler.reset();
|
||||
this->connection_state = connection_state::INITIALIZING;
|
||||
|
||||
{ /* initialize pow handler */
|
||||
this->pow.state = pow_state::COOKIE_SET;
|
||||
this->pow.last_buffer = pipes::buffer{};
|
||||
this->pow.last_resend = system_clock::time_point{};
|
||||
this->pow.last_response = system_clock::time_point{};
|
||||
|
||||
this->pow.client_control_data[0] = 0; /* clear set flag, so the client generates a new pack */
|
||||
}
|
||||
|
||||
{
|
||||
this->crypto.alpha[0] = 0;
|
||||
this->crypto.initiv_command = "";
|
||||
this->crypto.beta_length = 0;
|
||||
|
||||
if(this->crypto.identity.k)
|
||||
ecc_free(&this->crypto.identity);
|
||||
memset(&this->crypto.identity, 0, sizeof(this->crypto.identity));
|
||||
}
|
||||
|
||||
for(auto& buffer : this->_packet_buffers) {
|
||||
lock_guard lock(buffer.buffer_lock);
|
||||
buffer.reset();
|
||||
}
|
||||
|
||||
this->_packet_id_manager.reset();
|
||||
this->crypt_handler.reset();
|
||||
|
||||
this->ping.ping_received_timestamp = system_clock::time_point{};
|
||||
}
|
||||
|
||||
void ProtocolHandler::connect() {
|
||||
this->connection_state = connection_state::INIT_LOW;
|
||||
this->connect_timestamp = system_clock::now();
|
||||
this->pow_send_cookie_get();
|
||||
|
||||
{
|
||||
auto command = this->generate_client_initiv();
|
||||
auto packet = make_shared<ClientPacket>(PacketTypeInfo::Command, pipes::buffer_view{command.data(), command.size()});
|
||||
packet->enable_flag(PacketFlag::NewProtocol);
|
||||
this->send_packet(packet);
|
||||
}
|
||||
}
|
||||
|
||||
void ProtocolHandler::execute_tick() {
|
||||
auto now = system_clock::now();
|
||||
if(this->connection_state < connection_state::DISCONNECTED) {
|
||||
if(!this->pow.last_buffer.empty() && this->pow.last_resend < now - seconds(1)) {
|
||||
this->pow.last_resend = now;
|
||||
this->send_packet(make_shared<ClientPacket>(PacketTypeInfo::Init1, PacketFlag::Unencrypted, this->pow.last_buffer));
|
||||
}
|
||||
|
||||
if(this->connection_state == connection_state::INIT_LOW || this->connection_state == connection_state::INIT_HIGH) {
|
||||
if(this->connect_timestamp < now - seconds(15)) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error("timeout (" + to_string(this->connection_state) + ")"), true);
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(this->connection_state == connection_state::DISCONNECTING) {
|
||||
if(this->disconnect_timestamp < now - seconds(5)) { /* disconnect timeout */
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->execute_resend();
|
||||
|
||||
/* ping */
|
||||
if(this->connection_state == connection_state::CONNECTED) {
|
||||
if(this->ping.ping_send_timestamp + seconds(1) < now)
|
||||
this->ping_send_request();
|
||||
|
||||
if(this->ping.ping_received_timestamp.time_since_epoch().count() > 0) {
|
||||
if(now - this->ping.ping_received_timestamp > seconds(30)) {
|
||||
this->handle->execute_callback_disconnect.call(tr("ping timeout"), true);
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
} else
|
||||
this->ping.ping_received_timestamp = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProtocolHandler::execute_resend() {
|
||||
if(this->connection_state >= connection_state::DISCONNECTED)
|
||||
return;
|
||||
|
||||
deque<pipes::buffer> buffers;
|
||||
auto now = system_clock::now();
|
||||
system_clock::time_point next = now + seconds(5); /* in real we're doing it all 500ms */
|
||||
string error;
|
||||
|
||||
auto resended = this->acknowledge_handler.execute_resend(now, next, buffers, error);
|
||||
if(resended < 0) {
|
||||
log_error(category::connection, tr("Failed to receive acknowledge: {}"), error);
|
||||
|
||||
this->handle->execute_callback_disconnect(tr("packet resend failed"));
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
|
||||
auto socket = this->handle->get_socket();
|
||||
if(socket) {
|
||||
for(const auto& buffer : buffers)
|
||||
socket->send_message(buffer);
|
||||
}
|
||||
|
||||
this->handle->schedule_resend(next);
|
||||
}
|
||||
|
||||
void ProtocolHandler::progress_packet(const pipes::buffer_view &buffer) {
|
||||
if(this->connection_state >= connection_state::DISCONNECTED)
|
||||
return;
|
||||
|
||||
if(buffer.length() < ServerPacket::META_SIZE) {
|
||||
log_error(category::connection, tr("Received a packet which is too small. ({})"), buffer.length());
|
||||
return;
|
||||
}
|
||||
|
||||
auto packet = std::shared_ptr<ts::protocol::ServerPacket>(ts::protocol::ServerPacket::from_buffer(buffer).release());
|
||||
auto packet_type = packet->type();
|
||||
auto packet_id = packet->packetId();
|
||||
auto ordered = packet_type.type() == protocol::COMMAND || packet_type.type() == protocol::COMMAND_LOW;
|
||||
|
||||
/* special handling */
|
||||
if(packet_type.type() == protocol::INIT1) {
|
||||
this->handlePacketInit(packet);
|
||||
return;
|
||||
}
|
||||
|
||||
if(packet_type.type() < 0 || packet_type.type() >= this->_packet_buffers.size()) {
|
||||
log_error(category::connection, tr("Received packet with invalid type. ({})"), packet_type.type());
|
||||
return;
|
||||
}
|
||||
|
||||
auto& read_queue = this->_packet_buffers[packet_type.type()];
|
||||
packet->generationId(read_queue.generation(packet_id));
|
||||
|
||||
if(ordered) {
|
||||
unique_lock queue_lock(read_queue.buffer_lock);
|
||||
auto result = read_queue.accept_index(packet_id);
|
||||
if(result != 0) { /* packet index is ahead buffer index */
|
||||
log_error(category::connection, tr("Failed to verify command packet: {} (Index: {} Current index: {})"), result, packet_id, read_queue.current_index());
|
||||
|
||||
if(result == -1) { /* underflow */
|
||||
/* we've already got the packet, but the client dosn't know that so we've to send the acknowledge again */
|
||||
if(packet->type() == PacketTypeInfo::Command || packet->type() == PacketTypeInfo::CommandLow)
|
||||
this->send_acknowledge(packet->packetId(), packet->type() == PacketTypeInfo::CommandLow);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
packet->setEncrypted(!packet->has_flag(PacketFlag::Unencrypted));
|
||||
if(packet->type() == PacketTypeInfo::Command || packet->type() == PacketTypeInfo::CommandLow){
|
||||
packet->setCompressed(packet->has_flag(PacketFlag::Compressed));
|
||||
}
|
||||
//NOTICE I found out that the Compressed flag is set if the packet contains an audio header
|
||||
|
||||
string error = "success";
|
||||
if(!this->crypt_handler.progressPacketIn(packet.get(), error, false)){
|
||||
if(!this->crypt_handler.use_default()) {
|
||||
if(!this->crypt_handler.progressPacketIn(packet.get(), error, true)){
|
||||
log_error(category::connection, tr("Failed to decrypt packet ({}), even with default key: {}"), packet_type.name(), error);
|
||||
return;
|
||||
} else {
|
||||
log_error(category::connection, tr("Successfully decrypt packet ({} | {}) with default key."), packet_type.name(), packet_id);
|
||||
//FIXME Test if we're in init high
|
||||
}
|
||||
} else {
|
||||
log_error(category::connection, tr("Failed to decrypt packet ({}) with default key: {}"), packet_type.name(), error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(packet->type() == PacketTypeInfo::Command || packet->type() == PacketTypeInfo::CommandLow){
|
||||
if(packet->has_flag(PacketFlag::Unencrypted))
|
||||
return;
|
||||
}
|
||||
|
||||
if(packet->type() == PacketTypeInfo::Command || packet->type() == PacketTypeInfo::CommandLow)
|
||||
this->send_acknowledge(packet->packetId(), packet->type() == PacketTypeInfo::CommandLow);
|
||||
|
||||
{
|
||||
unique_lock queue_lock(read_queue.buffer_lock);
|
||||
|
||||
if(ordered) { /* ordered */
|
||||
if(!read_queue.insert_index(packet_id, std::forward<shared_ptr<ServerPacket>>(packet))) {
|
||||
log_warn(category::connection, tr("Failed to insert ordered packet into queue. ({} | {} | {})"), packet_type.name(), read_queue.current_index(), packet_id);
|
||||
}
|
||||
} else {
|
||||
if(!read_queue.push_back(std::forward<shared_ptr<ServerPacket>>(packet))) {
|
||||
log_warn(category::connection, tr("Failed to insert unordered packet into queue. ({} | {} | {})"), packet_type.name(), read_queue.current_index(), packet_id);
|
||||
/* return; dont stop here because we've to progress the packets */
|
||||
} else {
|
||||
read_queue.index_set(packet_id); /* may we've skipped one packet id */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while(this->handle_packets());
|
||||
}
|
||||
|
||||
bool ProtocolHandler::handle_packets() {
|
||||
if(this->connection_state >= connection_state::DISCONNECTED)
|
||||
return false;
|
||||
|
||||
bool reexecute_handle = false;
|
||||
shared_ptr<ServerPacket> current_packet = nullptr;
|
||||
|
||||
packet_buffer_t* buffer = nullptr;
|
||||
unique_lock<std::recursive_timed_mutex> buffer_lock;
|
||||
unique_lock<std::recursive_timed_mutex> buffer_execute_lock;
|
||||
std::string error = "success";
|
||||
|
||||
|
||||
{
|
||||
auto base_index = this->_packet_buffers_index;
|
||||
auto select_index = base_index;
|
||||
auto max_index = this->_packet_buffers.size();
|
||||
for(uint8_t index = 0; index < max_index; index++) {
|
||||
if(!buffer) select_index++;
|
||||
|
||||
auto& buf = this->_packet_buffers[base_index++ % max_index];
|
||||
unique_lock ring_lock(buf.buffer_lock, try_to_lock);
|
||||
if(!ring_lock.owns_lock()) continue;
|
||||
|
||||
if(buf.front_set()) {
|
||||
if(!buffer) { /* lets still test for reexecute */
|
||||
buffer_execute_lock = unique_lock(buf.execute_lock, try_to_lock);
|
||||
if(!buffer_execute_lock.owns_lock()) continue;
|
||||
|
||||
buffer_lock = move(ring_lock);
|
||||
buffer = &buf;
|
||||
} else {
|
||||
reexecute_handle |= true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->_packet_buffers_index = select_index % max_index; /* garante that we will not hangup with commands! */
|
||||
}
|
||||
|
||||
if(buffer){
|
||||
uint16_t sequence_length = 0;
|
||||
current_packet = buffer->slot_value(sequence_length++);
|
||||
|
||||
if(current_packet) {
|
||||
if((current_packet->type() == PacketTypeInfo::Command || current_packet->type() == PacketTypeInfo::CommandLow) && current_packet->has_flag(PacketFlag::Fragmented)) {
|
||||
do {
|
||||
if(sequence_length >= buffer->capacity()) {
|
||||
log_warn(category::connection, tr("Received fragmented packets which have a too long order. Dropping queue, which will cause a client drop."));
|
||||
buffer->clear();
|
||||
return false;
|
||||
}
|
||||
current_packet = buffer->slot_value(sequence_length++);
|
||||
} while(current_packet && !current_packet->has_flag(PacketFlag::Fragmented));
|
||||
}
|
||||
} else {
|
||||
log_critical(category::connection, tr("buffer->slot_value(sequence_length++) returned nullptr!"));
|
||||
//FIXME!
|
||||
//logCritical(this->client->getServer()->getServerId(), "buffer->slot_value(sequence_length++) returned nullptr!")
|
||||
};
|
||||
|
||||
if(current_packet) { //We could reconstruct a new packet!
|
||||
if(sequence_length > 1) { //We have to merge
|
||||
vector<pipes::buffer> append;
|
||||
append.reserve(sequence_length - 1);
|
||||
|
||||
uint16_t packet_count = 0;
|
||||
current_packet = buffer->pop_front();
|
||||
packet_count++;
|
||||
do {
|
||||
auto packet = buffer->pop_front();
|
||||
packet_count++;
|
||||
if(!packet) {
|
||||
log_critical(category::connection, tr("readQueue->peekNext(seqIndex++) => nullptr_t!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
append.push_back(packet->data());
|
||||
if(packet->has_flag(PacketFlag::Fragmented)) break;
|
||||
} while(packet_count < sequence_length);
|
||||
|
||||
if(packet_count != sequence_length) {
|
||||
log_critical(category::connection, tr("seqIndex != index failed! seqIndex: {} seqLength: {} This may cause a application crash!"), packet_count, sequence_length);
|
||||
sequence_length = packet_count;
|
||||
current_packet = nullptr;
|
||||
} else {
|
||||
current_packet->append_data(append);
|
||||
}
|
||||
} else {
|
||||
if(buffer->pop_front() != current_packet) {
|
||||
log_critical(category::connection, tr("buffer->pop_front() != current_packet failed."));
|
||||
}
|
||||
}
|
||||
reexecute_handle |= buffer->front_set();
|
||||
buffer_lock.unlock(); //We got our packet so release it
|
||||
|
||||
if(current_packet) {
|
||||
if(!this->compression_handler.progressPacketIn(current_packet.get(), error)) {
|
||||
log_error(category::connection, tr("Failed to decompress received packet. Error: {}"), error);
|
||||
current_packet = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(current_packet){
|
||||
auto startTime = chrono::system_clock::now();
|
||||
try {
|
||||
if(current_packet->type() == PacketTypeInfo::Command || current_packet->type() == PacketTypeInfo::CommandLow)
|
||||
this->handlePacketCommand(current_packet);
|
||||
else if(current_packet->type() == PacketTypeInfo::Ack || current_packet->type() == PacketTypeInfo::AckLow)
|
||||
this->handlePacketAck(current_packet);
|
||||
else if(current_packet->type() == PacketTypeInfo::Voice || current_packet->type() == PacketTypeInfo::VoiceWhisper)
|
||||
this->handlePacketVoice(current_packet);
|
||||
else if(current_packet->type() == PacketTypeInfo::Ping || current_packet->type() == PacketTypeInfo::Pong)
|
||||
this->handlePacketPing(current_packet);
|
||||
} catch (std::exception& ex) {
|
||||
log_critical(category::connection, tr("Exception reached root tree! {}"), ex.what());
|
||||
}
|
||||
|
||||
auto end = chrono::system_clock::now();
|
||||
if(end - startTime > chrono::milliseconds(10)) {
|
||||
if(current_packet->type() != PacketTypeInfo::Command && current_packet->type() != PacketTypeInfo::CommandLow) {
|
||||
//FIXME!
|
||||
/*
|
||||
logError(this->client->getServerId(),
|
||||
"{} Handling of packet {} needs more than 10ms ({}ms)",
|
||||
CLIENT_STR_LOG_PREFIX_(this->client),
|
||||
current_packet->type().name(),
|
||||
duration_cast<milliseconds>(end - startTime).count()
|
||||
);
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
if(buffer_execute_lock.owns_lock())
|
||||
buffer_execute_lock.unlock();
|
||||
|
||||
return reexecute_handle;
|
||||
}
|
||||
|
||||
bool ProtocolHandler::create_datagram_packets(std::vector<pipes::buffer> &result, const std::shared_ptr<ts::protocol::ClientPacket> &packet) {
|
||||
string error = "success";
|
||||
|
||||
if(packet->type().compressable() && !packet->memory_state.fragment_entry) {
|
||||
packet->enable_flag(PacketFlag::Compressed);
|
||||
if(!this->compression_handler.progressPacketOut(packet.get(), error)) {
|
||||
log_error(category::connection, tr("Could not compress outgoing packet.\nThis could cause fatal failed for the client.\nError: {}"), error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(packet->data().length() > packet->type().max_length()){
|
||||
if(!packet->type().fragmentable()) {
|
||||
log_error(category::connection, tr("We've tried to send a too long, not fragmentable packet. Dropping packet of type {} with length {}"), packet->type().name(), packet->data().length());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<shared_ptr<ClientPacket>> siblings;
|
||||
siblings.reserve(8);
|
||||
|
||||
|
||||
{ //Split packets
|
||||
auto buffer = packet->data();
|
||||
|
||||
const auto max_length = packet->type().max_length();
|
||||
while(buffer.length() > max_length * 2) {
|
||||
siblings.push_back(make_shared<ClientPacket>(packet->type(), buffer.view(0, max_length).dup(ts::buffer::allocate_buffer(max_length))));
|
||||
buffer = buffer.range(max_length);
|
||||
}
|
||||
|
||||
if(buffer.length() > max_length) { //Divide rest by 2
|
||||
siblings.push_back(make_shared<ClientPacket>(packet->type(), buffer.view(0, buffer.length() / 2).dup(ts::buffer::allocate_buffer(buffer.length() / 2))));
|
||||
buffer = buffer.range(buffer.length() / 2);
|
||||
}
|
||||
siblings.push_back(make_shared<ClientPacket>(packet->type(), buffer));
|
||||
|
||||
for(const auto& frag : siblings) {
|
||||
frag->setFragmentedEntry(true);
|
||||
frag->enable_flag(PacketFlag::NewProtocol);
|
||||
}
|
||||
}
|
||||
|
||||
assert(siblings.size() >= 2);
|
||||
siblings.front()->enable_flag(PacketFlag::Fragmented);
|
||||
if(packet->has_flag(PacketFlag::Compressed))
|
||||
siblings.front()->enable_flag(PacketFlag::Compressed);
|
||||
|
||||
siblings.back()->enable_flag(PacketFlag::Fragmented);
|
||||
|
||||
if(packet->getListener())
|
||||
siblings.back()->setListener(std::move(packet->getListener())); //Move the listener to the last :)
|
||||
|
||||
result.reserve(siblings.size());
|
||||
for(const auto& frag : siblings)
|
||||
create_datagram_packets(result, frag);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!packet->memory_state.id_branded) {
|
||||
packet->clientId(this->client_id);
|
||||
if(packet->type().type() == PacketType::INIT1) {
|
||||
packet->applyPacketId(101, 0);
|
||||
} else {
|
||||
packet->applyPacketId(this->_packet_id_manager);
|
||||
}
|
||||
//log_trace(category::connection, tr("Packet {} got packet id {}"), packet->type().name(), packet->packetId());
|
||||
}
|
||||
if(!this->crypt_handler.progressPacketOut(packet.get(), error, false)) {
|
||||
log_error(category::connection, tr("Failed to encrypt packet: {}"), error);
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
#ifndef CONNECTION_NO_STATISTICS
|
||||
if(this->client && this->client->getServer())
|
||||
this->client->connectionStatistics->logOutgoingPacket(packet);
|
||||
#endif
|
||||
*/
|
||||
result.push_back(packet->buffer());
|
||||
|
||||
this->acknowledge_handler.process_packet(*packet);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProtocolHandler::send_command(const ts::Command &cmd, const std::function<void(bool)> &ack_callback) {
|
||||
auto data = cmd.build();
|
||||
auto packet = make_shared<ClientPacket>(PacketTypeInfo::Command, pipes::buffer_view{data.data(), data.size()});
|
||||
if(ack_callback) {
|
||||
auto begin = chrono::system_clock::now();
|
||||
packet->setListener(make_unique<threads::Future<bool>>());
|
||||
packet->getListener()->waitAndGetLater([ack_callback, begin](bool f) {
|
||||
auto end = chrono::system_clock::now();
|
||||
if(ack_callback)
|
||||
ack_callback(f);
|
||||
|
||||
log_trace(category::connection, tr("Time needed for command: {}"), chrono::duration_cast<chrono::milliseconds>(end - begin).count());
|
||||
});
|
||||
}
|
||||
packet->enable_flag(PacketFlag::NewProtocol);
|
||||
this->send_packet(packet);
|
||||
}
|
||||
|
||||
void ProtocolHandler::send_packet(const std::shared_ptr<ts::protocol::ClientPacket> &packet) {
|
||||
std::vector<pipes::buffer> result;
|
||||
if(!this->create_datagram_packets(result, packet) || result.empty()) {
|
||||
log_error(category::connection, tr("Failed to create datagram packets!"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto socket = this->handle->get_socket();
|
||||
if(!socket) {
|
||||
log_error(category::connection, tr("Failed to get socket!"));
|
||||
return;
|
||||
}
|
||||
|
||||
for(const auto& buffer : result)
|
||||
socket->send_message(buffer);
|
||||
}
|
||||
|
||||
void ProtocolHandler::send_acknowledge(uint16_t packet_id, bool low) {
|
||||
char buffer[2];
|
||||
le2be16(packet_id, buffer);
|
||||
auto packet = make_shared<protocol::ClientPacket>(low ? protocol::PacketTypeInfo::AckLow : protocol::PacketTypeInfo::Ack, 0, pipes::buffer_view{buffer, 2});
|
||||
if(this->connection_state >= connection_state::CONNECTING) {
|
||||
;//packet->toggle(protocol::PacketFlag::NewProtocol, !low);
|
||||
//LivingBots DDOS protection dont want a new protocol here!
|
||||
}
|
||||
this->send_packet(packet);
|
||||
}
|
||||
|
||||
void ProtocolHandler::do_close_connection() {
|
||||
this->connection_state = connection_state::DISCONNECTED;
|
||||
for(auto& buffer : this->_packet_buffers) {
|
||||
lock_guard lock(buffer.buffer_lock);
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void ProtocolHandler::disconnect(const std::string &reason) {
|
||||
if(this->connection_state >= connection_state::DISCONNECTING)
|
||||
return;
|
||||
|
||||
this->connection_state = connection_state::DISCONNECTING;
|
||||
this->disconnect_timestamp = system_clock::now();
|
||||
|
||||
auto did = ++this->disconnect_id;
|
||||
Command cmd("clientdisconnect");
|
||||
cmd["reasonmsg"] = reason;
|
||||
this->send_command(cmd, [&, did](bool success){
|
||||
/* if !success then we'll have prop already triggered the timeout and this here is obsolete */
|
||||
if(success && this->connection_state == connection_state::DISCONNECTING && this->disconnect_id == did)
|
||||
this->handle->close_connection();
|
||||
});
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
#define NO_LOG
|
||||
#ifdef WIN32
|
||||
#include <WinSock2.h> //Needs to be included; No clue why
|
||||
#endif
|
||||
|
||||
#include <protocol/ringbuffer.h>
|
||||
#include <protocol/Packet.h>
|
||||
#include <protocol/CryptionHandler.h>
|
||||
#include <protocol/CompressionHandler.h>
|
||||
#include <protocol/AcknowledgeManager.h>
|
||||
#include "ServerConnection.h"
|
||||
|
||||
namespace ts {
|
||||
namespace connection {
|
||||
class CryptionHandler;
|
||||
class CompressionHandler;
|
||||
}
|
||||
}
|
||||
|
||||
namespace tc {
|
||||
namespace connection {
|
||||
class ServerConnection;
|
||||
|
||||
namespace connection_state {
|
||||
enum value {
|
||||
INITIALIZING,
|
||||
INIT_LOW,
|
||||
INIT_HIGH,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
DISCONNECTING,
|
||||
DISCONNECTED
|
||||
};
|
||||
};
|
||||
namespace pow_state {
|
||||
enum value : uint8_t {
|
||||
COOKIE_GET,
|
||||
COOKIE_SET,
|
||||
PUZZLE_GET,
|
||||
PUZZLE_SET,
|
||||
PUZZLE_SOLVE,
|
||||
PUZZLE_RESET,
|
||||
COMPLETED,
|
||||
COMMAND_RESET = 127,
|
||||
UNSET = 0xFB
|
||||
};
|
||||
};
|
||||
|
||||
class ProtocolHandler {
|
||||
typedef ts::protocol::PacketRingBuffer<ts::protocol::ServerPacket, 86> packet_buffer_t;
|
||||
typedef std::array<packet_buffer_t, 8> packet_buffers_t;
|
||||
friend class ServerConnection;
|
||||
public:
|
||||
ProtocolHandler(ServerConnection*);
|
||||
~ProtocolHandler();
|
||||
|
||||
void reset();
|
||||
void connect();
|
||||
void execute_tick();
|
||||
void execute_resend();
|
||||
|
||||
void progress_packet(const pipes::buffer_view& /* buffer */);
|
||||
bool handle_packets(); /* if true we have more left */
|
||||
void send_packet(const std::shared_ptr<ts::protocol::ClientPacket>& /* packet */);
|
||||
void send_command(const ts::Command& /* command */, const std::function<void(bool)> & /* acknowledge callback */ = NULL);
|
||||
|
||||
void disconnect(const std::string& /* message */);
|
||||
|
||||
void send_acknowledge(uint16_t /* packet id */, bool /* low */);
|
||||
|
||||
ecc_key& get_identity_key() { return this->crypto.identity; }
|
||||
|
||||
inline std::chrono::microseconds current_ping() { return this->ping.value; }
|
||||
|
||||
connection_state::value connection_state = connection_state::INITIALIZING;
|
||||
server_type::value server_type = server_type::TEASPEAK;
|
||||
private:
|
||||
void do_close_connection(); /* only call from ServerConnection. Close all connections via ServerConnection! */
|
||||
|
||||
void handlePacketCommand(const std::shared_ptr<ts::protocol::ServerPacket>&);
|
||||
void handlePacketAck(const std::shared_ptr<ts::protocol::ServerPacket>&);
|
||||
void handlePacketVoice(const std::shared_ptr<ts::protocol::ServerPacket>&);
|
||||
void handlePacketPing(const std::shared_ptr<ts::protocol::ServerPacket>&);
|
||||
void handlePacketInit(const std::shared_ptr<ts::protocol::ServerPacket>&);
|
||||
|
||||
bool create_datagram_packets(std::vector<pipes::buffer> &result, const std::shared_ptr<ts::protocol::ClientPacket> &packet);
|
||||
|
||||
ServerConnection* handle;
|
||||
|
||||
std::chrono::system_clock::time_point connect_timestamp;
|
||||
std::chrono::system_clock::time_point disconnect_timestamp;
|
||||
uint8_t disconnect_id = 0;
|
||||
|
||||
struct {
|
||||
pow_state::value state;
|
||||
|
||||
uint32_t client_ts3_build_timestamp = 173265950 /* TS3 */; /* needs to be lower than 173265950 for old stuff, else new protocol */
|
||||
uint8_t client_control_data[4] = {0,0,0,0};
|
||||
uint8_t server_control_data[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
|
||||
uint8_t server_data[100];
|
||||
|
||||
std::chrono::system_clock::time_point last_response;
|
||||
std::chrono::system_clock::time_point last_resend;
|
||||
pipes::buffer last_buffer;
|
||||
} pow;
|
||||
void pow_send_cookie_get();
|
||||
|
||||
struct {
|
||||
uint8_t alpha[10];
|
||||
uint8_t beta[54];
|
||||
uint8_t beta_length; /* 10 or 54 */
|
||||
|
||||
ecc_key identity{};
|
||||
|
||||
std::string initiv_command;
|
||||
} crypto;
|
||||
std::string generate_client_initiv();
|
||||
|
||||
uint16_t client_id = 0;
|
||||
ts::protocol::PacketIdManager _packet_id_manager;
|
||||
packet_buffers_t _packet_buffers;
|
||||
uint8_t _packet_buffers_index = 0;
|
||||
|
||||
ts::connection::CryptionHandler crypt_handler;
|
||||
ts::connection::CompressionHandler compression_handler;
|
||||
ts::connection::AcknowledgeManager acknowledge_handler;
|
||||
|
||||
void handleCommandInitIVExpend(ts::Command&);
|
||||
void handleCommandInitIVExpend2(ts::Command&);
|
||||
void handleCommandInitServer(ts::Command&);
|
||||
|
||||
struct {
|
||||
std::chrono::system_clock::time_point ping_send_timestamp;
|
||||
std::chrono::system_clock::time_point ping_received_timestamp;
|
||||
std::chrono::microseconds value;
|
||||
uint16_t ping_id;
|
||||
|
||||
std::chrono::microseconds interval = std::chrono::microseconds(2500);
|
||||
} ping;
|
||||
void ping_send_request();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
#include "ProtocolHandler.h"
|
||||
#include "ServerConnection.h"
|
||||
#include "../logger.h"
|
||||
#include <protocol/buffers.h>
|
||||
#include <thread>
|
||||
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::connection;
|
||||
using namespace ts::protocol;
|
||||
using namespace ts;
|
||||
|
||||
void ProtocolHandler::handleCommandInitServer(ts::Command &cmd) {
|
||||
this->client_id = cmd["aclid"];
|
||||
this->connection_state = connection_state::CONNECTED;
|
||||
|
||||
log_info(category::connection, tr("Received own client id: {}"), this->client_id);
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
#include "ProtocolHandler.h"
|
||||
#include "ServerConnection.h"
|
||||
#include "Socket.h"
|
||||
#include "../logger.h"
|
||||
#include <protocol/buffers.h>
|
||||
#include <thread>
|
||||
#include <iostream>
|
||||
#include <tomcrypt.h>
|
||||
#include <tommath.h>
|
||||
#include <misc/base64.h>
|
||||
#include <misc/digest.h>
|
||||
#include <License.h>
|
||||
#include <ed25519/ed25519.h>
|
||||
#include <ed25519/sha512.h>
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::connection;
|
||||
using namespace ts::protocol;
|
||||
using namespace ts;
|
||||
|
||||
inline void generate_random(uint8_t *destination, size_t length) {
|
||||
while(length-- > 0)
|
||||
*(destination++) = (uint8_t) rand();
|
||||
}
|
||||
|
||||
std::string ProtocolHandler::generate_client_initiv() {
|
||||
if(!this->crypto.initiv_command.empty())
|
||||
return this->crypto.initiv_command;
|
||||
|
||||
/* setup basic parameters */
|
||||
if((this->crypto.alpha[0] & 0x01) == 0) {
|
||||
generate_random(this->crypto.alpha, 10);
|
||||
this->crypto.alpha[0] |= 0x01;
|
||||
}
|
||||
|
||||
Command command("clientinitiv");
|
||||
command["alpha"] = base64::encode((char*) this->crypto.alpha, 10);
|
||||
command["ot"] = 1;
|
||||
command["ip"] = "unknown";
|
||||
|
||||
if(this->server_type != server_type::TEAMSPEAK) /* if TEAMSPEAK then TS3 has been enforced */
|
||||
command.enableParm("teaspeak"); /* using "old" encryption system, we expect a teaspeak=1 within the response */
|
||||
{
|
||||
size_t buffer_length = 265;
|
||||
char buffer[265];
|
||||
auto result = ecc_export((unsigned char *) buffer, (unsigned long*) &buffer_length, PK_PUBLIC, &this->crypto.identity);
|
||||
if(result == CRYPT_OK)
|
||||
command["omega"] = base64::encode(buffer, buffer_length);
|
||||
else
|
||||
log_error(category::connection, tr("Failed to export identiry ({})"), result);
|
||||
}
|
||||
|
||||
this->crypto.initiv_command = command.build(true);
|
||||
return this->crypto.initiv_command;
|
||||
}
|
||||
|
||||
void ProtocolHandler::handleCommandInitIVExpend(ts::Command &cmd) {
|
||||
this->pow.last_buffer = pipes::buffer{};
|
||||
|
||||
auto alpha = base64::decode(cmd["alpha"]);
|
||||
auto beta = base64::decode(cmd["beta"]);
|
||||
auto omega = base64::decode(cmd["omega"]);
|
||||
|
||||
if(alpha.length() != 10 || memcmp(alpha.data(), this->crypto.alpha, 10) != 0) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("alpha key missmatch")), true);
|
||||
this->handle->close_connection();
|
||||
|
||||
log_error(category::connection, tr("InitIVExpend contains invalid alpha"));
|
||||
return;
|
||||
}
|
||||
|
||||
ecc_key server_key{};
|
||||
if(ecc_import((u_char*) omega.data(), omega.length(), &server_key) != CRYPT_OK) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("failed to import server key")), true);
|
||||
this->handle->close_connection();
|
||||
|
||||
log_error(category::connection, tr("InitIVExpend contains invalid key"));
|
||||
return;
|
||||
}
|
||||
|
||||
string error;
|
||||
if(!this->crypt_handler.setupSharedSecret(alpha, beta, &server_key, &this->crypto.identity, error)) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("failed to setup encryption")), true);
|
||||
this->handle->close_connection();
|
||||
|
||||
log_error(category::connection, tr("Failed to setup crypto ({})"), error);
|
||||
return;
|
||||
}
|
||||
|
||||
if(this->server_type == server_type::UNKNOWN) {
|
||||
if(cmd[0].has("teaspeak") && cmd["teaspeak"].as<bool>()) {
|
||||
this->server_type = server_type::TEASPEAK;
|
||||
} else {
|
||||
this->server_type = server_type::TEAMSPEAK;
|
||||
}
|
||||
}
|
||||
|
||||
this->handle->call_connect_result.call(0, true);
|
||||
this->connection_state = connection_state::CONNECTING;
|
||||
}
|
||||
|
||||
|
||||
int __ed_sha512_init(sha512_context* ctx) {
|
||||
ctx->context = new hash_state{};
|
||||
return sha512_init((hash_state*) ctx->context) == CRYPT_OK;
|
||||
}
|
||||
|
||||
int __ed_sha512_final(sha512_context* ctx, unsigned char *out) {
|
||||
assert(ctx->context);
|
||||
|
||||
auto result = sha512_done((hash_state*) ctx->context, out) == CRYPT_OK;
|
||||
delete (hash_state*) ctx->context;
|
||||
return result;
|
||||
}
|
||||
int __ed_sha512_update(sha512_context* ctx, const unsigned char *msg, size_t len) {
|
||||
assert(ctx->context);
|
||||
return sha512_process((hash_state*) ctx->context, msg, len) == CRYPT_OK;
|
||||
}
|
||||
|
||||
static sha512_functions __ed_sha512_functions {
|
||||
__ed_sha512_init,
|
||||
__ed_sha512_final,
|
||||
__ed_sha512_update
|
||||
};
|
||||
|
||||
void ProtocolHandler::handleCommandInitIVExpend2(ts::Command &cmd) {
|
||||
this->pow.last_buffer = pipes::buffer{};
|
||||
|
||||
/* setup ed functions */
|
||||
if(&__ed_sha512_functions != &_ed_sha512_functions)
|
||||
_ed_sha512_functions = __ed_sha512_functions;
|
||||
|
||||
auto beta = base64::decode(cmd["beta"]);
|
||||
auto omega = base64::decode(cmd["omega"]);
|
||||
auto proof = base64::decode(cmd["proof"]);
|
||||
|
||||
auto crypto_chain_data = base64::decode(cmd["l"]);
|
||||
auto crypto_root = cmd[0].has("root") ? base64::decode(cmd["root"]) : string((char*) license::teamspeak::public_root, 32);
|
||||
auto crypto_hash = digest::sha256(crypto_chain_data);
|
||||
|
||||
/* suspecius, tries the server to hide himself? We dont know */
|
||||
if(this->server_type == server_type::UNKNOWN) {
|
||||
if(cmd[0].has("root"))
|
||||
this->server_type = server_type::TEASPEAK;
|
||||
else
|
||||
this->server_type = server_type::TEAMSPEAK;
|
||||
}
|
||||
|
||||
ecc_key server_key{};
|
||||
if(ecc_import((u_char*) omega.data(), omega.length(), &server_key) != CRYPT_OK) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("failed to import server key")), true);
|
||||
this->handle->close_connection();
|
||||
|
||||
log_error(category::connection, tr("InitIVExpend contains invalid key"));
|
||||
return;
|
||||
}
|
||||
|
||||
int result, crypt_result;
|
||||
if((crypt_result = ecc_verify_hash((u_char*) proof.data(), proof.length(), (u_char*) crypto_hash.data(), crypto_hash.length(), &result, &server_key)) != CRYPT_OK || result != 1) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("failed to verify server integrity")), true);
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
|
||||
string error;
|
||||
auto crypto_chain = license::teamspeak::LicenseChain::parse(crypto_chain_data, error, false);
|
||||
if(!crypto_chain) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("failed to read crypto chain")), true);
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
|
||||
auto server_public_key = crypto_chain->generatePublicKey(*(license::teamspeak::LicensePublicKey*) crypto_root.data());
|
||||
crypto_chain->print();
|
||||
|
||||
u_char seed[32];
|
||||
ed25519_create_seed(seed);
|
||||
u_char public_key[32], private_key[64]; /* We need 64 bytes because we're doing some SHA512 actions */
|
||||
ed25519_create_keypair(public_key, private_key, seed);
|
||||
|
||||
/* send clientek response */
|
||||
{
|
||||
size_t sign_buffer_length = 200;
|
||||
char sign_buffer[200];
|
||||
|
||||
prng_state prng_state{};
|
||||
memset(&prng_state, 0, sizeof(prng_state));
|
||||
|
||||
auto proof_data = digest::sha256(string((char*) public_key, 32) + beta);
|
||||
if(ecc_sign_hash((uint8_t*) proof_data.data(), proof_data.length(), (uint8_t*) sign_buffer, (unsigned long*) &sign_buffer_length, &prng_state, find_prng("sprng"), &this->crypto.identity) != CRYPT_OK) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("failed to generate proof of identity")), true);
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
|
||||
Command response("clientek");
|
||||
response["ek"] = base64::encode((char*) public_key, 32);
|
||||
response["proof"] = base64::encode(sign_buffer, sign_buffer_length);
|
||||
/* no need to send this because we're sending the clientinit as the begin packet along with the POW init */
|
||||
//this->_packet_id_manager.nextPacketId(PacketTypeInfo::Command); /* skip the first because we've send our first command within the low level handshake packets */
|
||||
this->send_command(response, [&](bool success){
|
||||
if(success) {
|
||||
/* trigger connected; because the connection has been established on protocol layer */
|
||||
this->handle->call_connect_result.call(0, true);
|
||||
this->connection_state = connection_state::CONNECTING;
|
||||
}
|
||||
}); /* needs to be encrypted at the time! */
|
||||
}
|
||||
|
||||
|
||||
if(!this->crypt_handler.setupSharedSecretNew(string((char*) this->crypto.alpha, 10), beta, (char*) private_key, server_public_key.data())) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("failed to setup encryption")), true);
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
#include "ProtocolHandler.h"
|
||||
#include "ServerConnection.h"
|
||||
#include "Socket.h"
|
||||
#include "../logger.h"
|
||||
#include "Error.h"
|
||||
|
||||
#include <misc/endianness.h>
|
||||
#include <protocol/buffers.h>
|
||||
#include <thread>
|
||||
#include <iostream>
|
||||
#include <tomcrypt.h>
|
||||
#include <tommath.h>
|
||||
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
using namespace tc::connection;
|
||||
using namespace ts::protocol;
|
||||
using namespace ts;
|
||||
|
||||
inline void generate_random(uint8_t *destination, size_t length) {
|
||||
while(length-- > 0)
|
||||
*(destination++) = (uint8_t) rand();
|
||||
}
|
||||
|
||||
inline void write_reversed(uint8_t* destination, uint8_t* source, size_t length) {
|
||||
destination += length;
|
||||
while(length-- > 0)
|
||||
*(--destination) = *(source++);
|
||||
}
|
||||
|
||||
inline bool solve_puzzle(mp_int& x, mp_int& n, mp_int& result, uint32_t level) {
|
||||
mp_int exp{};
|
||||
mp_init(&exp);
|
||||
mp_2expt(&exp, level);
|
||||
|
||||
|
||||
if (mp_exptmod(&x, &exp, &n, &result) != CRYPT_OK) { //Sometimes it fails (unknown why :D)
|
||||
mp_clear(&exp);
|
||||
return false;
|
||||
}
|
||||
|
||||
mp_clear(&exp);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProtocolHandler::handlePacketInit(const std::shared_ptr<ts::protocol::ServerPacket> &packet) {
|
||||
this->pow.last_response = system_clock::now();
|
||||
|
||||
auto data = packet->data();
|
||||
auto packet_state = static_cast<pow_state::value>(data[0]);
|
||||
|
||||
if(packet_state == pow_state::COMMAND_RESET) {
|
||||
log_trace(category::connection, tr("[POW] Received reset"));
|
||||
this->pow.state = pow_state::COOKIE_SET; /* next expected packet state */
|
||||
this->pow_send_cookie_get();
|
||||
return;
|
||||
}
|
||||
|
||||
log_trace(category::connection, tr("[POW] State {} | {}"), packet_state, data.length());
|
||||
if(packet_state != this->pow.state)
|
||||
return; //TODO handle error?
|
||||
this->acknowledge_handler.reset(); /* we don't need an ack anymore for our init packet */
|
||||
|
||||
if(packet_state == pow_state::COOKIE_SET) {
|
||||
if(data.length() != 21 && data.length() != 5) {
|
||||
log_trace(category::connection, tr("[POW] Dropping cookie packet (got {} bytes expect 21 or 5 bytes)"), data.length());
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.length() == 21) {
|
||||
memcpy(&this->pow.server_control_data[0], &data[1], 16);
|
||||
//TODO test client data reserved bytes
|
||||
} else {
|
||||
auto errc = ntohl(*(uint32_t*) &data[1]);
|
||||
auto err = ts::findError(errc);
|
||||
|
||||
log_error(category::connection, tr("[POW] Received error code: {:x} ({})"), errc, err.message);
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("received error: ") + to_string(errc) + " (" + err.message + ")"), true);
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
|
||||
/* send puzzle get request */
|
||||
{
|
||||
this->pow.state = pow_state::PUZZLE_SET; /* next expected packet state */
|
||||
|
||||
uint8_t response_buffer[25];
|
||||
le2be32(this->pow.client_ts3_build_timestamp, &response_buffer[0]);
|
||||
response_buffer[4] = pow_state::PUZZLE_GET;
|
||||
|
||||
memcpy(&response_buffer[5], this->pow.server_control_data, 16);
|
||||
memcpy(&response_buffer[21], &data[17], 4);
|
||||
|
||||
this->pow.last_buffer = pipes::buffer_view{response_buffer, 25}.own_buffer();
|
||||
this->pow.last_resend = system_clock::now();
|
||||
|
||||
this->send_packet(make_shared<ClientPacket>(PacketTypeInfo::Init1, PacketFlag::Unencrypted, this->pow.last_buffer));
|
||||
}
|
||||
|
||||
return;
|
||||
} else if(packet_state == pow_state::PUZZLE_SET) {
|
||||
constexpr auto expected_bytes = 1 + 64 * 2 + 4 + 100;
|
||||
if(data.length() != 1 + 64 * 2 + 4 + 100) {
|
||||
log_trace(category::connection, tr("[POW] Dropping puzzle packet (got {} bytes expect {} bytes)"), data.length(), expected_bytes);
|
||||
return;
|
||||
}
|
||||
|
||||
mp_int point_x{}, point_n{}, result{};
|
||||
if(mp_read_unsigned_bin(&point_x, (u_char*) &data[1], 64) < 0)
|
||||
return; //TODO handle error
|
||||
|
||||
if(mp_read_unsigned_bin(&point_n, (u_char*) &data[65], 64) < 0) {
|
||||
mp_clear_multi(&point_x, nullptr);
|
||||
return; //TODO handle error
|
||||
}
|
||||
|
||||
log_trace(category::connection, tr("[POW] Received puzzle with level {}"), be2le32(&data[1 + 64 + 64]));
|
||||
if(!solve_puzzle(point_x, point_n, result, be2le32(&data[1 + 64 + 64]))) {
|
||||
mp_clear_multi(&point_x, &point_n, nullptr);
|
||||
log_trace(connection, tr("[POW] Failed to solve puzzle!"));
|
||||
return; //TODO handle error
|
||||
}
|
||||
|
||||
{
|
||||
auto command = this->generate_client_initiv();
|
||||
size_t response_buffer_length = 301 + command.size();
|
||||
auto response_buffer = buffer::allocate_buffer(response_buffer_length);
|
||||
|
||||
le2be32(this->pow.client_ts3_build_timestamp, &response_buffer[0]);
|
||||
response_buffer[4] = pow_state::PUZZLE_SOLVE;
|
||||
memcpy(&response_buffer[5], &data[1], 64 * 2 + 100 + 4);
|
||||
|
||||
auto offset = 4 + 1 + 2 * 64 + 04 + 100;
|
||||
memset(&response_buffer[offset], 0, 64);
|
||||
mp_to_unsigned_bin(&result, (u_char*) &response_buffer[offset]);
|
||||
|
||||
memcpy(&response_buffer[301], command.data(), command.size());
|
||||
|
||||
this->pow.last_buffer = response_buffer;
|
||||
this->pow.last_resend = system_clock::now();
|
||||
this->send_packet(make_shared<ClientPacket>(PacketTypeInfo::Init1, PacketFlag::Unencrypted, this->pow.last_buffer));
|
||||
}
|
||||
mp_clear_multi(&point_x, &point_n, &result, nullptr);
|
||||
this->connection_state = connection_state::INIT_HIGH;
|
||||
}
|
||||
}
|
||||
|
||||
void ProtocolHandler::pow_send_cookie_get() {
|
||||
this->pow.state = pow_state::COOKIE_SET; /* next expected packet state */
|
||||
|
||||
if((this->pow.client_control_data[0] & 0x01U) == 0) {
|
||||
generate_random(this->pow.client_control_data, 4);
|
||||
this->pow.client_control_data[0] |= 0x01U;
|
||||
}
|
||||
|
||||
this->pow.client_ts3_build_timestamp = floor < seconds > ((system_clock::now() - hours{24}).time_since_epoch()).count();
|
||||
|
||||
uint8_t response_buffer[21];
|
||||
le2be32(this->pow.client_ts3_build_timestamp, &response_buffer[0]);
|
||||
response_buffer[4] = pow_state::COOKIE_GET;
|
||||
memset(&response_buffer[5], 0, 4);
|
||||
memcpy(&response_buffer[9], &this->pow.client_control_data, 4);
|
||||
memset(&response_buffer[13], 0, 8);
|
||||
|
||||
this->pow.last_buffer = pipes::buffer_view{response_buffer, 21}.own_buffer();
|
||||
this->pow.last_resend = system_clock::now();
|
||||
this->send_packet(make_shared<ClientPacket>(PacketTypeInfo::Init1, PacketFlag::Unencrypted, this->pow.last_buffer));
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
#include "ProtocolHandler.h"
|
||||
#include "ServerConnection.h"
|
||||
#include "Socket.h"
|
||||
#include <protocol/buffers.h>
|
||||
#include <thread>
|
||||
#include <iostream>
|
||||
#include <tomcrypt.h>
|
||||
#include <tommath.h>
|
||||
#include <misc/base64.h>
|
||||
#include <misc/endianness.h>
|
||||
#include <query/command2.h>
|
||||
#include <protocol/Packet.h>
|
||||
#include "audio/VoiceConnection.h"
|
||||
#include "../logger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::connection;
|
||||
using namespace ts::protocol;
|
||||
using namespace ts;
|
||||
|
||||
//#define LOG_PING
|
||||
|
||||
void ProtocolHandler::handlePacketAck(const std::shared_ptr<ts::protocol::ServerPacket> &ack) {
|
||||
string error;
|
||||
log_trace(category::connection, tr("Handle packet acknowledge for {}"), be2le16(&ack->data()[0]));
|
||||
if(!this->acknowledge_handler.process_acknowledge(*ack, error)) { }
|
||||
}
|
||||
|
||||
void ProtocolHandler::handlePacketCommand(const std::shared_ptr<ts::protocol::ServerPacket> &packet) {
|
||||
//cout << "Received command: " << packet->data().string() << endl;
|
||||
|
||||
std::unique_ptr<Command> command;
|
||||
try {
|
||||
command = make_unique<Command>(packet->asCommand());
|
||||
} catch(const std::invalid_argument& ex) {
|
||||
log_error(category::connection, tr("Failed to parse command (invalid_argument): {}"), ex.what());
|
||||
return;
|
||||
} catch(const ts::command_malformed_exception& ex) {
|
||||
log_error(category::connection, tr("Failed to parse command (command_malformed_exception): {}@{}"), ex.what(), ex.index());
|
||||
return;
|
||||
} catch(const std::exception& ex) {
|
||||
log_error(category::connection, tr("Failed to parse command (exception): {}"), ex.what());
|
||||
return;
|
||||
}
|
||||
|
||||
if(command->command() == "initivexpand") {
|
||||
this->handleCommandInitIVExpend(*command);
|
||||
} else if(command->command() == "initivexpand2") {
|
||||
this->handleCommandInitIVExpend2(*command);
|
||||
} else if(command->command() == "initserver") {
|
||||
this->handleCommandInitServer(*command);
|
||||
}
|
||||
|
||||
{
|
||||
lock_guard lock(this->handle->pending_commands_lock);
|
||||
this->handle->pending_commands.push_back(move(command));
|
||||
}
|
||||
this->handle->execute_pending_commands();
|
||||
}
|
||||
|
||||
void ProtocolHandler::handlePacketVoice(const std::shared_ptr<ts::protocol::ServerPacket> &packet) {
|
||||
this->handle->voice_connection->process_packet(packet);
|
||||
}
|
||||
|
||||
void ProtocolHandler::handlePacketPing(const std::shared_ptr<ts::protocol::ServerPacket> &packet) {
|
||||
if(packet->type() == PacketTypeInfo::Pong) {
|
||||
uint16_t id = be2le16((char*) packet->data().data_ptr());
|
||||
#ifdef LOG_PING
|
||||
cout << "Received pong (" << id << "|" << this->ping.ping_id << ")" << endl;
|
||||
#endif
|
||||
if(id == this->ping.ping_id) {
|
||||
this->ping.ping_received_timestamp = chrono::system_clock::now();
|
||||
this->ping.value = chrono::duration_cast<chrono::microseconds>(this->ping.ping_received_timestamp - this->ping.ping_send_timestamp);
|
||||
#ifdef LOG_PING
|
||||
cout << "Updating client ping: " << chrono::duration_cast<chrono::microseconds>(this->ping.value).count() << "us" << endl;
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
#ifdef LOG_PING
|
||||
cout << "Received ping, sending pong" << endl;
|
||||
#endif
|
||||
|
||||
char buffer[2];
|
||||
le2be16(packet->packetId(), buffer);
|
||||
this->send_packet(make_shared<ClientPacket>(PacketTypeInfo::Pong, PacketFlag::Unencrypted, pipes::buffer_view{buffer, 2}));
|
||||
}
|
||||
}
|
||||
|
||||
void ProtocolHandler::ping_send_request() {
|
||||
auto packet = make_shared<ClientPacket>(PacketTypeInfo::Ping, pipes::buffer_view{});
|
||||
packet->enable_flag(PacketFlag::Unencrypted);
|
||||
this->send_packet(packet);
|
||||
assert(packet->memory_state.id_branded);
|
||||
|
||||
this->ping.ping_send_timestamp = chrono::system_clock::now();
|
||||
this->ping.ping_id = packet->packetId();
|
||||
}
|
||||
@@ -1,727 +0,0 @@
|
||||
#include "ServerConnection.h"
|
||||
#include "ProtocolHandler.h"
|
||||
#include "Socket.h"
|
||||
#include "audio/VoiceConnection.h"
|
||||
#include "audio/AudioSender.h"
|
||||
#include "../logger.h"
|
||||
#include "../hwuid.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <iostream>
|
||||
#include <misc/net.h>
|
||||
#include <misc/base64.h>
|
||||
#include <misc/endianness.h>
|
||||
#include <misc/strobf.h>
|
||||
#include <iomanip>
|
||||
|
||||
//#define FUZZ_VOICE
|
||||
//#define SHUFFLE_VOICE
|
||||
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
using namespace tc::connection;
|
||||
|
||||
string ErrorHandler::get_message(ErrorHandler::error_id id) {
|
||||
if(id == 0)
|
||||
return "success";
|
||||
|
||||
auto index = (-id - 1) % this->error_varianz;
|
||||
assert(index >= 0 && index < this->error_varianz);
|
||||
return this->error_messages[index];
|
||||
}
|
||||
|
||||
ErrorHandler::error_id ErrorHandler::register_error(const string &message) {
|
||||
auto index = this->error_index++ % this->error_varianz;
|
||||
this->error_messages[index] = message;
|
||||
return -index - 1;
|
||||
}
|
||||
|
||||
ServerConnection::ServerConnection() {
|
||||
logger::debug(category::connection, tr("Allocated ServerConnection {}."), (void*) this);
|
||||
}
|
||||
|
||||
ServerConnection::~ServerConnection() {
|
||||
logger::debug(category::connection, tr("Begin deallocating ServerConnection {}."), (void*) this);
|
||||
if(this->protocol_handler && this->protocol_handler->connection_state == connection_state::CONNECTED)
|
||||
this->protocol_handler->disconnect("server connection has been destoryed");
|
||||
this->close_connection();
|
||||
this->finalize();
|
||||
|
||||
logger::debug(category::connection, tr("Finished deallocating ServerConnection {}."), (void*) this);
|
||||
}
|
||||
|
||||
NAN_MODULE_INIT(ServerConnection::Init) {
|
||||
auto klass = Nan::New<v8::FunctionTemplate>(ServerConnection::new_instance);
|
||||
klass->SetClassName(Nan::New("NativeServerConnection").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "connect", ServerConnection::_connect);
|
||||
Nan::SetPrototypeMethod(klass, "connected", ServerConnection::_connected);
|
||||
Nan::SetPrototypeMethod(klass, "disconnect", ServerConnection::_disconnect);
|
||||
Nan::SetPrototypeMethod(klass, "error_message", ServerConnection::_error_message);
|
||||
Nan::SetPrototypeMethod(klass, "send_command", ServerConnection::_send_command);
|
||||
Nan::SetPrototypeMethod(klass, "send_voice_data", ServerConnection::_send_voice_data);
|
||||
Nan::SetPrototypeMethod(klass, "send_voice_data_raw", ServerConnection::_send_voice_data_raw);
|
||||
Nan::SetPrototypeMethod(klass, "current_ping", ServerConnection::_current_ping);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(ServerConnection::new_instance) {
|
||||
//info.GetReturnValue().Set(Nan::New<v8::String>("Hello World").ToLocalChecked());
|
||||
|
||||
if (info.IsConstructCall()) {
|
||||
auto instance = new ServerConnection();
|
||||
instance->Wrap(info.This());
|
||||
instance->initialize();
|
||||
//Nan::Set(info.This(), New<String>("type").ToLocalChecked(), New<Number>(type));
|
||||
info.GetReturnValue().Set(info.This());
|
||||
} else {
|
||||
v8::Local<v8::Function> cons = Nan::New(constructor());
|
||||
|
||||
Nan::TryCatch try_catch;
|
||||
auto result = Nan::NewInstance(cons, 0, nullptr);
|
||||
if(try_catch.HasCaught()) {
|
||||
try_catch.ReThrow();
|
||||
return;
|
||||
}
|
||||
info.GetReturnValue().Set(result.ToLocalChecked());
|
||||
}
|
||||
}
|
||||
|
||||
void ServerConnection::initialize() {
|
||||
this->event_loop_exit = false;
|
||||
this->event_thread = thread(&ServerConnection::event_loop, this);
|
||||
this->protocol_handler = make_unique<ProtocolHandler>(this);
|
||||
this->voice_connection = make_shared<VoiceConnection>(this);
|
||||
this->voice_connection->_ref = this->voice_connection;
|
||||
this->voice_connection->initialize_js_object();
|
||||
|
||||
this->execute_pending_commands = Nan::async_callback([&]{
|
||||
Nan::HandleScope scope;
|
||||
this->_execute_callback_commands();
|
||||
});
|
||||
this->execute_pending_voice = Nan::async_callback([&]{
|
||||
Nan::HandleScope scope;
|
||||
this->_execute_callback_voice();
|
||||
});
|
||||
this->execute_callback_disconnect = Nan::async_callback([&](std::string reason){
|
||||
Nan::HandleScope scope;
|
||||
this->_execute_callback_disconnect(reason);
|
||||
});
|
||||
|
||||
|
||||
this->call_connect_result = Nan::async_callback([&](ErrorHandler::error_id error_id) {
|
||||
Nan::HandleScope scope;
|
||||
/* lets update the server type */
|
||||
{
|
||||
auto js_this = this->handle();
|
||||
Nan::Set(js_this, Nan::New<v8::String>("server_type").ToLocalChecked(), Nan::New<v8::Number>(this->protocol_handler->server_type));
|
||||
}
|
||||
|
||||
/* lets call the connect callback */
|
||||
{
|
||||
v8::Local<v8::Value> argv[1];
|
||||
argv[0] = Nan::New<v8::Number>(error_id);
|
||||
|
||||
if(this->callback_connect)
|
||||
Nan::Call(*this->callback_connect, 1, argv);
|
||||
this->callback_connect = nullptr;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this->call_disconnect_result = Nan::async_callback([&](ErrorHandler::error_id error_id) {
|
||||
Nan::HandleScope scope;
|
||||
v8::Local<v8::Value> argv[1];
|
||||
argv[0] = Nan::New<v8::Number>(error_id);
|
||||
|
||||
if(this->callback_disconnect)
|
||||
Nan::Call(*this->callback_disconnect, 1, argv);
|
||||
this->callback_disconnect = nullptr;
|
||||
});
|
||||
|
||||
auto js_this = this->handle();
|
||||
Nan::Set(js_this, Nan::New<v8::String>("_voice_connection").ToLocalChecked(), this->voice_connection->js_handle());
|
||||
|
||||
}
|
||||
|
||||
void ServerConnection::finalize() {
|
||||
this->event_loop_exit = true;
|
||||
this->event_condition.notify_all();
|
||||
this->event_thread.join();
|
||||
}
|
||||
|
||||
void ServerConnection::event_loop() {
|
||||
auto eval_timeout = [&]{
|
||||
auto best = this->next_tick;
|
||||
if(this->next_resend < best)
|
||||
best = this->next_resend;
|
||||
if(this->event_loop_execute_connection_close)
|
||||
return system_clock::time_point{};
|
||||
return best;
|
||||
};
|
||||
|
||||
while(!this->event_loop_exit) {
|
||||
auto timeout = eval_timeout();
|
||||
{
|
||||
unique_lock lock(this->event_lock);
|
||||
this->event_condition.wait_until(lock, timeout, [&]{
|
||||
if(eval_timeout() != timeout)
|
||||
return true;
|
||||
return this->event_loop_exit;
|
||||
});
|
||||
|
||||
if(this->event_loop_exit)
|
||||
break;
|
||||
}
|
||||
if(this->event_loop_execute_connection_close) {
|
||||
this->close_connection();
|
||||
this->event_loop_execute_connection_close = false;
|
||||
}
|
||||
auto date = chrono::system_clock::now();
|
||||
if(this->next_tick <= date) {
|
||||
this->next_tick = date + chrono::milliseconds(500);
|
||||
this->execute_tick();
|
||||
}
|
||||
if(this->next_resend <= date) {
|
||||
this->next_resend = date + chrono::seconds(5);
|
||||
if(this->protocol_handler)
|
||||
this->protocol_handler->execute_resend();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ServerConnection::schedule_resend(const std::chrono::system_clock::time_point &timeout) {
|
||||
if(this->next_resend > timeout) {
|
||||
this->next_resend = timeout;
|
||||
this->event_condition.notify_one();
|
||||
}
|
||||
}
|
||||
NAN_METHOD(ServerConnection::connect) {
|
||||
if(!this->protocol_handler) {
|
||||
Nan::ThrowError("ServerConnection not initialized");
|
||||
return;
|
||||
}
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError(tr("Invalid argument count"));
|
||||
return;
|
||||
}
|
||||
|
||||
v8::Local arguments = info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked();
|
||||
|
||||
if(!arguments->IsObject()) {
|
||||
Nan::ThrowError(tr("Invalid argument"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto remote_host = Nan::Get(arguments, Nan::New<v8::String>("remote_host").ToLocalChecked()).ToLocalChecked();
|
||||
auto remote_port = Nan::Get(arguments, Nan::New<v8::String>("remote_port").ToLocalChecked()).ToLocalChecked();
|
||||
auto timeout = Nan::Get(arguments, Nan::New<v8::String>("timeout").ToLocalChecked()).ToLocalChecked();
|
||||
|
||||
auto callback = Nan::Get(arguments, Nan::New<v8::String>("callback").ToLocalChecked()).ToLocalChecked();
|
||||
|
||||
auto identity_key = Nan::Get(arguments, Nan::New<v8::String>("identity_key").ToLocalChecked()).ToLocalChecked();
|
||||
auto teamspeak = Nan::Get(arguments, Nan::New<v8::String>("teamspeak").ToLocalChecked()).ToLocalChecked();
|
||||
|
||||
if(!identity_key->IsString() && !identity_key->IsNullOrUndefined()) {
|
||||
Nan::ThrowError(tr("Invalid identity"));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!remote_host->IsString() || !remote_port->IsNumber()) {
|
||||
Nan::ThrowError(tr("Invalid argument host/port"));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!callback->IsFunction() ) {
|
||||
Nan::ThrowError(tr("Invalid callback"));
|
||||
return;
|
||||
}
|
||||
|
||||
unique_lock _disconnect_lock(this->disconnect_lock, defer_lock);
|
||||
if(!_disconnect_lock.try_lock_for(chrono::milliseconds(500))) {
|
||||
Nan::ThrowError(tr("failed to acquire disconnect lock"));
|
||||
return;
|
||||
}
|
||||
|
||||
this->callback_connect = make_unique<Nan::Callback>(callback.As<v8::Function>());
|
||||
|
||||
this->voice_connection->reset();
|
||||
this->protocol_handler->reset();
|
||||
if(identity_key->IsString()) {
|
||||
auto& identity = this->protocol_handler->get_identity_key();
|
||||
auto key = base64::decode(*Nan::Utf8String(identity_key->ToString(Nan::GetCurrentContext()).ToLocalChecked()));
|
||||
if(ecc_import((u_char*) key.data(), (unsigned long) key.length(), &identity) != CRYPT_OK) {
|
||||
Nan::ThrowError(tr("failed to import identity"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
auto& identity = this->protocol_handler->get_identity_key();
|
||||
prng_state rndState{};
|
||||
memset(&rndState, 0, sizeof(prng_state));
|
||||
int err;
|
||||
|
||||
if((err = ecc_make_key_ex(&rndState, find_prng("sprng"), &identity, <c_ecc_sets[5])) != CRYPT_OK) {
|
||||
Nan::ThrowError(tr("failed to generate ephemeral identity"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sockaddr_storage remote_address{};
|
||||
/* resolve address */
|
||||
{
|
||||
addrinfo hints{}, *result;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
|
||||
auto _remote_host = Nan::Utf8String(remote_host->ToString(Nan::GetCurrentContext()).ToLocalChecked());
|
||||
if(getaddrinfo(*_remote_host, nullptr, &hints, &result) != 0 || !result) {
|
||||
this->call_connect_result(this->errors.register_error(tr("failed to resolve hostname")));
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(&remote_address, result->ai_addr, result->ai_addrlen);
|
||||
freeaddrinfo(result);
|
||||
}
|
||||
switch(remote_address.ss_family) {
|
||||
case AF_INET:
|
||||
((sockaddr_in*) &remote_address)->sin_port = htons(remote_port->Int32Value(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
case AF_INET6:
|
||||
((sockaddr_in6*) &remote_address)->sin6_port = htons(remote_port->Int32Value(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
default:break;
|
||||
}
|
||||
|
||||
logger::info(category::connection, tr("Connecting to {}."), net::to_string(remote_address));
|
||||
this->socket = make_shared<UDPSocket>(remote_address);
|
||||
if(!this->socket->initialize()) {
|
||||
this->call_connect_result(this->errors.register_error("failed to initialize socket"));
|
||||
this->socket = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
this->socket->on_data = [&](const pipes::buffer_view& buffer) { this->protocol_handler->progress_packet(buffer); };
|
||||
if(teamspeak->IsBoolean() && teamspeak->BooleanValue(info.GetIsolate()))
|
||||
this->protocol_handler->server_type = server_type::TEAMSPEAK;
|
||||
this->protocol_handler->connect();
|
||||
}
|
||||
|
||||
NAN_METHOD(ServerConnection::_connected) {
|
||||
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->connected(info);
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(ServerConnection::_connect) {
|
||||
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->connect(info);
|
||||
}
|
||||
NAN_METHOD(ServerConnection::connected) {
|
||||
bool result = this->protocol_handler && this->protocol_handler->connection_state >= connection_state::CONNECTING && this->protocol_handler->connection_state < connection_state::DISCONNECTING;
|
||||
info.GetReturnValue().Set(result);
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(ServerConnection::_disconnect) {
|
||||
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->disconnect(info);
|
||||
}
|
||||
NAN_METHOD(ServerConnection::disconnect) {
|
||||
if(info.Length() != 2) {
|
||||
Nan::ThrowError("Invalid argument count");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!info[1]->IsFunction() || !info[0]->IsString()) {
|
||||
Nan::ThrowError("Invalid argument");
|
||||
return;
|
||||
}
|
||||
this->callback_disconnect = make_unique<Nan::Callback>(info[1].As<v8::Function>());
|
||||
if(!this->socket) {
|
||||
this->call_disconnect_result(0); /* this->errors.register_error("not connected") */
|
||||
return;
|
||||
}
|
||||
|
||||
if(this->protocol_handler) {
|
||||
this->protocol_handler->disconnect(*Nan::Utf8String(info[0]));
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(ServerConnection::_error_message) {
|
||||
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->error_message(info);
|
||||
}
|
||||
NAN_METHOD(ServerConnection::error_message) {
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("Invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
auto error = this->errors.get_message((ErrorHandler::error_id) info[0]->IntegerValue(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
info.GetReturnValue().Set(Nan::New<v8::String>(error).ToLocalChecked());
|
||||
}
|
||||
|
||||
//send_command(command: string, arguments: any[], switches: string[]);
|
||||
template <typename T>
|
||||
std::string _to_string(const T value) {
|
||||
std::string result(15, '\0');
|
||||
auto written = std::snprintf(&result[0], result.size(), "%.6f", value);
|
||||
if(written < 0) {
|
||||
log_warn(general, "Failed to format float value: {}; {}", value, written);
|
||||
return "0";
|
||||
}
|
||||
result.resize(written);
|
||||
return result;
|
||||
}
|
||||
|
||||
NAN_METHOD(ServerConnection::_send_command) {
|
||||
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->send_command(info);
|
||||
}
|
||||
|
||||
struct TS3VersionSettings {
|
||||
std::string build;
|
||||
std::string platform;
|
||||
std::string sign;
|
||||
};
|
||||
NAN_METHOD(ServerConnection::send_command) {
|
||||
if(!this->protocol_handler) {
|
||||
Nan::ThrowError("ServerConnection not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 3) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!info[0]->IsString() || !info[1]->IsArray() || !info[2]->IsArray()) {
|
||||
Nan::ThrowError("invalid argument type");
|
||||
return;
|
||||
}
|
||||
|
||||
auto begin = chrono::system_clock::now();
|
||||
auto command = info[0]->ToString(Nan::GetCurrentContext()).ToLocalChecked();
|
||||
auto arguments = info[1].As<v8::Array>();
|
||||
auto switches = info[2].As<v8::Array>();
|
||||
|
||||
ts::Command cmd(*Nan::Utf8String(command));
|
||||
for(size_t index = 0; index < arguments->Length(); index++) {
|
||||
auto object = arguments->Get((uint32_t) index);
|
||||
if(!object->IsObject()) {
|
||||
Nan::ThrowError(Nan::New<v8::String>("invalid parameter (" + to_string(index) + ")").ToLocalChecked());
|
||||
return;
|
||||
}
|
||||
|
||||
v8::Local<v8::Array> properties = object->ToObject(Nan::GetCurrentContext()).ToLocalChecked()->GetOwnPropertyNames(Nan::GetCurrentContext()).ToLocalChecked();
|
||||
for(uint32_t i = 0; i < properties->Length(); i++) {
|
||||
auto key = properties->Get(i)->ToString(Nan::GetCurrentContext()).ToLocalChecked();
|
||||
auto value = object->ToObject(Nan::GetCurrentContext()).ToLocalChecked()->Get(Nan::GetCurrentContext(), key).ToLocalChecked();
|
||||
|
||||
string key_string = *Nan::Utf8String(key);
|
||||
if(value->IsInt32())
|
||||
cmd[index][key_string] = value->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
else if(value->IsNumber() || value->IsNumberObject())
|
||||
cmd[index][key_string] = _to_string<double>(value->NumberValue(Nan::GetCurrentContext()).FromMaybe(0)); /* requires our own conversation because node overrides stuff to 0,0000*/
|
||||
else if(value->IsString())
|
||||
cmd[index][key_string] = *Nan::Utf8String(value->ToString(Nan::GetCurrentContext()).ToLocalChecked());
|
||||
else if(value->IsBoolean() || value->IsBooleanObject())
|
||||
cmd[index][key_string] = value->BooleanValue(info.GetIsolate());
|
||||
else if(value->IsNullOrUndefined())
|
||||
cmd[index][key_string] = "";
|
||||
else {
|
||||
Nan::ThrowError(Nan::New<v8::String>("invalid parameter (" + to_string(index) + ":" + key_string + ")").ToLocalChecked());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for(size_t index = 0; index < switches->Length(); index++) {
|
||||
auto object = switches->Get((uint32_t) index);
|
||||
if(!object->IsString()) {
|
||||
Nan::ThrowError(Nan::New<v8::String>("invalid switch (" + to_string(index) + ")").ToLocalChecked());
|
||||
return;
|
||||
}
|
||||
|
||||
cmd.enableParm(*Nan::Utf8String(object));
|
||||
}
|
||||
|
||||
if(this->protocol_handler->server_type == server_type::TEAMSPEAK) {
|
||||
if(cmd.command() == "clientinit") {
|
||||
/* If we have a return code here some strange stuff happens (Ghost client) */
|
||||
if(cmd[0].has("return_code"))
|
||||
cmd["return_code"] = nullptr;
|
||||
|
||||
TS3VersionSettings ts_version{};
|
||||
#ifdef WIN32
|
||||
/*
|
||||
ts_version = {
|
||||
"0.0.1 [Build: 1549713549]",
|
||||
"Linux",
|
||||
"7XvKmrk7uid2ixHFeERGqcC8vupeQqDypLtw2lY9slDNPojEv//F47UaDLG+TmVk4r6S0TseIKefzBpiRtLDAQ=="
|
||||
};
|
||||
*/
|
||||
|
||||
ts_version = {
|
||||
"3.?.? [Build: 5680278000]",
|
||||
"Windows",
|
||||
"DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg=="
|
||||
};
|
||||
#else
|
||||
/*
|
||||
ts_version = {
|
||||
"0.0.1 [Build: 1549713549]",
|
||||
"Linux",
|
||||
"7XvKmrk7uid2ixHFeERGqcC8vupeQqDypLtw2lY9slDNPojEv//F47UaDLG+TmVk4r6S0TseIKefzBpiRtLDAQ=="
|
||||
};
|
||||
*/
|
||||
ts_version = {
|
||||
"3.?.? [Build: 5680278000]",
|
||||
"Linux",
|
||||
"Hjd+N58Gv3ENhoKmGYy2bNRBsNNgm5kpiaQWxOj5HN2DXttG6REjymSwJtpJ8muC2gSwRuZi0R+8Laan5ts5CQ=="
|
||||
};
|
||||
#endif
|
||||
if(std::getenv("teaclient_ts3_build") && std::getenv("teaclient_ts3_platform") && std::getenv("teaclient_ts3_sign")) {
|
||||
ts_version = {
|
||||
std::getenv("teaclient_ts3_build"),
|
||||
std::getenv("teaclient_ts3_platform"),
|
||||
std::getenv("teaclient_ts3_sign")
|
||||
};
|
||||
}
|
||||
|
||||
cmd["client_version"] = ts_version.build;
|
||||
cmd["client_platform"] = ts_version.platform;
|
||||
cmd["client_version_sign"] = ts_version.sign;
|
||||
cmd[strobf("hwid").string()] = system_uuid(); /* we dont want anybody to patch this out */
|
||||
}
|
||||
}
|
||||
this->protocol_handler->send_command(cmd);
|
||||
auto end = chrono::system_clock::now();
|
||||
}
|
||||
NAN_METHOD(ServerConnection::_send_voice_data) {
|
||||
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->send_voice_data(info);
|
||||
}
|
||||
|
||||
NAN_METHOD(ServerConnection::send_voice_data) {
|
||||
if(!this->protocol_handler) {
|
||||
Nan::ThrowError("ServerConnection not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 3) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!info[0]->IsUint8Array() || !info[1]->IsInt32() || !info[2]->IsBoolean()) {
|
||||
Nan::ThrowError("invalid argument type");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
auto voice_data = info[0].As<v8::Uint8Array>()->Buffer();
|
||||
this->send_voice_data(voice_data->GetContents().Data(), voice_data->GetContents().ByteLength(), (uint8_t) info[1]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0), info[2]->BooleanValue(info.GetIsolate()));
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(ServerConnection::_send_voice_data_raw) {
|
||||
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->send_voice_data_raw(info);
|
||||
}
|
||||
|
||||
NAN_METHOD(ServerConnection::send_voice_data_raw) {
|
||||
//send_voice_data_raw(buffer: Float32Array, channels: number, sample_rate: number, header: boolean);
|
||||
|
||||
if(info.Length() != 4) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!info[0]->IsFloat32Array() || !info[1]->IsInt32() || !info[2]->IsInt32()) {
|
||||
Nan::ThrowError("invalid argument type");
|
||||
return;
|
||||
}
|
||||
|
||||
auto channels = info[1]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
auto sample_rate = info[2]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
auto flag_head = info[2]->BooleanValue(info.GetIsolate());
|
||||
|
||||
auto voice_data = info[0].As<v8::Float32Array>()->Buffer();
|
||||
auto vs = this->voice_connection ? this->voice_connection->voice_sender() : nullptr;
|
||||
if(vs) vs->send_data(voice_data->GetContents().Data(), voice_data->GetContents().ByteLength() / (4 * channels), sample_rate, channels);
|
||||
}
|
||||
|
||||
#ifdef SHUFFLE_VOICE
|
||||
static shared_ptr<ts::protocol::ClientPacket> shuffle_cached_packet;
|
||||
#endif
|
||||
void ServerConnection::send_voice_data(const void *buffer, size_t buffer_length, uint8_t codec, bool head) {
|
||||
auto _buffer = pipes::buffer{ts::protocol::ClientPacket::META_SIZE + buffer_length + 3};
|
||||
auto packet = ts::protocol::ClientPacket::from_buffer(_buffer);
|
||||
memset(&_buffer[ts::protocol::ClientPacket::META_MAC_SIZE], 0, ts::protocol::ClientPacket::META_HEADER_SIZE); /* reset all header data */
|
||||
packet->type(ts::protocol::PacketTypeInfo::Voice);
|
||||
|
||||
auto data_buffer = packet->data();
|
||||
le2be16(this->voice_packet_id++, &data_buffer[0]); /* set voice packet id */
|
||||
data_buffer[2] = (uint8_t) codec; /* set voice codec */
|
||||
if(buffer_length > 0 && buffer)
|
||||
memcpy(&data_buffer[3], buffer, buffer_length);
|
||||
|
||||
if(head) /* head packet */
|
||||
packet->enable_flag(ts::protocol::PacketFlag::Compressed);
|
||||
packet->enable_flag(ts::protocol::PacketFlag::Unencrypted);
|
||||
|
||||
#ifdef FUZZ_VOICE
|
||||
if((rand() % 10) < 2) {
|
||||
log_info(category::connection, tr("Dropping voice packet"));
|
||||
} else {
|
||||
this->protocol_handler->send_packet(packet);
|
||||
}
|
||||
#elif defined(SHUFFLE_VOICE)
|
||||
if(shuffle_cached_packet) {
|
||||
this->protocol_handler->send_packet(packet);
|
||||
this->protocol_handler->send_packet(std::exchange(shuffle_cached_packet, nullptr));
|
||||
} else {
|
||||
shuffle_cached_packet = packet;
|
||||
}
|
||||
#else
|
||||
this->protocol_handler->send_packet(std::shared_ptr<ts::protocol::ClientPacket>(packet.release()));
|
||||
#endif
|
||||
}
|
||||
|
||||
void ServerConnection::close_connection() {
|
||||
lock_guard lock(this->disconnect_lock);
|
||||
if(this->socket && this_thread::get_id() == this->socket->io_thread().get_id()) {
|
||||
logger::debug(category::connection, tr("close_connection() called in IO thread. Closing connection within event loop!"));
|
||||
if(!this->event_loop_execute_connection_close) {
|
||||
this->event_loop_execute_connection_close = true;
|
||||
this->event_condition.notify_one();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this->event_loop_execute_connection_close = false;
|
||||
if(this->socket)
|
||||
this->socket->finalize();
|
||||
if(this->protocol_handler)
|
||||
this->protocol_handler->do_close_connection();
|
||||
this->socket = nullptr;
|
||||
this->call_disconnect_result.call(0, true);
|
||||
}
|
||||
|
||||
void ServerConnection::execute_tick() {
|
||||
if(this->protocol_handler)
|
||||
this->protocol_handler->execute_tick();
|
||||
}
|
||||
|
||||
void ServerConnection::_execute_callback_commands() {
|
||||
unique_ptr<ts::Command> next_command;
|
||||
v8::Local<v8::Function> callback;
|
||||
|
||||
while(true) {
|
||||
{
|
||||
lock_guard lock(this->pending_commands_lock);
|
||||
if(this->pending_commands.empty())
|
||||
return;
|
||||
next_command = move(this->pending_commands.front());
|
||||
this->pending_commands.pop_front();
|
||||
}
|
||||
if(!next_command)
|
||||
continue;
|
||||
|
||||
if(callback.IsEmpty()) {
|
||||
callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_command").ToLocalChecked()).ToLocalChecked().As<v8::Function>();
|
||||
if(callback.IsEmpty()) {
|
||||
logger::warn(category::connection, tr("Missing command callback! Dropping commands."));
|
||||
lock_guard lock(this->pending_commands_lock);
|
||||
this->pending_commands.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
v8::Local<v8::Value> arguments[3];
|
||||
arguments[0] = Nan::New<v8::String>(next_command->command()).ToLocalChecked();
|
||||
|
||||
auto parameters = Nan::New<v8::Array>((int) next_command->bulkCount());
|
||||
for(size_t index = 0; index < next_command->bulkCount(); index++) {
|
||||
auto object = Nan::New<v8::Object>();
|
||||
auto& bulk = next_command->operator[](index);
|
||||
|
||||
for(const auto& key : bulk.keys())
|
||||
Nan::Set(object, Nan::New<v8::String>(key).ToLocalChecked(), Nan::New<v8::String>(bulk[key].string()).ToLocalChecked());
|
||||
|
||||
parameters->Set((uint32_t) index, object);
|
||||
}
|
||||
arguments[1] = parameters;
|
||||
|
||||
|
||||
auto switched = Nan::New<v8::Array>((int) next_command->parms().size());
|
||||
for(size_t index = 0; index < next_command->parms().size(); index++) {
|
||||
auto& key = next_command->parms()[index];
|
||||
parameters->Set((uint32_t) index, Nan::New<v8::String>(key).ToLocalChecked());
|
||||
}
|
||||
arguments[2] = switched;
|
||||
|
||||
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 3, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
void ServerConnection::_execute_callback_voice() {
|
||||
unique_ptr<VoicePacket> next_packet;
|
||||
v8::Local<v8::Function> callback;
|
||||
|
||||
while(true) {
|
||||
{
|
||||
lock_guard lock(this->pending_voice_lock);
|
||||
if(this->pending_voice.empty())
|
||||
return;
|
||||
next_packet = move(this->pending_voice.front());
|
||||
this->pending_voice.pop_front();
|
||||
}
|
||||
if(!next_packet)
|
||||
continue;
|
||||
|
||||
if(callback.IsEmpty()) {
|
||||
auto _callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_voice_data").ToLocalChecked()).ToLocalChecked();
|
||||
if(_callback.IsEmpty() || _callback->IsUndefined()) {
|
||||
logger::warn(category::audio, tr("Missing voice callback! Dropping packets!"));
|
||||
lock_guard lock(this->pending_voice_lock);
|
||||
this->pending_voice.clear();
|
||||
return;
|
||||
}
|
||||
callback = _callback.As<v8::Function>();
|
||||
}
|
||||
|
||||
v8::Local<v8::Value> arguments[5];
|
||||
|
||||
v8::Local<v8::ArrayBuffer> buffer = v8::ArrayBuffer::New(
|
||||
Nan::GetCurrentContext()->GetIsolate(),
|
||||
next_packet->voice_data.length()
|
||||
);
|
||||
memcpy(buffer->GetContents().Data(), next_packet->voice_data.data_ptr(), next_packet->voice_data.length());
|
||||
arguments[0] = v8::Uint8Array::New(buffer, 0, buffer->ByteLength());
|
||||
arguments[1] = Nan::New<v8::Integer>(next_packet->client_id);
|
||||
arguments[2] = Nan::New<v8::Integer>(next_packet->codec_id);
|
||||
arguments[3] = Nan::New<v8::Boolean>(next_packet->flag_head);
|
||||
arguments[4] = Nan::New<v8::Integer>(next_packet->packet_id);
|
||||
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 5, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
void ServerConnection::_execute_callback_disconnect(const std::string &reason) {
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_disconnect").ToLocalChecked()).ToLocalChecked().As<v8::Function>();
|
||||
if(callback.IsEmpty()) {
|
||||
cout << "Missing disconnect callback!" << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
v8::Local<v8::Value> arguments[1];
|
||||
arguments[0] = Nan::New<v8::String>(reason).ToLocalChecked();
|
||||
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, arguments);
|
||||
|
||||
}
|
||||
|
||||
NAN_METHOD(ServerConnection::_current_ping) {
|
||||
auto connection = ObjectWrap::Unwrap<ServerConnection>(info.Holder());
|
||||
auto& phandler = connection->protocol_handler;
|
||||
if(phandler)
|
||||
info.GetReturnValue().Set((uint32_t) chrono::floor<microseconds>(phandler->current_ping()).count());
|
||||
else
|
||||
info.GetReturnValue().Set(-1);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <nan.h>
|
||||
#include <NanEventCallback.h>
|
||||
#include <condition_variable>
|
||||
#include <pipes/buffer.h>
|
||||
|
||||
namespace ts {
|
||||
class Command;
|
||||
}
|
||||
|
||||
namespace tc {
|
||||
namespace connection {
|
||||
namespace server_type {
|
||||
enum value : uint8_t {
|
||||
UNKNOWN,
|
||||
TEASPEAK,
|
||||
TEAMSPEAK
|
||||
};
|
||||
}
|
||||
|
||||
class UDPSocket;
|
||||
class ProtocolHandler;
|
||||
class VoiceConnection;
|
||||
|
||||
class ErrorHandler {
|
||||
public:
|
||||
typedef int16_t error_id;
|
||||
static constexpr error_id error_success = 0;
|
||||
static constexpr error_id error_varianz = 5;
|
||||
|
||||
|
||||
std::array<std::string, error_varianz> error_messages;
|
||||
error_id error_index = 0;
|
||||
|
||||
std::string get_message(error_id);
|
||||
error_id register_error(const std::string& /* message */);
|
||||
};
|
||||
|
||||
class ServerConnection : public Nan::ObjectWrap {
|
||||
friend class ProtocolHandler;
|
||||
public:
|
||||
static NAN_MODULE_INIT(Init);
|
||||
|
||||
static NAN_METHOD(new_instance);
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
ServerConnection();
|
||||
~ServerConnection() override;
|
||||
|
||||
NAN_METHOD(connect);
|
||||
NAN_METHOD(connected);
|
||||
NAN_METHOD(disconnect);
|
||||
|
||||
NAN_METHOD(error_message);
|
||||
NAN_METHOD(send_command);
|
||||
NAN_METHOD(send_voice_data);
|
||||
void send_voice_data(const void* /* buffer */, size_t /* buffer length */, uint8_t /* codec */, bool /* head */);
|
||||
|
||||
NAN_METHOD(send_voice_data_raw);
|
||||
|
||||
void initialize();
|
||||
void finalize();
|
||||
|
||||
void close_connection(); /* directly closes connection without notify etc */
|
||||
std::shared_ptr<UDPSocket> get_socket() { return this->socket; }
|
||||
private:
|
||||
struct VoicePacket {
|
||||
pipes::buffer voice_data;
|
||||
uint16_t client_id;
|
||||
uint16_t packet_id;
|
||||
uint8_t codec_id;
|
||||
bool flag_head;
|
||||
};
|
||||
|
||||
static NAN_METHOD(_connect);
|
||||
static NAN_METHOD(_connected);
|
||||
static NAN_METHOD(_disconnect);
|
||||
static NAN_METHOD(_send_command);
|
||||
static NAN_METHOD(_send_voice_data);
|
||||
static NAN_METHOD(_send_voice_data_raw);
|
||||
static NAN_METHOD(_error_message);
|
||||
static NAN_METHOD(_current_ping);
|
||||
|
||||
std::unique_ptr<Nan::Callback> callback_connect;
|
||||
std::unique_ptr<Nan::Callback> callback_disconnect;
|
||||
Nan::callback_t<ErrorHandler::error_id> call_connect_result;
|
||||
Nan::callback_t<ErrorHandler::error_id> call_disconnect_result;
|
||||
Nan::callback_t<> execute_pending_commands;
|
||||
Nan::callback_t<> execute_pending_voice;
|
||||
Nan::callback_t<std::string> execute_callback_disconnect;
|
||||
|
||||
ErrorHandler errors;
|
||||
std::shared_ptr<UDPSocket> socket;
|
||||
std::unique_ptr<ProtocolHandler> protocol_handler;
|
||||
std::recursive_timed_mutex disconnect_lock;
|
||||
|
||||
std::thread event_thread;
|
||||
std::mutex event_lock;
|
||||
std::condition_variable event_condition;
|
||||
bool event_loop_exit = false; /* set to true if we want to exit */
|
||||
void event_loop();
|
||||
|
||||
bool event_loop_execute_connection_close = false;
|
||||
|
||||
std::chrono::system_clock::time_point next_tick;
|
||||
std::chrono::system_clock::time_point next_resend;
|
||||
void execute_tick();
|
||||
|
||||
void schedule_resend(const std::chrono::system_clock::time_point& /* timestamp */);
|
||||
|
||||
std::mutex pending_commands_lock;
|
||||
std::deque<std::unique_ptr<ts::Command>> pending_commands;
|
||||
void _execute_callback_commands();
|
||||
|
||||
std::mutex pending_voice_lock;
|
||||
std::deque<std::unique_ptr<VoicePacket>> pending_voice;
|
||||
void _execute_callback_voice();
|
||||
uint16_t voice_packet_id = 0;
|
||||
|
||||
void _execute_callback_disconnect(const std::string&);
|
||||
|
||||
std::shared_ptr<VoiceConnection> voice_connection;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
#include "Socket.h"
|
||||
#include "../logger.h"
|
||||
#include <thread>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
|
||||
#ifdef WIN32
|
||||
#include <WinSock2.h>
|
||||
typedef int socklen_t;
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <netinet/ip.h>
|
||||
#endif
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::connection;
|
||||
|
||||
UDPSocket::UDPSocket(const sockaddr_storage &address) {
|
||||
memcpy(&this->_remote_address, &address, sizeof(sockaddr_storage));
|
||||
}
|
||||
|
||||
UDPSocket::~UDPSocket() {
|
||||
this->finalize();
|
||||
}
|
||||
|
||||
bool UDPSocket::initialize() {
|
||||
if(this->file_descriptor > 0)
|
||||
return false;
|
||||
|
||||
this->file_descriptor = socket(this->_remote_address.ss_family, SOCK_DGRAM, 0);
|
||||
if(this->file_descriptor < 2) {
|
||||
this->file_descriptor = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: Make configurable
|
||||
*/
|
||||
//uint8_t value = IPTOS_DSCP_EF;
|
||||
//if(setsockopt(this->file_descriptor, IPPROTO_IP, IP_TOS, &value, sizeof(value)) < 0)
|
||||
// log_warn(category::connection, "Failed to set TOS high priority on socket");
|
||||
|
||||
this->io_base = event_base_new();
|
||||
if(!this->io_base) { /* may too many file descriptors already open */
|
||||
this->finalize();
|
||||
return false;
|
||||
}
|
||||
this->event_read = event_new(this->io_base, this->file_descriptor, EV_READ | EV_PERSIST, &UDPSocket::_callback_read, this);
|
||||
this->event_write = event_new(this->io_base, this->file_descriptor, EV_WRITE, &UDPSocket::_callback_write, this);
|
||||
event_add(this->event_read, nullptr);
|
||||
|
||||
this->_io_thread = thread(&UDPSocket::_io_execute, this);
|
||||
#ifdef WIN32
|
||||
//TODO set thread name
|
||||
#else
|
||||
auto handle = this->_io_thread.native_handle();
|
||||
pthread_setname_np(handle, "UDPSocket loop");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
void UDPSocket::finalize() {
|
||||
if(this->file_descriptor == 0)
|
||||
return;
|
||||
|
||||
unique_lock lock(this->io_lock);
|
||||
auto event_read = this->event_read, event_write = this->event_write;
|
||||
auto io_base = this->io_base;
|
||||
this->io_base = nullptr;
|
||||
this->event_read = nullptr;
|
||||
this->event_write = nullptr;
|
||||
lock.unlock();
|
||||
|
||||
assert(this_thread::get_id() != this->_io_thread.get_id());
|
||||
if(event_read)
|
||||
event_del_block(event_read);
|
||||
if(event_write)
|
||||
event_del_block(event_write);
|
||||
|
||||
if(event_write)
|
||||
event_del(event_write);
|
||||
if(event_read)
|
||||
event_del(event_read);
|
||||
|
||||
if(io_base) {
|
||||
timeval seconds{1, 0};
|
||||
event_base_loopexit(io_base, &seconds);
|
||||
event_base_loopexit(io_base, nullptr);
|
||||
}
|
||||
|
||||
if(this->_io_thread.joinable())
|
||||
this->_io_thread.join();
|
||||
|
||||
if(io_base)
|
||||
event_base_free(io_base);
|
||||
|
||||
#ifdef WIN32
|
||||
if(::closesocket(this->file_descriptor) != 0) {
|
||||
#else
|
||||
if(::close(this->file_descriptor) != 0) {
|
||||
#endif
|
||||
if(errno != EBADF)
|
||||
logger::warn(category::socket, tr("Failed to close file descriptor ({}/{})"), to_string(errno), strerror(errno));
|
||||
}
|
||||
this->file_descriptor = 0;
|
||||
}
|
||||
|
||||
void UDPSocket::_callback_write(evutil_socket_t fd, short, void *_ptr_socket) {
|
||||
((UDPSocket*) _ptr_socket)->callback_write(fd);
|
||||
}
|
||||
|
||||
void UDPSocket::_callback_read(evutil_socket_t fd, short, void *_ptr_socket) {
|
||||
((UDPSocket*) _ptr_socket)->callback_read(fd);
|
||||
}
|
||||
|
||||
void UDPSocket::_io_execute(void *_ptr_socket) {
|
||||
((UDPSocket*) _ptr_socket)->io_execute();
|
||||
}
|
||||
|
||||
void UDPSocket::io_execute() {
|
||||
while(this->io_base) {
|
||||
event_base_loop(this->io_base, 0);
|
||||
}
|
||||
}
|
||||
void UDPSocket::callback_read(evutil_socket_t fd) {
|
||||
sockaddr source_address{};
|
||||
socklen_t source_address_length = sizeof(sockaddr);
|
||||
|
||||
ssize_t buffer_length = 1600; /* IPv6 MTU is ~1.5k */
|
||||
char buffer[1600];
|
||||
|
||||
buffer_length = recvfrom(fd, (char*) buffer, buffer_length, 0, &source_address, &source_address_length);
|
||||
if(buffer_length <= 0) {
|
||||
if(errno == EAGAIN)
|
||||
return;
|
||||
|
||||
logger::warn(category::socket, tr("Failed to receive data: {}/{}"), errno, strerror(errno));
|
||||
return; /* this should never happen! */
|
||||
}
|
||||
|
||||
if(this->on_data)
|
||||
this->on_data(pipes::buffer_view{buffer, (size_t) buffer_length});
|
||||
}
|
||||
|
||||
void UDPSocket::callback_write(evutil_socket_t fd) {
|
||||
unique_lock lock(this->io_lock);
|
||||
if(this->write_queue.empty())
|
||||
return;
|
||||
|
||||
auto buffer = this->write_queue.front();
|
||||
this->write_queue.pop_front();
|
||||
lock.unlock();
|
||||
|
||||
auto written = sendto(fd, buffer.data_ptr<char>(), buffer.length(), 0, (sockaddr*) &this->_remote_address, sizeof(this->_remote_address));
|
||||
if(written != buffer.length()) {
|
||||
if(errno == EAGAIN) {
|
||||
lock.lock();
|
||||
this->write_queue.push_front(buffer);
|
||||
if(this->event_write)
|
||||
event_add(this->event_write, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
return; /* this should never happen! */
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
if(!this->write_queue.empty() && this->event_write)
|
||||
event_add(this->event_write, nullptr);
|
||||
}
|
||||
|
||||
void UDPSocket::send_message(const pipes::buffer_view &buffer) {
|
||||
auto buf = buffer.own_buffer();
|
||||
|
||||
unique_lock lock(this->io_lock);
|
||||
this->write_queue.push_back(buf);
|
||||
if(this->event_write)
|
||||
event_add(this->event_write, nullptr);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <thread>
|
||||
#include <event.h>
|
||||
#include <memory>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
|
||||
#include <pipes/buffer.h>
|
||||
#include <functional>
|
||||
|
||||
#ifndef WIN32
|
||||
#include <netinet/in.h>
|
||||
#else
|
||||
#include <WinSock2.h>
|
||||
#endif
|
||||
|
||||
namespace tc {
|
||||
namespace connection {
|
||||
class UDPSocket {
|
||||
public:
|
||||
UDPSocket(const sockaddr_storage& /* target */);
|
||||
~UDPSocket();
|
||||
|
||||
const sockaddr_storage& remote_address() { return this->_remote_address; }
|
||||
|
||||
bool initialize();
|
||||
void finalize();
|
||||
|
||||
void send_message(const pipes::buffer_view& /* message */);
|
||||
|
||||
std::function<void(const pipes::buffer_view& /* message */)> on_data;
|
||||
|
||||
const std::thread& io_thread() { return this->_io_thread; }
|
||||
private:
|
||||
static void _io_execute(void *_ptr_socket);
|
||||
static void _callback_read(evutil_socket_t, short, void*);
|
||||
static void _callback_write(evutil_socket_t, short, void*);
|
||||
|
||||
void io_execute();
|
||||
void callback_read(evutil_socket_t);
|
||||
void callback_write(evutil_socket_t);
|
||||
|
||||
sockaddr_storage _remote_address;
|
||||
|
||||
int file_descriptor = 0;
|
||||
|
||||
std::recursive_mutex io_lock;
|
||||
std::thread _io_thread;
|
||||
event_base* io_base = nullptr;
|
||||
|
||||
event* event_read = nullptr;
|
||||
event* event_write = nullptr;
|
||||
|
||||
std::deque<pipes::buffer> write_queue;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
//
|
||||
// Created by wolverindev on 19.06.19.
|
||||
//
|
||||
|
||||
#include "AudioEventLoop.h"
|
||||
using namespace tc;
|
||||
|
||||
event::EventExecutor* audio::decode_event_loop = nullptr;
|
||||
event::EventExecutor* audio::encode_event_loop = nullptr;
|
||||
|
||||
void audio::init_event_loops() {
|
||||
audio::shutdown_event_loops(); /* just to ensure */
|
||||
|
||||
audio::decode_event_loop = new event::EventExecutor("a en/decode ");
|
||||
audio::encode_event_loop = audio::decode_event_loop;
|
||||
|
||||
audio::decode_event_loop->initialize(2);
|
||||
}
|
||||
|
||||
void audio::shutdown_event_loops() {
|
||||
if(audio::decode_event_loop) {
|
||||
delete audio::decode_event_loop;
|
||||
audio::decode_event_loop = nullptr;
|
||||
}
|
||||
audio::encode_event_loop = nullptr;
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../EventLoop.h"
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
extern event::EventExecutor* encode_event_loop;
|
||||
extern event::EventExecutor* decode_event_loop;
|
||||
|
||||
extern void init_event_loops();
|
||||
extern void shutdown_event_loops();
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
#include "AudioSender.h"
|
||||
#include "VoiceConnection.h"
|
||||
#include "../ServerConnection.h"
|
||||
#include "../../logger.h"
|
||||
#include "AudioEventLoop.h"
|
||||
#include "../../audio/AudioMerger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc;
|
||||
using namespace tc::audio;
|
||||
using namespace tc::audio::codec;
|
||||
using namespace tc::connection;
|
||||
|
||||
VoiceSender::VoiceSender(tc::connection::VoiceConnection *handle) : handle(handle) {}
|
||||
|
||||
VoiceSender::~VoiceSender() {
|
||||
audio::encode_event_loop->cancel(static_pointer_cast<event::EventEntry>(this->_ref.lock()));
|
||||
this->clear_buffer(); /* buffer might be accessed within encode_raw_frame, but this could not be trigered while this will be deallocated! */
|
||||
}
|
||||
|
||||
bool VoiceSender::initialize_codec(std::string& error, connection::codec::value codec, size_t channels, size_t rate, bool reset_encoder) {
|
||||
auto& data = this->codec[codec];
|
||||
bool new_allocated = !data;
|
||||
if(new_allocated) {
|
||||
data = make_unique<AudioCodec>();
|
||||
data->packet_counter = 0;
|
||||
data->last_packet = chrono::system_clock::now();
|
||||
}
|
||||
|
||||
auto info = codec::get_info(codec);
|
||||
if(!info || !info->supported) {
|
||||
if(new_allocated)
|
||||
log_error(category::voice_connection, tr("Tried to send voice packet but we dont support the current codec ({})"), codec);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!data->converter) {
|
||||
data->converter = info->new_converter(error);
|
||||
if(!data->converter)
|
||||
return false;
|
||||
} else if(reset_encoder) {
|
||||
data->converter->reset_encoder();
|
||||
}
|
||||
|
||||
if(!data->resampler || data->resampler->input_rate() != rate)
|
||||
data->resampler = make_shared<AudioResampler>(rate, data->converter->sample_rate(), data->converter->channels());
|
||||
|
||||
if(!data->resampler->valid()) {
|
||||
error = "resampler is invalid";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VoiceSender::set_voice_send_enabled(bool flag) {
|
||||
this->voice_send_enabled = flag;
|
||||
}
|
||||
|
||||
void VoiceSender::send_data(const void *data, size_t samples, size_t rate, size_t channels) {
|
||||
unique_lock lock(this->_execute_lock);
|
||||
if(!this->handle) {
|
||||
log_warn(category::voice_connection, tr("Dropping raw audio frame because of an invalid handle."));
|
||||
return;
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
if(!this->voice_send_enabled) {
|
||||
log_warn(category::voice_connection, tr("Dropping raw audio frame because voice sending has been disabled!"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto frame = make_unique<AudioFrame>();
|
||||
frame->sample_rate = rate;
|
||||
frame->channels = channels;
|
||||
frame->buffer = pipes::buffer{(void*) data, samples * channels * 4};
|
||||
frame->timestamp = chrono::system_clock::now();
|
||||
|
||||
{
|
||||
lock_guard buffer_lock(this->raw_audio_buffer_lock);
|
||||
this->raw_audio_buffers.push_back(move(frame));
|
||||
}
|
||||
|
||||
audio::encode_event_loop->schedule(static_pointer_cast<event::EventEntry>(this->_ref.lock()));
|
||||
}
|
||||
|
||||
void VoiceSender::send_stop() {
|
||||
unique_lock lock(this->_execute_lock);
|
||||
if(!this->handle) {
|
||||
log_warn(category::voice_connection, tr("Dropping audio end frame because of an invalid handle."));
|
||||
return;
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
|
||||
auto frame = make_unique<AudioFrame>();
|
||||
frame->sample_rate = 0;
|
||||
frame->channels = 0;
|
||||
frame->buffer = pipes::buffer{nullptr, 0};
|
||||
frame->timestamp = chrono::system_clock::now();
|
||||
|
||||
{
|
||||
lock_guard buffer_lock(this->raw_audio_buffer_lock);
|
||||
this->raw_audio_buffers.push_back(move(frame));
|
||||
}
|
||||
|
||||
audio::encode_event_loop->schedule(static_pointer_cast<event::EventEntry>(this->_ref.lock()));
|
||||
}
|
||||
|
||||
void VoiceSender::finalize() {
|
||||
lock_guard lock(this->_execute_lock);
|
||||
this->handle = nullptr;
|
||||
}
|
||||
|
||||
void VoiceSender::event_execute(const std::chrono::system_clock::time_point &point) {
|
||||
static auto max_time = chrono::milliseconds(10);
|
||||
|
||||
bool reschedule = false;
|
||||
auto now = chrono::system_clock::now();
|
||||
while(true) {
|
||||
unique_lock buffer_lock(this->raw_audio_buffer_lock);
|
||||
if(this->raw_audio_buffers.empty())
|
||||
break;
|
||||
|
||||
if(chrono::system_clock::now() - now > max_time) {
|
||||
reschedule = true;
|
||||
break;
|
||||
}
|
||||
|
||||
auto entry = move(this->raw_audio_buffers.front());
|
||||
this->raw_audio_buffers.pop_front();
|
||||
buffer_lock.unlock();
|
||||
|
||||
//TODO: Drop too old buffers!
|
||||
this->encode_raw_frame(entry);
|
||||
}
|
||||
|
||||
if(reschedule) {
|
||||
log_warn(category::voice_connection, tr("Audio data decode will take longer than {} us. Enqueueing for later"), chrono::duration_cast<chrono::microseconds>(max_time).count());
|
||||
audio::decode_event_loop->schedule(static_pointer_cast<event::EventEntry>(this->_ref.lock()));
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceSender::encode_raw_frame(const std::unique_ptr<AudioFrame> &frame) {
|
||||
auto codec = this->_current_codec;
|
||||
auto& codec_data = this->codec[codec];
|
||||
|
||||
bool flag_head = true, flag_reset = true;
|
||||
if(codec_data) {
|
||||
if(codec_data->last_packet + chrono::seconds(1) < frame->timestamp)
|
||||
codec_data->packet_counter = 0;
|
||||
|
||||
flag_head = codec_data->packet_counter < 5;
|
||||
flag_reset = codec_data->packet_counter == 0;
|
||||
|
||||
codec_data->packet_counter++;
|
||||
codec_data->last_packet = frame->timestamp;
|
||||
}
|
||||
|
||||
if(frame->channels == 0 || frame->sample_rate == 0 || frame->buffer.empty()) {
|
||||
lock_guard lock(this->_execute_lock);
|
||||
if(!this->handle) {
|
||||
log_warn(category::voice_connection, tr("Dropping audio end because of an invalid handle."));
|
||||
return;
|
||||
}
|
||||
|
||||
if(codec_data)
|
||||
codec_data->packet_counter = 0;
|
||||
auto server = this->handle->handle();
|
||||
server->send_voice_data(this->_buffer, 0, codec, flag_head);
|
||||
return;
|
||||
}
|
||||
|
||||
string error;
|
||||
if(flag_reset) {
|
||||
log_trace(category::voice_connection, tr("Resetting encoder for voice sender"));
|
||||
}
|
||||
if(!this->initialize_codec(error, codec, frame->channels, frame->sample_rate, flag_reset)) {
|
||||
log_error(category::voice_connection, tr("Failed to initialize codec: {}"), error);
|
||||
return;
|
||||
}
|
||||
|
||||
auto merged_channel_byte_size = codec_data->converter->channels() * (frame->buffer.length() / frame->channels);
|
||||
auto estimated_resampled_byte_size = codec_data->resampler->estimated_output_size(merged_channel_byte_size);
|
||||
this->ensure_buffer(max(estimated_resampled_byte_size, merged_channel_byte_size));
|
||||
|
||||
auto codec_channels = codec_data->converter->channels();
|
||||
if(!audio::merge::merge_channels_interleaved(this->_buffer, codec_channels, frame->buffer.data_ptr(), frame->channels, frame->buffer.length() / frame->channels / 4)) {
|
||||
log_warn(category::voice_connection, tr("Failed to merge channels to output stream channel count! Dropping local voice packet"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto resampled_samples = codec_data->resampler->process(this->_buffer, this->_buffer, merged_channel_byte_size / codec_channels / 4);
|
||||
if(resampled_samples <= 0) {
|
||||
log_error(category::voice_connection, tr("Resampler returned {}"), resampled_samples);
|
||||
return;
|
||||
}
|
||||
|
||||
if(resampled_samples * codec_channels * 4 != codec_data->converter->bytes_per_frame()) {
|
||||
log_error(category::voice_connection,
|
||||
tr("Could not encode audio frame. Frame length is not equal to code frame length! Codec: {}, Packet: {}"),
|
||||
codec_data->converter->bytes_per_frame(), resampled_samples * codec_channels * 4
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
char _packet_buffer[512];
|
||||
auto encoded_bytes = codec_data->converter->encode(error, this->_buffer, _packet_buffer, 512);
|
||||
if(encoded_bytes <= 0) {
|
||||
log_error(category::voice_connection, tr("Failed to encode voice: {}"), error);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
lock_guard lock(this->_execute_lock);
|
||||
if(!this->handle) {
|
||||
log_warn(category::voice_connection, tr("Dropping audio frame because of an invalid handle."));
|
||||
return;
|
||||
}
|
||||
|
||||
auto server = this->handle->handle();
|
||||
server->send_voice_data(_packet_buffer, encoded_bytes, codec, flag_head);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include "VoiceClient.h"
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
namespace codec {
|
||||
class Converter;
|
||||
}
|
||||
|
||||
class AudioResampler;
|
||||
class AudioOutputSource;
|
||||
}
|
||||
|
||||
namespace connection {
|
||||
class VoiceConnection;
|
||||
class VoiceSender : private event::EventEntry {
|
||||
template<typename _Tp, typename _Up>
|
||||
friend inline std::shared_ptr<_Tp> std::static_pointer_cast(const std::shared_ptr<_Up>& __r) noexcept;
|
||||
friend class VoiceConnection;
|
||||
public:
|
||||
VoiceSender(VoiceConnection*);
|
||||
virtual ~VoiceSender();
|
||||
|
||||
codec::value get_codec() { return this->_current_codec; }
|
||||
void set_codec(codec::value value) { this->_current_codec = value; }
|
||||
|
||||
|
||||
void finalize();
|
||||
void send_data(const void* /* buffer */, size_t /* samples */, size_t /* sample rate */, size_t /* channels */);
|
||||
void send_stop();
|
||||
|
||||
void set_voice_send_enabled(bool /* flag */);
|
||||
private:
|
||||
std::weak_ptr<VoiceSender> _ref;
|
||||
VoiceConnection* handle;
|
||||
|
||||
struct AudioCodec {
|
||||
size_t packet_counter = 0;
|
||||
std::chrono::system_clock::time_point last_packet;
|
||||
|
||||
std::shared_ptr<audio::codec::Converter> converter;
|
||||
std::shared_ptr<audio::AudioResampler> resampler;
|
||||
};
|
||||
std::array<std::unique_ptr<AudioCodec>, codec::MAX + 1> codec{nullptr};
|
||||
|
||||
bool initialize_codec(std::string&, codec::value /* codec */, size_t /* channels */, size_t /* source sample rate */, bool /* reset decoder */);
|
||||
|
||||
codec::value _current_codec = codec::OPUS_VOICE;
|
||||
|
||||
std::mutex _execute_lock;
|
||||
|
||||
void* _buffer = nullptr;
|
||||
size_t _buffer_size = 0;
|
||||
void clear_buffer() {
|
||||
if(this->_buffer)
|
||||
::free(this->_buffer);
|
||||
this->_buffer = nullptr;
|
||||
this->_buffer_size = 0;
|
||||
}
|
||||
|
||||
void ensure_buffer(size_t length) {
|
||||
if(!this->_buffer || this->_buffer_size < length) {
|
||||
if(this->_buffer)
|
||||
::free(this->_buffer);
|
||||
this->_buffer = malloc(length);
|
||||
this->_buffer_size = length;
|
||||
}
|
||||
}
|
||||
|
||||
struct AudioFrame {
|
||||
pipes::buffer buffer;
|
||||
size_t sample_rate;
|
||||
size_t channels;
|
||||
|
||||
std::chrono::system_clock::time_point timestamp;
|
||||
};
|
||||
|
||||
std::mutex raw_audio_buffer_lock;
|
||||
std::deque<std::unique_ptr<AudioFrame>> raw_audio_buffers;
|
||||
|
||||
bool voice_send_enabled = false;
|
||||
|
||||
void encode_raw_frame(const std::unique_ptr<AudioFrame>&);
|
||||
void event_execute(const std::chrono::system_clock::time_point &point) override;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,569 +0,0 @@
|
||||
#include "VoiceClient.h"
|
||||
#include "../../audio/AudioOutput.h"
|
||||
#include "../../audio/codec/Converter.h"
|
||||
#include "../../audio/codec/OpusConverter.h"
|
||||
#include "../../audio/AudioMerger.h"
|
||||
#include "../../audio/js/AudioOutputStream.h"
|
||||
#include "AudioEventLoop.h"
|
||||
#include "../../logger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc;
|
||||
using namespace tc::audio::codec;
|
||||
using namespace tc::connection;
|
||||
|
||||
extern tc::audio::AudioOutput* global_audio_output;
|
||||
|
||||
#define DEBUG_PREMATURE_PACKETS
|
||||
|
||||
#ifdef WIN32
|
||||
#define _field_(name, value) value
|
||||
#else
|
||||
#define _field_(name, value) .name = value
|
||||
#endif
|
||||
const codec::condec_info codec::info[6] = {
|
||||
{
|
||||
_field_(supported, false),
|
||||
_field_(name, "speex_narrowband"),
|
||||
_field_(new_converter, nullptr)
|
||||
},
|
||||
{
|
||||
_field_(supported, false),
|
||||
_field_(name, "speex_wideband"),
|
||||
_field_(new_converter, nullptr)
|
||||
},
|
||||
{
|
||||
_field_(supported, false),
|
||||
_field_(name, "speex_ultra_wideband"),
|
||||
_field_(new_converter, nullptr)
|
||||
},
|
||||
{
|
||||
_field_(supported, false),
|
||||
_field_(name, "celt_mono"),
|
||||
_field_(new_converter, nullptr)
|
||||
},
|
||||
{
|
||||
_field_(supported, true),
|
||||
_field_(name, "opus_voice"),
|
||||
_field_(new_converter, [](string& error) -> shared_ptr<Converter> {
|
||||
auto result = make_shared<OpusConverter>(1, 48000, 960);
|
||||
if(!result->initialize(error, OPUS_APPLICATION_VOIP))
|
||||
return nullptr;
|
||||
return dynamic_pointer_cast<Converter>(result);
|
||||
})
|
||||
},
|
||||
{
|
||||
_field_(supported, true),
|
||||
_field_(name, "opus_music"),
|
||||
_field_(new_converter, [](string& error) -> shared_ptr<Converter> {
|
||||
auto result = make_shared<OpusConverter>(2, 48000, 960);
|
||||
if(!result->initialize(error, OPUS_APPLICATION_AUDIO))
|
||||
return nullptr;
|
||||
return dynamic_pointer_cast<Converter>(result);
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
void VoiceClientWrap::do_wrap(const v8::Local<v8::Object> &object) {
|
||||
this->Wrap(object);
|
||||
|
||||
auto handle = this->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
Nan::Set(object, Nan::New<v8::String>("client_id").ToLocalChecked(), Nan::New<v8::Number>(handle->client_id()));
|
||||
handle->on_state_changed = [&]{ this->call_state_changed(); };
|
||||
|
||||
this->call_state_changed = Nan::async_callback([&]{
|
||||
Nan::HandleScope scope;
|
||||
this->_call_state_changed();
|
||||
});
|
||||
}
|
||||
|
||||
void VoiceClientWrap::_call_state_changed() {
|
||||
auto handle = this->_handle.lock();
|
||||
if(!handle) {
|
||||
log_warn(category::voice_connection, tr("State changed on invalid handle!"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto state = handle->state();
|
||||
auto call_playback_callback = state == VoiceClient::state::playing && !this->_currently_playing;
|
||||
auto call_stopped_callback = state == VoiceClient::state::stopped && this->_currently_playing;
|
||||
|
||||
if(state == VoiceClient::state::stopped)
|
||||
this->_currently_playing = false;
|
||||
if(state == VoiceClient::state::playing)
|
||||
this->_currently_playing = true;
|
||||
|
||||
if(call_playback_callback) {
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_playback").ToLocalChecked()).ToLocalChecked();
|
||||
if(callback->IsFunction())
|
||||
callback.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
|
||||
}
|
||||
if(call_stopped_callback) {
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_stopped").ToLocalChecked()).ToLocalChecked();
|
||||
if(callback->IsFunction())
|
||||
callback.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
|
||||
}
|
||||
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_state_changed").ToLocalChecked()).ToLocalChecked();
|
||||
if(callback->IsFunction()) {
|
||||
v8::Local<v8::Value> argv[1] = {
|
||||
Nan::New<v8::Number>(state)
|
||||
};
|
||||
callback.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, argv);
|
||||
}
|
||||
}
|
||||
|
||||
NAN_MODULE_INIT(VoiceClientWrap::Init) {
|
||||
auto klass = Nan::New<v8::FunctionTemplate>(VoiceClientWrap::NewInstance);
|
||||
klass->SetClassName(Nan::New("VoiceConnection").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_state", VoiceClientWrap::_get_state);
|
||||
Nan::SetPrototypeMethod(klass, "get_volume", VoiceClientWrap::_get_volume);
|
||||
Nan::SetPrototypeMethod(klass, "set_volume", VoiceClientWrap::_set_volume);
|
||||
Nan::SetPrototypeMethod(klass, "abort_replay", VoiceClientWrap::_abort_replay);
|
||||
Nan::SetPrototypeMethod(klass, "get_stream", VoiceClientWrap::_get_stream);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceClientWrap::NewInstance) {
|
||||
if(!info.IsConstructCall())
|
||||
Nan::ThrowError("invalid invoke!");
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceClientWrap::_get_volume) {
|
||||
auto client = ObjectWrap::Unwrap<VoiceClientWrap>(info.Holder());
|
||||
|
||||
auto handle = client->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set(handle->get_volume());
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceClientWrap::_set_volume) {
|
||||
auto client = ObjectWrap::Unwrap<VoiceClientWrap>(info.Holder());
|
||||
|
||||
auto handle = client->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("Invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
handle->set_volume(info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
}
|
||||
NAN_METHOD(VoiceClientWrap::_abort_replay) {
|
||||
auto client = ObjectWrap::Unwrap<VoiceClientWrap>(info.Holder());
|
||||
|
||||
auto handle = client->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
handle->cancel_replay();
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceClientWrap::_get_state) {
|
||||
auto client = ObjectWrap::Unwrap<VoiceClientWrap>(info.Holder());
|
||||
|
||||
auto handle = client->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set(handle->state());
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(VoiceClientWrap::_get_stream) {
|
||||
auto client = ObjectWrap::Unwrap<VoiceClientWrap>(info.Holder());
|
||||
|
||||
auto handle = client->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
auto wrapper = new audio::AudioOutputStreamWrapper(handle->output_stream(), false);
|
||||
auto object = Nan::NewInstance(Nan::New(audio::AudioOutputStreamWrapper::constructor()), 0, nullptr).ToLocalChecked();
|
||||
wrapper->do_wrap(object);
|
||||
info.GetReturnValue().Set(object);
|
||||
}
|
||||
|
||||
VoiceClientWrap::VoiceClientWrap(const std::shared_ptr<VoiceClient>& client) : _handle(client) { }
|
||||
|
||||
VoiceClientWrap::~VoiceClientWrap() {}
|
||||
|
||||
VoiceClient::VoiceClient(const std::shared_ptr<VoiceConnection>&, uint16_t client_id) : _client_id(client_id) {
|
||||
this->output_source = global_audio_output->create_source();
|
||||
this->output_source->overflow_strategy = audio::overflow_strategy::ignore;
|
||||
this->output_source->max_latency = (size_t) ceil(this->output_source->sample_rate * 1);
|
||||
this->output_source->min_buffer = (size_t) ceil(this->output_source->sample_rate * 0.04);
|
||||
|
||||
this->output_source->on_underflow = [&]{
|
||||
if(this->_state == state::stopping)
|
||||
this->set_state(state::stopped);
|
||||
else if(this->_state != state::stopped) {
|
||||
if(this->_last_received_packet + chrono::seconds(1) < chrono::system_clock::now()) {
|
||||
this->set_state(state::stopped);
|
||||
log_warn(category::audio, tr("Client {} has a audio buffer underflow and not received any data for one second. Stopping replay."), this->_client_id);
|
||||
} else {
|
||||
if(this->_state != state::buffering) {
|
||||
log_warn(category::audio, tr("Client {} has a audio buffer underflow. Buffer again and try to replay prematured packets."), this->_client_id);
|
||||
this->set_state(state::buffering);
|
||||
}
|
||||
|
||||
play_premature_packets = true; /* try to replay any premature packets because we assume that the other packets got lost */
|
||||
audio::decode_event_loop->schedule(static_pointer_cast<event::EventEntry>(this->ref()));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
this->output_source->on_overflow = [&](size_t count){
|
||||
log_warn(category::audio, tr("Client {} has a audio buffer overflow of {}."), this->_client_id, count);
|
||||
};
|
||||
}
|
||||
|
||||
VoiceClient::~VoiceClient() {
|
||||
if(v8::Isolate::GetCurrent())
|
||||
this->finalize_js_object();
|
||||
else {
|
||||
assert(this->_js_handle.IsEmpty());
|
||||
}
|
||||
|
||||
this->output_source->on_underflow = nullptr; /* to ensure */
|
||||
global_audio_output->delete_source(this->output_source);
|
||||
}
|
||||
|
||||
void VoiceClient::initialize_js_object() {
|
||||
if(!this->_js_handle.IsEmpty())
|
||||
return;
|
||||
|
||||
auto object_wrap = new VoiceClientWrap(this->ref());
|
||||
auto object = Nan::NewInstance(Nan::New(VoiceClientWrap::constructor()), 0, nullptr).ToLocalChecked();
|
||||
Nan::TryCatch tc;
|
||||
object_wrap->do_wrap(object);
|
||||
if(tc.HasCaught()) {
|
||||
tc.ReThrow();
|
||||
return;
|
||||
}
|
||||
|
||||
this->_js_handle.Reset(Nan::GetCurrentContext()->GetIsolate(), object);
|
||||
}
|
||||
|
||||
void VoiceClient::finalize_js_object() {
|
||||
this->_js_handle.Reset();
|
||||
}
|
||||
|
||||
#define target_buffer_length 16384
|
||||
void VoiceClient::process_packet(uint16_t packet_id, const pipes::buffer_view& buffer, codec::value codec, bool head) {
|
||||
if(this->_volume == 0)
|
||||
return;
|
||||
|
||||
if(codec < 0 || codec > this->codec.size()) {
|
||||
log_warn(category::voice_connection, tr("Received voice packet from client {} with unknown codec ({})"), this->_client_id, codec);
|
||||
return;
|
||||
}
|
||||
|
||||
auto encoded_buffer = make_unique<EncodedBuffer>();
|
||||
encoded_buffer->packet_id = packet_id;
|
||||
encoded_buffer->codec = codec;
|
||||
encoded_buffer->receive_timestamp = chrono::system_clock::now();
|
||||
encoded_buffer->buffer = buffer.own_buffer();
|
||||
encoded_buffer->head = head;
|
||||
|
||||
this->_last_received_packet = encoded_buffer->receive_timestamp;
|
||||
{
|
||||
lock_guard lock(this->audio_decode_queue_lock);
|
||||
this->audio_decode_queue.push_back(move(encoded_buffer));
|
||||
}
|
||||
|
||||
audio::decode_event_loop->schedule(static_pointer_cast<event::EventEntry>(this->ref()));
|
||||
}
|
||||
|
||||
void VoiceClient::cancel_replay() {
|
||||
log_trace(category::voice_connection, tr("Cancel replay for client {}"), this->_client_id);
|
||||
this->output_source->clear();
|
||||
this->set_state(state::stopped);
|
||||
}
|
||||
|
||||
void VoiceClient::event_execute(const std::chrono::system_clock::time_point &scheduled) {
|
||||
static auto max_time = chrono::milliseconds(10);
|
||||
|
||||
bool reschedule = false;
|
||||
auto now = chrono::system_clock::now();
|
||||
while(true) {
|
||||
unique_lock buffer_lock(this->audio_decode_queue_lock);
|
||||
if(this->play_premature_packets) {
|
||||
this->play_premature_packets = false;
|
||||
|
||||
for(auto& codec_data : this->codec) {
|
||||
if(!codec_data) continue;
|
||||
|
||||
if(!codec_data->premature_packets.empty()) {
|
||||
size_t play_count = 0;
|
||||
while(!codec_data->premature_packets.empty()) {
|
||||
auto& packet = codec_data->premature_packets.front();
|
||||
|
||||
//Test if we're able to replay stuff again
|
||||
if((uint16_t) (codec_data->last_packet_id + 1) < packet.packet_id && play_count > 0) //Only check for the order if we replayed one already
|
||||
break; //Nothing new
|
||||
|
||||
this->output_source->enqueue_samples(packet.buffer);
|
||||
codec_data->last_packet_id = packet.packet_id;
|
||||
codec_data->premature_packets.pop_front();
|
||||
play_count++;
|
||||
}
|
||||
|
||||
#ifdef DEBUG_PREMATURE_PACKETS
|
||||
if(play_count > 0)
|
||||
log_debug(category::audio, tr("Replayed (buffer underflow) {} premature packets for client {}"), play_count, this->_client_id);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(this->audio_decode_queue.empty())
|
||||
break;
|
||||
|
||||
if(chrono::system_clock::now() - now > max_time) {
|
||||
reschedule = true;
|
||||
break;
|
||||
}
|
||||
|
||||
auto entry = move(this->audio_decode_queue.front());
|
||||
this->audio_decode_queue.pop_front();
|
||||
buffer_lock.unlock();
|
||||
|
||||
//TODO: Drop too old buffers!
|
||||
this->process_encoded_buffer(entry);
|
||||
}
|
||||
|
||||
if(audio_decode_event_dropped.exchange(false) && !reschedule) {
|
||||
//Is not really a warning, it happens all the time and isn't really an issue
|
||||
//log_warn(category::voice_connection, tr("Dropped auto enqueue event execution for client {}. No reschedulling planned, hopefully we processed all buffers."), this->_client_id);
|
||||
}
|
||||
|
||||
if(reschedule) {
|
||||
log_warn(category::voice_connection, tr("Audio data decode will take longer than {} us. Enqueueing for later"), chrono::duration_cast<chrono::microseconds>(max_time).count());
|
||||
audio::decode_event_loop->schedule(static_pointer_cast<event::EventEntry>(this->ref()));
|
||||
}
|
||||
}
|
||||
|
||||
#define MAX_LOST_PACKETS (6)
|
||||
//Note: This function must be executed single threaded
|
||||
void VoiceClient::process_encoded_buffer(const std::unique_ptr<EncodedBuffer> &buffer) {
|
||||
string error;
|
||||
|
||||
auto& codec_data = this->codec[buffer->codec];
|
||||
|
||||
if(!codec_data) {
|
||||
auto info = codec::get_info(buffer->codec);
|
||||
if(!info || !info->supported) {
|
||||
log_warn(category::voice_connection, tr("Received voice packet from client {}, but we dont support it ({})"), this->_client_id, buffer->codec);
|
||||
return;
|
||||
}
|
||||
|
||||
auto instance = make_unique<AudioCodec>();
|
||||
instance->successfully_initialized = false;
|
||||
|
||||
instance->last_packet_id = (uint16_t) (buffer->packet_id - 1); /* could be 0xFFFF */
|
||||
instance->converter = info->new_converter(error);
|
||||
if(!instance->converter) {
|
||||
codec_data = move(instance);
|
||||
|
||||
log_warn(category::voice_connection, tr("Failed to initialize new codec {} for client {}: {}"), buffer->codec, this->_client_id, error);
|
||||
return;
|
||||
}
|
||||
|
||||
instance->resampler = make_shared<audio::AudioResampler>(instance->converter->sample_rate(), this->output_source->sample_rate, instance->converter->channels());
|
||||
if(!instance->resampler->valid()) {
|
||||
codec_data = move(instance);
|
||||
|
||||
log_warn(category::voice_connection, tr("Failed to initialize new codec resampler {} for client {}"), buffer->codec, this->_client_id);
|
||||
return;
|
||||
}
|
||||
instance->successfully_initialized = true;
|
||||
codec_data = move(instance);
|
||||
} else if(!codec_data->successfully_initialized) {
|
||||
return; /* already failed ignore that stuff */
|
||||
}
|
||||
|
||||
uint16_t diff;
|
||||
bool premature = false;
|
||||
|
||||
if(codec_data->last_packet_timestamp + chrono::seconds(1) < buffer->receive_timestamp || this->_state >= state::stopping) {
|
||||
diff = 0xFFFF;
|
||||
} else {
|
||||
if(codec_data->last_packet_id > buffer->packet_id) {
|
||||
auto local_index = (uint16_t) (codec_data->last_packet_id + MAX_LOST_PACKETS);
|
||||
if(local_index < buffer->packet_id)
|
||||
diff = 0xFF; /* we got in a new generation */
|
||||
else {
|
||||
log_warn(category::audio,
|
||||
tr("Received voice packet for client {} with is older than the last we received (Current index: {}, Packet index: {}). Dropping packet."),
|
||||
this->_client_id, buffer->packet_id, codec_data->last_packet_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
diff = buffer->packet_id - codec_data->last_packet_id;
|
||||
}
|
||||
}
|
||||
|
||||
const auto old_packet_id = codec_data->last_packet_id;
|
||||
codec_data->last_packet_timestamp = buffer->receive_timestamp;
|
||||
|
||||
if(buffer->buffer.empty()) {
|
||||
/* lets playback the last samples and we're done */
|
||||
this->set_state(state::stopping);
|
||||
|
||||
/* enqueue all premature packets (list should be already ordered!) */
|
||||
{
|
||||
unique_lock buffer_lock(this->audio_decode_queue_lock);
|
||||
for(const auto& packet : codec_data->premature_packets)
|
||||
this->output_source->enqueue_samples(packet.buffer);
|
||||
codec_data->premature_packets.clear();
|
||||
}
|
||||
|
||||
log_trace(category::voice_connection, tr("Stopping replay for client {}. Empty buffer!"), this->_client_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if(diff == 0) {
|
||||
//Duplicated packets
|
||||
log_warn(category::audio, tr("Received voice packet with the same ID then the last one. Dropping packet."));
|
||||
return;
|
||||
} else
|
||||
diff--; /* because the diff is normally 1 (ofc) */
|
||||
|
||||
if(diff <= MAX_LOST_PACKETS) {
|
||||
if(diff > 0) {
|
||||
/* lets first handle packet as "lost", even thou we're enqueueing it as premature */
|
||||
//auto status = codec_data->converter->decode_lost(error, diff);
|
||||
//if(status < 0)
|
||||
// log_warn(category::voice_connection, tr("Failed to decode (skip) dropped packets. Return code {} => {}"), status, error);
|
||||
premature = !buffer->head && this->state() != state::stopped;
|
||||
|
||||
log_debug(category::voice_connection,
|
||||
tr("Client {} dropped one or more audio packets. Old packet id: {}, New packet id: {}, Diff: {}. Head: {}. Flagging chunk as premature: {}"),
|
||||
this->_client_id, old_packet_id, buffer->packet_id, diff, buffer->head, premature);
|
||||
}
|
||||
} else {
|
||||
log_debug(category::voice_connection, tr("Client {} resetted decoder. Old packet id: {}, New packet id: {}, diff: {}"), this->_client_id, old_packet_id, buffer->packet_id, diff);
|
||||
codec_data->converter->reset_decoder();
|
||||
if(!codec_data->converter) {
|
||||
log_warn(category::voice_connection, tr("Failed to reset codec decoder {} for client {}: {}"), buffer->codec, this->_client_id, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!premature)
|
||||
codec_data->last_packet_id = buffer->packet_id;
|
||||
|
||||
char target_buffer[target_buffer_length];
|
||||
if(target_buffer_length < codec_data->converter->expected_decoded_length(buffer->buffer.data_ptr(), buffer->buffer.length())) {
|
||||
log_warn(category::voice_connection, tr("Failed to decode audio data. Target buffer is smaller then expected bytes ({} < {})"), target_buffer_length, codec_data->converter->expected_decoded_length(buffer->buffer.data_ptr(), buffer->buffer.length()));
|
||||
return;
|
||||
}
|
||||
auto samples = codec_data->converter->decode(error, buffer->buffer.data_ptr(), buffer->buffer.length(), target_buffer);
|
||||
if(samples < 0) {
|
||||
log_warn(category::voice_connection, tr("Failed to decode audio data: {}"), error);
|
||||
return;
|
||||
}
|
||||
if(target_buffer_length < codec_data->resampler->estimated_output_size(samples) * codec_data->resampler->channels() * 4) {
|
||||
log_warn(category::voice_connection, tr("Failed to resample audio data. Target buffer is smaller then expected bytes ({} < {})"), target_buffer_length, (codec_data->resampler->estimated_output_size(samples) * codec_data->resampler->channels() * 4));
|
||||
return;
|
||||
}
|
||||
|
||||
auto resampled_samples = codec_data->resampler->process(target_buffer, target_buffer, samples);
|
||||
if(resampled_samples <= 0) {
|
||||
log_warn(category::voice_connection, tr("Failed to resample audio data. Resampler resulted in {}"), resampled_samples);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!audio::merge::merge_channels_interleaved(target_buffer, this->output_source->channel_count, target_buffer, codec_data->resampler->channels(), resampled_samples)) {
|
||||
log_warn(category::voice_connection, tr("Failed to merge channels to output stream channel count!"));
|
||||
return;
|
||||
}
|
||||
|
||||
if(this->_volume != 1) {
|
||||
auto buf = (float*) target_buffer;
|
||||
auto count = this->output_source->channel_count * resampled_samples;
|
||||
while(count-- > 0)
|
||||
*(buf++) *= this->_volume;
|
||||
}
|
||||
|
||||
if(premature) {
|
||||
auto audio_buffer = audio::SampleBuffer::allocate((uint8_t) this->output_source->channel_count, (uint16_t) resampled_samples);
|
||||
|
||||
audio_buffer->sample_index = 0;
|
||||
memcpy(audio_buffer->sample_data, target_buffer, this->output_source->channel_count * resampled_samples * 4);
|
||||
|
||||
{
|
||||
unique_lock buffer_lock(this->audio_decode_queue_lock);
|
||||
auto it = codec_data->premature_packets.begin();
|
||||
for(; it != codec_data->premature_packets.end(); it++) {
|
||||
if(it->packet_id > buffer->packet_id) {
|
||||
break; /* it is set to the right position */
|
||||
}
|
||||
}
|
||||
codec_data->premature_packets.insert(it, {
|
||||
buffer->packet_id,
|
||||
move(audio_buffer)
|
||||
});
|
||||
std::stable_sort(codec_data->premature_packets.begin(), codec_data->premature_packets.end(), [](const PrematureAudioPacket& a, const PrematureAudioPacket& b) {
|
||||
return a.packet_id < b.packet_id;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
auto enqueued = this->output_source->enqueue_samples(target_buffer, resampled_samples);
|
||||
if(enqueued != resampled_samples)
|
||||
log_warn(category::voice_connection, tr("Failed to enqueue all samples for client {}. Enqueued {} of {}"), this->_client_id, enqueued, resampled_samples);
|
||||
this->set_state(state::playing);
|
||||
|
||||
/* test if any premature got its original place */
|
||||
{
|
||||
unique_lock buffer_lock(this->audio_decode_queue_lock);
|
||||
|
||||
size_t play_count = 0;
|
||||
while(!codec_data->premature_packets.empty()) {
|
||||
auto& packet = codec_data->premature_packets[0];
|
||||
|
||||
//Test if we're able to replay stuff again
|
||||
if((uint16_t) (codec_data->last_packet_id + 1) < packet.packet_id)
|
||||
break; //Nothing new
|
||||
|
||||
this->output_source->enqueue_samples(packet.buffer);
|
||||
codec_data->last_packet_id = packet.packet_id;
|
||||
codec_data->premature_packets.pop_front();
|
||||
play_count++;
|
||||
}
|
||||
#ifdef DEBUG_PREMATURE_PACKETS
|
||||
if(play_count > 0)
|
||||
log_debug(category::audio, tr("Replayed (id match) {} premature packets for client {}"), play_count, this->_client_id);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceClient::event_execute_dropped(const std::chrono::system_clock::time_point &point) {
|
||||
if(audio_decode_event_dropped.exchange(true))
|
||||
//Is not really a warning, it happens all the time and isn't really an issue
|
||||
;//log_warn(category::voice_connection, tr("Dropped auto enqueue event execution two or more times in a row for client {}"), this->_client_id);
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <nan.h>
|
||||
#include <NanEventCallback.h>
|
||||
#include <functional>
|
||||
#include <pipes/buffer.h>
|
||||
#include "../../audio/AudioResampler.h"
|
||||
#include "../../audio/codec/Converter.h"
|
||||
#include "../../audio/AudioOutput.h"
|
||||
#include "../../EventLoop.h"
|
||||
|
||||
namespace tc {
|
||||
namespace connection {
|
||||
class ServerConnection;
|
||||
class VoiceConnection;
|
||||
class VoiceClient;
|
||||
|
||||
namespace codec {
|
||||
enum value {
|
||||
MIN = 0,
|
||||
|
||||
SPEEX_NARROWBAND = 0,
|
||||
SPEEX_WIDEBAND = 1,
|
||||
SPEEX_ULTRA_WIDEBAND = 2,
|
||||
|
||||
CELT_MONO = 3,
|
||||
|
||||
OPUS_VOICE = 4,
|
||||
OPUS_MUSIC = 5,
|
||||
|
||||
MAX = 5,
|
||||
};
|
||||
|
||||
struct condec_info {
|
||||
bool supported;
|
||||
std::string name;
|
||||
std::function<std::shared_ptr<audio::codec::Converter>(std::string&)> new_converter;
|
||||
};
|
||||
|
||||
extern const condec_info info[6];
|
||||
inline const condec_info* get_info(value codec) {
|
||||
if(codec > value::MAX || codec < value::MIN)
|
||||
return nullptr;
|
||||
return &info[codec];
|
||||
}
|
||||
}
|
||||
|
||||
class VoiceClient : private event::EventEntry {
|
||||
friend class VoiceConnection;
|
||||
|
||||
template<typename _Tp, typename _Up>
|
||||
friend inline std::shared_ptr<_Tp> std::static_pointer_cast(const std::shared_ptr<_Up>& __r) noexcept;
|
||||
public:
|
||||
struct state {
|
||||
enum value {
|
||||
buffering, /* this state is never active */
|
||||
playing,
|
||||
stopping,
|
||||
stopped
|
||||
};
|
||||
};
|
||||
VoiceClient(const std::shared_ptr<VoiceConnection>& /* connection */, uint16_t /* client id */);
|
||||
virtual ~VoiceClient();
|
||||
|
||||
inline uint16_t client_id() { return this->_client_id; }
|
||||
|
||||
void initialize_js_object();
|
||||
void finalize_js_object();
|
||||
|
||||
v8::Local<v8::Object> js_handle() {
|
||||
assert(v8::Isolate::GetCurrent());
|
||||
return this->_js_handle.Get(Nan::GetCurrentContext()->GetIsolate());
|
||||
}
|
||||
|
||||
inline std::shared_ptr<VoiceClient> ref() { return this->_ref.lock(); }
|
||||
|
||||
void process_packet(uint16_t packet_id, const pipes::buffer_view& /* buffer */, codec::value /* codec */, bool /* head */);
|
||||
|
||||
inline float get_volume() { return this->_volume; }
|
||||
inline void set_volume(float value) { this->_volume = value; }
|
||||
|
||||
inline state::value state() { return this->_state; }
|
||||
|
||||
void cancel_replay();
|
||||
|
||||
std::function<void()> on_state_changed;
|
||||
|
||||
inline std::shared_ptr<audio::AudioOutputSource> output_stream() { return this->output_source; }
|
||||
private:
|
||||
struct PrematureAudioPacket {
|
||||
uint16_t packet_id = 0;
|
||||
std::shared_ptr<tc::audio::SampleBuffer> buffer{};
|
||||
};
|
||||
|
||||
struct AudioCodec {
|
||||
uint16_t last_packet_id = 0;
|
||||
std::chrono::system_clock::time_point last_packet_timestamp;
|
||||
|
||||
bool successfully_initialized;
|
||||
std::shared_ptr<audio::codec::Converter> converter;
|
||||
std::shared_ptr<audio::AudioResampler> resampler;
|
||||
|
||||
std::deque<PrematureAudioPacket> premature_packets;
|
||||
};
|
||||
|
||||
std::array<std::unique_ptr<AudioCodec>, codec::MAX + 1> codec{
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr
|
||||
};
|
||||
std::shared_ptr<audio::AudioOutputSource> output_source;
|
||||
|
||||
std::weak_ptr<VoiceClient> _ref;
|
||||
v8::Persistent<v8::Object> _js_handle;
|
||||
|
||||
uint16_t _client_id;
|
||||
float _volume = 1.f;
|
||||
|
||||
bool play_premature_packets = false;
|
||||
|
||||
std::chrono::system_clock::time_point _last_received_packet;
|
||||
state::value _state = state::stopped;
|
||||
inline void set_state(state::value value) {
|
||||
if(value == this->_state)
|
||||
return;
|
||||
this->_state = value;
|
||||
if(this->on_state_changed)
|
||||
this->on_state_changed();
|
||||
}
|
||||
|
||||
struct EncodedBuffer {
|
||||
bool head;
|
||||
uint16_t packet_id;
|
||||
pipes::buffer buffer;
|
||||
codec::value codec;
|
||||
std::chrono::system_clock::time_point receive_timestamp;
|
||||
};
|
||||
|
||||
std::atomic_bool audio_decode_event_dropped{false};
|
||||
std::mutex audio_decode_queue_lock;
|
||||
std::deque<std::unique_ptr<EncodedBuffer>> audio_decode_queue;
|
||||
void event_execute(const std::chrono::system_clock::time_point &point) override;
|
||||
void event_execute_dropped(const std::chrono::system_clock::time_point &point) override;
|
||||
void process_encoded_buffer(const std::unique_ptr<EncodedBuffer>& /* buffer */);
|
||||
};
|
||||
|
||||
|
||||
class VoiceClientWrap : public Nan::ObjectWrap {
|
||||
public:
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(NewInstance);
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
|
||||
VoiceClientWrap(const std::shared_ptr<VoiceClient>&);
|
||||
virtual ~VoiceClientWrap();
|
||||
|
||||
void do_wrap(const v8::Local<v8::Object>&);
|
||||
private:
|
||||
static NAN_METHOD(_get_state);
|
||||
static NAN_METHOD(_get_volume);
|
||||
static NAN_METHOD(_set_volume);
|
||||
static NAN_METHOD(_abort_replay);
|
||||
static NAN_METHOD(_get_stream);
|
||||
|
||||
std::weak_ptr<VoiceClient> _handle;
|
||||
|
||||
bool _currently_playing = false;
|
||||
Nan::callback_t<> call_state_changed;
|
||||
void _call_state_changed();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
#include "VoiceConnection.h"
|
||||
#include "VoiceClient.h"
|
||||
#include "../ServerConnection.h"
|
||||
#include "AudioSender.h"
|
||||
#include "../../audio/js/AudioConsumer.h"
|
||||
#include "../../audio/AudioInput.h"
|
||||
#include "../../logger.h"
|
||||
#include <misc/endianness.h> /* MUST be included as last file */
|
||||
|
||||
using namespace std;
|
||||
using namespace tc;
|
||||
using namespace tc::connection;
|
||||
using namespace ts;
|
||||
using namespace ts::protocol;
|
||||
using namespace audio::recorder;
|
||||
|
||||
VoiceConnectionWrap::VoiceConnectionWrap(const std::shared_ptr<VoiceConnection>& handle) : handle(handle) {}
|
||||
VoiceConnectionWrap::~VoiceConnectionWrap() {
|
||||
if(!this->_voice_recoder_handle.IsEmpty()) {
|
||||
auto old_consumer = this->_voice_recoder_ptr;
|
||||
assert(old_consumer);
|
||||
|
||||
lock_guard read_lock(old_consumer->native_consumer()->on_read_lock);
|
||||
old_consumer->native_consumer()->on_read = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceConnectionWrap::do_wrap(const v8::Local<v8::Object> &object) {
|
||||
this->Wrap(object);
|
||||
}
|
||||
|
||||
NAN_MODULE_INIT(VoiceConnectionWrap::Init) {
|
||||
auto klass = Nan::New<v8::FunctionTemplate>(VoiceConnectionWrap::NewInstance);
|
||||
klass->SetClassName(Nan::New("VoiceConnection").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "decoding_supported", VoiceConnectionWrap::_decoding_supported);
|
||||
Nan::SetPrototypeMethod(klass, "encoding_supported", VoiceConnectionWrap::_encoding_supported);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "register_client", VoiceConnectionWrap::_register_client);
|
||||
Nan::SetPrototypeMethod(klass, "available_clients", VoiceConnectionWrap::_available_clients);
|
||||
Nan::SetPrototypeMethod(klass, "unregister_client", VoiceConnectionWrap::_unregister_client);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "audio_source", VoiceConnectionWrap::_audio_source);
|
||||
Nan::SetPrototypeMethod(klass, "set_audio_source", VoiceConnectionWrap::_set_audio_source);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_encoder_codec", VoiceConnectionWrap::_get_encoder_codec);
|
||||
Nan::SetPrototypeMethod(klass, "set_encoder_codec", VoiceConnectionWrap::_set_encoder_codec);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "enable_voice_send", VoiceConnectionWrap::_enable_voice_send);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::NewInstance) {
|
||||
if(!info.IsConstructCall())
|
||||
Nan::ThrowError("invalid invoke!");
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_connected) {
|
||||
info.GetReturnValue().Set(true);
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_encoding_supported) {
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
auto codec = info[0]->Uint32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
|
||||
info.GetReturnValue().Set(codec >= 4 && codec <= 5); /* ignore SPEX currently :/ */
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_decoding_supported) {
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
auto codec = info[0]->Uint32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
|
||||
info.GetReturnValue().Set(codec >= 4 && codec <= 5); /* ignore SPEX currently :/ */
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_register_client) {
|
||||
return ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder())->register_client(info);
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::register_client) {
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
auto id = info[0]->Uint32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
auto handle = this->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
auto client = handle->register_client(id);
|
||||
if(!client) {
|
||||
Nan::ThrowError("failed to register client");
|
||||
return;
|
||||
}
|
||||
client->initialize_js_object();
|
||||
info.GetReturnValue().Set(client->js_handle());
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_available_clients) {
|
||||
return ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder())->available_clients(info);
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::available_clients) {
|
||||
auto handle = this->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
auto client = handle->clients();
|
||||
|
||||
v8::Local<v8::Array> result = Nan::New<v8::Array>(client.size());
|
||||
for(size_t index = 0; index < client.size(); index++)
|
||||
Nan::Set(result, index, client[index]->js_handle());
|
||||
|
||||
info.GetReturnValue().Set(result);
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_unregister_client) {
|
||||
return ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder())->unregister_client(info);
|
||||
}
|
||||
NAN_METHOD(VoiceConnectionWrap::unregister_client) {
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
auto id = info[0]->Uint32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
auto handle = this->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
auto client = handle->find_client(id);
|
||||
if(!client) {
|
||||
Nan::ThrowError("missing client");
|
||||
return;
|
||||
}
|
||||
|
||||
client->finalize_js_object();
|
||||
handle->delete_client(client);
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_audio_source) {
|
||||
auto client = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
info.GetReturnValue().Set(client->_voice_recoder_handle.Get(info.GetIsolate()));
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_set_audio_source) {
|
||||
ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder())->set_audio_source(info);
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::set_audio_source) {
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if(!Nan::New(AudioConsumerWrapper::constructor_template())->HasInstance(info[0]) && !info[0]->IsNullOrUndefined()) {
|
||||
Nan::ThrowError("invalid consumer (Consumer must be native!)");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this->handle.lock()) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
this->release_recorder();
|
||||
if(!info[0]->IsNullOrUndefined()) {
|
||||
this->_voice_recoder_ptr = ObjectWrap::Unwrap<audio::recorder::AudioConsumerWrapper>(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked());
|
||||
this->_voice_recoder_handle.Reset(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked());
|
||||
|
||||
auto native_consumer = this->_voice_recoder_ptr->native_consumer();
|
||||
|
||||
weak_ptr weak_handle = this->handle;
|
||||
auto sample_rate = native_consumer->sample_rate;
|
||||
auto channels = native_consumer->channel_count;
|
||||
|
||||
lock_guard read_lock(this->_voice_recoder_ptr->native_read_callback_lock);
|
||||
this->_voice_recoder_ptr->native_read_callback = [weak_handle, sample_rate, channels](const void* buffer, size_t length) {
|
||||
auto handle = weak_handle.lock();
|
||||
if(!handle) {
|
||||
log_warn(category::audio, tr("Missing voice connection handle. Dropping input!"));
|
||||
return;
|
||||
}
|
||||
|
||||
shared_ptr<VoiceSender> sender = handle->voice_sender();
|
||||
if(sender) {
|
||||
if(length > 0 && buffer)
|
||||
sender->send_data(buffer, length, sample_rate, channels);
|
||||
else
|
||||
sender->send_stop();
|
||||
} else {
|
||||
log_warn(category::audio, tr("Missing voice connection audio sender. Dropping input!"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_get_encoder_codec) {
|
||||
auto _this = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
auto handle = _this->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set(handle->get_encoder_codec());
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_set_encoder_codec) {
|
||||
auto _this = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
auto handle = _this->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("Invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
handle->set_encoder_codec((uint8_t) info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_enable_voice_send) {
|
||||
auto _this = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
auto handle = _this->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsBoolean()) {
|
||||
Nan::ThrowError("Invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
auto sender = handle->voice_sender();
|
||||
if(!sender) {
|
||||
Nan::ThrowError("Voice sender has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
sender->set_voice_send_enabled(info[0]->BooleanValue(info.GetIsolate()));
|
||||
}
|
||||
|
||||
|
||||
void VoiceConnectionWrap::release_recorder() {
|
||||
if(!this->_voice_recoder_handle.IsEmpty()) {
|
||||
assert(v8::Isolate::GetCurrent());
|
||||
|
||||
auto old_consumer = this->_voice_recoder_ptr;
|
||||
assert(old_consumer);
|
||||
|
||||
lock_guard read_lock(this->_voice_recoder_ptr->native_read_callback_lock);
|
||||
this->_voice_recoder_ptr->native_read_callback = nullptr;
|
||||
} else {
|
||||
assert(!this->_voice_recoder_ptr);
|
||||
}
|
||||
|
||||
this->_voice_recoder_ptr = nullptr;
|
||||
this->_voice_recoder_handle.Reset();
|
||||
}
|
||||
|
||||
|
||||
VoiceConnection::VoiceConnection(ServerConnection *handle) : _handle(handle) {
|
||||
this->_voice_sender = make_shared<VoiceSender>(this);
|
||||
this->_voice_sender->_ref = this->_voice_sender;
|
||||
this->_voice_sender->set_codec(codec::OPUS_MUSIC);
|
||||
}
|
||||
VoiceConnection::~VoiceConnection() {
|
||||
if(v8::Isolate::GetCurrent())
|
||||
this->finalize_js_object();
|
||||
else {
|
||||
assert(this->_js_handle.IsEmpty());
|
||||
}
|
||||
|
||||
this->_voice_sender->finalize();
|
||||
}
|
||||
|
||||
void VoiceConnection::reset() {
|
||||
lock_guard lock(this->_clients_lock);
|
||||
this->_clients.clear();
|
||||
}
|
||||
|
||||
void VoiceConnection::initialize_js_object() {
|
||||
auto object_wrap = new VoiceConnectionWrap(this->ref());
|
||||
auto object = Nan::NewInstance(Nan::New(VoiceConnectionWrap::constructor()), 0, nullptr).ToLocalChecked();
|
||||
object_wrap->do_wrap(object);
|
||||
|
||||
this->_js_handle.Reset(Nan::GetCurrentContext()->GetIsolate(), object);
|
||||
}
|
||||
|
||||
void VoiceConnection::finalize_js_object() {
|
||||
this->_js_handle.Reset();
|
||||
}
|
||||
|
||||
std::shared_ptr<VoiceClient> VoiceConnection::find_client(uint16_t client_id) {
|
||||
lock_guard lock(this->_clients_lock);
|
||||
for(const auto& client : this->_clients)
|
||||
if(client->client_id() == client_id)
|
||||
return client;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<VoiceClient> VoiceConnection::register_client(uint16_t client_id) {
|
||||
lock_guard lock(this->_clients_lock);
|
||||
auto client = this->find_client(client_id);
|
||||
if(client) return client;
|
||||
|
||||
client = make_shared<VoiceClient>(this->ref(), client_id);
|
||||
client->_ref = client;
|
||||
this->_clients.push_back(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
void VoiceConnection::delete_client(const std::shared_ptr<tc::connection::VoiceClient> &client) {
|
||||
{
|
||||
lock_guard lock(this->_clients_lock);
|
||||
auto it = find(this->_clients.begin(), this->_clients.end(), client);
|
||||
if(it != this->_clients.end()) {
|
||||
this->_clients.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO deinitialize client
|
||||
}
|
||||
|
||||
void VoiceConnection::process_packet(const std::shared_ptr<ts::protocol::ServerPacket> &packet) {
|
||||
if(packet->type() == PacketTypeInfo::Voice) {
|
||||
if(packet->data().length() < 5) {
|
||||
//TODO log invalid voice packet
|
||||
return;
|
||||
}
|
||||
auto packet_id = be2le16(&packet->data()[0]);
|
||||
auto client_id = be2le16(&packet->data()[2]);
|
||||
auto codec_id = (uint8_t) packet->data()[4];
|
||||
auto flag_head = packet->has_flag(PacketFlag::Compressed);
|
||||
//container->voice_data = packet->data().length() > 5 ? packet->data().range(5) : pipes::buffer{};
|
||||
|
||||
//log_info(category::voice_connection, tr("Received voice packet from {}. Packet ID: {}"), client_id, packet_id);
|
||||
auto client = this->find_client(client_id);
|
||||
if(!client) {
|
||||
log_warn(category::voice_connection, tr("Received voice packet from unknown client {}. Dropping packet!"), client_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if(packet->data().length() > 5)
|
||||
client->process_packet(packet_id, packet->data().range(5), (codec::value) codec_id, flag_head);
|
||||
else
|
||||
client->process_packet(packet_id, pipes::buffer_view{nullptr, 0}, (codec::value) codec_id, flag_head);
|
||||
} else {
|
||||
//TODO implement whisper
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceConnection::set_encoder_codec(const uint8_t &codec) {
|
||||
if(codec > codec::MAX) return;
|
||||
|
||||
auto vs = this->_voice_sender;
|
||||
if(vs)
|
||||
vs->set_codec((codec::value) codec);
|
||||
}
|
||||
|
||||
uint8_t VoiceConnection::get_encoder_codec() {
|
||||
auto vs = this->_voice_sender;
|
||||
return vs ? vs->get_codec() : 0;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <v8.h>
|
||||
#include <nan.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <protocol/Packet.h>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
namespace recorder {
|
||||
class AudioConsumerWrapper;
|
||||
}
|
||||
}
|
||||
|
||||
namespace connection {
|
||||
class ServerConnection;
|
||||
class VoiceConnection;
|
||||
class VoiceClient;
|
||||
class VoiceSender;
|
||||
|
||||
class VoiceConnectionWrap : public Nan::ObjectWrap {
|
||||
public:
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(NewInstance);
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
|
||||
explicit VoiceConnectionWrap(const std::shared_ptr<VoiceConnection>&);
|
||||
virtual ~VoiceConnectionWrap();
|
||||
|
||||
void do_wrap(const v8::Local<v8::Object>&);
|
||||
private:
|
||||
static NAN_METHOD(_connected);
|
||||
static NAN_METHOD(_encoding_supported);
|
||||
static NAN_METHOD(_decoding_supported);
|
||||
|
||||
static NAN_METHOD(_register_client);
|
||||
NAN_METHOD(register_client);
|
||||
static NAN_METHOD(_available_clients);
|
||||
NAN_METHOD(available_clients);
|
||||
static NAN_METHOD(_unregister_client);
|
||||
NAN_METHOD(unregister_client);
|
||||
|
||||
static NAN_METHOD(_audio_source);
|
||||
static NAN_METHOD(_set_audio_source);
|
||||
NAN_METHOD(set_audio_source);
|
||||
|
||||
static NAN_METHOD(_get_encoder_codec);
|
||||
static NAN_METHOD(_set_encoder_codec);
|
||||
static NAN_METHOD(_enable_voice_send);
|
||||
|
||||
void release_recorder();
|
||||
|
||||
std::function<void(const void* /* buffer */, size_t /* samples */)> _read_callback;
|
||||
audio::recorder::AudioConsumerWrapper* _voice_recoder_ptr = nullptr;
|
||||
Nan::Persistent<v8::Object> _voice_recoder_handle;
|
||||
std::weak_ptr<VoiceConnection> handle;
|
||||
};
|
||||
|
||||
class VoiceConnection {
|
||||
friend class ServerConnection;
|
||||
friend class VoiceConnectionWrap;
|
||||
public:
|
||||
explicit VoiceConnection(ServerConnection*);
|
||||
virtual ~VoiceConnection();
|
||||
|
||||
void reset();
|
||||
|
||||
void initialize_js_object();
|
||||
void finalize_js_object();
|
||||
|
||||
ServerConnection* handle() { return this->_handle; }
|
||||
v8::Local<v8::Object> js_handle() {
|
||||
assert(v8::Isolate::GetCurrent());
|
||||
return this->_js_handle.Get(Nan::GetCurrentContext()->GetIsolate());
|
||||
}
|
||||
|
||||
inline std::shared_ptr<VoiceConnection> ref() { return this->_ref.lock(); }
|
||||
inline std::deque<std::shared_ptr<VoiceClient>> clients() {
|
||||
std::lock_guard lock(this->_clients_lock);
|
||||
return this->_clients;
|
||||
}
|
||||
|
||||
inline std::shared_ptr<VoiceSender> voice_sender() { return this->_voice_sender; }
|
||||
|
||||
std::shared_ptr<VoiceClient> find_client(uint16_t /* client id */);
|
||||
std::shared_ptr<VoiceClient> register_client(uint16_t /* client id */);
|
||||
void delete_client(const std::shared_ptr<VoiceClient>&);
|
||||
|
||||
void process_packet(const std::shared_ptr<ts::protocol::ServerPacket>&);
|
||||
|
||||
void set_encoder_codec(const uint8_t& /* codec */);
|
||||
uint8_t get_encoder_codec();
|
||||
private:
|
||||
ServerConnection* _handle;
|
||||
std::weak_ptr<VoiceConnection> _ref;
|
||||
|
||||
v8::Persistent<v8::Object> _js_handle;
|
||||
|
||||
std::recursive_mutex _clients_lock;
|
||||
std::deque<std::shared_ptr<VoiceClient>> _clients;
|
||||
|
||||
std::shared_ptr<VoiceSender> _voice_sender;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,773 +0,0 @@
|
||||
#include "FileTransferManager.h"
|
||||
#include "FileTransferObject.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fcntl.h>
|
||||
#include <iostream>
|
||||
|
||||
#ifndef WIN32
|
||||
#include <unistd.h>
|
||||
#include <misc/net.h>
|
||||
#else
|
||||
#include <ws2tcpip.h>
|
||||
#define SOCK_NONBLOCK (0)
|
||||
#define MSG_DONTWAIT (0)
|
||||
#endif
|
||||
|
||||
using namespace tc;
|
||||
using namespace tc::ft;
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
|
||||
tc::ft::FileTransferManager* transfer_manager = nullptr;
|
||||
|
||||
Transfer::~Transfer() {
|
||||
log_free("Transfer", this);
|
||||
}
|
||||
|
||||
bool Transfer::initialize(std::string &error) {
|
||||
if(this->_state != state::UNINITIALIZED) {
|
||||
error = tr("invalid state");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!this->_transfer_object->initialize(error)) {
|
||||
error = tr("failed to initialize transfer object: ") + error;
|
||||
return false;
|
||||
}
|
||||
this->_state = state::CONNECTING;
|
||||
|
||||
/* resolve address */
|
||||
{
|
||||
addrinfo hints{}, *result;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
|
||||
if(getaddrinfo(this->_options->remote_address.data(), nullptr, &hints, &result) != 0 || !result) {
|
||||
error = tr("failed to resolve hostname");
|
||||
this->_state = state::UNINITIALIZED;
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(&this->remote_address, result->ai_addr, result->ai_addrlen);
|
||||
freeaddrinfo(result);
|
||||
}
|
||||
switch(this->remote_address.ss_family) {
|
||||
case AF_INET:
|
||||
((sockaddr_in*) &this->remote_address)->sin_port = htons(this->_options->remote_port);
|
||||
case AF_INET6:
|
||||
((sockaddr_in6*) &this->remote_address)->sin6_port = htons(this->_options->remote_port);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
log_info(category::file_transfer, tr("Setting remote port to {}"), this->_options->remote_port);
|
||||
this->_socket = (int) ::socket(this->remote_address.ss_family, SOCK_STREAM | SOCK_NONBLOCK, 0);
|
||||
if(this->_socket < 0) {
|
||||
this->finalize();
|
||||
error = tr("failed to spawn socket");
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
u_long enabled = 0;
|
||||
auto non_block_rs = ioctlsocket(this->_socket, FIONBIO, &enabled);
|
||||
if (non_block_rs != NO_ERROR) {
|
||||
this->finalize();
|
||||
error = "failed to enable non blocking more";
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
{
|
||||
|
||||
lock_guard lock(this->event_lock);
|
||||
this->event_read = event_new(this->event_io, this->_socket, EV_READ | EV_PERSIST, &Transfer::_callback_read, this);
|
||||
this->event_write = event_new(this->event_io, this->_socket, EV_WRITE, &Transfer::_callback_write, this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Transfer::connect() {
|
||||
int result = ::connect(this->_socket, reinterpret_cast<sockaddr *> (&this->remote_address), sizeof(this->remote_address));
|
||||
if (result < 0) {
|
||||
#ifdef WIN32
|
||||
auto error = WSAGetLastError();
|
||||
|
||||
if(error != WSAEWOULDBLOCK) {
|
||||
wchar_t *s = nullptr;
|
||||
FormatMessageW(
|
||||
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr,
|
||||
error,
|
||||
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||
(LPWSTR)&s,
|
||||
0,
|
||||
nullptr
|
||||
);
|
||||
fprintf(stdout, "Connect failed with code %d. Error: %ld/%S\n", result, error, s);
|
||||
LocalFree(s);
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if(errno != EINPROGRESS) {
|
||||
log_error(category::file_transfer, tr("Failed to connect with code: {} => {}/{}"), result, errno, strerror(errno));
|
||||
this->finalize();
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
this->_state = state::CONNECTED; /* we're connected */
|
||||
}
|
||||
log_debug(category::file_transfer, tr("Connect result: {} - {}"), result, errno);
|
||||
|
||||
timeval connect_timeout{5, 0};
|
||||
event_add(this->event_write, &connect_timeout); /* enabled if socket is connected */
|
||||
////event_add(this->event_read, &connect_timeout); /* enabled if socket is connected */
|
||||
this->handle()->execute_event_loop();
|
||||
|
||||
if(this->_state == state::CONNECTED)
|
||||
this->handle_connected();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Transfer::finalize(bool blocking) {
|
||||
if(this->_state == state::UNINITIALIZED)
|
||||
return;
|
||||
|
||||
this->_state = state::UNINITIALIZED;
|
||||
|
||||
{
|
||||
unique_lock lock(this->event_lock);
|
||||
auto event_read = this->event_read, event_write = this->event_write;
|
||||
this->event_read = nullptr;
|
||||
this->event_write = nullptr;
|
||||
lock.unlock();
|
||||
if(event_read) {
|
||||
if(blocking)
|
||||
event_del_block(event_read);
|
||||
else
|
||||
event_del_noblock(event_read);
|
||||
event_free(event_read);
|
||||
}
|
||||
if(event_write) {
|
||||
if(blocking)
|
||||
event_del_block(event_write);
|
||||
else
|
||||
event_del_noblock(event_write);
|
||||
event_free(event_write);
|
||||
}
|
||||
}
|
||||
|
||||
if(this->_socket > 0) {
|
||||
#ifdef WIN32
|
||||
closesocket(this->_socket);
|
||||
#else
|
||||
shutdown(this->_socket, SHUT_RDWR);
|
||||
close(this->_socket);
|
||||
#endif
|
||||
this->_socket = 0;
|
||||
}
|
||||
|
||||
this->_transfer_object->finalize();
|
||||
|
||||
this->_handle->remove_transfer(this);
|
||||
}
|
||||
|
||||
void Transfer::_callback_write(evutil_socket_t, short flags, void *_ptr_transfer) {
|
||||
reinterpret_cast<Transfer*>(_ptr_transfer)->callback_write(flags);
|
||||
}
|
||||
|
||||
void Transfer::_callback_read(evutil_socket_t, short flags, void *_ptr_transfer) {
|
||||
reinterpret_cast<Transfer*>(_ptr_transfer)->callback_read(flags);
|
||||
}
|
||||
|
||||
void Transfer::callback_read(short flags) {
|
||||
if(this->_state < state::CONNECTING && this->_state > state::DISCONNECTING)
|
||||
return;
|
||||
|
||||
if(flags & EV_TIMEOUT) {
|
||||
auto target = dynamic_pointer_cast<TransferTarget>(this->_transfer_object);
|
||||
if(target) {
|
||||
if(this->last_target_write.time_since_epoch().count() == 0)
|
||||
this->last_target_write = system_clock::now();
|
||||
else if(system_clock::now() - this->last_target_write > seconds(5)) {
|
||||
this->call_callback_failed("timeout (write)");
|
||||
this->finalize(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if(this->last_source_read.time_since_epoch().count() == 0)
|
||||
this->last_source_read = system_clock::now();
|
||||
else if(system_clock::now() - this->last_source_read > seconds(5)) {
|
||||
this->call_callback_failed("timeout (read)");
|
||||
this->finalize(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
lock_guard lock(this->event_lock);
|
||||
if(this->event_read) {
|
||||
event_add(this->event_read, &this->alive_check_timeout);
|
||||
this->handle()->execute_event_loop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(flags & EV_READ) {
|
||||
if(this->_state == state::CONNECTING) {
|
||||
log_debug(category::file_transfer, tr("Connected (read event)"));
|
||||
this->handle_connected();
|
||||
}
|
||||
|
||||
int64_t buffer_length = 1024;
|
||||
char buffer[1024];
|
||||
|
||||
buffer_length = recv(this->_socket, buffer, (int) buffer_length, MSG_DONTWAIT);
|
||||
if(buffer_length < 0) {
|
||||
#ifdef WIN32
|
||||
auto error = WSAGetLastError();
|
||||
|
||||
if(error != WSAEWOULDBLOCK)
|
||||
return;
|
||||
#else
|
||||
if(errno == EAGAIN)
|
||||
return;
|
||||
#endif
|
||||
|
||||
log_error(category::file_transfer, tr("Received an error while receivig data: {}/{}"), errno, strerror(errno));
|
||||
//TODO may handle this error message?
|
||||
this->handle_disconnect();
|
||||
return;
|
||||
} else if(buffer_length == 0) {
|
||||
log_info(category::file_transfer, tr("Received an disconnect"));
|
||||
this->handle_disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
auto target = dynamic_pointer_cast<TransferTarget>(this->_transfer_object);
|
||||
if(target) {
|
||||
log_trace(category::file_transfer, tr("Read {} bytes"), buffer_length);
|
||||
string error;
|
||||
auto state = target->write_bytes(error, (uint8_t*) buffer, buffer_length);
|
||||
this->last_target_write = system_clock::now();
|
||||
if(state == error::out_of_space) {
|
||||
log_error(category::file_transfer, tr("Failed to write read data (out of space)"));
|
||||
this->call_callback_failed(tr("out of local space"));
|
||||
this->finalize(true);
|
||||
return;
|
||||
} else if(state == error::custom) {
|
||||
log_error(category::file_transfer, tr("Failed to write read data ({})"), error);
|
||||
this->call_callback_failed(error);
|
||||
this->finalize(true);
|
||||
return;
|
||||
} else if(state == error::custom_recoverable) {
|
||||
log_error(category::file_transfer, tr("Failed to write read data ({})"), error);
|
||||
} else if(state != error::success) {
|
||||
log_error(category::file_transfer, tr("invalid local write return code! ({})"), state);
|
||||
}
|
||||
|
||||
auto stream_index = target->stream_index();
|
||||
auto expected_bytes = target->expected_length();
|
||||
if(stream_index >= expected_bytes) {
|
||||
this->call_callback_finished(false);
|
||||
this->finalize(false);
|
||||
}
|
||||
this->call_callback_process(stream_index, expected_bytes);
|
||||
} else {
|
||||
log_warn(category::file_transfer, tr("Read {} bytes, but we're not in download mode"), buffer_length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Transfer::callback_write(short flags) {
|
||||
if(this->_state < state::CONNECTING && this->_state > state::DISCONNECTING)
|
||||
return;
|
||||
|
||||
if(flags & EV_TIMEOUT) {
|
||||
//we received a timeout! (May just for creating buffers)
|
||||
if(this->_state == state::CONNECTING) {
|
||||
this->call_callback_failed(tr("failed to connect"));
|
||||
this->finalize(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool readd_write = false, readd_write_for_read = false;
|
||||
if(flags & EV_WRITE) {
|
||||
if(this->_state == state::CONNECTING)
|
||||
this->handle_connected();
|
||||
|
||||
pipes::buffer buffer;
|
||||
while(true) {
|
||||
{
|
||||
lock_guard lock(this->queue_lock);
|
||||
auto size = this->write_queue.size();
|
||||
if(size == 0)
|
||||
break;
|
||||
|
||||
buffer = this->write_queue.front();
|
||||
this->write_queue.pop_front();
|
||||
|
||||
readd_write = size > 1;
|
||||
}
|
||||
|
||||
auto written = send(this->_socket, buffer.data_ptr<char>(), buffer.length(), MSG_DONTWAIT);
|
||||
if(written <= 0) {
|
||||
{
|
||||
lock_guard lock(this->queue_lock);
|
||||
this->write_queue.push_front(buffer);
|
||||
readd_write = true;
|
||||
}
|
||||
#ifdef WIN32
|
||||
auto _error = WSAGetLastError();
|
||||
#else
|
||||
auto _error = errno;
|
||||
#define WSAEWOULDBLOCK (0)
|
||||
#define WSAECONNREFUSED (0)
|
||||
#define WSAECONNRESET (0)
|
||||
#define WSAENOTCONN (0)
|
||||
#endif
|
||||
if(_error == EAGAIN || _error == WSAEWOULDBLOCK) {
|
||||
break; /* event will be added with e.g. a timeout */
|
||||
} else if(_error == ECONNREFUSED || _error == WSAECONNREFUSED) {
|
||||
this->call_callback_failed("connection refused");
|
||||
this->finalize(false);
|
||||
} else if(_error == ECONNRESET || _error == WSAECONNRESET) {
|
||||
this->call_callback_failed("connection reset");
|
||||
this->finalize(false);
|
||||
} else if(_error == ENOTCONN || _error == WSAENOTCONN) {
|
||||
this->call_callback_failed("not connected");
|
||||
this->finalize(false);
|
||||
} else if(written == 0) {
|
||||
this->handle_disconnect();
|
||||
} else {
|
||||
log_error(category::file_transfer, "Encountered write error: {}/{}", _error, strerror(_error));
|
||||
this->handle_disconnect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(written < buffer.length()) {
|
||||
lock_guard lock(this->queue_lock);
|
||||
this->write_queue.push_front(buffer.range(written));
|
||||
readd_write = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(this->_state == state::CONNECTED) {
|
||||
auto source = dynamic_pointer_cast<TransferSource>(this->_transfer_object);
|
||||
if(source) {
|
||||
size_t queue_length = 0;
|
||||
{
|
||||
lock_guard lock(this->queue_lock);
|
||||
queue_length = this->write_queue.size();
|
||||
}
|
||||
|
||||
|
||||
string error;
|
||||
auto total_bytes = source->byte_length();
|
||||
auto bytes_to_write = total_bytes - source->stream_index();
|
||||
|
||||
while(queue_length < 8 && bytes_to_write > 0) {
|
||||
uint64_t buffer_size = 1400; /* best TCP packet size (equal to the MTU) */
|
||||
pipes::buffer buffer{buffer_size};
|
||||
auto read_status = source->read_bytes(error, buffer.data_ptr<uint8_t>(), buffer_size);
|
||||
this->last_source_read = system_clock::now();
|
||||
if(read_status != error::success) {
|
||||
if(read_status == error::would_block) {
|
||||
readd_write_for_read = true;
|
||||
break;
|
||||
} else if(read_status == error::custom) {
|
||||
this->call_callback_failed(tr("failed to read from source: ") + error);
|
||||
this->finalize(false);
|
||||
return;
|
||||
} else if(read_status == error::custom_recoverable) {
|
||||
log_warn(category::file_transfer, tr("Failed to read from source (but its recoverable): {}"), error);
|
||||
break;
|
||||
} else {
|
||||
log_error(category::file_transfer, tr("invalid source read status ({})"), read_status);
|
||||
this->finalize(false);
|
||||
return;
|
||||
}
|
||||
} else if(buffer_size == 0) {
|
||||
log_warn(category::file_transfer, tr("Invalid source read size! ({})"), buffer_size);
|
||||
break;
|
||||
}
|
||||
|
||||
{
|
||||
lock_guard lock(this->queue_lock);
|
||||
this->write_queue.push_back(buffer.range(0, buffer_size));
|
||||
queue_length = this->write_queue.size();
|
||||
}
|
||||
|
||||
bytes_to_write -= buffer_size;
|
||||
}
|
||||
|
||||
this->call_callback_process(total_bytes - bytes_to_write, total_bytes);
|
||||
if(queue_length == 0) {
|
||||
if(source->stream_index() == source->byte_length()) {
|
||||
this->call_callback_finished(false);
|
||||
this->finalize(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
readd_write = queue_length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* we only need write for connect */
|
||||
if(readd_write || readd_write_for_read) {
|
||||
lock_guard lock(this->event_lock);
|
||||
if(this->event_write) {
|
||||
timeval timeout{};
|
||||
if(readd_write) {
|
||||
/* we should be writeable within the next second or we do a keep alive circle */
|
||||
timeout.tv_sec = 1;
|
||||
timeout.tv_usec = 0;
|
||||
} else if(readd_write_for_read) {
|
||||
/* Schedule a next read attempt of our source */
|
||||
timeout.tv_sec = 0;
|
||||
timeout.tv_usec = 50000;
|
||||
}
|
||||
event_add(this->event_write, &timeout);
|
||||
this->handle()->execute_event_loop();
|
||||
}
|
||||
} else {
|
||||
log_debug(category::general, tr("No readd"));
|
||||
}
|
||||
}
|
||||
|
||||
void Transfer::_write_message(const pipes::buffer_view &buffer) {
|
||||
{
|
||||
lock_guard lock(this->queue_lock);
|
||||
this->write_queue.push_back(buffer.own_buffer());
|
||||
}
|
||||
if(this->_state >= state::CONNECTED) {
|
||||
lock_guard lock(this->event_lock);
|
||||
if(this->event_write) {
|
||||
event_add(this->event_write, nullptr);
|
||||
this->handle()->execute_event_loop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Transfer::handle_disconnect() {
|
||||
if(this->_state != state::DISCONNECTING) {
|
||||
auto source = dynamic_pointer_cast<TransferSource>(this->_transfer_object);
|
||||
auto target = dynamic_pointer_cast<TransferTarget>(this->_transfer_object);
|
||||
|
||||
if(source && source->stream_index() != source->byte_length()) {
|
||||
this->call_callback_failed("received disconnect while transmitting (" + to_string(target->stream_index()) + "/" + to_string(target->expected_length()) + ")");
|
||||
} else if(target && target->stream_index() != target->expected_length()) {
|
||||
this->call_callback_failed("received disconnect while receiving (" + to_string(target->stream_index()) + "/" + to_string(target->expected_length()) + ")");
|
||||
} else
|
||||
this->call_callback_finished(false);
|
||||
}
|
||||
this->finalize(false);
|
||||
}
|
||||
|
||||
void Transfer::handle_connected() {
|
||||
log_info(category::file_transfer, tr("Transfer connected. Sending key {}"), this->_options->transfer_key);
|
||||
|
||||
this->_state = state::CONNECTED;
|
||||
event_add(this->event_read, &this->alive_check_timeout);
|
||||
this->handle()->execute_event_loop();
|
||||
|
||||
this->_write_message(pipes::buffer_view{this->_options->transfer_key.data(), this->_options->transfer_key.length()});
|
||||
this->call_callback_connected();
|
||||
//We dont have to add a timeout to write for prebuffering because its already done by writing this message
|
||||
}
|
||||
|
||||
void Transfer::call_callback_connected() {
|
||||
if(this->callback_start)
|
||||
this->callback_start();
|
||||
}
|
||||
|
||||
void Transfer::call_callback_failed(const std::string &error) {
|
||||
if(this->callback_failed)
|
||||
this->callback_failed(error);
|
||||
}
|
||||
|
||||
void Transfer::call_callback_finished(bool aborted) {
|
||||
if(this->callback_finished)
|
||||
this->callback_finished(aborted);
|
||||
}
|
||||
|
||||
void Transfer::call_callback_process(size_t current, size_t max) {
|
||||
auto now = system_clock::now();
|
||||
if(now - milliseconds{500} > this->last_process_call)
|
||||
this->last_process_call = now;
|
||||
else
|
||||
return;
|
||||
if(this->callback_process)
|
||||
this->callback_process(current, max);
|
||||
}
|
||||
|
||||
FileTransferManager::FileTransferManager() {}
|
||||
FileTransferManager::~FileTransferManager() {}
|
||||
|
||||
void FileTransferManager::initialize() {
|
||||
this->event_io_canceled = false;
|
||||
this->event_io = event_base_new();
|
||||
this->event_io_thread = std::thread(&FileTransferManager::_execute_event_loop, this);
|
||||
}
|
||||
|
||||
void FileTransferManager::finalize() {
|
||||
this->event_io_canceled = true;
|
||||
this->event_io_condition.notify_all();
|
||||
event_base_loopexit(this->event_io, nullptr);
|
||||
this->event_io_thread.join();
|
||||
|
||||
//TODO drop all file transfers!
|
||||
event_base_free(this->event_io);
|
||||
this->event_io = nullptr;
|
||||
}
|
||||
|
||||
void FileTransferManager::_execute_event_loop() {
|
||||
while(!this->event_io_canceled) {
|
||||
this->event_execute = false;
|
||||
event_base_loop(this->event_io, 0);
|
||||
if(this->running_transfers().size() > 0) {
|
||||
this_thread::sleep_for(milliseconds(50));
|
||||
} else {
|
||||
unique_lock lock(this->event_io_lock);
|
||||
this->event_io_condition.wait_for(lock, minutes(1), [&]{
|
||||
return this->event_io_canceled || this->event_execute;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Transfer> FileTransferManager::register_transfer(std::string& error, const std::shared_ptr<tc::ft::TransferObject> &object, std::unique_ptr<tc::ft::TransferOptions> options) {
|
||||
auto transfer = make_shared<Transfer>(this, object, move(options));
|
||||
|
||||
transfer->event_io = this->event_io;
|
||||
if(!transfer->initialize(error)) {
|
||||
//error = "failed to initialize transfer: " + error;
|
||||
return nullptr;
|
||||
}
|
||||
{
|
||||
lock_guard lock(this->_transfer_lock);
|
||||
this->_running_transfers.push_back(transfer);
|
||||
}
|
||||
|
||||
return transfer;
|
||||
}
|
||||
|
||||
void FileTransferManager::drop_transfer(const std::shared_ptr<Transfer> &transfer) {
|
||||
transfer->finalize(true);
|
||||
{
|
||||
lock_guard lock(this->_transfer_lock);
|
||||
auto it = find(this->_running_transfers.begin(), this->_running_transfers.end(), transfer);
|
||||
if(it != this->_running_transfers.end())
|
||||
this->_running_transfers.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferManager::remove_transfer(tc::ft::Transfer *transfer) {
|
||||
lock_guard lock(this->_transfer_lock);
|
||||
this->_running_transfers.erase(remove_if(this->_running_transfers.begin(), this->_running_transfers.end(), [&](const shared_ptr<Transfer>& _t) {
|
||||
return &*_t == transfer;
|
||||
}), this->_running_transfers.end());
|
||||
}
|
||||
|
||||
#ifdef NODEJS_API
|
||||
|
||||
NAN_MODULE_INIT(JSTransfer::Init) {
|
||||
auto klass = Nan::New<v8::FunctionTemplate>(JSTransfer::NewInstance);
|
||||
klass->SetClassName(Nan::New("JSTransfer").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "start", JSTransfer::_start);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(JSTransfer::NewInstance) {
|
||||
if (info.IsConstructCall()) {
|
||||
if(info.Length() != 1 || !info[0]->IsObject()) {
|
||||
Nan::ThrowError("invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* transfer_key: string;
|
||||
* client_transfer_id: number;
|
||||
* server_transfer_id: number;
|
||||
* object: HandledTransferObject;
|
||||
*/
|
||||
auto options = info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked();
|
||||
v8::Local<v8::String> key = options->Get(Nan::New<v8::String>("transfer_key").ToLocalChecked()).As<v8::String>();
|
||||
v8::Local<v8::Number> client_transfer_id = options->Get(Nan::New<v8::String>("client_transfer_id").ToLocalChecked()).As<v8::Number>();
|
||||
v8::Local<v8::Number> server_transfer_id = options->Get(Nan::New<v8::String>("server_transfer_id").ToLocalChecked()).As<v8::Number>();
|
||||
v8::Local<v8::String> remote_address = options->Get(Nan::New<v8::String>("remote_address").ToLocalChecked()).As<v8::String>();
|
||||
v8::Local<v8::Number> remote_port = options->Get(Nan::New<v8::String>("remote_port").ToLocalChecked()).As<v8::Number>();
|
||||
|
||||
if(
|
||||
key.IsEmpty() || !key->IsString() ||
|
||||
remote_address.IsEmpty() || !remote_address->IsString() ||
|
||||
remote_port.IsEmpty() || !remote_port->IsInt32() ||
|
||||
client_transfer_id.IsEmpty() || !client_transfer_id->IsInt32() ||
|
||||
server_transfer_id.IsEmpty() || !server_transfer_id->IsInt32()
|
||||
) {
|
||||
Nan::ThrowError("invalid argument types");
|
||||
return;
|
||||
}
|
||||
|
||||
auto wrapped_options = options->Get(Nan::New<v8::String>("object").ToLocalChecked()).As<v8::Object>();
|
||||
if(!TransferObjectWrap::is_wrap(wrapped_options)) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
auto transfer_object = ObjectWrap::Unwrap<TransferObjectWrap>(wrapped_options)->target();
|
||||
assert(transfer_object);
|
||||
|
||||
|
||||
auto t_options = make_unique<TransferOptions>();
|
||||
t_options->transfer_key = *Nan::Utf8String(key);
|
||||
t_options->client_transfer_id = client_transfer_id->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
t_options->server_transfer_id = server_transfer_id->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
t_options->remote_address = *Nan::Utf8String(remote_address);
|
||||
t_options->remote_port = remote_port->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
|
||||
string error;
|
||||
auto transfer = transfer_manager->register_transfer(error, transfer_object, move(t_options));
|
||||
if(!transfer) {
|
||||
Nan::ThrowError(Nan::New<v8::String>("failed to create transfer: " + error).ToLocalChecked());
|
||||
return;
|
||||
}
|
||||
|
||||
auto js_instance = new JSTransfer(transfer);
|
||||
js_instance->Wrap(info.This());
|
||||
js_instance->_self_ref = true;
|
||||
js_instance->Ref(); /* increase ref counter because file transfer is running */
|
||||
info.GetReturnValue().Set(info.This());
|
||||
} else {
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
|
||||
v8::Local<v8::Function> cons = Nan::New(constructor());
|
||||
v8::Local<v8::Value> argv[1] = {info[0]};
|
||||
|
||||
Nan::TryCatch try_catch;
|
||||
auto result = Nan::NewInstance(cons, info.Length(), argv);
|
||||
if(try_catch.HasCaught()) {
|
||||
try_catch.ReThrow();
|
||||
return;
|
||||
}
|
||||
info.GetReturnValue().Set(result.ToLocalChecked());
|
||||
}
|
||||
}
|
||||
|
||||
JSTransfer::JSTransfer(std::shared_ptr<tc::ft::Transfer> transfer) : _transfer(move(transfer)) {
|
||||
this->call_failed = Nan::async_callback([&](std::string error) {
|
||||
Nan::HandleScope scope;
|
||||
this->callback_failed(error);
|
||||
});
|
||||
this->call_finished = Nan::async_callback([&](bool f) {
|
||||
Nan::HandleScope scope;
|
||||
this->callback_finished(f);
|
||||
});
|
||||
this->call_start = Nan::async_callback([&] {
|
||||
Nan::HandleScope scope;
|
||||
this->callback_start();
|
||||
});
|
||||
this->call_progress = Nan::async_callback([&](uint64_t a, uint64_t b) {
|
||||
Nan::HandleScope scope;
|
||||
this->callback_progress(a, b);
|
||||
});
|
||||
|
||||
this->_transfer->callback_failed = [&](std::string error) { this->call_failed(std::forward<string>(error)); };
|
||||
this->_transfer->callback_finished = [&](bool f) { this->call_finished(std::forward<bool>(f)); };
|
||||
this->_transfer->callback_start = [&] { this->call_start(); };
|
||||
this->_transfer->callback_process = [&](uint64_t a, uint64_t b) { this->call_progress.call_cpy(a, b); };
|
||||
}
|
||||
|
||||
JSTransfer::~JSTransfer() {
|
||||
cout << "JS dealloc" << endl;
|
||||
this->_transfer->callback_failed = NULL;
|
||||
this->_transfer->callback_finished = NULL;
|
||||
this->_transfer->callback_start = NULL;
|
||||
this->_transfer->callback_process = NULL;
|
||||
}
|
||||
|
||||
NAN_METHOD(JSTransfer::destory_transfer) {
|
||||
//TODO!
|
||||
Nan::ThrowError("Not implemented!");
|
||||
}
|
||||
|
||||
NAN_METHOD(JSTransfer::_start) {
|
||||
return ObjectWrap::Unwrap<JSTransfer>(info.Holder())->start(info);
|
||||
}
|
||||
NAN_METHOD(JSTransfer::start) {
|
||||
if(!this->_transfer->connect()) {
|
||||
Nan::ThrowError("failed to connect");
|
||||
return;
|
||||
}
|
||||
|
||||
log_info(category::file_transfer, tr("Connecting to {}:{}"), this->_transfer->options().remote_address, this->_transfer->options().remote_port);
|
||||
}
|
||||
|
||||
NAN_METHOD(JSTransfer::_abort) {
|
||||
return ObjectWrap::Unwrap<JSTransfer>(info.Holder())->abort(info);
|
||||
}
|
||||
NAN_METHOD(JSTransfer::abort) {
|
||||
//TODO!
|
||||
}
|
||||
|
||||
void JSTransfer::callback_finished(bool flag) {
|
||||
if(this->_self_ref) {
|
||||
this->_self_ref = false;
|
||||
this->Unref();
|
||||
}
|
||||
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_finished").ToLocalChecked()).ToLocalChecked().As<v8::Function>();
|
||||
if(callback.IsEmpty() || !callback->IsFunction())
|
||||
return;
|
||||
|
||||
v8::Local<v8::Value> arguments[1];
|
||||
arguments[0] = Nan::New<v8::Boolean>(flag);
|
||||
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, arguments);
|
||||
}
|
||||
|
||||
void JSTransfer::callback_start() {
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_start").ToLocalChecked()).ToLocalChecked().As<v8::Function>();
|
||||
if(callback.IsEmpty() || !callback->IsFunction())
|
||||
return;
|
||||
|
||||
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
|
||||
}
|
||||
|
||||
void JSTransfer::callback_progress(uint64_t a, uint64_t b) {
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_progress").ToLocalChecked()).ToLocalChecked().As<v8::Function>();
|
||||
if(callback.IsEmpty() || !callback->IsFunction())
|
||||
return;
|
||||
|
||||
v8::Local<v8::Value> arguments[2];
|
||||
arguments[0] = Nan::New<v8::Number>(a);
|
||||
arguments[1] = Nan::New<v8::Number>(b);
|
||||
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 2, arguments);
|
||||
}
|
||||
|
||||
void JSTransfer::callback_failed(std::string error) {
|
||||
if(this->_self_ref) {
|
||||
this->_self_ref = false;
|
||||
this->Unref();
|
||||
}
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_failed").ToLocalChecked()).ToLocalChecked().As<v8::Function>();
|
||||
if(callback.IsEmpty() || !callback->IsFunction())
|
||||
return;
|
||||
|
||||
v8::Local<v8::Value> arguments[1];
|
||||
arguments[0] = Nan::New<v8::String>(error).ToLocalChecked();
|
||||
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, arguments);
|
||||
}
|
||||
#endif
|
||||
@@ -1,228 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <functional>
|
||||
#include <condition_variable>
|
||||
|
||||
#include <event.h>
|
||||
#include <pipes/buffer.h>
|
||||
#include <cstring>
|
||||
|
||||
#if NODEJS_API
|
||||
#include <nan.h>
|
||||
#include <NanEventCallback.h>
|
||||
#include "../../logger.h"
|
||||
|
||||
#endif
|
||||
|
||||
namespace tc {
|
||||
namespace ft {
|
||||
namespace error {
|
||||
enum value : int8_t {
|
||||
success = 0,
|
||||
custom = 1,
|
||||
custom_recoverable = 2,
|
||||
would_block = 3,
|
||||
out_of_space = 4
|
||||
};
|
||||
}
|
||||
class TransferObject {
|
||||
public:
|
||||
explicit TransferObject() {}
|
||||
|
||||
virtual std::string name() const = 0;
|
||||
virtual bool initialize(std::string& /* error */) = 0;
|
||||
virtual void finalize() = 0;
|
||||
};
|
||||
|
||||
class TransferSource : public TransferObject {
|
||||
public:
|
||||
virtual uint64_t byte_length() const = 0;
|
||||
virtual uint64_t stream_index() const = 0;
|
||||
|
||||
virtual error::value read_bytes(std::string& /* error */, uint8_t* /* buffer */, uint64_t& /* max length/result length */) = 0;
|
||||
private:
|
||||
};
|
||||
|
||||
class TransferTarget : public TransferObject {
|
||||
public:
|
||||
TransferTarget() {}
|
||||
|
||||
virtual uint64_t expected_length() const = 0;
|
||||
|
||||
virtual uint64_t stream_index() const = 0;
|
||||
virtual error::value write_bytes(std::string& /* error */, uint8_t* /* buffer */, uint64_t /* max length */) = 0;
|
||||
};
|
||||
|
||||
struct TransferOptions {
|
||||
std::string remote_address;
|
||||
uint16_t remote_port = 0;
|
||||
std::string transfer_key{};
|
||||
uint32_t client_transfer_id = 0;
|
||||
uint32_t server_transfer_id = 0;
|
||||
};
|
||||
|
||||
class FileTransferManager;
|
||||
class Transfer {
|
||||
friend class FileTransferManager;
|
||||
public:
|
||||
struct state {
|
||||
enum value {
|
||||
UNINITIALIZED,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
DISCONNECTING
|
||||
};
|
||||
};
|
||||
typedef std::function<void()> callback_start_t;
|
||||
typedef std::function<void(bool /* aborted */)> callback_finished_t;
|
||||
typedef std::function<void(const std::string& /* error */)> callback_failed_t;
|
||||
typedef std::function<void(uint64_t /* current index */, uint64_t /* max index */)> callback_process_t;
|
||||
|
||||
explicit Transfer(FileTransferManager* handle, std::shared_ptr<TransferObject> transfer_object, std::unique_ptr<TransferOptions> options) :
|
||||
_transfer_object(std::move(transfer_object)),
|
||||
_handle(handle),
|
||||
_options(std::move(options)) {
|
||||
log_allocate("Transfer", this);
|
||||
}
|
||||
~Transfer();
|
||||
|
||||
bool initialize(std::string& /* error */);
|
||||
void finalize(bool /* blocking */ = true);
|
||||
|
||||
bool connect();
|
||||
bool connected() { return this->_state > state::UNINITIALIZED; }
|
||||
|
||||
FileTransferManager* handle() { return this->_handle; }
|
||||
std::shared_ptr<TransferObject> transfer_object() { return this->_transfer_object; }
|
||||
const TransferOptions& options() { return *this->_options; }
|
||||
|
||||
callback_start_t callback_start{nullptr};
|
||||
callback_finished_t callback_finished{nullptr};
|
||||
callback_failed_t callback_failed{nullptr};
|
||||
callback_process_t callback_process{nullptr};
|
||||
private:
|
||||
static void _callback_read(evutil_socket_t, short, void*);
|
||||
static void _callback_write(evutil_socket_t, short, void*);
|
||||
|
||||
sockaddr_storage remote_address{};
|
||||
FileTransferManager* _handle;
|
||||
std::unique_ptr<TransferOptions> _options;
|
||||
state::value _state = state::UNINITIALIZED;
|
||||
std::shared_ptr<TransferObject> _transfer_object;
|
||||
|
||||
std::mutex event_lock;
|
||||
event_base* event_io = nullptr; /* gets assigned by the manager */
|
||||
::event* event_read = nullptr;
|
||||
::event* event_write = nullptr;
|
||||
|
||||
std::chrono::system_clock::time_point last_source_read;
|
||||
std::chrono::system_clock::time_point last_target_write;
|
||||
std::mutex queue_lock;
|
||||
std::deque<pipes::buffer> write_queue;
|
||||
void _write_message(const pipes::buffer_view& /* buffer */);
|
||||
int _socket = 0;
|
||||
|
||||
timeval alive_check_timeout{1, 0};
|
||||
timeval write_timeout{1, 0};
|
||||
|
||||
/*
|
||||
* Upload mode:
|
||||
* Write the buffers left in write_queue, and if the queue length is less then 12 create new buffers.
|
||||
* This event will as well be triggered every second as timeout, to create new buffers if needed
|
||||
*/
|
||||
void callback_write(short /* flags */);
|
||||
void callback_read(short /* flags */);
|
||||
|
||||
/* called within the write/read callback */
|
||||
void handle_disconnect();
|
||||
void handle_connected();
|
||||
|
||||
void call_callback_connected();
|
||||
void call_callback_failed(const std::string& /* reason */);
|
||||
void call_callback_finished(bool /* aborted */);
|
||||
void call_callback_process(size_t /* current */, size_t /* max */);
|
||||
|
||||
std::chrono::system_clock::time_point last_process_call;
|
||||
};
|
||||
|
||||
class FileTransferManager {
|
||||
public:
|
||||
FileTransferManager();
|
||||
~FileTransferManager();
|
||||
|
||||
void initialize();
|
||||
void finalize();
|
||||
|
||||
std::shared_ptr<Transfer> register_transfer(std::string& error, const std::shared_ptr<TransferObject>& /* object */, std::unique_ptr<TransferOptions> /* options */);
|
||||
std::deque<std::shared_ptr<Transfer>> running_transfers() {
|
||||
std::lock_guard lock(this->_transfer_lock);
|
||||
return this->_running_transfers;
|
||||
}
|
||||
void drop_transfer(const std::shared_ptr<Transfer>& /* transfer */);
|
||||
void remove_transfer(Transfer*); /* internal use */
|
||||
inline void execute_event_loop() {
|
||||
this->event_execute = true;
|
||||
this->event_io_condition.notify_all();
|
||||
}
|
||||
private:
|
||||
bool event_execute = false;
|
||||
bool event_io_canceled = false;
|
||||
std::mutex event_io_lock;
|
||||
std::condition_variable event_io_condition;
|
||||
std::thread event_io_thread;
|
||||
event_base* event_io = nullptr;
|
||||
::event* event_cleanup = nullptr;
|
||||
|
||||
std::mutex _transfer_lock;
|
||||
std::deque<std::shared_ptr<Transfer>> _running_transfers;
|
||||
|
||||
void _execute_event_loop();
|
||||
};
|
||||
|
||||
#ifdef NODEJS_API
|
||||
class JSTransfer : public Nan::ObjectWrap {
|
||||
public:
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(NewInstance);
|
||||
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
|
||||
explicit JSTransfer(std::shared_ptr<Transfer> transfer);
|
||||
~JSTransfer();
|
||||
|
||||
NAN_METHOD(start);
|
||||
NAN_METHOD(abort);
|
||||
|
||||
static NAN_METHOD(destory_transfer);
|
||||
private:
|
||||
static NAN_METHOD(_start);
|
||||
static NAN_METHOD(_abort);
|
||||
|
||||
std::shared_ptr<Transfer> _transfer;
|
||||
|
||||
Nan::callback_t<bool> call_finished;
|
||||
Nan::callback_t<> call_start;
|
||||
Nan::callback_t<uint64_t, uint64_t> call_progress;
|
||||
Nan::callback_t<std::string> call_failed;
|
||||
|
||||
void callback_finished(bool);
|
||||
void callback_start();
|
||||
void callback_progress(uint64_t, uint64_t);
|
||||
void callback_failed(std::string);
|
||||
|
||||
bool _self_ref = false;
|
||||
};
|
||||
#endif
|
||||
}
|
||||
}
|
||||
extern tc::ft::FileTransferManager* transfer_manager;
|
||||
@@ -1,290 +0,0 @@
|
||||
#include "FileTransferObject.h"
|
||||
#include "../../logger.h"
|
||||
#include <iostream>
|
||||
|
||||
#include <experimental/filesystem>
|
||||
|
||||
namespace fs = std::experimental::filesystem;
|
||||
using namespace tc;
|
||||
using namespace tc::ft;
|
||||
using namespace std;
|
||||
|
||||
#ifdef NODEJS_API
|
||||
TransferJSBufferTarget::TransferJSBufferTarget() {
|
||||
log_allocate("TransferJSBufferTarget", this);
|
||||
|
||||
if(!this->_js_buffer.IsEmpty()) {
|
||||
assert(v8::Isolate::GetCurrent());
|
||||
this->_js_buffer.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
TransferJSBufferTarget::~TransferJSBufferTarget() {
|
||||
log_free("TransferJSBufferTarget", this);
|
||||
}
|
||||
|
||||
bool TransferJSBufferTarget::initialize(std::string &error) {
|
||||
return true; /* we've already have data */
|
||||
}
|
||||
|
||||
void TransferJSBufferTarget::finalize() { }
|
||||
|
||||
uint64_t TransferJSBufferTarget::stream_index() const {
|
||||
return this->_js_buffer_index;
|
||||
}
|
||||
|
||||
error::value TransferJSBufferTarget::write_bytes(std::string &error, uint8_t *source, uint64_t length) {
|
||||
uint64_t write_length = length;
|
||||
if(length > this->_js_buffer_length - this->_js_buffer_index)
|
||||
write_length = this->_js_buffer_length - this->_js_buffer_index;
|
||||
|
||||
if(write_length > 0) {
|
||||
memcpy((char*) this->_js_buffer_source + this->_js_buffer_index, source, write_length);
|
||||
this->_js_buffer_index += write_length;
|
||||
}
|
||||
|
||||
if(write_length == 0)
|
||||
return error::out_of_space;
|
||||
|
||||
return error::success;
|
||||
}
|
||||
|
||||
NAN_METHOD(TransferJSBufferTarget::create_from_buffer) {
|
||||
if(info.Length() != 1 || !info[0]->IsArrayBuffer()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
auto buffer = info[0].As<v8::ArrayBuffer>();
|
||||
|
||||
auto instance = make_shared<TransferJSBufferTarget>();
|
||||
instance->_js_buffer = v8::Global<v8::ArrayBuffer>(info.GetIsolate(), info[0].As<v8::ArrayBuffer>());
|
||||
|
||||
instance->_js_buffer_source = buffer->GetContents().Data();
|
||||
instance->_js_buffer_length = buffer->GetContents().ByteLength();
|
||||
instance->_js_buffer_index = 0;
|
||||
|
||||
|
||||
auto object_wrap = new TransferObjectWrap(instance);
|
||||
auto object = Nan::NewInstance(Nan::New(TransferObjectWrap::constructor()), 0, nullptr).ToLocalChecked();
|
||||
object_wrap->do_wrap(object);
|
||||
info.GetReturnValue().Set(object);
|
||||
}
|
||||
|
||||
TransferJSBufferSource::~TransferJSBufferSource() {
|
||||
log_free("TransferJSBufferSource", this);
|
||||
|
||||
if(!this->_js_buffer.IsEmpty()) {
|
||||
assert(v8::Isolate::GetCurrent());
|
||||
this->_js_buffer.Reset();
|
||||
}
|
||||
}
|
||||
TransferJSBufferSource::TransferJSBufferSource() {
|
||||
log_allocate("TransferJSBufferSource", this);
|
||||
}
|
||||
|
||||
bool TransferJSBufferSource::initialize(std::string &string) { return true; }
|
||||
|
||||
void TransferJSBufferSource::finalize() { }
|
||||
|
||||
uint64_t TransferJSBufferSource::stream_index() const {
|
||||
return this->_js_buffer_index;
|
||||
}
|
||||
|
||||
uint64_t TransferJSBufferSource::byte_length() const {
|
||||
return this->_js_buffer_length;
|
||||
}
|
||||
|
||||
error::value TransferJSBufferSource::read_bytes(std::string &error, uint8_t *target, uint64_t &length) {
|
||||
auto org_length = length;
|
||||
if(this->_js_buffer_index + length > this->_js_buffer_length)
|
||||
length = this->_js_buffer_length - this->_js_buffer_index;
|
||||
|
||||
|
||||
memcpy(target, (char*) this->_js_buffer_source + this->_js_buffer_index, length);
|
||||
this->_js_buffer_index += length;
|
||||
|
||||
if(org_length != 0 && length == 0)
|
||||
return error::out_of_space;
|
||||
return error::success;
|
||||
}
|
||||
|
||||
NAN_METHOD(TransferJSBufferSource::create_from_buffer) {
|
||||
if(info.Length() != 1 || !info[0]->IsArrayBuffer()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
auto buffer = info[0].As<v8::ArrayBuffer>();
|
||||
|
||||
auto instance = make_shared<TransferJSBufferSource>();
|
||||
instance->_js_buffer = v8::Global<v8::ArrayBuffer>(info.GetIsolate(), info[0].As<v8::ArrayBuffer>());
|
||||
|
||||
instance->_js_buffer_source = buffer->GetContents().Data();
|
||||
instance->_js_buffer_length = buffer->GetContents().ByteLength();
|
||||
instance->_js_buffer_index = 0;
|
||||
|
||||
|
||||
auto object_wrap = new TransferObjectWrap(instance);
|
||||
auto object = Nan::NewInstance(Nan::New(TransferObjectWrap::constructor()), 0, nullptr).ToLocalChecked();
|
||||
object_wrap->do_wrap(object);
|
||||
info.GetReturnValue().Set(object);
|
||||
}
|
||||
|
||||
|
||||
NAN_MODULE_INIT(TransferObjectWrap::Init) {
|
||||
auto klass = Nan::New<v8::FunctionTemplate>(TransferObjectWrap::NewInstance);
|
||||
klass->SetClassName(Nan::New("TransferObjectWrap").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(TransferObjectWrap::NewInstance) {
|
||||
if(!info.IsConstructCall())
|
||||
Nan::ThrowError("invalid invoke!");
|
||||
}
|
||||
|
||||
void TransferObjectWrap::do_wrap(v8::Local<v8::Object> object) {
|
||||
this->Wrap(object);
|
||||
|
||||
auto source = dynamic_pointer_cast<TransferSource>(this->target());
|
||||
auto target = dynamic_pointer_cast<TransferTarget>(this->target());
|
||||
|
||||
auto direction = source ? "upload" : "download";
|
||||
Nan::Set(object,
|
||||
Nan::New<v8::String>("direction").ToLocalChecked(),
|
||||
v8::String::NewFromUtf8(Nan::GetCurrentContext()->GetIsolate(), direction).ToLocalChecked()
|
||||
);
|
||||
Nan::Set(object,
|
||||
Nan::New<v8::String>("name").ToLocalChecked(),
|
||||
v8::String::NewFromUtf8(Nan::GetCurrentContext()->GetIsolate(), this->target()->name().c_str()).ToLocalChecked()
|
||||
);
|
||||
|
||||
if(source) {
|
||||
Nan::Set(object,
|
||||
Nan::New<v8::String>("total_size").ToLocalChecked(),
|
||||
Nan::New<v8::Number>(source->byte_length())
|
||||
);
|
||||
}
|
||||
|
||||
if(target) {
|
||||
Nan::Set(object,
|
||||
Nan::New<v8::String>("expected_size").ToLocalChecked(),
|
||||
Nan::New<v8::Number>(target->expected_length())
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
TransferFileSource::TransferFileSource(std::string path, std::string name) : _path{std::move(path)}, _name{std::move(name)} {
|
||||
if(!this->_path.empty()) {
|
||||
if(this->_path.back() == '/')
|
||||
this->_path.pop_back();
|
||||
#ifdef WIN32
|
||||
if(this->_path.back() == '\\')
|
||||
this->_path.pop_back();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
#define u8path path
|
||||
#endif
|
||||
|
||||
uint64_t TransferFileSource::byte_length() const {
|
||||
if(file_size.has_value())
|
||||
return file_size.value();
|
||||
|
||||
auto file = fs::u8path(this->_path) / fs::u8path(this->_name);
|
||||
error_code error;
|
||||
auto size = fs::file_size(file,error);
|
||||
if(error)
|
||||
size = 0;
|
||||
return (this->file_size = std::make_optional<size_t>(size)).value();
|
||||
}
|
||||
|
||||
bool TransferFileSource::initialize(std::string &error) {
|
||||
auto file = fs::u8path(this->_path) / fs::u8path(this->_name);
|
||||
|
||||
error_code errc;
|
||||
if(!fs::exists(file)) {
|
||||
error = "file not found";
|
||||
return false;
|
||||
}
|
||||
if(errc) {
|
||||
error = "failed to test for file existence: " + to_string(errc.value()) + "/" + errc.message();
|
||||
return false;
|
||||
}
|
||||
if(!fs::is_regular_file(file, errc)) {
|
||||
error = "target file isn't a regular file";
|
||||
return false;
|
||||
}
|
||||
if(errc) {
|
||||
error = "failed to test for file regularity: " + to_string(errc.value()) + "/" + errc.message();
|
||||
return false;
|
||||
}
|
||||
|
||||
this->file_stream = std::ifstream{file, std::ifstream::in | std::ifstream::binary};
|
||||
if(!this->file_stream) {
|
||||
error = "failed to open file";
|
||||
return false;
|
||||
}
|
||||
|
||||
this->file_stream.seekg(0, std::ifstream::end);
|
||||
auto length = this->file_stream.tellg();
|
||||
if(length != this->byte_length()) {
|
||||
error = "file length missmatch";
|
||||
return false;
|
||||
}
|
||||
this->file_stream.seekg(0, std::ifstream::beg);
|
||||
|
||||
this->position = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TransferFileSource::finalize() {
|
||||
if(this->file_stream)
|
||||
this->file_stream.close();
|
||||
|
||||
this->position = 0;
|
||||
}
|
||||
|
||||
error::value TransferFileSource::read_bytes(std::string &error, uint8_t *buffer, uint64_t &length) {
|
||||
auto result = this->file_stream.readsome((char*) buffer, length);
|
||||
if(result > 0) {
|
||||
length = result;
|
||||
this->position += result;
|
||||
return error::success;
|
||||
}
|
||||
|
||||
if(!this->file_stream) {
|
||||
if(this->file_stream.eof())
|
||||
error = "eof reached";
|
||||
else
|
||||
error = "io error. failed to read";
|
||||
} else {
|
||||
error = "read returned " + to_string(result) + "/" + to_string(length);
|
||||
}
|
||||
return error::custom;
|
||||
}
|
||||
|
||||
uint64_t TransferFileSource::stream_index() const {
|
||||
return this->position;
|
||||
}
|
||||
|
||||
#ifdef NODEJS_API
|
||||
NAN_METHOD(TransferFileSource::create) {
|
||||
if(info.Length() != 2 || !info[0]->IsString() || !info[1]->IsString()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
auto instance = make_shared<TransferFileSource>(*Nan::Utf8String{info[0]}, *Nan::Utf8String{info[1]});
|
||||
auto object_wrap = new TransferObjectWrap(instance);
|
||||
auto object = Nan::NewInstance(Nan::New(TransferObjectWrap::constructor()), 0, nullptr).ToLocalChecked();
|
||||
object_wrap->do_wrap(object);
|
||||
info.GetReturnValue().Set(object);
|
||||
}
|
||||
#endif
|
||||
@@ -1,115 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <fstream>
|
||||
#include "FileTransferManager.h"
|
||||
|
||||
namespace tc {
|
||||
namespace ft {
|
||||
class TransferFileSource : public TransferSource {
|
||||
public:
|
||||
TransferFileSource(std::string /* path */, std::string /* name */);
|
||||
|
||||
[[nodiscard]] inline std::string file_path() const { return this->_path; }
|
||||
[[nodiscard]] inline std::string file_name() const { return this->_name; }
|
||||
|
||||
std::string name() const override { return "TransferFileSource"; }
|
||||
bool initialize(std::string &string) override;
|
||||
void finalize() override;
|
||||
|
||||
uint64_t byte_length() const override;
|
||||
uint64_t stream_index() const override;
|
||||
error::value read_bytes(std::string &string, uint8_t *uint8, uint64_t &uint64) override;
|
||||
|
||||
#ifdef NODEJS_API
|
||||
static NAN_METHOD(create);
|
||||
#endif
|
||||
private:
|
||||
std::string _path;
|
||||
std::string _name;
|
||||
|
||||
uint64_t position{0};
|
||||
std::ifstream file_stream{};
|
||||
mutable std::optional<size_t> file_size;
|
||||
};
|
||||
|
||||
#ifdef NODEJS_API
|
||||
class TransferObjectWrap : public Nan::ObjectWrap {
|
||||
public:
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(NewInstance);
|
||||
|
||||
static inline bool is_wrap(const v8::Local<v8::Value>& value) {
|
||||
if(value.As<v8::Object>().IsEmpty())
|
||||
return false;
|
||||
|
||||
return value->InstanceOf(Nan::GetCurrentContext(), Nan::New<v8::Function>(constructor())).FromMaybe(false);
|
||||
}
|
||||
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
|
||||
explicit TransferObjectWrap(std::shared_ptr<TransferObject> object) : _transfer(std::move(object)) {
|
||||
}
|
||||
|
||||
~TransferObjectWrap() = default;
|
||||
|
||||
void do_wrap(v8::Local<v8::Object> object);
|
||||
|
||||
std::shared_ptr<TransferObject> target() { return this->_transfer; }
|
||||
private:
|
||||
std::shared_ptr<TransferObject> _transfer;
|
||||
};
|
||||
|
||||
class TransferJSBufferSource : public TransferSource {
|
||||
public:
|
||||
TransferJSBufferSource();
|
||||
virtual ~TransferJSBufferSource();
|
||||
|
||||
std::string name() const override { return "TransferJSBufferSource"; }
|
||||
bool initialize(std::string &string) override;
|
||||
|
||||
void finalize() override;
|
||||
|
||||
uint64_t stream_index() const override;
|
||||
|
||||
uint64_t byte_length() const override;
|
||||
|
||||
error::value read_bytes(std::string &string, uint8_t *uint8, uint64_t &uint64) override;
|
||||
|
||||
static NAN_METHOD(create_from_buffer);
|
||||
|
||||
private:
|
||||
v8::Global<v8::ArrayBuffer> _js_buffer;
|
||||
void* _js_buffer_source;
|
||||
uint64_t _js_buffer_length;
|
||||
uint64_t _js_buffer_index;
|
||||
};
|
||||
|
||||
class TransferJSBufferTarget : public TransferTarget {
|
||||
public:
|
||||
TransferJSBufferTarget();
|
||||
virtual ~TransferJSBufferTarget();
|
||||
|
||||
std::string name() const override { return "TransferJSBufferTarget"; }
|
||||
bool initialize(std::string &string) override;
|
||||
|
||||
void finalize() override;
|
||||
|
||||
uint64_t stream_index() const override;
|
||||
uint64_t expected_length() const override { return this->_js_buffer_length; }
|
||||
|
||||
error::value write_bytes(std::string &string, uint8_t *uint8, uint64_t uint64) override;
|
||||
|
||||
static NAN_METHOD(create_from_buffer);
|
||||
|
||||
private:
|
||||
v8::Global<v8::ArrayBuffer> _js_buffer;
|
||||
void* _js_buffer_source;
|
||||
uint64_t _js_buffer_length;
|
||||
uint64_t _js_buffer_index;
|
||||
};
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
#include "./hwuid.h"
|
||||
#include <tomcrypt.h>
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <misc/strobf.h>
|
||||
#include <misc/base64.h>
|
||||
|
||||
#ifndef WIN32
|
||||
#ifdef DARWIN
|
||||
/*
|
||||
#include <mach-o/arch.h>
|
||||
unsigned short getCpuHash() {
|
||||
const NXArchInfo* info = NXGetLocalArchInfo();
|
||||
unsigned short val = 0;
|
||||
val += (unsigned short)info->cputype;
|
||||
val += (unsigned short)info->cpusubtype;
|
||||
return val;
|
||||
}
|
||||
*/
|
||||
#else // !DARWIN
|
||||
static inline void native_cpuid(unsigned int *eax, unsigned int *ebx,
|
||||
unsigned int *ecx, unsigned int *edx)
|
||||
{
|
||||
/* ecx is often an input as well as an output. */
|
||||
asm volatile("cpuid"
|
||||
: "=a" (*eax),
|
||||
"=b" (*ebx),
|
||||
"=c" (*ecx),
|
||||
"=d" (*edx)
|
||||
: "0" (*eax), "2" (*ecx));
|
||||
}
|
||||
|
||||
uint32_t calculate_cpu_hash() {
|
||||
uint32_t cpuinfo[4] = { 0, 0, 0, 0 };
|
||||
native_cpuid(cpuinfo, cpuinfo + 1, cpuinfo + 2, cpuinfo + 3);
|
||||
|
||||
uint32_t hash = 0;
|
||||
uint32_t* ptr = (&cpuinfo[0]);
|
||||
for (uint32_t i = 0; i < 4; i++)
|
||||
hash += ptr[i];
|
||||
|
||||
return hash;
|
||||
}
|
||||
#endif // !DARWIN
|
||||
|
||||
constexpr uint8_t hex_value(char c) {
|
||||
if(c >= '0' && c <= '9')
|
||||
return c - '0';
|
||||
if(c >= 'a' && c <= 'f')
|
||||
return c - 'A' + 10;
|
||||
if(c >= 'A' && c <= 'F')
|
||||
return c - '0' + 10;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool read_machine_id(uint8_t(&uuid)[16]) {
|
||||
strobf_define(_path, "/etc/machine-id");
|
||||
|
||||
memset(uuid, 0, 16);
|
||||
std::ifstream in(strobf_val(_path).string());
|
||||
if(in) {
|
||||
char buffer[32];
|
||||
if(!in.read(buffer, 32)) return false;
|
||||
|
||||
auto it = buffer;
|
||||
auto index = 0;
|
||||
while(index < 16) {
|
||||
uuid[index] = hex_value(*it) << 4UL;
|
||||
uuid[index] = hex_value(*it);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool generate_uuid(std::string& result, uint32_t& check_sum) {
|
||||
uint8_t buffer[16];
|
||||
if(!read_machine_id(buffer))
|
||||
memcpy(buffer, "AAAABBBBCCCCDDDD", 16);
|
||||
|
||||
auto cpu_hash = calculate_cpu_hash();
|
||||
auto it = (uint32_t*) buffer;
|
||||
for(int i = 0; i < 4; i++)
|
||||
*it++ = cpu_hash;
|
||||
|
||||
result = base64::encode((char*) buffer, 16);
|
||||
|
||||
{
|
||||
crc32_state state;
|
||||
crc32_init(&state);
|
||||
crc32_update(&state, (u_char*) result.data(), result.length());
|
||||
|
||||
crc32_finish(&state, &check_sum, sizeof(check_sum));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
#include "./smbios.h"
|
||||
#include <Windows.h>
|
||||
#include <sysinfoapi.h>
|
||||
#include <intrin.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
//https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_2.7.1.pdf
|
||||
int get_uuid(uint8_t* uuid) {
|
||||
#ifdef WIN32
|
||||
typedef DWORD api_size_t;
|
||||
#else
|
||||
typedef size_t api_size_t;
|
||||
#endif
|
||||
unique_ptr<SMBIOSDataHeader, decltype(::free)*> smbios_data{ nullptr, ::free };
|
||||
api_size_t smbios_size = 0;
|
||||
api_size_t bytes_written = 0;
|
||||
|
||||
// Query size of SMBIOS data.
|
||||
smbios_size = GetSystemFirmwareTable('RSMB', 0, nullptr, 0);
|
||||
smbios_data.reset((SMBIOSDataHeader*) malloc(smbios_size));
|
||||
if (!smbios_data) return -1; //failed to allocate memory
|
||||
|
||||
bytes_written = GetSystemFirmwareTable('RSMB', 0, &*smbios_data, smbios_size);
|
||||
|
||||
if (smbios_size != bytes_written) return -2; //failed to read the firmware table
|
||||
|
||||
if (smbios_data->length > bytes_written - SMBIOSDataHeaderLength) return -3; //invalid length
|
||||
|
||||
//Lets iterate over the headers:
|
||||
uint8_t* ptr = &smbios_data->table_data;
|
||||
SMBIOSEntryHeader* header;
|
||||
while (true) {
|
||||
header = (SMBIOSEntryHeader*) ptr;
|
||||
if (header->type == 127 && header->length == 4) {
|
||||
break; //End marker
|
||||
}
|
||||
|
||||
if (header->type == 1) {
|
||||
//Type 1: System Information
|
||||
if (header->length < 0x18) {
|
||||
//If version is under 2.1 no uuid is set!
|
||||
//TODO: Check before for the version?
|
||||
return -4; //system information table is too short
|
||||
}
|
||||
|
||||
memcpy(uuid, ptr + 0x08, 16);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ptr = ptr + header->length; // point to struct end
|
||||
while (0 != (*ptr | *(ptr + 1)) && ptr + 1 < (uint8_t*) &*smbios_data + smbios_data->length)
|
||||
ptr++; // skip string area
|
||||
|
||||
ptr += 2;
|
||||
if (ptr >= (uint8_t*) &*smbios_data + smbios_data->length - 2)
|
||||
break;
|
||||
}
|
||||
return -6;
|
||||
}
|
||||
|
||||
uint32_t calculate_cpu_hash() {
|
||||
int cpuinfo[4] = { 0, 0, 0, 0 };
|
||||
__cpuid(cpuinfo, 0);
|
||||
|
||||
uint32_t hash = 0;
|
||||
auto ptr = (uint32_t*) (&cpuinfo[0]);
|
||||
for (uint32_t i = 0; i < 4; i++)
|
||||
hash += ptr[i];
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
inline bool generate_uuid(std::string& result, uint32_t& check_sum) {
|
||||
uint8_t buffer[16];
|
||||
auto code = get_uuid(buffer);
|
||||
if(code != 0) {
|
||||
memcpy(buffer, "DDDDCCCCBBBBAAAA", 16); /* Reversed to linux (just for the foxic) */
|
||||
buffer[15] = code; /* So we could get the error code */
|
||||
}
|
||||
|
||||
auto cpu_hash = calculate_cpu_hash();
|
||||
auto it = (uint32_t*) buffer;
|
||||
for(int i = 0; i < 4; i++)
|
||||
*it++ = cpu_hash;
|
||||
|
||||
result = base64::encode((char*) buffer, 16);
|
||||
|
||||
{
|
||||
crc32_state state;
|
||||
crc32_init(&state);
|
||||
crc32_update(&state, (uint8_t*) result.data(), (unsigned long) result.length());
|
||||
|
||||
crc32_finish(&state, &check_sum, sizeof(check_sum));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
inline bool check_uuid(std::string& uuid, uint32_t check_sum) {
|
||||
crc32_state state;
|
||||
crc32_init(&state);
|
||||
crc32_update(&state, (uint8_t*) uuid.data(), (unsigned long) uuid.length());
|
||||
|
||||
uint32_t result;
|
||||
crc32_finish(&state, &result, sizeof(result));
|
||||
|
||||
return result == check_sum;
|
||||
}
|
||||
|
||||
static std::string _cached_system_uuid{};
|
||||
static uint32_t _cached_system_uuid_cksm = 0;
|
||||
std::string system_uuid() {
|
||||
if(!_cached_system_uuid.empty() && check_uuid(_cached_system_uuid, _cached_system_uuid_cksm))
|
||||
return _cached_system_uuid;
|
||||
|
||||
if(!generate_uuid(_cached_system_uuid, _cached_system_uuid_cksm))
|
||||
_cached_system_uuid = "";
|
||||
return _cached_system_uuid;
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "UUID: " << system_uuid() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
//ADaX2mIRrC1uv83h
|
||||
//NEg5KzRIOSs0SDkrNEg5Kw==
|
||||
@@ -1,4 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
extern std::string system_uuid();
|
||||
@@ -1,116 +0,0 @@
|
||||
#include <NanEventCallback.h>
|
||||
#include <iostream>
|
||||
#include "logger.h"
|
||||
|
||||
/* Basic */
|
||||
void(*logger::_force_log)(logger::category::value, logger::level::level_enum /* lvl */, const std::string_view& /* message */);
|
||||
|
||||
/* NODE JS */
|
||||
struct LogMessage {
|
||||
uint8_t level;
|
||||
uint8_t category;
|
||||
|
||||
std::string message;
|
||||
|
||||
LogMessage* next_message;
|
||||
};
|
||||
std::mutex log_messages_lock;
|
||||
LogMessage* log_messages_head = nullptr;
|
||||
LogMessage** log_messages_tail = &log_messages_head;
|
||||
|
||||
Nan::callback_t<> log_messages_callback;
|
||||
void force_log_node(logger::category::value, spdlog::level::level_enum, const std::string_view &);
|
||||
|
||||
/* Normal */
|
||||
void force_log_raw(logger::category::value, spdlog::level::level_enum level, const std::string_view &message);
|
||||
|
||||
struct StdExternalStringResourceBase : public v8::String::ExternalOneByteStringResource {
|
||||
public:
|
||||
explicit StdExternalStringResourceBase(const std::string& message) : message(message) {}
|
||||
|
||||
const char *data() const override {
|
||||
return this->message.data();
|
||||
}
|
||||
|
||||
size_t length() const override {
|
||||
return this->message.length();
|
||||
}
|
||||
|
||||
private:
|
||||
std::string message;
|
||||
};
|
||||
|
||||
inline v8::Local<v8::Value> get_logger_method() {
|
||||
v8::Local<v8::Object> global_context = Nan::GetCurrentContext()->Global();
|
||||
|
||||
v8::Local<v8::Object> logger_context = global_context->Get(Nan::New<v8::String>("logger").ToLocalChecked()).As<v8::Object>();
|
||||
v8::Local<v8::Value> logger_method = logger_context->Get(Nan::New<v8::String>("log").ToLocalChecked());
|
||||
if(!logger_method.IsEmpty() && !logger_method->IsNullOrUndefined())
|
||||
return logger_method;
|
||||
|
||||
logger_context = global_context->Get(Nan::New<v8::String>("console").ToLocalChecked()).As<v8::Object>();
|
||||
return logger_context->Get(Nan::New<v8::String>("log").ToLocalChecked());
|
||||
}
|
||||
|
||||
void logger::initialize_node() {
|
||||
log_messages_callback = Nan::async_callback([]{
|
||||
Nan::HandleScope scope;
|
||||
|
||||
auto isolate = Nan::GetCurrentContext()->GetIsolate();
|
||||
v8::Local<v8::Value> logger_method = get_logger_method();
|
||||
|
||||
v8::Local<v8::Value> arguments[3];
|
||||
while(true) {
|
||||
std::unique_lock messages_lock(log_messages_lock);
|
||||
if(!log_messages_head)
|
||||
break;
|
||||
|
||||
auto entry = log_messages_head;
|
||||
log_messages_head = entry->next_message;
|
||||
if(!log_messages_head)
|
||||
log_messages_tail = &log_messages_head;
|
||||
messages_lock.unlock();
|
||||
|
||||
if(!logger_method.IsEmpty() && !logger_method->IsNullOrUndefined()) {
|
||||
arguments[0] = Nan::New<v8::Number>(entry->category);
|
||||
arguments[1] = Nan::New<v8::Number>(entry->level);
|
||||
arguments[2] = v8::String::NewExternalOneByte(isolate, new StdExternalStringResourceBase(entry->message)).ToLocalChecked();
|
||||
|
||||
logger_method.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 3, arguments);
|
||||
} else {
|
||||
std::cout << "Failed to log message! Invalid method!" << std::endl;
|
||||
}
|
||||
|
||||
delete entry;
|
||||
}
|
||||
});
|
||||
|
||||
logger::_force_log = force_log_node;
|
||||
}
|
||||
|
||||
void logger::initialize_raw() {
|
||||
logger::_force_log = force_log_raw;
|
||||
}
|
||||
|
||||
void force_log_node(logger::category::value category, spdlog::level::level_enum level, const std::string_view &message) {
|
||||
auto entry = new LogMessage{};
|
||||
entry->level = level;
|
||||
entry->category = category;
|
||||
entry->message = std::string(message.data(), message.length());
|
||||
entry->next_message = nullptr;
|
||||
|
||||
{
|
||||
std::lock_guard lock(log_messages_lock);
|
||||
*log_messages_tail = entry;
|
||||
log_messages_tail = &(entry->next_message);
|
||||
}
|
||||
log_messages_callback();
|
||||
}
|
||||
|
||||
void force_log_raw(logger::category::value category, spdlog::level::level_enum level, const std::string_view &message) {
|
||||
std::cout << "[" << level << "][" << category << "] " << message << std::endl;
|
||||
}
|
||||
|
||||
void logger::err_handler(const std::string &message) {
|
||||
std::cout << "[ERROR] " << message << std::endl;
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <spdlog/details/log_msg.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
namespace logger {
|
||||
namespace level = spdlog::level;
|
||||
|
||||
namespace category {
|
||||
enum value {
|
||||
general,
|
||||
audio,
|
||||
connection,
|
||||
voice_connection,
|
||||
socket,
|
||||
|
||||
file_transfer,
|
||||
|
||||
memory
|
||||
};
|
||||
|
||||
using category = value;
|
||||
}
|
||||
|
||||
extern void initialize_raw();
|
||||
extern void initialize_node();
|
||||
extern void(*_force_log)(category::value /* category */, level::level_enum /* lvl */, const std::string_view& /* message */);
|
||||
extern void err_handler(const std::string& /* error */);
|
||||
|
||||
inline bool should_log(level::level_enum lvl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
inline void force_log(category::value category, level::level_enum lvl, const char *fmt, const Args &... args) {
|
||||
try {
|
||||
fmt::MemoryWriter fmt_writer;
|
||||
fmt_writer.write(fmt, args...);
|
||||
|
||||
_force_log(category, lvl, std::string_view{fmt_writer.data(), fmt_writer.size()});
|
||||
}
|
||||
catch (const std::exception &ex)
|
||||
{
|
||||
err_handler(ex.what());
|
||||
}
|
||||
catch(...)
|
||||
{
|
||||
err_handler("Unknown exception in logger");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
inline void log(category::value category, level::level_enum lvl, const char *fmt, const Args &... args) {
|
||||
if (should_log(lvl))
|
||||
force_log(category, lvl, fmt, args...);
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
inline void trace(category::value category, const char *fmt, const Args &... args) {
|
||||
log(category, level::trace, fmt, args...);
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
inline void debug(category::value category, const char *fmt, const Args &... args) {
|
||||
log(category, level::debug, fmt, args...);
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
inline void info(category::value category, const char *fmt, const Args &... args) {
|
||||
log(category, level::info, fmt, args...);
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
inline void warn(category::value category, const char *fmt, const Args &... args) {
|
||||
log(category, level::warn, fmt, args...);
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
inline void error(category::value category, const char *fmt, const Args &... args) {
|
||||
log(category, level::err, fmt, args...);
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
inline void critical(category::value category, const char *fmt, const Args &... args) {
|
||||
log(category, level::critical, fmt, args...);
|
||||
}
|
||||
}
|
||||
|
||||
namespace category = logger::category;
|
||||
|
||||
#define tr(message) message
|
||||
|
||||
#define log_trace(_category, message, ...) logger::trace(::logger::category::_category, message, ##__VA_ARGS__)
|
||||
#define log_debug(_category, message, ...) logger::debug(::logger::category::_category, message, ##__VA_ARGS__)
|
||||
#define log_info(_category, message, ...) logger::info(::logger::category::_category, message, ##__VA_ARGS__)
|
||||
#define log_warn(_category, message, ...) logger::warn(::logger::category::_category, message, ##__VA_ARGS__)
|
||||
#define log_error(_category, message, ...) logger::error(::logger::category::_category, message, ##__VA_ARGS__)
|
||||
#define log_critical(_category, message, ...) logger::critical(::logger::category::_category, message, ##__VA_ARGS__)
|
||||
|
||||
#define log_allocate(class, this) log_trace(memory, "Allocated new " class ": {}", (void*) this)
|
||||
#define log_free(class, this) log_trace(memory, "Deallocated " class ": {}", (void*) this)
|
||||
@@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#pragma pack(push)
|
||||
#pragma pack(1)
|
||||
typedef struct _SMBIOSDataHeader
|
||||
{
|
||||
uint8_t _unused;
|
||||
uint8_t major_version;
|
||||
uint8_t minor_version;
|
||||
uint8_t dmi_revision;
|
||||
uint32_t length;
|
||||
|
||||
uint8_t table_data;
|
||||
} SMBIOSDataHeader;
|
||||
|
||||
static constexpr size_t SMBIOSDataHeaderLength = 8;
|
||||
static_assert(sizeof(SMBIOSDataHeader) == SMBIOSDataHeaderLength + 1, "invalid struct size");
|
||||
|
||||
|
||||
typedef struct _SMBIOSEntryHeader
|
||||
{
|
||||
uint8_t type;
|
||||
uint8_t length;
|
||||
uint16_t header;
|
||||
|
||||
uint8_t data;
|
||||
} SMBIOSEntryHeader;
|
||||
|
||||
static constexpr size_t SMBIOSEntryHeaderLength = 4;
|
||||
static_assert(sizeof(SMBIOSEntryHeader) == SMBIOSEntryHeaderLength + 1, "invalid struct size");
|
||||
|
||||
#pragma pack(pop)
|
||||
Reference in New Issue
Block a user