first commit

This commit is contained in:
sanya
2025-09-01 14:20:39 +00:00
committed by ExternPointer
commit 490fc11f6a
4328 changed files with 1796224 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
#pragma once
#ifdef USE_ALSA_ADAPTER
#include <alsa/asoundlib.h>
#include <spdlog/spdlog.h>
#include <exception>
#include <stdexcept>
#include <functional>
#include <cstring>
#include <thread>
#include <atomic>
#include "Concepts/AudioAdapterConcept.h"
namespace snoop {
class AlsaAudioAdapter {
using TAudioCallback = std::function<void( const char* input, size_t size )>;
snd_pcm_t* m_handle = nullptr;
TAudioCallback m_audioCallback;
int m_framesPerBuffer;
std::thread m_thread;
std::atomic<bool> m_running;
public:
AlsaAudioAdapter( int sampleRate, int channels, int framesPerBuffer, TAudioCallback audioCallback, const std::string& deviceName = "default" )
: m_audioCallback( std::move( audioCallback ) ), m_framesPerBuffer( framesPerBuffer ), m_running( true ) {
int err;
if( ( err = snd_pcm_open( &m_handle, deviceName.c_str(), SND_PCM_STREAM_CAPTURE, 0 ) ) < 0 ) {
spdlog::error( "ALSA error while opening device: {}", snd_strerror( err ) );
throw std::runtime_error( std::string( "ALSA error while opening device: " ) + snd_strerror( err ) );
}
spdlog::info( "ALSA device opened successfully: {}", deviceName );
snd_pcm_hw_params_t* hw_params;
snd_pcm_hw_params_malloc( &hw_params );
snd_pcm_hw_params_any( m_handle, hw_params );
snd_pcm_hw_params_set_access( m_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED );
snd_pcm_hw_params_set_format( m_handle, hw_params, SND_PCM_FORMAT_FLOAT_LE );
snd_pcm_hw_params_set_rate( m_handle, hw_params, sampleRate, 0 );
snd_pcm_hw_params_set_channels( m_handle, hw_params, channels );
snd_pcm_hw_params_set_period_size( m_handle, hw_params, framesPerBuffer, 0 );
snd_pcm_hw_params_set_buffer_size( m_handle, hw_params, framesPerBuffer * 4 );
if( ( err = snd_pcm_hw_params( m_handle, hw_params ) ) < 0 ) {
spdlog::error( "ALSA error while setting hardware parameters: {}", snd_strerror( err ) );
snd_pcm_hw_params_free( hw_params );
throw std::runtime_error( std::string( "ALSA error while setting hardware parameters: " ) + snd_strerror( err ) );
}
snd_pcm_hw_params_free( hw_params );
spdlog::info( "ALSA hardware parameters set successfully." );
if( ( err = snd_pcm_prepare( m_handle ) ) < 0 ) {
spdlog::error( "ALSA error while preparing device: {}", snd_strerror( err ) );
throw std::runtime_error( std::string( "ALSA error while preparing device: " ) + snd_strerror( err ) );
}
spdlog::info( "ALSA device prepared successfully." );
m_thread = std::thread( &AlsaAudioAdapter::start, this );
}
~AlsaAudioAdapter() {
m_running = false;
if( m_thread.joinable() ) {
m_thread.join();
}
snd_pcm_close( m_handle );
spdlog::info( "ALSA device closed." );
}
private:
void start() {
int err;
std::vector<float> buffer( m_framesPerBuffer );
while( m_running ) {
err = snd_pcm_readi( m_handle, buffer.data(), m_framesPerBuffer );
if( err == -EPIPE ) {
spdlog::warn( "ALSA buffer overrun detected!" );
snd_pcm_prepare( m_handle );
} else if( err < 0 ) {
spdlog::error( "ALSA error while reading: {}", snd_strerror( err ) );
} else {
m_audioCallback( reinterpret_cast<const char*>(buffer.data()), err * sizeof( float ) );
}
}
}
};
static_assert( AudioAdapterConcept<AlsaAudioAdapter>, "AlsaAudioAdapter does not satisfy AudioAdapterConcept" );
}
#endif

