diff --git a/CMakeLists.txt b/CMakeLists.txt index de9a794..5490008 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,6 +144,10 @@ set(LINUX_SOURCES src/vaapi_decoder.c src/audio_capture.c src/audio_playback.c + src/audio_capture_pulse.c + src/audio_playback_pulse.c + src/audio_capture_dummy.c + src/audio_playback_dummy.c src/input.c src/discovery.c src/service.c @@ -219,6 +223,11 @@ if(UNIX AND NOT APPLE) target_include_directories(rootstream PRIVATE ${ALSA_INCLUDE_DIRS}) endif() + if(PULSEAUDIO_FOUND) + target_link_libraries(rootstream PRIVATE ${PULSEAUDIO_LIBRARIES}) + target_include_directories(rootstream PRIVATE ${PULSEAUDIO_INCLUDE_DIRS}) + endif() + if(NOT HEADLESS AND GTK3_FOUND) target_link_libraries(rootstream PRIVATE ${GTK3_LIBRARIES}) target_include_directories(rootstream PRIVATE ${GTK3_INCLUDE_DIRS}) @@ -262,6 +271,8 @@ if(UNIX AND NOT APPLE) src/input.c src/opus_codec.c src/audio_playback.c + src/audio_playback_pulse.c + src/audio_playback_dummy.c src/latency.c ${PLATFORM_SOURCES} ) @@ -280,6 +291,11 @@ if(UNIX AND NOT APPLE) m pthread ) + if(PULSEAUDIO_FOUND) + target_link_libraries(rstr-player PRIVATE ${PULSEAUDIO_LIBRARIES}) + target_include_directories(rstr-player PRIVATE ${PULSEAUDIO_INCLUDE_DIRS}) + endif() + if(Opus_FOUND) target_link_libraries(rstr-player PRIVATE Opus::opus) else() diff --git a/include/rootstream.h b/include/rootstream.h index c6619cf..44995e8 100644 --- a/include/rootstream.h +++ b/include/rootstream.h @@ -373,6 +373,29 @@ typedef struct { char filename[256]; /* Output filename */ } recording_ctx_t; +/* ============================================================================ + * AUDIO BACKEND ABSTRACTION - Multi-fallback support + * ============================================================================ */ + +/* Forward declaration */ +typedef struct rootstream_ctx rootstream_ctx_t; + +typedef struct { + const char *name; + int (*init_fn)(rootstream_ctx_t *ctx); + int (*capture_fn)(rootstream_ctx_t *ctx, int16_t *samples, size_t *num_samples); + void (*cleanup_fn)(rootstream_ctx_t *ctx); + bool (*is_available_fn)(void); +} audio_capture_backend_t; + +typedef struct { + const char *name; + int (*init_fn)(rootstream_ctx_t *ctx); + int (*playback_fn)(rootstream_ctx_t *ctx, int16_t *samples, size_t num_samples); + void (*cleanup_fn)(rootstream_ctx_t *ctx); + bool (*is_available_fn)(void); +} audio_playback_backend_t; + /* ============================================================================ * MAIN CONTEXT - Application state * ============================================================================ */ @@ -398,6 +421,10 @@ typedef struct rootstream_ctx { /* Audio (client) */ audio_playback_ctx_t audio_playback; + /* Audio backends (Phase 3) */ + const audio_capture_backend_t *audio_capture_backend; + const audio_playback_backend_t *audio_playback_backend; + /* Network */ rs_socket_t sock_fd; /* UDP socket */ uint16_t port; /* Listening port */ @@ -554,6 +581,7 @@ int rootstream_opus_get_frame_size(void); int rootstream_opus_get_sample_rate(void); int rootstream_opus_get_channels(void); +/* Audio capture/playback - backward compatibility */ int audio_capture_init(rootstream_ctx_t *ctx); int audio_capture_frame(rootstream_ctx_t *ctx, int16_t *samples, size_t *num_samples); @@ -564,6 +592,43 @@ int audio_playback_write(rootstream_ctx_t *ctx, int16_t *samples, size_t num_samples); void audio_playback_cleanup(rootstream_ctx_t *ctx); +/* Audio backends - ALSA */ +bool audio_capture_alsa_available(void); +int audio_capture_init_alsa(rootstream_ctx_t *ctx); +int audio_capture_frame_alsa(rootstream_ctx_t *ctx, int16_t *samples, + size_t *num_samples); +void audio_capture_cleanup_alsa(rootstream_ctx_t *ctx); + +bool audio_playback_alsa_available(void); +int audio_playback_init_alsa(rootstream_ctx_t *ctx); +int audio_playback_write_alsa(rootstream_ctx_t *ctx, int16_t *samples, + size_t num_samples); +void audio_playback_cleanup_alsa(rootstream_ctx_t *ctx); + +/* Audio backends - PulseAudio */ +bool audio_capture_pulse_available(void); +int audio_capture_init_pulse(rootstream_ctx_t *ctx); +int audio_capture_frame_pulse(rootstream_ctx_t *ctx, int16_t *samples, + size_t *num_samples); +void audio_capture_cleanup_pulse(rootstream_ctx_t *ctx); + +bool audio_playback_pulse_available(void); +int audio_playback_init_pulse(rootstream_ctx_t *ctx); +int audio_playback_write_pulse(rootstream_ctx_t *ctx, int16_t *samples, + size_t num_samples); +void audio_playback_cleanup_pulse(rootstream_ctx_t *ctx); + +/* Audio backends - Dummy (silent/discard) */ +int audio_capture_init_dummy(rootstream_ctx_t *ctx); +int audio_capture_frame_dummy(rootstream_ctx_t *ctx, int16_t *samples, + size_t *num_samples); +void audio_capture_cleanup_dummy(rootstream_ctx_t *ctx); + +int audio_playback_init_dummy(rootstream_ctx_t *ctx); +int audio_playback_write_dummy(rootstream_ctx_t *ctx, int16_t *samples, + size_t num_samples); +void audio_playback_cleanup_dummy(rootstream_ctx_t *ctx); + /* --- Recording (Phase 7) --- */ int recording_init(rootstream_ctx_t *ctx, const char *filename); int recording_write_frame(rootstream_ctx_t *ctx, const uint8_t *data, diff --git a/src/audio_capture.c b/src/audio_capture.c index ba9a496..5259a5f 100644 --- a/src/audio_capture.c +++ b/src/audio_capture.c @@ -29,7 +29,21 @@ typedef struct { int channels; int frame_size; bool initialized; -} audio_capture_ctx_t; +} alsa_capture_ctx_t; + +/* + * Check if ALSA is available + */ +bool audio_capture_alsa_available(void) { + /* Try to open and immediately close a test handle */ + snd_pcm_t *handle = NULL; + int err = snd_pcm_open(&handle, "default", SND_PCM_STREAM_CAPTURE, 0); + if (err >= 0 && handle) { + snd_pcm_close(handle); + return true; + } + return false; +} /* * Initialize ALSA audio capture @@ -37,14 +51,14 @@ typedef struct { * @param ctx RootStream context * @return 0 on success, -1 on error */ -int audio_capture_init(rootstream_ctx_t *ctx) { +int audio_capture_init_alsa(rootstream_ctx_t *ctx) { if (!ctx) { fprintf(stderr, "ERROR: Invalid context for audio capture\n"); return -1; } /* Allocate capture context */ - audio_capture_ctx_t *capture = calloc(1, sizeof(audio_capture_ctx_t)); + alsa_capture_ctx_t *capture = calloc(1, sizeof(alsa_capture_ctx_t)); if (!capture) { fprintf(stderr, "ERROR: Cannot allocate audio capture context\n"); return -1; @@ -167,13 +181,13 @@ int audio_capture_init(rootstream_ctx_t *ctx) { * @param num_samples Output sample count * @return 0 on success, -1 on error */ -int audio_capture_frame(rootstream_ctx_t *ctx, int16_t *samples, - size_t *num_samples) { +int audio_capture_frame_alsa(rootstream_ctx_t *ctx, int16_t *samples, + size_t *num_samples) { if (!ctx || !samples || !num_samples) { return -1; } - audio_capture_ctx_t *capture = (audio_capture_ctx_t*)(intptr_t)ctx->uinput_mouse_fd; + alsa_capture_ctx_t *capture = (alsa_capture_ctx_t*)(intptr_t)ctx->uinput_mouse_fd; if (!capture || !capture->initialized) { return -1; } @@ -218,12 +232,12 @@ int audio_capture_frame(rootstream_ctx_t *ctx, int16_t *samples, /* * Cleanup audio capture */ -void audio_capture_cleanup(rootstream_ctx_t *ctx) { +void audio_capture_cleanup_alsa(rootstream_ctx_t *ctx) { if (!ctx || !ctx->uinput_mouse_fd) { return; } - audio_capture_ctx_t *capture = (audio_capture_ctx_t*)(intptr_t)ctx->uinput_mouse_fd; + alsa_capture_ctx_t *capture = (alsa_capture_ctx_t*)(intptr_t)ctx->uinput_mouse_fd; if (capture->handle) { snd_pcm_drain(capture->handle); @@ -235,3 +249,16 @@ void audio_capture_cleanup(rootstream_ctx_t *ctx) { printf("✓ Audio capture cleanup complete\n"); } + +/* Backward compatibility wrappers */ +int audio_capture_init(rootstream_ctx_t *ctx) { + return audio_capture_init_alsa(ctx); +} + +int audio_capture_frame(rootstream_ctx_t *ctx, int16_t *samples, size_t *num_samples) { + return audio_capture_frame_alsa(ctx, samples, num_samples); +} + +void audio_capture_cleanup(rootstream_ctx_t *ctx) { + audio_capture_cleanup_alsa(ctx); +} diff --git a/src/audio_capture_dummy.c b/src/audio_capture_dummy.c new file mode 100644 index 0000000..2660c25 --- /dev/null +++ b/src/audio_capture_dummy.c @@ -0,0 +1,69 @@ +/* + * audio_capture_dummy.c - Dummy audio capture (silent) + * + * Always available fallback that generates silence. + * Allows video-only streaming when audio hardware is unavailable. + * + * Parameters: + * - 48000 Hz sample rate + * - 2 channels (stereo) + * - 240 samples per frame (5ms at 48kHz) + */ + +#include "../include/rootstream.h" +#include +#include +#include + +#define SAMPLE_RATE 48000 +#define CHANNELS 2 +#define FRAME_SIZE 240 /* 5ms at 48kHz */ + +/* + * Initialize dummy audio capture + * + * @param ctx RootStream context + * @return 0 on success (always succeeds) + */ +int audio_capture_init_dummy(rootstream_ctx_t *ctx) { + if (!ctx) { + return -1; + } + + printf("✓ Dummy audio capture ready (silent): %d Hz, %d channels, %d samples/frame\n", + SAMPLE_RATE, CHANNELS, FRAME_SIZE); + + return 0; +} + +/* + * Capture one audio frame (returns silence) + * + * @param ctx RootStream context + * @param samples Output PCM samples (interleaved stereo, 16-bit) + * @param num_samples Output sample count + * @return 0 on success (always succeeds) + */ +int audio_capture_frame_dummy(rootstream_ctx_t *ctx, int16_t *samples, + size_t *num_samples) { + if (!ctx || !samples || !num_samples) { + return -1; + } + + /* Fill with silence (zeros) */ + memset(samples, 0, FRAME_SIZE * CHANNELS * sizeof(int16_t)); + *num_samples = FRAME_SIZE; + + return 0; +} + +/* + * Cleanup dummy audio capture + */ +void audio_capture_cleanup_dummy(rootstream_ctx_t *ctx) { + if (!ctx) { + return; + } + + printf("✓ Dummy audio capture cleanup complete\n"); +} diff --git a/src/audio_capture_pulse.c b/src/audio_capture_pulse.c new file mode 100644 index 0000000..fd2c034 --- /dev/null +++ b/src/audio_capture_pulse.c @@ -0,0 +1,202 @@ +/* + * audio_capture_pulse.c - PulseAudio capture fallback + * + * Fallback audio capture using PulseAudio Simple API. + * More robust than ALSA on modern Linux distributions. + * + * Parameters: + * - 48000 Hz sample rate + * - 2 channels (stereo) + * - 16-bit signed PCM + * - 240 samples per frame (5ms at 48kHz) + */ + +#include "../include/rootstream.h" +#include +#include +#include +#include + +#ifdef HAVE_PULSEAUDIO +#include +#include +#endif + +#define SAMPLE_RATE 48000 +#define CHANNELS 2 +#define FRAME_SIZE 240 /* 5ms at 48kHz */ + +typedef struct { +#ifdef HAVE_PULSEAUDIO + pa_simple *stream; +#else + void *dummy; +#endif + int sample_rate; + int channels; + int frame_size; + bool initialized; +} audio_capture_pulse_ctx_t; + +/* + * Check if PulseAudio is available + */ +bool audio_capture_pulse_available(void) { +#ifdef HAVE_PULSEAUDIO + /* Try to create a test connection */ + int error; + pa_sample_spec ss = { + .format = PA_SAMPLE_S16LE, + .rate = SAMPLE_RATE, + .channels = CHANNELS + }; + + pa_simple *test = pa_simple_new(NULL, "RootStream-Test", PA_STREAM_RECORD, + NULL, "test", &ss, NULL, NULL, &error); + if (test) { + pa_simple_free(test); + return true; + } +#endif + return false; +} + +/* + * Initialize PulseAudio audio capture + * + * @param ctx RootStream context + * @return 0 on success, -1 on error + */ +int audio_capture_init_pulse(rootstream_ctx_t *ctx) { +#ifdef HAVE_PULSEAUDIO + if (!ctx) { + fprintf(stderr, "ERROR: Invalid context for PulseAudio capture\n"); + return -1; + } + + /* Allocate capture context */ + audio_capture_pulse_ctx_t *capture = calloc(1, sizeof(audio_capture_pulse_ctx_t)); + if (!capture) { + fprintf(stderr, "ERROR: Cannot allocate PulseAudio capture context\n"); + return -1; + } + + capture->sample_rate = SAMPLE_RATE; + capture->channels = CHANNELS; + capture->frame_size = FRAME_SIZE; + + /* Configure sample format */ + pa_sample_spec ss = { + .format = PA_SAMPLE_S16LE, + .rate = capture->sample_rate, + .channels = capture->channels + }; + + /* Configure buffer attributes for low latency */ + pa_buffer_attr attr = { + .maxlength = (uint32_t)-1, + .tlength = (uint32_t)-1, + .prebuf = (uint32_t)-1, + .minreq = (uint32_t)-1, + .fragsize = capture->frame_size * sizeof(int16_t) * capture->channels + }; + + /* Create PulseAudio stream */ + int error; + capture->stream = pa_simple_new( + NULL, /* Use default server */ + "RootStream", /* Application name */ + PA_STREAM_RECORD, /* Recording mode */ + NULL, /* Use default device */ + "Audio Capture", /* Stream description */ + &ss, /* Sample format */ + NULL, /* Use default channel map */ + &attr, /* Buffer attributes */ + &error /* Error code */ + ); + + if (!capture->stream) { + fprintf(stderr, "ERROR: Cannot open PulseAudio stream: %s\n", + pa_strerror(error)); + free(capture); + return -1; + } + + capture->initialized = true; + + /* Store in context (reuse mouse fd field) */ + ctx->uinput_mouse_fd = (int)(intptr_t)capture; + + printf("✓ PulseAudio capture ready: %d Hz, %d channels, %d samples/frame\n", + capture->sample_rate, capture->channels, capture->frame_size); + + return 0; +#else + fprintf(stderr, "ERROR: PulseAudio support not compiled\n"); + return -1; +#endif +} + +/* + * Capture one audio frame + * + * @param ctx RootStream context + * @param samples Output PCM samples (interleaved stereo, 16-bit) + * @param num_samples Output sample count + * @return 0 on success, -1 on error + */ +int audio_capture_frame_pulse(rootstream_ctx_t *ctx, int16_t *samples, + size_t *num_samples) { +#ifdef HAVE_PULSEAUDIO + if (!ctx || !samples || !num_samples) { + return -1; + } + + audio_capture_pulse_ctx_t *capture = (audio_capture_pulse_ctx_t*)(intptr_t)ctx->uinput_mouse_fd; + if (!capture || !capture->initialized || !capture->stream) { + return -1; + } + + /* Read PCM samples from PulseAudio */ + size_t bytes_to_read = capture->frame_size * sizeof(int16_t) * capture->channels; + int error; + + if (pa_simple_read(capture->stream, samples, bytes_to_read, &error) < 0) { + fprintf(stderr, "ERROR: PulseAudio read failed: %s\n", + pa_strerror(error)); + return -1; + } + + *num_samples = capture->frame_size; + return 0; +#else + (void)ctx; + (void)samples; + (void)num_samples; + return -1; +#endif +} + +/* + * Cleanup PulseAudio capture + */ +void audio_capture_cleanup_pulse(rootstream_ctx_t *ctx) { +#ifdef HAVE_PULSEAUDIO + if (!ctx || !ctx->uinput_mouse_fd) { + return; + } + + audio_capture_pulse_ctx_t *capture = (audio_capture_pulse_ctx_t*)(intptr_t)ctx->uinput_mouse_fd; + + if (capture->stream) { + pa_simple_free(capture->stream); + } + + free(capture); + ctx->uinput_mouse_fd = 0; + + printf("✓ PulseAudio capture cleanup complete\n"); +#else + (void)ctx; +#endif +} diff --git a/src/audio_playback.c b/src/audio_playback.c index 6057c61..1ec1bff 100644 --- a/src/audio_playback.c +++ b/src/audio_playback.c @@ -35,7 +35,7 @@ typedef struct { * @param ctx RootStream context * @return 0 on success, -1 on error */ -int audio_playback_init(rootstream_ctx_t *ctx) { +int audio_playback_init_alsa(rootstream_ctx_t *ctx) { if (!ctx) { fprintf(stderr, "ERROR: Invalid context for audio playback\n"); return -1; @@ -174,8 +174,8 @@ int audio_playback_init(rootstream_ctx_t *ctx) { * @param num_samples Sample count per channel * @return 0 on success, -1 on error */ -int audio_playback_write(rootstream_ctx_t *ctx, int16_t *samples, - size_t num_samples) { +int audio_playback_write_alsa(rootstream_ctx_t *ctx, int16_t *samples, + size_t num_samples) { if (!ctx || !samples || num_samples == 0) { return -1; } @@ -224,7 +224,7 @@ int audio_playback_write(rootstream_ctx_t *ctx, int16_t *samples, /* * Cleanup audio playback */ -void audio_playback_cleanup(rootstream_ctx_t *ctx) { +void audio_playback_cleanup_alsa(rootstream_ctx_t *ctx) { if (!ctx || !ctx->tray.menu) { return; } @@ -241,3 +241,16 @@ void audio_playback_cleanup(rootstream_ctx_t *ctx) { printf("✓ Audio playback cleanup complete\n"); } + +/* Backward compatibility wrappers */ +int audio_playback_init(rootstream_ctx_t *ctx) { + return audio_playback_init_alsa(ctx); +} + +int audio_playback_write(rootstream_ctx_t *ctx, int16_t *samples, size_t num_samples) { + return audio_playback_write_alsa(ctx, samples, num_samples); +} + +void audio_playback_cleanup(rootstream_ctx_t *ctx) { + audio_playback_cleanup_alsa(ctx); +} diff --git a/src/audio_playback_dummy.c b/src/audio_playback_dummy.c new file mode 100644 index 0000000..716f56a --- /dev/null +++ b/src/audio_playback_dummy.c @@ -0,0 +1,62 @@ +/* + * audio_playback_dummy.c - Dummy audio playback (discard) + * + * Always available fallback that discards audio. + * Allows video-only viewing when audio hardware is unavailable. + * + * Parameters: + * - 48000 Hz sample rate + * - 2 channels (stereo) + */ + +#include "../include/rootstream.h" +#include +#include + +#define SAMPLE_RATE 48000 +#define CHANNELS 2 + +/* + * Initialize dummy audio playback + * + * @param ctx RootStream context + * @return 0 on success (always succeeds) + */ +int audio_playback_init_dummy(rootstream_ctx_t *ctx) { + if (!ctx) { + return -1; + } + + printf("✓ Dummy audio playback ready (silent): %d Hz, %d channels\n", + SAMPLE_RATE, CHANNELS); + + return 0; +} + +/* + * Play audio samples (discards them) + * + * @param ctx RootStream context + * @param samples PCM samples (interleaved stereo, 16-bit) + * @param num_samples Sample count per channel + * @return 0 on success (always succeeds) + */ +int audio_playback_write_dummy(rootstream_ctx_t *ctx, int16_t *samples, + size_t num_samples) { + /* Do nothing - discard audio */ + (void)ctx; + (void)samples; + (void)num_samples; + return 0; +} + +/* + * Cleanup dummy audio playback + */ +void audio_playback_cleanup_dummy(rootstream_ctx_t *ctx) { + if (!ctx) { + return; + } + + printf("✓ Dummy audio playback cleanup complete\n"); +} diff --git a/src/audio_playback_pulse.c b/src/audio_playback_pulse.c new file mode 100644 index 0000000..5151c78 --- /dev/null +++ b/src/audio_playback_pulse.c @@ -0,0 +1,200 @@ +/* + * audio_playback_pulse.c - PulseAudio playback fallback + * + * Fallback audio playback using PulseAudio Simple API. + * More robust than ALSA on modern Linux distributions. + * + * Parameters: + * - 48000 Hz sample rate + * - 2 channels (stereo) + * - 16-bit signed PCM + */ + +#include "../include/rootstream.h" +#include +#include +#include +#include + +#ifdef HAVE_PULSEAUDIO +#include +#include +#endif + +#define SAMPLE_RATE 48000 +#define CHANNELS 2 + +typedef struct { +#ifdef HAVE_PULSEAUDIO + pa_simple *stream; +#else + void *dummy; +#endif + int sample_rate; + int channels; + bool initialized; +} audio_playback_pulse_ctx_t; + +/* + * Check if PulseAudio is available + */ +bool audio_playback_pulse_available(void) { +#ifdef HAVE_PULSEAUDIO + /* Try to create a test connection */ + int error; + pa_sample_spec ss = { + .format = PA_SAMPLE_S16LE, + .rate = SAMPLE_RATE, + .channels = CHANNELS + }; + + pa_simple *test = pa_simple_new(NULL, "RootStream-Test", PA_STREAM_PLAYBACK, + NULL, "test", &ss, NULL, NULL, &error); + if (test) { + pa_simple_free(test); + return true; + } +#endif + return false; +} + +/* + * Initialize PulseAudio audio playback + * + * @param ctx RootStream context + * @return 0 on success, -1 on error + */ +int audio_playback_init_pulse(rootstream_ctx_t *ctx) { +#ifdef HAVE_PULSEAUDIO + if (!ctx) { + fprintf(stderr, "ERROR: Invalid context for PulseAudio playback\n"); + return -1; + } + + /* Allocate playback context */ + audio_playback_pulse_ctx_t *playback = calloc(1, sizeof(audio_playback_pulse_ctx_t)); + if (!playback) { + fprintf(stderr, "ERROR: Cannot allocate PulseAudio playback context\n"); + return -1; + } + + playback->sample_rate = SAMPLE_RATE; + playback->channels = CHANNELS; + + /* Configure sample format */ + pa_sample_spec ss = { + .format = PA_SAMPLE_S16LE, + .rate = playback->sample_rate, + .channels = playback->channels + }; + + /* Configure buffer attributes for low latency */ + pa_buffer_attr attr = { + .maxlength = (uint32_t)-1, + .tlength = 240 * sizeof(int16_t) * playback->channels * 4, /* 20ms buffer */ + .prebuf = (uint32_t)-1, + .minreq = (uint32_t)-1, + .fragsize = (uint32_t)-1 + }; + + /* Create PulseAudio stream */ + int error; + playback->stream = pa_simple_new( + NULL, /* Use default server */ + "RootStream", /* Application name */ + PA_STREAM_PLAYBACK, /* Playback mode */ + NULL, /* Use default device */ + "Audio Playback", /* Stream description */ + &ss, /* Sample format */ + NULL, /* Use default channel map */ + &attr, /* Buffer attributes */ + &error /* Error code */ + ); + + if (!playback->stream) { + fprintf(stderr, "ERROR: Cannot open PulseAudio stream: %s\n", + pa_strerror(error)); + free(playback); + return -1; + } + + playback->initialized = true; + + /* Store in context (reuse tray menu field) */ + ctx->tray.menu = playback; + + printf("✓ PulseAudio playback ready: %d Hz, %d channels\n", + playback->sample_rate, playback->channels); + + return 0; +#else + fprintf(stderr, "ERROR: PulseAudio support not compiled\n"); + return -1; +#endif +} + +/* + * Play audio samples + * + * @param ctx RootStream context + * @param samples PCM samples (interleaved stereo, 16-bit) + * @param num_samples Sample count per channel + * @return 0 on success, -1 on error + */ +int audio_playback_write_pulse(rootstream_ctx_t *ctx, int16_t *samples, + size_t num_samples) { +#ifdef HAVE_PULSEAUDIO + if (!ctx || !samples || num_samples == 0) { + return -1; + } + + audio_playback_pulse_ctx_t *playback = (audio_playback_pulse_ctx_t*)ctx->tray.menu; + if (!playback || !playback->initialized || !playback->stream) { + return -1; + } + + /* Write PCM samples to PulseAudio */ + size_t bytes_to_write = num_samples * sizeof(int16_t) * playback->channels; + int error; + + if (pa_simple_write(playback->stream, samples, bytes_to_write, &error) < 0) { + fprintf(stderr, "ERROR: PulseAudio write failed: %s\n", + pa_strerror(error)); + return -1; + } + + return 0; +#else + (void)ctx; + (void)samples; + (void)num_samples; + return -1; +#endif +} + +/* + * Cleanup PulseAudio playback + */ +void audio_playback_cleanup_pulse(rootstream_ctx_t *ctx) { +#ifdef HAVE_PULSEAUDIO + if (!ctx || !ctx->tray.menu) { + return; + } + + audio_playback_pulse_ctx_t *playback = (audio_playback_pulse_ctx_t*)ctx->tray.menu; + + if (playback->stream) { + /* Drain any remaining audio */ + int error; + pa_simple_drain(playback->stream, &error); + pa_simple_free(playback->stream); + } + + free(playback); + ctx->tray.menu = NULL; + + printf("✓ PulseAudio playback cleanup complete\n"); +#else + (void)ctx; +#endif +} diff --git a/src/service.c b/src/service.c index afadd0a..b01fd2c 100644 --- a/src/service.c +++ b/src/service.c @@ -301,16 +301,74 @@ int service_run_host(rootstream_ctx_t *ctx) { fprintf(stderr, "WARNING: Input init failed (continuing without input)\n"); } - /* Initialize audio capture and Opus encoder */ + /* Initialize audio capture with fallback */ if (ctx->settings.audio_enabled) { - if (audio_capture_init(ctx) < 0) { - fprintf(stderr, "WARNING: Audio capture init failed (continuing without audio)\n"); - } else if (rootstream_opus_encoder_init(ctx) < 0) { - fprintf(stderr, "WARNING: Opus encoder init failed (continuing without audio)\n"); - audio_capture_cleanup(ctx); + printf("INFO: Initializing audio capture...\n"); + + /* Backend list has static storage duration - safe to store pointers */ + static const audio_capture_backend_t capture_backends[] = { + { + .name = "ALSA", + .init_fn = audio_capture_init_alsa, + .capture_fn = audio_capture_frame_alsa, + .cleanup_fn = audio_capture_cleanup_alsa, + .is_available_fn = audio_capture_alsa_available, + }, + { + .name = "PulseAudio", + .init_fn = audio_capture_init_pulse, + .capture_fn = audio_capture_frame_pulse, + .cleanup_fn = audio_capture_cleanup_pulse, + .is_available_fn = audio_capture_pulse_available, + }, + { + .name = "Dummy (Silent)", + .init_fn = audio_capture_init_dummy, + .capture_fn = audio_capture_frame_dummy, + .cleanup_fn = audio_capture_cleanup_dummy, + .is_available_fn = NULL, /* Always available */ + }, + {NULL} + }; + + int capture_idx = 0; + while (capture_backends[capture_idx].name) { + printf("INFO: Attempting audio capture backend: %s\n", capture_backends[capture_idx].name); + + if (capture_backends[capture_idx].is_available_fn && + !capture_backends[capture_idx].is_available_fn()) { + printf(" → Not available on this system\n"); + capture_idx++; + continue; + } + + if (capture_backends[capture_idx].init_fn(ctx) == 0) { + printf("✓ Audio capture backend '%s' initialized\n", capture_backends[capture_idx].name); + ctx->audio_capture_backend = &capture_backends[capture_idx]; + break; + } else { + printf("WARNING: Audio capture backend '%s' failed, trying next...\n", + capture_backends[capture_idx].name); + capture_idx++; + } + } + + if (!ctx->audio_capture_backend) { + printf("WARNING: All audio capture backends failed, streaming video only\n"); + } else { + /* Initialize Opus encoder */ + printf("INFO: Initializing Opus encoder...\n"); + if (rootstream_opus_encoder_init(ctx) < 0) { + printf("WARNING: Opus encoder init failed, audio disabled\n"); + if (ctx->audio_capture_backend && ctx->audio_capture_backend->cleanup_fn) { + ctx->audio_capture_backend->cleanup_fn(ctx); + } + ctx->audio_capture_backend = NULL; + } } } else { printf("INFO: Audio disabled in settings\n"); + ctx->audio_capture_backend = NULL; } /* Announce service */ @@ -373,8 +431,16 @@ int service_run_host(rootstream_ctx_t *ctx) { size_t audio_size = 0; size_t num_samples = 0; - if (ctx->settings.audio_enabled && - audio_capture_frame(ctx, audio_samples, &num_samples) == 0) { + if (ctx->audio_capture_backend && ctx->audio_capture_backend->capture_fn) { + int audio_result = ctx->audio_capture_backend->capture_fn(ctx, audio_samples, &num_samples); + if (audio_result < 0) { + /* Audio capture failed, continue with video only */ + num_samples = 0; + } + } + + /* Encode audio if we have samples */ + if (num_samples > 0) { if (rootstream_opus_encode(ctx, audio_samples, audio_buf, &audio_size) < 0) { /* Audio encode failed, continue with video only */ audio_size = 0; @@ -467,16 +533,74 @@ int service_run_client(rootstream_ctx_t *ctx) { return -1; } - /* Initialize audio playback and Opus decoder */ + /* Initialize audio playback with fallback */ if (ctx->settings.audio_enabled) { - if (audio_playback_init(ctx) < 0) { - fprintf(stderr, "WARNING: Audio playback init failed (continuing without audio)\n"); - } else if (rootstream_opus_decoder_init(ctx) < 0) { - fprintf(stderr, "WARNING: Opus decoder init failed (continuing without audio)\n"); - audio_playback_cleanup(ctx); + printf("INFO: Initializing audio playback...\n"); + + /* Backend list has static storage duration - safe to store pointers */ + static const audio_playback_backend_t playback_backends[] = { + { + .name = "ALSA", + .init_fn = audio_playback_init_alsa, + .playback_fn = audio_playback_write_alsa, + .cleanup_fn = audio_playback_cleanup_alsa, + .is_available_fn = audio_playback_alsa_available, + }, + { + .name = "PulseAudio", + .init_fn = audio_playback_init_pulse, + .playback_fn = audio_playback_write_pulse, + .cleanup_fn = audio_playback_cleanup_pulse, + .is_available_fn = audio_playback_pulse_available, + }, + { + .name = "Dummy (Silent)", + .init_fn = audio_playback_init_dummy, + .playback_fn = audio_playback_write_dummy, + .cleanup_fn = audio_playback_cleanup_dummy, + .is_available_fn = NULL, /* Always available */ + }, + {NULL} + }; + + int playback_idx = 0; + while (playback_backends[playback_idx].name) { + printf("INFO: Attempting audio playback backend: %s\n", playback_backends[playback_idx].name); + + if (playback_backends[playback_idx].is_available_fn && + !playback_backends[playback_idx].is_available_fn()) { + printf(" → Not available on this system\n"); + playback_idx++; + continue; + } + + if (playback_backends[playback_idx].init_fn(ctx) == 0) { + printf("✓ Audio playback backend '%s' initialized\n", playback_backends[playback_idx].name); + ctx->audio_playback_backend = &playback_backends[playback_idx]; + break; + } else { + printf("WARNING: Audio playback backend '%s' failed, trying next...\n", + playback_backends[playback_idx].name); + playback_idx++; + } + } + + if (!ctx->audio_playback_backend) { + printf("WARNING: All audio playback backends failed, watching video only\n"); + } else { + /* Initialize Opus decoder */ + printf("INFO: Initializing Opus decoder...\n"); + if (rootstream_opus_decoder_init(ctx) < 0) { + printf("WARNING: Opus decoder init failed, audio disabled\n"); + if (ctx->audio_playback_backend && ctx->audio_playback_backend->cleanup_fn) { + ctx->audio_playback_backend->cleanup_fn(ctx); + } + ctx->audio_playback_backend = NULL; + } } } else { printf("INFO: Audio disabled in settings\n"); + ctx->audio_playback_backend = NULL; } printf("✓ Client initialized - ready to receive video and audio\n"); @@ -542,8 +666,8 @@ int service_run_client(rootstream_ctx_t *ctx) { free(decoded_frame.data); } - if (ctx->settings.audio_enabled) { - audio_playback_cleanup(ctx); + if (ctx->audio_playback_backend && ctx->audio_playback_backend->cleanup_fn) { + ctx->audio_playback_backend->cleanup_fn(ctx); rootstream_opus_cleanup(ctx); } display_cleanup(ctx);