View File

@@ -0,0 +1,146 @@
#pragma once
#include <portaudio.h>
#include <spdlog/spdlog.h>
#include <exception>
#include <stdexcept>
#include <functional>
#include "Concepts/AudioAdapterConcept.h"
namespace snoop {
class PortAudioAdapter {
using TAudioCallback = std::function<void( const char* input, size_t size )>;
PaStream* m_stream = nullptr;
TAudioCallback m_audioCallback;
public:
PortAudioAdapter( int sampleRate, int channels, int framesPerBuffer, TAudioCallback audioCallback, int deviceIndex = paNoDevice )
: m_audioCallback( std::move( audioCallback ) ) {
auto error = Pa_Initialize();
if( error != paNoError ) {
const char* errorText = Pa_GetErrorText( error );
spdlog::error( "PortAudio initialization error: {}", errorText );
throw std::runtime_error( std::string( "PortAudio initialization error: " ) + errorText );
}
spdlog::info( "PortAudio initialized successfully." );
// Output information about available host APIs
int numApis = Pa_GetHostApiCount();
spdlog::info( "Number of Host APIs: {}", numApis );
for( int i = 0; i < numApis; ++i ) {
const PaHostApiInfo* apiInfo = Pa_GetHostApiInfo( i );
spdlog::info( "Host API [{}]: {}", i, apiInfo->name );
}
// Output information about available devices
int numDevices = Pa_GetDeviceCount();
PaSampleFormat sampleFormats[] = {
paFloat32, paInt32, paInt24, paInt16, paInt8, paUInt8
};
const char* formatNames[] = {
"paFloat32", "paInt32", "paInt24", "paInt16", "paInt8", "paUInt8"
};
spdlog::info( "Number of Devices: {}", numDevices );
for( int i = 0; i < numDevices; ++i ) {
const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo( i );
spdlog::info( "Device [{}]: {}", i, deviceInfo->name );
spdlog::info( " Max Input Channels: {}", deviceInfo->maxInputChannels );
spdlog::info( " Max Output Channels: {}", deviceInfo->maxOutputChannels );
spdlog::info( " Default Sample Rate: {}", deviceInfo->defaultSampleRate );
for (int j = 0; j < 6; ++j) {
PaStreamParameters inputParameters;
inputParameters.device = i;
inputParameters.channelCount = deviceInfo->maxInputChannels;
inputParameters.sampleFormat = sampleFormats[j];
inputParameters.suggestedLatency = deviceInfo->defaultLowInputLatency;
inputParameters.hostApiSpecificStreamInfo = nullptr;
PaError err = Pa_IsFormatSupported(&inputParameters, nullptr, deviceInfo->defaultSampleRate);
if (err == paFormatIsSupported) {
spdlog::info(" Supported Sample Format: {}", formatNames[j]);
} else {
spdlog::info(" Unsupported Sample Format: {}", formatNames[j]);
}
}
}
// Select the device (default or user-provided)
const PaDeviceInfo* selectedDeviceInfo;
if( deviceIndex == paNoDevice ) {
// Use default device
selectedDeviceInfo = Pa_GetDeviceInfo( Pa_GetDefaultInputDevice() );
spdlog::info( "Using default input device: {}", selectedDeviceInfo->name );
} else if( deviceIndex >= 0 && deviceIndex < numDevices ) {
// Use specified device
selectedDeviceInfo = Pa_GetDeviceInfo( deviceIndex );
spdlog::info( "Using specified input device [{}]: {}", deviceIndex, selectedDeviceInfo->name );
} else {
spdlog::error( "Invalid device index: {}", deviceIndex );
throw std::runtime_error( "Invalid device index" );
}
// Configure input parameters
PaStreamParameters inputParameters;
inputParameters.device = ( deviceIndex == paNoDevice ) ? Pa_GetDefaultInputDevice() : deviceIndex;
inputParameters.channelCount = channels;
inputParameters.sampleFormat = paFloat32;
inputParameters.suggestedLatency = selectedDeviceInfo->defaultLowInputLatency;
inputParameters.hostApiSpecificStreamInfo = nullptr;
// Define the callback function
auto portAudioCallback = []( const void* input, void* output,
unsigned long frameCount,
const PaStreamCallbackTimeInfo* timeInfo,
PaStreamCallbackFlags statusFlags,
void* userData ) -> int {
auto* audioCallback = static_cast<TAudioCallback*>(userData);
( *audioCallback )( static_cast<const char*>(input), frameCount * sizeof( float ) );
return paContinue;
};
// Open the stream with the chosen device and parameters
error = Pa_OpenStream( &this->m_stream,
&inputParameters,
nullptr, // No output parameters
sampleRate,
framesPerBuffer,
paClipOff, // No output clipping
portAudioCallback,
&this->m_audioCallback );
if( error != paNoError ) {
const char* errorText = Pa_GetErrorText( error );
spdlog::error( "PortAudio error while opening stream: {}", errorText );
throw std::runtime_error( std::string( "PortAudio error while opening stream: " ) + errorText );
}
spdlog::info( "Stream opened successfully." );
error = Pa_StartStream( this->m_stream );
if( error != paNoError ) {
const char* errorText = Pa_GetErrorText( error );
spdlog::error( "PortAudio error while starting stream: {}", errorText );
throw std::runtime_error( std::string( "PortAudio error while starting stream: " ) + errorText );
}
spdlog::info( "Stream started successfully." );
}
~PortAudioAdapter() {
Pa_StopStream( m_stream );
Pa_CloseStream( m_stream );
Pa_Terminate();
spdlog::info( "Stream stopped and PortAudio terminated." );
}
};
static_assert( AudioAdapterConcept<PortAudioAdapter>, "PortAudioAdapter does not satisfy AudioAdapterConcept" );
}

View File

@@ -0,0 +1,64 @@
#pragma once
#include <opus.h>
#include <memory>
#include <vector>
#include <functional>
#include <stdexcept>
#include "Concepts/AudioAdapterConcept.h"
#include "Concepts/AudioEncderConcept.h"
#include "AudioAdapters/PortAudioAdapter.h"
namespace snoop {
template<AudioAdapterConcept TAudioAdapter>
class OpusEncoder {
using TAudioCallback = std::function<void( const char* data, size_t size )>;
std::shared_ptr<TAudioAdapter> m_audioAdapter;
::OpusEncoder* m_encoder;
std::vector<char> m_encodedBuffer;
size_t m_maxDataBytes;
public:
OpusEncoder( int sampleRate, int channels, int framesPerBuffer, TAudioCallback audioCallback )
: m_maxDataBytes( framesPerBuffer * sizeof( float ) * 2 ), m_encodedBuffer( framesPerBuffer * sizeof( float ) * 2 ) {
int error;
m_encoder = opus_encoder_create( sampleRate, channels, OPUS_APPLICATION_AUDIO, &error );
if( error != OPUS_OK ) {
throw std::runtime_error( "Failed to create Opus encoder: " + std::string( opus_strerror( error ) ) );
}
m_audioAdapter = std::make_shared<TAudioAdapter>( sampleRate, channels, framesPerBuffer,
[this, audioCallback]( const char* input, size_t size ) {
this->encode( reinterpret_cast<const float*>(input), static_cast<int>(size / sizeof( float )),
audioCallback );
} );
}
~OpusEncoder() {
if( m_encoder ) {
opus_encoder_destroy( m_encoder );
}
}
private:
void encode( const float* input, int frameCount, const TAudioCallback& audioCallback ) {
int numBytes = opus_encode_float( m_encoder, input, frameCount, reinterpret_cast<unsigned char*>(m_encodedBuffer.data()), m_maxDataBytes );
if( numBytes < 0 ) {
spdlog::error( "Opus encoding failed: {}", std::string( opus_strerror( numBytes ) ) );
throw std::runtime_error( "Opus encoding failed: " + std::string( opus_strerror( numBytes ) ) );
}
audioCallback( m_encodedBuffer.data(), numBytes );
}
};
static_assert( AudioEncoderConcept<OpusEncoder<PortAudioAdapter>>, "OpusEncoder does not satisfy AudioEncoderConcept" );
}

View File

@@ -0,0 +1,140 @@
#pragma once
#include <fstream>
#include <string>
#include <stdexcept>
#include <ogg/ogg.h>
#include <mutex>
#include "Concepts/AudioWriterConcept.h"
#include <spdlog/spdlog.h>
namespace snoop {
class OggAudioWriter {
std::ofstream m_outputFile;
ogg_stream_state m_oggStream = {};
bool m_isInWriting = false;
long m_packetNumber = 0;
int64_t m_granulePos = 0;
int m_sampleRate;
int m_channels;
std::mutex m_mutex;
public:
OggAudioWriter( int sampleRate, int channels )
: m_sampleRate( sampleRate ), m_channels( channels ) {
}
~OggAudioWriter() {
this->StopWriting();
}
void StartWriting( const std::string& filePath ) {
std::lock_guard l( this->m_mutex );
StartWritingInternal( filePath );
}
void StopWriting() {
std::lock_guard l( this->m_mutex );
StopWritingInternal();
}
void Write( const char* data, size_t size, size_t frames ) {
std::lock_guard l( this->m_mutex );
WriteInternal( data, size, frames );
}
private:
void StartWritingInternal( std::string filePath ) {
if( this->m_isInWriting ) {
this->StopWritingInternal();
}
m_isInWriting = true;
this->m_packetNumber = 0;
this->m_granulePos = 0;
this->m_oggStream = {};
this->m_outputFile = {};
ogg_stream_init( &this->m_oggStream, 1 );
this->m_outputFile.open( filePath, std::ios::binary );
if( !this->m_outputFile.is_open() ) {
throw std::runtime_error( "Failed to open file for writing: " + filePath );
}
this->WriteOpusHeader();
this->WriteOpusTags();
spdlog::info( "Started writing Ogg audio to {}", filePath );
}
void StopWritingInternal() {
if( !m_isInWriting ) {
return;
}
if( this->m_outputFile.is_open() ) {
ogg_packet packet = {};
packet.e_o_s = 1;
packet.granulepos = this->m_granulePos;
packet.packetno = this->m_packetNumber++;
ogg_stream_packetin( &m_oggStream, &packet );
this->WriteOggPages( true );
this->m_outputFile.close();
spdlog::info( "Stopped writing Ogg audio" );
}
ogg_stream_clear( &this->m_oggStream );
this->m_isInWriting = false;
}
void WriteInternal( const char* data, size_t size, size_t frames ) {
if( !this->m_isInWriting ) {
return;
}
ogg_packet packet;
packet.packet = reinterpret_cast<unsigned char*>(const_cast<char*>(data));
packet.bytes = static_cast<long>(size);
packet.b_o_s = this->m_packetNumber == 0 ? 1 : 0;
packet.e_o_s = 0;
packet.granulepos = this->m_granulePos += static_cast<ssize_t>(frames) * m_channels;
packet.packetno = this->m_packetNumber++;
ogg_stream_packetin( &this->m_oggStream, &packet );
this->WriteOggPages();
}
void WriteOpusHeader() {
static constexpr char magic[ ] = "OpusHead";
unsigned char header[ 19 ] = { 0 };
memcpy( header, reinterpret_cast<const void*>(magic), 8 );
header[ 8 ] = 1; // version
header[ 9 ] = m_channels;
*reinterpret_cast<int*>( &header[ 12 ] ) = m_sampleRate;
this->WriteInternal( reinterpret_cast<const char*>(header), sizeof( header ), 0 );
}
void WriteOpusTags() {
constexpr char tags[ ] = "OpusTags\0\0\0\0\0\0\0\0";
this->WriteInternal( tags, sizeof( tags ) - 1, 0 );
}
void WriteOggPages( bool flush = false ) {
ogg_page page;
while( ogg_stream_pageout( &m_oggStream, &page ) ) {
this->m_outputFile.write( reinterpret_cast<const char*>(page.header), page.header_len );
this->m_outputFile.write( reinterpret_cast<const char*>(page.body), page.body_len );
}
if( flush ) {
while( ogg_stream_flush( &m_oggStream, &page ) ) {
this->m_outputFile.write( reinterpret_cast<const char*>(page.header), page.header_len );
this->m_outputFile.write( reinterpret_cast<const char*>(page.body), page.body_len );
}
}
}
};
static_assert( AudioWriterConcept<OggAudioWriter>, "OggAudioWriter does not satisfy AudioWriterConcept" );
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include <concepts>
#include <functional>
namespace snoop {
template<typename T>
concept AudioAdapterConcept = requires( T driver, int sampleRate, int channels, int framesPerBuffer, std::function<int( const char*, size_t )> callback )
{
T( sampleRate, channels, framesPerBuffer, callback );
};
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include <concepts>
#include <functional>
namespace snoop {
template<typename T>
concept AudioEncoderConcept = requires( T encoder, int sampleRate, int channels, int framesPerBuffer, std::function<int( const char*, size_t )> callback )
{
T( sampleRate, channels, framesPerBuffer, callback );
};
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <concepts>
#include <functional>
namespace snoop {
template<typename T>
concept AudioWriterConcept = requires( T writer, int sampleRate, int channels, std::string filePath, const char* data, size_t size, size_t frames )
{
T( sampleRate, channels );
{ writer.StartWriting( filePath ) } -> std::same_as<void>;
{ writer.StopWriting() } -> std::same_as<void>;
{ writer.Write( data, size, frames ) } -> std::same_as<void>;
};
}

View File

@@ -0,0 +1,102 @@
#pragma once
#include <sio_client.h>
#include <atomic>
#include <spdlog/spdlog.h>
#include <memory>
#include <utility>
namespace snoop {
class AudioStreamService {
std::shared_ptr<sio::client> m_client;
std::string m_guid;
std::atomic<bool> m_isConnected = false;
std::atomic<bool> m_isInStreaming = false;
std::vector<char> m_audioBuffer;
std::mutex m_bufferMutex;
const unsigned long long int FLUSH_PERIOD = 5000;
const std::vector<char> PACKET_DELIMITER = {
static_cast<char>(0xFF),
static_cast<char>(0xFE),
static_cast<char>(0xFD),
static_cast<char>(0xFC)
};
unsigned long long int m_flushedAt = 0;
public:
explicit AudioStreamService( std::shared_ptr<sio::client> client, std::string guid ) :
m_client( std::move( client ) ),
m_guid( std::move( guid ) ) {
SetupEventListeners();
}
~AudioStreamService() {
this->m_isConnected = false;
this->m_isInStreaming = false;
}
void SendAudioData( const char* input, size_t size ) {
if( !this->m_isConnected || !this->m_isInStreaming ) {
return;
}
std::lock_guard lock( m_bufferMutex );
this->m_audioBuffer.insert(m_audioBuffer.end(), PACKET_DELIMITER.begin(), PACKET_DELIMITER.end());
this->m_audioBuffer.insert( m_audioBuffer.end(), input, input + size );
auto now = std::chrono::system_clock::now();
auto currentTime = std::chrono::duration_cast<std::chrono::milliseconds>( now.time_since_epoch() ).count();
if( currentTime >= this->m_flushedAt + FLUSH_PERIOD ) {
FlushBuffer();
}
}
private:
void SetupEventListeners() {
this->m_client->set_open_listener( [this]() {
spdlog::info( "Connected to server" );
this->m_client->socket( "/livestream" )->emit( "register_device", m_guid );
this->m_isConnected = true;
} );
this->m_client->set_close_listener( [this]( sio::client::close_reason const& reason ) {
this->m_isConnected = false;
this->m_isInStreaming = false;
spdlog::info( "Disconnected from server" );
} );
this->m_client->set_fail_listener( []() {
spdlog::info( "Failed to connect to server" );
} );
this->m_client->socket( "/livestream" )->on( "start_streaming", [this]( sio::event& ev ) {
spdlog::info( "Start streaming command received" );
this->m_isInStreaming = true;
auto now = std::chrono::system_clock::now();
this->m_flushedAt = std::chrono::duration_cast<std::chrono::milliseconds>( now.time_since_epoch() ).count();
} );
this->m_client->socket( "/livestream" )->on( "stop_streaming", [this]( sio::event& ev ) {
spdlog::info( "Stop streaming command received" );
this->m_isInStreaming = false;
std::lock_guard lock( this->m_bufferMutex );
this->m_audioBuffer.clear();
} );
}
void FlushBuffer() {
if( this->m_audioBuffer.empty() ) {
return;
}
this->m_client->socket( "/livestream" )->emit( "audio_data",
std::make_shared<std::string>( this->m_audioBuffer.data(), this->m_audioBuffer.size() )
);
this->m_audioBuffer.clear();
auto now = std::chrono::system_clock::now();
this->m_flushedAt = std::chrono::duration_cast<std::chrono::milliseconds>( now.time_since_epoch() ).count();
}
};
}

View File

@@ -0,0 +1,181 @@
#pragma once
#include <memory>
#include <utility>
#include <thread>
#include <atomic>
#include <chrono>
#include <httplib.h>
#include <mutex>
#include "AudioWriters/OggAudioWriter.h"
#include "ConfigService.h"
namespace snoop {
class AudioWriterService {
std::shared_ptr<ConfigService> m_configService;
std::shared_ptr<OggAudioWriter> m_oggWriter;
std::string m_destinationDirectoryPath;
std::string m_queueDirectoryPath;
std::thread m_writingThread;
std::thread m_uploadThread;
std::mutex m_fetchFilePathsMutex;
unsigned long long int m_currentRecordStartedAt = 0;
std::string m_currentRecordFilePath;
std::atomic<bool> m_isIntermission = false;
public:
explicit AudioWriterService( std::shared_ptr<ConfigService> configService, std::string destinationDirectoryPath ) :
m_configService( std::move( configService ) ),
m_destinationDirectoryPath( std::move( destinationDirectoryPath ) ),
m_oggWriter( std::make_shared<OggAudioWriter>( 48000, 1 ) ) {
if( !this->m_destinationDirectoryPath.empty() && !this->m_destinationDirectoryPath.ends_with( "/" ) ) {
this->m_destinationDirectoryPath.append( "/" );
}
this->m_queueDirectoryPath = this->m_destinationDirectoryPath + "queue/";
std::filesystem::create_directories( this->m_queueDirectoryPath );
this->MoveToQueueUncompletedRecords();
this->m_writingThread = std::thread( [this]() {
this->WritingThread();
} );
this->m_uploadThread = std::thread( [this]() {
this->UploadThread();
} );
spdlog::info( "AudioWriterService::AudioWriterService()" );
}
void WriteAudioData( const char* data, size_t size, size_t frames ) {
this->m_oggWriter->Write( data, size, frames );
}
~AudioWriterService() {
this->m_isIntermission = true;
this->m_writingThread.join();
this->m_uploadThread.join();
}
private:
void MoveToQueueUncompletedRecords() {
std::vector<std::filesystem::path> files;
for( const auto& entry: std::filesystem::directory_iterator( this->m_destinationDirectoryPath ) ) {
files.push_back( entry.path() );
}
for( const auto& file: files ) {
if( file.filename().string() != "queue" ) {
spdlog::info( "Move uncompleted record {} to queue", file.filename().string() );
this->MoveToUploadQueue( file.string() );
}
}
}
void WritingThread() {
auto now = std::chrono::system_clock::now();
this->m_currentRecordStartedAt = std::chrono::duration_cast<std::chrono::milliseconds>( now.time_since_epoch() ).count();
this->m_currentRecordFilePath = this->m_destinationDirectoryPath + std::to_string( this->m_currentRecordStartedAt );
this->m_oggWriter->StartWriting( this->m_currentRecordFilePath );
while( !m_isIntermission ) {
now = std::chrono::system_clock::now();
auto currentRecordDuration = std::chrono::duration_cast<std::chrono::milliseconds>( now.time_since_epoch() ).count() -
this->m_currentRecordStartedAt;
if( currentRecordDuration >= this->m_configService->GetRecordingDuration() ) {
this->m_oggWriter->StopWriting();
this->MoveToUploadQueue( this->m_currentRecordFilePath );
this->m_currentRecordStartedAt = std::chrono::duration_cast<std::chrono::milliseconds>( now.time_since_epoch() ).count();
this->m_currentRecordFilePath = this->m_destinationDirectoryPath + std::to_string( this->m_currentRecordStartedAt );
this->m_oggWriter->StartWriting( this->m_currentRecordFilePath );
}
std::this_thread::sleep_for( std::chrono::milliseconds( 1000 ) );
}
this->m_oggWriter->StopWriting();
this->MoveToUploadQueue( this->m_currentRecordFilePath );
// TODO: Move to upload queue
}
void MoveToUploadQueue( const std::string& filePath ) {
spdlog::info( "AudioWriterService::MoveToUploadQueue( {} )", filePath );
std::lock_guard lock( this->m_fetchFilePathsMutex );
auto now = std::chrono::system_clock::now();
auto recordStoppedAt = std::chrono::duration_cast<std::chrono::milliseconds>( now.time_since_epoch() ).count();
if( std::filesystem::exists( filePath ) ) {
auto fileName = std::filesystem::path( filePath ).filename().string() + "-" + std::to_string( recordStoppedAt );
std::filesystem::rename( filePath, m_queueDirectoryPath + fileName );
}
}
void UploadThread() {
while (!m_isIntermission) {
std::vector<std::filesystem::path> files;
{
std::lock_guard l(this->m_fetchFilePathsMutex);
try {
for (const auto& entry : std::filesystem::directory_iterator(this->m_queueDirectoryPath)) {
files.push_back(entry.path());
}
} catch (const std::exception& e) {
spdlog::error("Error reading queue directory: {}", e.what());
}
}
for (const auto& filePath : files) {
auto fileName = filePath.filename().string();
spdlog::info("Processing file: {}", fileName);
size_t delimiterPos = fileName.find('-');
if (delimiterPos != std::string::npos) {
std::string startedAt = fileName.substr(0, delimiterPos);
std::string stoppedAt = fileName.substr(delimiterPos + 1);
try {
spdlog::info("Attempting to upload file...");
if (SendRecordedFile(filePath.string(), stoull(startedAt), stoull(stoppedAt))) {
spdlog::info("File uploaded, deleting: {}", filePath.string());
std::filesystem::remove(filePath);
} else {
spdlog::warn("Failed to upload file: {}", filePath.string());
}
} catch (const std::exception& e) {
spdlog::error("Exception during file upload: {}", e.what());
}
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
}
bool SendRecordedFile( const std::string& filepath, unsigned long long int startedAt, unsigned long long stoppedAt ) {
spdlog::info( "SendRecordedFile: {}", filepath );
httplib::Client client( this->m_configService->GetBaseUrl() );
std::ifstream ifs( filepath, std::ios::binary );
if( !ifs ) {
throw std::runtime_error( "Failed to open file" );
}
std::vector<char> buffer( ( std::istreambuf_iterator<char>( ifs ) ), ( std::istreambuf_iterator<char>() ) );
auto res = client.Post(
std::string( "/records/upload/" ),
httplib::MultipartFormDataItems{
{ "file", std::string( buffer.begin(), buffer.end() ), "file.ogg", "audio/ogg" },
{ "guid", this->m_configService->GetGuid() },
{ "startedAt", std::to_string( startedAt ) },
{ "stoppedAt", std::to_string( stoppedAt ) },
} );
if( res && res->status == 201 ) {
spdlog::info( "File uploaded successfully" );
return true;
}
spdlog::error( "Failed to upload file" );
return false;
}
};
}

View File

@@ -0,0 +1,76 @@
#pragma once
#include <string>
#include <memory>
#include <mutex>
#include <nlohmann/json.hpp>
#include <filesystem>
#include <fstream>
#include <sstream>
namespace snoop {
class Config {
public:
std::string m_guid;
unsigned long long m_recordingDuration = 0;
std::string m_baseUrl;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Config, m_guid, m_recordingDuration, m_baseUrl )
class ConfigService {
std::shared_ptr<Config> m_config;
std::string m_configFilePath;
std::mutex m_mutex;
public:
explicit ConfigService( const std::string& configFilePath ) :
m_configFilePath( configFilePath ) {
if( !std::filesystem::exists( this->m_configFilePath ) ) {
throw std::runtime_error( std::string( "ConfigService: Config not found " ) + this->m_configFilePath );
}
std::string configFileContent = this->ReadFile( this->m_configFilePath );
nlohmann::json jsonConfig = nlohmann::json::parse( configFileContent );
this->m_config = std::make_shared<Config>( jsonConfig.get<Config>() );
}
[[nodiscard]] std::string GetGuid() const {
return this->m_config->m_guid;
}
[[nodiscard]] unsigned long long int GetRecordingDuration() const {
return this->m_config->m_recordingDuration;
}
void SetRecordingDuration( unsigned long long int recordingDuration ) {
this->m_config->m_recordingDuration = recordingDuration;
this->RewriteConfig();
}
[[nodiscard]] std::string GetBaseUrl() const {
return this->m_config->m_baseUrl;
}
private:
std::string ReadFile( const std::string& path ) {
std::fstream f;
f.open( path, std::ios::in );
std::stringstream ss;
ss << f.rdbuf();
return ss.str();
}
void RewriteFile( const std::string& path, const std::string& fileContent ) {
std::fstream f;
f.open( path, std::ios::out );
f << fileContent;
f.close();
}
void RewriteConfig() {
this->RewriteFile( this->m_configFilePath, nlohmann::json( *this->m_config ).dump() );
}
};
}

58
src/main.cpp Normal file
View File

@@ -0,0 +1,58 @@
#include <fstream>
#include <spdlog/spdlog.h>
#include "AudioEncoders/OpusEncoder.h"
#include "AudioWriters/OggAudioWriter.h"
#include "Concepts/AudioEncderConcept.h"
#include "Concepts/AudioWriterConcept.h"
#include "Services/AudioStreamService.h"
#include "Services/AudioWriterService.h"
#include "Services/ConfigService.h"
#ifdef USE_ALSA_ADAPTER
#include "AudioAdapters/AlsaAudioAdapter.h"
using AudioAdapter = snoop::AlsaAudioAdapter;
#else
#include "AudioAdapters/PortAudioAdapter.h"
using AudioAdapter = snoop::PortAudioAdapter;
#endif
// sudo apt-get install libasound2-dev
namespace snoop {
template<AudioEncoderConcept TAudioEncoder, AudioWriterConcept TAudioWriter>
void Main() {
try {
int sampleRate = 48000;
int channels = 1;
int framesPerBuffer = 2880;
auto configService = std::make_shared<ConfigService>( "config.json" );
auto sioClient = std::make_shared<sio::client>();
sioClient->connect( configService->GetBaseUrl() );
auto writerService = std::make_shared<AudioWriterService>( configService, "records" );
AudioStreamService streamer( sioClient, configService->GetGuid() );
TAudioEncoder encoder( sampleRate, channels, framesPerBuffer, [&]( auto input, auto size ) -> int {
streamer.SendAudioData( input, size );
writerService->WriteAudioData( input, size, framesPerBuffer );
return paContinue;
} );
while( true ) {
sleep( 1000 );
}
} catch( const std::exception& ex ) {
spdlog::error( "Exception: {}", ex.what() );
}
}
}
int main() {
snoop::Main<snoop::OpusEncoder<AudioAdapter>, snoop::OggAudioWriter>();
return 0;
}