From 6ccd9fdf1982ab632e5ec993face313d84bbd754 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 1 Oct 2025 20:13:13 +0300 Subject: [PATCH] [WIP] Updates: use native C binaries for audio --- .devcontainer/install-deps.sh | 10 +- .devcontainer/install_audio_deps.sh | 16 +- .gitignore | 1 + Makefile | 44 ++- internal/audio/c/ipc_protocol.c | 309 ++++++++++++++++++++ internal/audio/c/ipc_protocol.h | 210 +++++++++++++ internal/audio/c/jetkvm_audio_input.c | 348 ++++++++++++++++++++++ internal/audio/c/jetkvm_audio_output.c | 389 +++++++++++++++++++++++++ internal/audio/embed.go | 123 ++++++++ internal/audio/input_supervisor.go | 49 ++-- internal/audio/output_supervisor.go | 30 +- scripts/dev_deploy.sh | 19 +- 12 files changed, 1491 insertions(+), 57 deletions(-) create mode 100644 internal/audio/c/ipc_protocol.c create mode 100644 internal/audio/c/ipc_protocol.h create mode 100644 internal/audio/c/jetkvm_audio_input.c create mode 100644 internal/audio/c/jetkvm_audio_output.c create mode 100644 internal/audio/embed.go diff --git a/.devcontainer/install-deps.sh b/.devcontainer/install-deps.sh index 6f8d4c4e..94106cc9 100755 --- a/.devcontainer/install-deps.sh +++ b/.devcontainer/install-deps.sh @@ -42,11 +42,17 @@ AUDIO_DEPS_SCRIPT="${PROJECT_ROOT}/install_audio_deps.sh" if [ -f "${AUDIO_DEPS_SCRIPT}" ]; then echo "Running audio dependencies installation..." - sudo bash "${AUDIO_DEPS_SCRIPT}" + # Pre-create audio libs directory with proper permissions + sudo mkdir -p /opt/jetkvm-audio-libs + sudo chmod 777 /opt/jetkvm-audio-libs + # Run installation script (now it can write without sudo) + bash "${AUDIO_DEPS_SCRIPT}" echo "Audio dependencies installation completed." if [ -d "/opt/jetkvm-audio-libs" ]; then echo "Audio libraries installed in /opt/jetkvm-audio-libs" - sudo chmod -R o+rw /opt/jetkvm-audio-libs + # Set recursive permissions for all subdirectories and files + sudo chmod -R 777 /opt/jetkvm-audio-libs + echo "Permissions set to allow all users access to audio libraries" else echo "Error: /opt/jetkvm-audio-libs directory not found after installation." exit 1 diff --git a/.devcontainer/install_audio_deps.sh b/.devcontainer/install_audio_deps.sh index 4fdebd4c..8d369db4 100755 --- a/.devcontainer/install_audio_deps.sh +++ b/.devcontainer/install_audio_deps.sh @@ -3,6 +3,18 @@ # Build ALSA and Opus static libs for ARM in /opt/jetkvm-audio-libs set -e +# Sudo wrapper function +SUDO_PATH=$(which sudo 2>/dev/null || echo "") +function use_sudo() { + if [ "$UID" -eq 0 ]; then + "$@" + elif [ -n "$SUDO_PATH" ]; then + ${SUDO_PATH} -E "$@" + else + "$@" + fi +} + # Accept version parameters or use defaults ALSA_VERSION="${1:-1.2.14}" OPUS_VERSION="${2:-1.5.2}" @@ -12,7 +24,9 @@ BUILDKIT_PATH="/opt/jetkvm-native-buildkit" BUILDKIT_FLAVOR="arm-rockchip830-linux-uclibcgnueabihf" CROSS_PREFIX="$BUILDKIT_PATH/bin/$BUILDKIT_FLAVOR" -mkdir -p "$AUDIO_LIBS_DIR" +# Create directory with proper permissions +use_sudo mkdir -p "$AUDIO_LIBS_DIR" +use_sudo chmod 777 "$AUDIO_LIBS_DIR" cd "$AUDIO_LIBS_DIR" # Download sources diff --git a/.gitignore b/.gitignore index 9b469860..beace99a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ node_modules # generated during the build process #internal/native/include #internal/native/lib +internal/audio/bin/ diff --git a/Makefile b/Makefile index 43be6bc9..ca7dd61f 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,7 @@ KVM_PKG_NAME := github.com/jetkvm/kvm BUILDKIT_FLAVOR := arm-rockchip830-linux-uclibcgnueabihf BUILDKIT_PATH ?= /opt/jetkvm-native-buildkit SKIP_NATIVE_IF_EXISTS ?= 0 +SKIP_AUDIO_BINARIES_IF_EXISTS ?= 0 SKIP_UI_BUILD ?= 0 GO_BUILD_ARGS := -tags netgo,timetzdata,nomsgpack GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS) @@ -87,7 +88,46 @@ build_native: ./scripts/build_cgo.sh; \ fi -build_dev: build_native build_audio_deps +# Build audio output C binary (ALSA capture → Opus encode → IPC) +build_audio_output: build_audio_deps + @if [ "$(SKIP_AUDIO_BINARIES_IF_EXISTS)" = "1" ] && [ -f "$(BIN_DIR)/jetkvm_audio_output" ]; then \ + echo "jetkvm_audio_output already exists, skipping build..."; \ + else \ + echo "Building audio output binary..."; \ + mkdir -p $(BIN_DIR); \ + $(CC) $(CGO_CFLAGS) \ + -o $(BIN_DIR)/jetkvm_audio_output \ + internal/audio/c/jetkvm_audio_output.c \ + internal/audio/c/ipc_protocol.c \ + internal/audio/c/audio.c \ + $(CGO_LDFLAGS); \ + fi + +# Build audio input C binary (IPC → Opus decode → ALSA playback) +build_audio_input: build_audio_deps + @if [ "$(SKIP_AUDIO_BINARIES_IF_EXISTS)" = "1" ] && [ -f "$(BIN_DIR)/jetkvm_audio_input" ]; then \ + echo "jetkvm_audio_input already exists, skipping build..."; \ + else \ + echo "Building audio input binary..."; \ + mkdir -p $(BIN_DIR); \ + $(CC) $(CGO_CFLAGS) \ + -o $(BIN_DIR)/jetkvm_audio_input \ + internal/audio/c/jetkvm_audio_input.c \ + internal/audio/c/ipc_protocol.c \ + internal/audio/c/audio.c \ + $(CGO_LDFLAGS); \ + fi + +# Build both audio binaries and copy to embed location +build_audio_binaries: build_audio_output build_audio_input + @echo "Audio binaries built successfully" + @echo "Copying binaries to embed location..." + @mkdir -p internal/audio/bin + @cp $(BIN_DIR)/jetkvm_audio_output internal/audio/bin/ + @cp $(BIN_DIR)/jetkvm_audio_input internal/audio/bin/ + @echo "Binaries ready for embedding" + +build_dev: build_native build_audio_deps build_audio_binaries $(CLEAN_GO_CACHE) @echo "Building..." go build \ @@ -153,7 +193,7 @@ dev_release: frontend build_dev rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION_DEV)/jetkvm_app rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION_DEV)/jetkvm_app.sha256 -build_release: frontend build_native build_audio_deps +build_release: frontend build_native build_audio_deps build_audio_binaries $(CLEAN_GO_CACHE) @echo "Building release..." go build \ diff --git a/internal/audio/c/ipc_protocol.c b/internal/audio/c/ipc_protocol.c new file mode 100644 index 00000000..372cfcee --- /dev/null +++ b/internal/audio/c/ipc_protocol.c @@ -0,0 +1,309 @@ +/* + * JetKVM Audio IPC Protocol Implementation + * + * Implements Unix domain socket communication with exact byte-level + * compatibility with Go implementation in internal/audio/ipc_*.go + */ + +#include "ipc_protocol.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ============================================================================ +// HELPER FUNCTIONS +// ============================================================================ + +/** + * Read exactly N bytes from socket (loops until complete or error). + * This is critical because read() may return partial data. + */ +int ipc_read_full(int sock, void *buf, size_t len) { + uint8_t *ptr = (uint8_t *)buf; + size_t remaining = len; + + while (remaining > 0) { + ssize_t n = read(sock, ptr, remaining); + + if (n < 0) { + if (errno == EINTR) { + continue; // Interrupted by signal, retry + } + return -1; // Read error + } + + if (n == 0) { + return -1; // EOF (connection closed) + } + + ptr += n; + remaining -= n; + } + + return 0; // Success +} + +/** + * Get current time in nanoseconds (Unix epoch). + * Compatible with Go time.Now().UnixNano(). + */ +int64_t ipc_get_time_ns(void) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) != 0) { + return 0; // Fallback on error + } + return (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; +} + +// ============================================================================ +// MESSAGE READ/WRITE +// ============================================================================ + +/** + * Read a complete IPC message from socket. + * Returns 0 on success, -1 on error. + * Caller MUST free msg->data if non-NULL! + */ +int ipc_read_message(int sock, ipc_message_t *msg, uint32_t expected_magic) { + if (msg == NULL) { + return -1; + } + + // Initialize message + memset(msg, 0, sizeof(ipc_message_t)); + + // 1. Read header (17 bytes) + if (ipc_read_full(sock, &msg->header, IPC_HEADER_SIZE) != 0) { + return -1; + } + + // 2. Convert from little-endian (required on big-endian systems) + msg->header.magic = le32toh(msg->header.magic); + msg->header.length = le32toh(msg->header.length); + msg->header.timestamp = le64toh(msg->header.timestamp); + // Note: type is uint8_t, no conversion needed + + // 3. Validate magic number + if (msg->header.magic != expected_magic) { + fprintf(stderr, "IPC: Invalid magic number: got 0x%08X, expected 0x%08X\n", + msg->header.magic, expected_magic); + return -1; + } + + // 4. Validate length + if (msg->header.length > IPC_MAX_FRAME_SIZE) { + fprintf(stderr, "IPC: Message too large: %u bytes (max %d)\n", + msg->header.length, IPC_MAX_FRAME_SIZE); + return -1; + } + + // 5. Read payload if present + if (msg->header.length > 0) { + msg->data = malloc(msg->header.length); + if (msg->data == NULL) { + fprintf(stderr, "IPC: Failed to allocate %u bytes for payload\n", + msg->header.length); + return -1; + } + + if (ipc_read_full(sock, msg->data, msg->header.length) != 0) { + free(msg->data); + msg->data = NULL; + return -1; + } + } + + return 0; // Success +} + +/** + * Write a complete IPC message to socket. + * Uses writev() for atomic header+payload write. + * Returns 0 on success, -1 on error. + */ +int ipc_write_message(int sock, uint32_t magic, uint8_t type, + const uint8_t *data, uint32_t length) { + // Validate length + if (length > IPC_MAX_FRAME_SIZE) { + fprintf(stderr, "IPC: Message too large: %u bytes (max %d)\n", + length, IPC_MAX_FRAME_SIZE); + return -1; + } + + // Prepare header + ipc_header_t header; + header.magic = htole32(magic); + header.type = type; + header.length = htole32(length); + header.timestamp = htole64(ipc_get_time_ns()); + + // Use writev for atomic write (if possible) + struct iovec iov[2]; + iov[0].iov_base = &header; + iov[0].iov_len = IPC_HEADER_SIZE; + iov[1].iov_base = (void *)data; + iov[1].iov_len = length; + + int iovcnt = (length > 0) ? 2 : 1; + size_t total_len = IPC_HEADER_SIZE + length; + + ssize_t written = writev(sock, iov, iovcnt); + + if (written < 0) { + if (errno == EINTR) { + // Retry once on interrupt + written = writev(sock, iov, iovcnt); + } + + if (written < 0) { + perror("IPC: writev failed"); + return -1; + } + } + + if ((size_t)written != total_len) { + fprintf(stderr, "IPC: Partial write: %zd/%zu bytes\n", written, total_len); + return -1; + } + + return 0; // Success +} + +// ============================================================================ +// CONFIGURATION PARSING +// ============================================================================ + +/** + * Parse Opus configuration from message data (36 bytes, little-endian). + */ +int ipc_parse_opus_config(const uint8_t *data, uint32_t length, ipc_opus_config_t *config) { + if (data == NULL || config == NULL) { + return -1; + } + + if (length != 36) { + fprintf(stderr, "IPC: Invalid Opus config size: %u bytes (expected 36)\n", length); + return -1; + } + + // Parse little-endian uint32 fields + const uint32_t *u32_data = (const uint32_t *)data; + config->sample_rate = le32toh(u32_data[0]); + config->channels = le32toh(u32_data[1]); + config->frame_size = le32toh(u32_data[2]); + config->bitrate = le32toh(u32_data[3]); + config->complexity = le32toh(u32_data[4]); + config->vbr = le32toh(u32_data[5]); + config->signal_type = le32toh(u32_data[6]); + config->bandwidth = le32toh(u32_data[7]); + config->dtx = le32toh(u32_data[8]); + + return 0; // Success +} + +/** + * Parse basic audio configuration from message data (12 bytes, little-endian). + */ +int ipc_parse_config(const uint8_t *data, uint32_t length, ipc_config_t *config) { + if (data == NULL || config == NULL) { + return -1; + } + + if (length != 12) { + fprintf(stderr, "IPC: Invalid config size: %u bytes (expected 12)\n", length); + return -1; + } + + // Parse little-endian uint32 fields + const uint32_t *u32_data = (const uint32_t *)data; + config->sample_rate = le32toh(u32_data[0]); + config->channels = le32toh(u32_data[1]); + config->frame_size = le32toh(u32_data[2]); + + return 0; // Success +} + +/** + * Free message resources. + */ +void ipc_free_message(ipc_message_t *msg) { + if (msg != NULL && msg->data != NULL) { + free(msg->data); + msg->data = NULL; + } +} + +// ============================================================================ +// SOCKET MANAGEMENT +// ============================================================================ + +/** + * Create Unix domain socket server. + */ +int ipc_create_server(const char *socket_path) { + if (socket_path == NULL) { + return -1; + } + + // 1. Create socket + int sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock < 0) { + perror("IPC: socket() failed"); + return -1; + } + + // 2. Remove existing socket file (ignore errors) + unlink(socket_path); + + // 3. Bind to path + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + + if (strlen(socket_path) >= sizeof(addr.sun_path)) { + fprintf(stderr, "IPC: Socket path too long: %s\n", socket_path); + close(sock); + return -1; + } + + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + + if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("IPC: bind() failed"); + close(sock); + return -1; + } + + // 4. Listen with backlog=1 (single client) + if (listen(sock, 1) < 0) { + perror("IPC: listen() failed"); + close(sock); + return -1; + } + + printf("IPC: Server listening on %s\n", socket_path); + return sock; +} + +/** + * Accept client connection. + */ +int ipc_accept_client(int server_sock) { + int client_sock = accept(server_sock, NULL, NULL); + + if (client_sock < 0) { + perror("IPC: accept() failed"); + return -1; + } + + printf("IPC: Client connected (fd=%d)\n", client_sock); + return client_sock; +} diff --git a/internal/audio/c/ipc_protocol.h b/internal/audio/c/ipc_protocol.h new file mode 100644 index 00000000..c5af32d4 --- /dev/null +++ b/internal/audio/c/ipc_protocol.h @@ -0,0 +1,210 @@ +/* + * JetKVM Audio IPC Protocol + * + * Wire protocol for Unix domain socket communication between main process + * and audio subprocesses. This protocol is 100% compatible with the Go + * implementation in internal/audio/ipc_*.go + * + * CRITICAL: All multi-byte integers use LITTLE-ENDIAN byte order. + */ + +#ifndef JETKVM_IPC_PROTOCOL_H +#define JETKVM_IPC_PROTOCOL_H + +#include +#include + +// ============================================================================ +// PROTOCOL CONSTANTS +// ============================================================================ + +// Magic numbers (ASCII representation when read as little-endian) +#define IPC_MAGIC_OUTPUT 0x4A4B4F55 // "JKOU" - JetKVM Output (device → browser) +#define IPC_MAGIC_INPUT 0x4A4B4D49 // "JKMI" - JetKVM Microphone Input (browser → device) + +// Message types (matches Go UnifiedMessageType enum) +#define IPC_MSG_TYPE_OPUS_FRAME 0 // Audio frame data (Opus encoded) +#define IPC_MSG_TYPE_CONFIG 1 // Basic audio config (12 bytes) +#define IPC_MSG_TYPE_OPUS_CONFIG 2 // Complete Opus config (36 bytes) +#define IPC_MSG_TYPE_STOP 3 // Shutdown signal +#define IPC_MSG_TYPE_HEARTBEAT 4 // Keep-alive ping +#define IPC_MSG_TYPE_ACK 5 // Acknowledgment + +// Size constraints +#define IPC_HEADER_SIZE 17 // Fixed header size +#define IPC_MAX_FRAME_SIZE 4096 // Maximum payload size (matches Go Config.MaxFrameSize) + +// Socket paths +#define IPC_SOCKET_OUTPUT "/var/run/audio_output.sock" +#define IPC_SOCKET_INPUT "/var/run/audio_input.sock" + +// ============================================================================ +// WIRE FORMAT STRUCTURES +// ============================================================================ + +/** + * IPC message header (17 bytes, little-endian) + * + * Byte layout: + * [0-3] magic uint32_t LE Magic number (0x4A4B4F55 or 0x4A4B4D49) + * [4] type uint8_t Message type (0-5) + * [5-8] length uint32_t LE Payload size in bytes + * [9-16] timestamp int64_t LE Unix nanoseconds (time.Now().UnixNano()) + * [17+] data uint8_t[] Variable payload + * + * CRITICAL: Must use __attribute__((packed)) to prevent padding. + */ +typedef struct __attribute__((packed)) { + uint32_t magic; // Magic number (LE) + uint8_t type; // Message type + uint32_t length; // Payload length in bytes (LE) + int64_t timestamp; // Unix nanoseconds (LE) +} ipc_header_t; + +/** + * Basic audio configuration (12 bytes) + * Message type: IPC_MSG_TYPE_CONFIG + * + * All fields are uint32_t little-endian. + */ +typedef struct __attribute__((packed)) { + uint32_t sample_rate; // Samples per second (e.g., 48000) + uint32_t channels; // Number of channels (e.g., 2 for stereo) + uint32_t frame_size; // Samples per frame (e.g., 960) +} ipc_config_t; + +/** + * Complete Opus encoder/decoder configuration (36 bytes) + * Message type: IPC_MSG_TYPE_OPUS_CONFIG + * + * All fields are uint32_t little-endian. + * Note: Negative values (like signal_type=-1000) are stored as two's complement uint32. + */ +typedef struct __attribute__((packed)) { + uint32_t sample_rate; // Samples per second (48000) + uint32_t channels; // Number of channels (2) + uint32_t frame_size; // Samples per frame (960) + uint32_t bitrate; // Bits per second (96000) + uint32_t complexity; // Encoder complexity 0-10 (1=fast, 10=best quality) + uint32_t vbr; // Variable bitrate: 0=disabled, 1=enabled + uint32_t signal_type; // Signal type: -1000=auto, 3001=music, 3002=voice + uint32_t bandwidth; // Bandwidth: 1101=narrowband, 1102=mediumband, 1103=wideband + uint32_t dtx; // Discontinuous transmission: 0=disabled, 1=enabled +} ipc_opus_config_t; + +/** + * Complete IPC message (header + payload) + */ +typedef struct { + ipc_header_t header; + uint8_t *data; // Dynamically allocated payload (NULL if length=0) +} ipc_message_t; + +// ============================================================================ +// FUNCTION DECLARATIONS +// ============================================================================ + +/** + * Read a complete IPC message from socket. + * + * This function: + * 1. Reads exactly 17 bytes (header) + * 2. Validates magic number + * 3. Validates length <= IPC_MAX_FRAME_SIZE + * 4. Allocates and reads payload if length > 0 + * 5. Stores result in msg->header and msg->data + * + * @param sock Socket file descriptor + * @param msg Output message (data will be malloc'd if length > 0) + * @param expected_magic Expected magic number (IPC_MAGIC_OUTPUT or IPC_MAGIC_INPUT) + * @return 0 on success, -1 on error + * + * CALLER MUST FREE msg->data if non-NULL! + */ +int ipc_read_message(int sock, ipc_message_t *msg, uint32_t expected_magic); + +/** + * Write a complete IPC message to socket. + * + * This function writes header + payload atomically (if possible via writev). + * Sets timestamp to current time. + * + * @param sock Socket file descriptor + * @param magic Magic number (IPC_MAGIC_OUTPUT or IPC_MAGIC_INPUT) + * @param type Message type (IPC_MSG_TYPE_*) + * @param data Payload data (can be NULL if length=0) + * @param length Payload length in bytes + * @return 0 on success, -1 on error + */ +int ipc_write_message(int sock, uint32_t magic, uint8_t type, + const uint8_t *data, uint32_t length); + +/** + * Parse Opus configuration from message data. + * + * @param data Payload data (must be exactly 36 bytes) + * @param length Payload length (must be 36) + * @param config Output Opus configuration + * @return 0 on success, -1 if length != 36 + */ +int ipc_parse_opus_config(const uint8_t *data, uint32_t length, ipc_opus_config_t *config); + +/** + * Parse basic audio configuration from message data. + * + * @param data Payload data (must be exactly 12 bytes) + * @param length Payload length (must be 12) + * @param config Output audio configuration + * @return 0 on success, -1 if length != 12 + */ +int ipc_parse_config(const uint8_t *data, uint32_t length, ipc_config_t *config); + +/** + * Free message resources. + * + * @param msg Message to free (frees msg->data if non-NULL) + */ +void ipc_free_message(ipc_message_t *msg); + +/** + * Get current time in nanoseconds (Unix epoch). + * + * @return Time in nanoseconds (compatible with Go time.Now().UnixNano()) + */ +int64_t ipc_get_time_ns(void); + +/** + * Create Unix domain socket server. + * + * This function: + * 1. Creates socket with AF_UNIX, SOCK_STREAM + * 2. Removes existing socket file + * 3. Binds to specified path + * 4. Listens with backlog=1 (single client) + * + * @param socket_path Path to Unix socket (e.g., "/var/run/audio_output.sock") + * @return Socket fd on success, -1 on error + */ +int ipc_create_server(const char *socket_path); + +/** + * Accept client connection with automatic retry. + * + * Blocks until client connects or error occurs. + * + * @param server_sock Server socket fd from ipc_create_server() + * @return Client socket fd on success, -1 on error + */ +int ipc_accept_client(int server_sock); + +/** + * Helper: Read exactly N bytes from socket (loops until complete or error). + * + * @param sock Socket file descriptor + * @param buf Output buffer + * @param len Number of bytes to read + * @return 0 on success, -1 on error + */ +int ipc_read_full(int sock, void *buf, size_t len); + +#endif // JETKVM_IPC_PROTOCOL_H diff --git a/internal/audio/c/jetkvm_audio_input.c b/internal/audio/c/jetkvm_audio_input.c new file mode 100644 index 00000000..19a5f239 --- /dev/null +++ b/internal/audio/c/jetkvm_audio_input.c @@ -0,0 +1,348 @@ +/* + * JetKVM Audio Input Server + * + * Standalone C binary for audio input path: + * Browser → WebRTC → Go Process → IPC Receive → Opus Decode → ALSA Playback (USB Gadget) + * + * This replaces the Go subprocess that was running with --audio-input-server flag. + * + * IMPORTANT: This binary only does OPUS DECODING (not encoding). + * The browser already encodes audio to Opus before sending via WebRTC. + */ + +#include "ipc_protocol.h" +#include +#include +#include +#include +#include +#include +#include + +// Forward declarations from audio.c +extern int jetkvm_audio_playback_init(void); +extern void jetkvm_audio_playback_close(void); +extern int jetkvm_audio_decode_write(void *opus_buf, int opus_size); +extern void update_audio_constants(int bitrate, int complexity, int vbr, int vbr_constraint, + int signal_type, int bandwidth, int dtx, int lsb_depth, + int sr, int ch, int fs, int max_pkt, + int sleep_us, int max_attempts, int max_backoff); +extern void set_trace_logging(int enabled); + +// Note: Input server uses decoder, not encoder, so no update_opus_encoder_params + +// ============================================================================ +// GLOBAL STATE +// ============================================================================ + +static volatile sig_atomic_t g_running = 1; // Shutdown flag + +// Audio configuration (from environment variables) +typedef struct { + const char *alsa_device; // ALSA playback device (default: "hw:1,0") + int opus_bitrate; // Opus bitrate (informational for decoder) + int opus_complexity; // Opus complexity (decoder ignores this) + int sample_rate; // Sample rate (default: 48000) + int channels; // Channels (default: 2) + int frame_size; // Frame size in samples (default: 960) + int trace_logging; // Enable trace logging (default: 0) +} audio_config_t; + +// ============================================================================ +// SIGNAL HANDLERS +// ============================================================================ + +static void signal_handler(int signo) { + if (signo == SIGTERM || signo == SIGINT) { + printf("Audio input server: Received signal %d, shutting down...\n", signo); + g_running = 0; + } +} + +static void setup_signal_handlers(void) { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = signal_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + sigaction(SIGTERM, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + + // Ignore SIGPIPE + signal(SIGPIPE, SIG_IGN); +} + +// ============================================================================ +// CONFIGURATION PARSING +// ============================================================================ + +static int parse_env_int(const char *name, int default_value) { + const char *str = getenv(name); + if (str == NULL || str[0] == '\0') { + return default_value; + } + return atoi(str); +} + +static const char* parse_env_string(const char *name, const char *default_value) { + const char *str = getenv(name); + if (str == NULL || str[0] == '\0') { + return default_value; + } + return str; +} + +static int is_trace_enabled(void) { + const char *pion_trace = getenv("PION_LOG_TRACE"); + if (pion_trace == NULL) { + return 0; + } + + // Check if "audio" is in comma-separated list + if (strstr(pion_trace, "audio") != NULL) { + return 1; + } + + return 0; +} + +static void load_audio_config(audio_config_t *config) { + // ALSA device configuration + config->alsa_device = parse_env_string("ALSA_PLAYBACK_DEVICE", "hw:1,0"); + + // Opus configuration (informational only for decoder) + config->opus_bitrate = parse_env_int("OPUS_BITRATE", 96000); + config->opus_complexity = parse_env_int("OPUS_COMPLEXITY", 1); + + // Audio format + config->sample_rate = parse_env_int("AUDIO_SAMPLE_RATE", 48000); + config->channels = parse_env_int("AUDIO_CHANNELS", 2); + config->frame_size = parse_env_int("AUDIO_FRAME_SIZE", 960); + + // Logging + config->trace_logging = is_trace_enabled(); + + // Log configuration + printf("Audio Input Server Configuration:\n"); + printf(" ALSA Device: %s\n", config->alsa_device); + printf(" Sample Rate: %d Hz\n", config->sample_rate); + printf(" Channels: %d\n", config->channels); + printf(" Frame Size: %d samples\n", config->frame_size); + printf(" Trace Logging: %s\n", config->trace_logging ? "enabled" : "disabled"); +} + +// ============================================================================ +// MESSAGE HANDLING +// ============================================================================ + +/** + * Handle OpusConfig message: informational only for decoder. + * Decoder config updates are less critical than encoder. + * Returns 0 on success. + */ +static int handle_opus_config(const uint8_t *data, uint32_t length) { + ipc_opus_config_t config; + + if (ipc_parse_opus_config(data, length, &config) != 0) { + fprintf(stderr, "Failed to parse Opus config\n"); + return -1; + } + + printf("Received Opus config (informational): bitrate=%u, complexity=%u\n", + config.bitrate, config.complexity); + + // Note: Decoder doesn't need most of these parameters. + // Opus decoder automatically adapts to encoder settings embedded in stream. + // FEC (Forward Error Correction) is enabled automatically when present in packets. + + return 0; +} + +/** + * Send ACK response for heartbeat messages. + */ +static int send_ack(int client_sock) { + return ipc_write_message(client_sock, IPC_MAGIC_INPUT, IPC_MSG_TYPE_ACK, NULL, 0); +} + +// ============================================================================ +// MAIN LOOP +// ============================================================================ + +/** + * Main audio decode and playback loop. + * Receives Opus frames via IPC, decodes, writes to ALSA. + */ +static int run_audio_loop(int client_sock) { + int consecutive_errors = 0; + const int max_consecutive_errors = 10; + int frame_count = 0; + + printf("Starting audio input loop...\n"); + + while (g_running) { + ipc_message_t msg; + + // Read message from client (blocking) + if (ipc_read_message(client_sock, &msg, IPC_MAGIC_INPUT) != 0) { + if (g_running) { + fprintf(stderr, "Failed to read message from client\n"); + } + break; // Client disconnected or error + } + + // Process message based on type + switch (msg.header.type) { + case IPC_MSG_TYPE_OPUS_FRAME: { + if (msg.header.length == 0 || msg.data == NULL) { + fprintf(stderr, "Warning: Empty Opus frame received\n"); + ipc_free_message(&msg); + continue; + } + + // Decode Opus and write to ALSA + int frames_written = jetkvm_audio_decode_write(msg.data, msg.header.length); + + if (frames_written < 0) { + consecutive_errors++; + fprintf(stderr, "Audio decode/write failed (error %d/%d)\n", + consecutive_errors, max_consecutive_errors); + + if (consecutive_errors >= max_consecutive_errors) { + fprintf(stderr, "Too many consecutive errors, giving up\n"); + ipc_free_message(&msg); + return -1; + } + } else { + // Success - reset error counter + consecutive_errors = 0; + frame_count++; + + // Trace logging (periodic) + if (frame_count % 1000 == 1) { + printf("Processed frame %d (opus_size=%u, pcm_frames=%d)\n", + frame_count, msg.header.length, frames_written); + } + } + + break; + } + + case IPC_MSG_TYPE_CONFIG: + printf("Received basic audio config\n"); + send_ack(client_sock); + break; + + case IPC_MSG_TYPE_OPUS_CONFIG: + handle_opus_config(msg.data, msg.header.length); + send_ack(client_sock); + break; + + case IPC_MSG_TYPE_STOP: + printf("Received stop message\n"); + ipc_free_message(&msg); + g_running = 0; + return 0; + + case IPC_MSG_TYPE_HEARTBEAT: + send_ack(client_sock); + break; + + default: + printf("Warning: Unknown message type: %u\n", msg.header.type); + break; + } + + ipc_free_message(&msg); + } + + printf("Audio input loop ended after %d frames\n", frame_count); + return 0; +} + +// ============================================================================ +// MAIN +// ============================================================================ + +int main(int argc, char **argv) { + printf("JetKVM Audio Input Server Starting...\n"); + + // Setup signal handlers + setup_signal_handlers(); + + // Load configuration from environment + audio_config_t config; + load_audio_config(&config); + + // Set trace logging + set_trace_logging(config.trace_logging); + + // Apply audio constants to audio.c + update_audio_constants( + config.opus_bitrate, + config.opus_complexity, + 1, // vbr + 1, // vbr_constraint + -1000, // signal_type (auto) + 1103, // bandwidth (wideband) + 0, // dtx + 16, // lsb_depth + config.sample_rate, + config.channels, + config.frame_size, + 1500, // max_packet_size + 1000, // sleep_microseconds + 5, // max_attempts + 500000 // max_backoff_us + ); + + // Initialize audio playback (Opus decoder + ALSA playback) + printf("Initializing audio playback on device: %s\n", config.alsa_device); + if (jetkvm_audio_playback_init() != 0) { + fprintf(stderr, "Failed to initialize audio playback\n"); + return 1; + } + + // Create IPC server + int server_sock = ipc_create_server(IPC_SOCKET_INPUT); + if (server_sock < 0) { + fprintf(stderr, "Failed to create IPC server\n"); + jetkvm_audio_playback_close(); + return 1; + } + + // Main connection loop + while (g_running) { + printf("Waiting for client connection...\n"); + + int client_sock = ipc_accept_client(server_sock); + if (client_sock < 0) { + if (g_running) { + fprintf(stderr, "Failed to accept client, retrying...\n"); + sleep(1); + continue; + } + break; // Shutting down + } + + // Run audio loop with this client + run_audio_loop(client_sock); + + // Close client connection + close(client_sock); + + if (g_running) { + printf("Client disconnected, waiting for next client...\n"); + } + } + + // Cleanup + printf("Shutting down audio input server...\n"); + close(server_sock); + unlink(IPC_SOCKET_INPUT); + jetkvm_audio_playback_close(); + + printf("Audio input server exited cleanly\n"); + return 0; +} diff --git a/internal/audio/c/jetkvm_audio_output.c b/internal/audio/c/jetkvm_audio_output.c new file mode 100644 index 00000000..1863961b --- /dev/null +++ b/internal/audio/c/jetkvm_audio_output.c @@ -0,0 +1,389 @@ +/* + * JetKVM Audio Output Server + * + * Standalone C binary for audio output path: + * ALSA Capture (TC358743 HDMI) → Opus Encode → IPC Send → Go Process → WebRTC → Browser + * + * This replaces the Go subprocess that was running with --audio-output-server flag. + */ + +#include "ipc_protocol.h" +#include +#include +#include +#include +#include +#include +#include + +// Forward declarations from audio.c +extern int jetkvm_audio_capture_init(void); +extern void jetkvm_audio_capture_close(void); +extern int jetkvm_audio_read_encode(void *opus_buf); +extern void update_audio_constants(int bitrate, int complexity, int vbr, int vbr_constraint, + int signal_type, int bandwidth, int dtx, int lsb_depth, + int sr, int ch, int fs, int max_pkt, + int sleep_us, int max_attempts, int max_backoff); +extern void set_trace_logging(int enabled); +extern int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_constraint, + int signal_type, int bandwidth, int dtx); + +// ============================================================================ +// GLOBAL STATE +// ============================================================================ + +static volatile sig_atomic_t g_running = 1; // Shutdown flag + +// Audio configuration (from environment variables) +typedef struct { + const char *alsa_device; // ALSA capture device (default: "hw:0,0") + int opus_bitrate; // Opus bitrate (default: 96000) + int opus_complexity; // Opus complexity 0-10 (default: 1) + int opus_vbr; // VBR enabled (default: 1) + int opus_vbr_constraint; // VBR constraint (default: 1) + int opus_signal_type; // Signal type (default: -1000 = auto) + int opus_bandwidth; // Bandwidth (default: 1103 = wideband) + int opus_dtx; // DTX enabled (default: 0) + int opus_lsb_depth; // LSB depth (default: 16) + int sample_rate; // Sample rate (default: 48000) + int channels; // Channels (default: 2) + int frame_size; // Frame size in samples (default: 960) + int trace_logging; // Enable trace logging (default: 0) +} audio_config_t; + +// ============================================================================ +// SIGNAL HANDLERS +// ============================================================================ + +static void signal_handler(int signo) { + if (signo == SIGTERM || signo == SIGINT) { + printf("Audio output server: Received signal %d, shutting down...\n", signo); + g_running = 0; + } +} + +static void setup_signal_handlers(void) { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = signal_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + sigaction(SIGTERM, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + + // Ignore SIGPIPE (write to closed socket should return error, not crash) + signal(SIGPIPE, SIG_IGN); +} + +// ============================================================================ +// CONFIGURATION PARSING +// ============================================================================ + +static int parse_env_int(const char *name, int default_value) { + const char *str = getenv(name); + if (str == NULL || str[0] == '\0') { + return default_value; + } + return atoi(str); +} + +static const char* parse_env_string(const char *name, const char *default_value) { + const char *str = getenv(name); + if (str == NULL || str[0] == '\0') { + return default_value; + } + return str; +} + +static int is_trace_enabled(void) { + const char *pion_trace = getenv("PION_LOG_TRACE"); + if (pion_trace == NULL) { + return 0; + } + + // Check if "audio" is in comma-separated list + if (strstr(pion_trace, "audio") != NULL) { + return 1; + } + + return 0; +} + +static void load_audio_config(audio_config_t *config) { + // ALSA device configuration + config->alsa_device = parse_env_string("ALSA_CAPTURE_DEVICE", "hw:0,0"); + + // Opus encoder configuration + config->opus_bitrate = parse_env_int("OPUS_BITRATE", 96000); + config->opus_complexity = parse_env_int("OPUS_COMPLEXITY", 1); + config->opus_vbr = parse_env_int("OPUS_VBR", 1); + config->opus_vbr_constraint = parse_env_int("OPUS_VBR_CONSTRAINT", 1); + config->opus_signal_type = parse_env_int("OPUS_SIGNAL_TYPE", -1000); + config->opus_bandwidth = parse_env_int("OPUS_BANDWIDTH", 1103); + config->opus_dtx = parse_env_int("OPUS_DTX", 0); + config->opus_lsb_depth = parse_env_int("OPUS_LSB_DEPTH", 16); + + // Audio format + config->sample_rate = parse_env_int("AUDIO_SAMPLE_RATE", 48000); + config->channels = parse_env_int("AUDIO_CHANNELS", 2); + config->frame_size = parse_env_int("AUDIO_FRAME_SIZE", 960); + + // Logging + config->trace_logging = is_trace_enabled(); + + // Log configuration + printf("Audio Output Server Configuration:\n"); + printf(" ALSA Device: %s\n", config->alsa_device); + printf(" Sample Rate: %d Hz\n", config->sample_rate); + printf(" Channels: %d\n", config->channels); + printf(" Frame Size: %d samples\n", config->frame_size); + printf(" Opus Bitrate: %d bps\n", config->opus_bitrate); + printf(" Opus Complexity: %d\n", config->opus_complexity); + printf(" Trace Logging: %s\n", config->trace_logging ? "enabled" : "disabled"); +} + +// ============================================================================ +// MESSAGE HANDLING +// ============================================================================ + +/** + * Handle OpusConfig message: update encoder parameters dynamically. + * Returns 0 on success, -1 on error. + */ +static int handle_opus_config(const uint8_t *data, uint32_t length) { + ipc_opus_config_t config; + + if (ipc_parse_opus_config(data, length, &config) != 0) { + fprintf(stderr, "Failed to parse Opus config\n"); + return -1; + } + + printf("Received Opus config: bitrate=%u, complexity=%u, vbr=%u\n", + config.bitrate, config.complexity, config.vbr); + + // Apply configuration to encoder + // Note: Signal type needs special handling for negative values + int signal_type = (int)(int32_t)config.signal_type; // Treat as signed + + int result = update_opus_encoder_params( + config.bitrate, + config.complexity, + config.vbr, + config.vbr, // Use VBR value for constraint (simplified) + signal_type, + config.bandwidth, + config.dtx + ); + + if (result != 0) { + fprintf(stderr, "Warning: Failed to apply some Opus encoder parameters\n"); + // Continue anyway - encoder may not be initialized yet + } + + return 0; +} + +/** + * Handle incoming IPC messages from client (non-blocking). + * Returns 0 on success, -1 on error. + */ +static int handle_incoming_messages(int client_sock) { + // Set non-blocking mode for client socket + int flags = fcntl(client_sock, F_GETFL, 0); + fcntl(client_sock, F_SETFL, flags | O_NONBLOCK); + + ipc_message_t msg; + + // Try to read message (non-blocking) + int result = ipc_read_message(client_sock, &msg, IPC_MAGIC_OUTPUT); + + // Restore blocking mode + fcntl(client_sock, F_SETFL, flags); + + if (result != 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return 0; // No message available, not an error + } + return -1; // Connection error + } + + // Process message based on type + switch (msg.header.type) { + case IPC_MSG_TYPE_OPUS_CONFIG: + handle_opus_config(msg.data, msg.header.length); + break; + + case IPC_MSG_TYPE_STOP: + printf("Received stop message\n"); + g_running = 0; + break; + + case IPC_MSG_TYPE_HEARTBEAT: + // Informational only, no response needed + break; + + default: + printf("Warning: Unknown message type: %u\n", msg.header.type); + break; + } + + ipc_free_message(&msg); + return 0; +} + +// ============================================================================ +// MAIN LOOP +// ============================================================================ + +/** + * Main audio capture and encode loop. + * Continuously reads from ALSA, encodes to Opus, sends via IPC. + */ +static int run_audio_loop(int client_sock) { + uint8_t opus_buffer[IPC_MAX_FRAME_SIZE]; + int consecutive_errors = 0; + const int max_consecutive_errors = 10; + int frame_count = 0; + + printf("Starting audio output loop...\n"); + + while (g_running) { + // Handle any incoming configuration messages (non-blocking) + if (handle_incoming_messages(client_sock) < 0) { + fprintf(stderr, "Client disconnected, waiting for reconnection...\n"); + break; // Client disconnected + } + + // Capture audio and encode to Opus + int opus_size = jetkvm_audio_read_encode(opus_buffer); + + if (opus_size < 0) { + consecutive_errors++; + fprintf(stderr, "Audio read/encode failed (error %d/%d)\n", + consecutive_errors, max_consecutive_errors); + + if (consecutive_errors >= max_consecutive_errors) { + fprintf(stderr, "Too many consecutive errors, giving up\n"); + return -1; + } + + usleep(10000); // 10ms backoff + continue; + } + + if (opus_size == 0) { + // No data available (non-blocking mode or empty frame) + usleep(1000); // 1ms sleep + continue; + } + + // Reset error counter on success + consecutive_errors = 0; + frame_count++; + + // Send Opus frame via IPC + if (ipc_write_message(client_sock, IPC_MAGIC_OUTPUT, IPC_MSG_TYPE_OPUS_FRAME, + opus_buffer, opus_size) != 0) { + fprintf(stderr, "Failed to send frame to client\n"); + break; // Client disconnected + } + + // Trace logging (periodic) + if (frame_count % 1000 == 1) { + printf("Sent frame %d (size=%d bytes)\n", frame_count, opus_size); + } + + // Small delay to prevent busy-waiting (frame rate ~50 FPS @ 48kHz/960) + usleep(1000); // 1ms + } + + printf("Audio output loop ended after %d frames\n", frame_count); + return 0; +} + +// ============================================================================ +// MAIN +// ============================================================================ + +int main(int argc, char **argv) { + printf("JetKVM Audio Output Server Starting...\n"); + + // Setup signal handlers + setup_signal_handlers(); + + // Load configuration from environment + audio_config_t config; + load_audio_config(&config); + + // Set trace logging + set_trace_logging(config.trace_logging); + + // Apply audio constants to audio.c + update_audio_constants( + config.opus_bitrate, + config.opus_complexity, + config.opus_vbr, + config.opus_vbr_constraint, + config.opus_signal_type, + config.opus_bandwidth, + config.opus_dtx, + config.opus_lsb_depth, + config.sample_rate, + config.channels, + config.frame_size, + 1500, // max_packet_size + 1000, // sleep_microseconds + 5, // max_attempts + 500000 // max_backoff_us + ); + + // Initialize audio capture + printf("Initializing audio capture on device: %s\n", config.alsa_device); + if (jetkvm_audio_capture_init() != 0) { + fprintf(stderr, "Failed to initialize audio capture\n"); + return 1; + } + + // Create IPC server + int server_sock = ipc_create_server(IPC_SOCKET_OUTPUT); + if (server_sock < 0) { + fprintf(stderr, "Failed to create IPC server\n"); + jetkvm_audio_capture_close(); + return 1; + } + + // Main connection loop + while (g_running) { + printf("Waiting for client connection...\n"); + + int client_sock = ipc_accept_client(server_sock); + if (client_sock < 0) { + if (g_running) { + fprintf(stderr, "Failed to accept client, retrying...\n"); + sleep(1); + continue; + } + break; // Shutting down + } + + // Run audio loop with this client + run_audio_loop(client_sock); + + // Close client connection + close(client_sock); + + if (g_running) { + printf("Client disconnected, waiting for next client...\n"); + } + } + + // Cleanup + printf("Shutting down audio output server...\n"); + close(server_sock); + unlink(IPC_SOCKET_OUTPUT); + jetkvm_audio_capture_close(); + + printf("Audio output server exited cleanly\n"); + return 0; +} diff --git a/internal/audio/embed.go b/internal/audio/embed.go new file mode 100644 index 00000000..0e926526 --- /dev/null +++ b/internal/audio/embed.go @@ -0,0 +1,123 @@ +//go:build cgo +// +build cgo + +package audio + +import ( + _ "embed" + "fmt" + "os" +) + +// Embedded C audio binaries (built during compilation) +// +//go:embed bin/jetkvm_audio_output +var audioOutputBinary []byte + +//go:embed bin/jetkvm_audio_input +var audioInputBinary []byte + +const ( + audioBinDir = "/userdata/jetkvm/bin" + audioOutputBinPath = audioBinDir + "/jetkvm_audio_output" + audioInputBinPath = audioBinDir + "/jetkvm_audio_input" + binaryFileMode = 0755 // rwxr-xr-x +) + +// ExtractEmbeddedBinaries extracts the embedded C audio binaries to disk +// This should be called during application startup before audio supervisors are started +func ExtractEmbeddedBinaries() error { + // Create bin directory if it doesn't exist + if err := os.MkdirAll(audioBinDir, 0755); err != nil { + return fmt.Errorf("failed to create audio bin directory: %w", err) + } + + // Extract audio output binary + if err := extractBinary(audioOutputBinary, audioOutputBinPath); err != nil { + return fmt.Errorf("failed to extract audio output binary: %w", err) + } + + // Extract audio input binary + if err := extractBinary(audioInputBinary, audioInputBinPath); err != nil { + return fmt.Errorf("failed to extract audio input binary: %w", err) + } + + return nil +} + +// extractBinary writes embedded binary data to disk with executable permissions +func extractBinary(data []byte, path string) error { + // Check if binary already exists and is valid + if info, err := os.Stat(path); err == nil { + // File exists - check if size matches + if info.Size() == int64(len(data)) { + // Binary already extracted and matches embedded version + return nil + } + // Size mismatch - need to update + } + + // Write to temporary file first for atomic replacement + tmpPath := path + ".tmp" + if err := os.WriteFile(tmpPath, data, binaryFileMode); err != nil { + return fmt.Errorf("failed to write binary to %s: %w", tmpPath, err) + } + + // Atomically rename to final path + if err := os.Rename(tmpPath, path); err != nil { + os.Remove(tmpPath) // Clean up on error + return fmt.Errorf("failed to rename binary to %s: %w", path, err) + } + + return nil +} + +// GetAudioOutputBinaryPath returns the path to the audio output binary +func GetAudioOutputBinaryPath() string { + return audioOutputBinPath +} + +// GetAudioInputBinaryPath returns the path to the audio input binary +func GetAudioInputBinaryPath() string { + return audioInputBinPath +} + +// CleanupBinaries removes extracted audio binaries (useful for cleanup/testing) +func CleanupBinaries() error { + var errs []error + + if err := os.Remove(audioOutputBinPath); err != nil && !os.IsNotExist(err) { + errs = append(errs, fmt.Errorf("failed to remove audio output binary: %w", err)) + } + + if err := os.Remove(audioInputBinPath); err != nil && !os.IsNotExist(err) { + errs = append(errs, fmt.Errorf("failed to remove audio input binary: %w", err)) + } + + // Try to remove directory (will only succeed if empty) + os.Remove(audioBinDir) + + if len(errs) > 0 { + return fmt.Errorf("cleanup errors: %v", errs) + } + + return nil +} + +// GetBinaryInfo returns information about embedded binaries +func GetBinaryInfo() map[string]int { + return map[string]int{ + "audio_output_size": len(audioOutputBinary), + "audio_input_size": len(audioInputBinary), + } +} + +// init ensures binaries are extracted when package is imported +func init() { + // Extract binaries on package initialization + // This ensures binaries are available before supervisors start + if err := ExtractEmbeddedBinaries(); err != nil { + // Log error but don't panic - let caller handle initialization failure + fmt.Fprintf(os.Stderr, "Warning: Failed to extract embedded audio binaries: %v\n", err) + } +} diff --git a/internal/audio/input_supervisor.go b/internal/audio/input_supervisor.go index e39e6a16..4f356f15 100644 --- a/internal/audio/input_supervisor.go +++ b/internal/audio/input_supervisor.go @@ -7,7 +7,6 @@ import ( "fmt" "os" "os/exec" - "path/filepath" "strconv" "strings" "sync/atomic" @@ -38,14 +37,15 @@ func (ais *AudioInputSupervisor) SetOpusConfig(bitrate, complexity, vbr, signalT ais.mutex.Lock() defer ais.mutex.Unlock() - // Store OPUS parameters as environment variables + // Store OPUS parameters as environment variables for C binary ais.opusEnv = []string{ - "JETKVM_OPUS_BITRATE=" + strconv.Itoa(bitrate), - "JETKVM_OPUS_COMPLEXITY=" + strconv.Itoa(complexity), - "JETKVM_OPUS_VBR=" + strconv.Itoa(vbr), - "JETKVM_OPUS_SIGNAL_TYPE=" + strconv.Itoa(signalType), - "JETKVM_OPUS_BANDWIDTH=" + strconv.Itoa(bandwidth), - "JETKVM_OPUS_DTX=" + strconv.Itoa(dtx), + "OPUS_BITRATE=" + strconv.Itoa(bitrate), + "OPUS_COMPLEXITY=" + strconv.Itoa(complexity), + "OPUS_VBR=" + strconv.Itoa(vbr), + "OPUS_SIGNAL_TYPE=" + strconv.Itoa(signalType), + "OPUS_BANDWIDTH=" + strconv.Itoa(bandwidth), + "OPUS_DTX=" + strconv.Itoa(dtx), + "ALSA_PLAYBACK_DEVICE=hw:1,0", // USB Gadget audio playback } } @@ -100,25 +100,19 @@ func (ais *AudioInputSupervisor) supervisionLoop() { // startProcess starts the audio input server process func (ais *AudioInputSupervisor) startProcess() error { - execPath, err := os.Executable() - if err != nil { - return fmt.Errorf("failed to get executable path: %w", err) - } + // Use embedded C binary path + binaryPath := GetAudioInputBinaryPath() ais.mutex.Lock() defer ais.mutex.Unlock() - // Build command arguments (only subprocess flag) - args := []string{"--audio-input-server"} - - // Create new command - ais.cmd = exec.CommandContext(ais.ctx, execPath, args...) + // Create new command (no args needed for C binary) + ais.cmd = exec.CommandContext(ais.ctx, binaryPath) ais.cmd.Stdout = os.Stdout ais.cmd.Stderr = os.Stderr - // Set environment variables for IPC and OPUS configuration - env := append(os.Environ(), "JETKVM_AUDIO_INPUT_IPC=true") // Enable IPC mode - env = append(env, ais.opusEnv...) // Add OPUS configuration + // Set environment variables for OPUS configuration + env := append(os.Environ(), ais.opusEnv...) // Pass logging environment variables directly to subprocess // The subprocess will inherit all PION_LOG_* variables from os.Environ() @@ -137,7 +131,7 @@ func (ais *AudioInputSupervisor) startProcess() error { } ais.processPID = ais.cmd.Process.Pid - ais.logger.Info().Int("pid", ais.processPID).Strs("args", args).Strs("opus_env", ais.opusEnv).Msg("audio input server process started") + ais.logger.Info().Int("pid", ais.processPID).Str("binary", binaryPath).Strs("opus_env", ais.opusEnv).Msg("audio input server process started") // Connect client to the server synchronously to avoid race condition ais.connectClient() @@ -260,15 +254,10 @@ func (ais *AudioInputSupervisor) SendOpusConfig(config UnifiedIPCOpusConfig) err // findExistingAudioInputProcess checks if there's already an audio input server process running func (ais *AudioInputSupervisor) findExistingAudioInputProcess() (int, error) { - // Get current executable path - execPath, err := os.Executable() - if err != nil { - return 0, fmt.Errorf("failed to get executable path: %w", err) - } + // Look for the C binary name + binaryName := "jetkvm_audio_input" - execName := filepath.Base(execPath) - - // Use ps to find processes with our executable name and audio-input-server argument + // Use ps to find processes with C binary name cmd := exec.Command("ps", "aux") output, err := cmd.Output() if err != nil { @@ -278,7 +267,7 @@ func (ais *AudioInputSupervisor) findExistingAudioInputProcess() (int, error) { // Parse ps output to find audio input server processes lines := strings.Split(string(output), "\n") for _, line := range lines { - if strings.Contains(line, execName) && strings.Contains(line, "--audio-input-server") { + if strings.Contains(line, binaryName) { // Extract PID from ps output (second column) fields := strings.Fields(line) if len(fields) >= 2 { diff --git a/internal/audio/output_supervisor.go b/internal/audio/output_supervisor.go index fa763aa1..310c07fe 100644 --- a/internal/audio/output_supervisor.go +++ b/internal/audio/output_supervisor.go @@ -77,14 +77,15 @@ func (s *AudioOutputSupervisor) SetOpusConfig(bitrate, complexity, vbr, signalTy s.mutex.Lock() defer s.mutex.Unlock() - // Store OPUS parameters as environment variables + // Store OPUS parameters as environment variables for C binary s.opusEnv = []string{ - "JETKVM_OPUS_BITRATE=" + strconv.Itoa(bitrate), - "JETKVM_OPUS_COMPLEXITY=" + strconv.Itoa(complexity), - "JETKVM_OPUS_VBR=" + strconv.Itoa(vbr), - "JETKVM_OPUS_SIGNAL_TYPE=" + strconv.Itoa(signalType), - "JETKVM_OPUS_BANDWIDTH=" + strconv.Itoa(bandwidth), - "JETKVM_OPUS_DTX=" + strconv.Itoa(dtx), + "OPUS_BITRATE=" + strconv.Itoa(bitrate), + "OPUS_COMPLEXITY=" + strconv.Itoa(complexity), + "OPUS_VBR=" + strconv.Itoa(vbr), + "OPUS_SIGNAL_TYPE=" + strconv.Itoa(signalType), + "OPUS_BANDWIDTH=" + strconv.Itoa(bandwidth), + "OPUS_DTX=" + strconv.Itoa(dtx), + "ALSA_CAPTURE_DEVICE=hw:0,0", // TC358743 HDMI audio capture } } @@ -183,19 +184,14 @@ func (s *AudioOutputSupervisor) supervisionLoop() { // startProcess starts the audio server process func (s *AudioOutputSupervisor) startProcess() error { - execPath, err := os.Executable() - if err != nil { - return fmt.Errorf("failed to get executable path: %w", err) - } + // Use embedded C binary path + binaryPath := GetAudioOutputBinaryPath() s.mutex.Lock() defer s.mutex.Unlock() - // Build command arguments (only subprocess flag) - args := []string{"--audio-output-server"} - - // Create new command - s.cmd = exec.CommandContext(s.ctx, execPath, args...) + // Create new command (no args needed for C binary) + s.cmd = exec.CommandContext(s.ctx, binaryPath) s.cmd.Stdout = os.Stdout s.cmd.Stderr = os.Stderr @@ -214,7 +210,7 @@ func (s *AudioOutputSupervisor) startProcess() error { } s.processPID = s.cmd.Process.Pid - s.logger.Info().Int("pid", s.processPID).Strs("args", args).Strs("opus_env", s.opusEnv).Msg("audio server process started") + s.logger.Info().Int("pid", s.processPID).Str("binary", binaryPath).Strs("opus_env", s.opusEnv).Msg("audio server process started") // Add process to monitoring diff --git a/scripts/dev_deploy.sh b/scripts/dev_deploy.sh index 8feb69c1..2d24fb9e 100755 --- a/scripts/dev_deploy.sh +++ b/scripts/dev_deploy.sh @@ -16,6 +16,7 @@ show_help() { echo " --run-go-tests-only Run go tests and exit" echo " --skip-ui-build Skip frontend/UI build" echo " --skip-native-build Skip native build" + echo " --skip-audio-binaries Skip audio binaries build if they exist" echo " --disable-docker Disable docker build (auto-detected if Docker unavailable)" echo " -i, --install Build for release and install the app" echo " --help Display this help message" @@ -32,6 +33,7 @@ REMOTE_PATH="/userdata/jetkvm/bin" SKIP_UI_BUILD=false SKIP_UI_BUILD_RELEASE=0 SKIP_NATIVE_BUILD=0 +SKIP_AUDIO_BINARIES=0 RESET_USB_HID_DEVICE=false LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc,audio}" RUN_GO_TESTS=false @@ -60,6 +62,10 @@ while [[ $# -gt 0 ]]; do SKIP_NATIVE_BUILD=1 shift ;; + --skip-audio-binaries) + SKIP_AUDIO_BINARIES=1 + shift + ;; --reset-usb-hid) RESET_USB_HID_DEVICE=true shift @@ -148,10 +154,13 @@ if [[ "$SKIP_UI_BUILD" = true && ! -f "static/index.html" ]]; then SKIP_UI_BUILD=false fi -if [[ "$SKIP_UI_BUILD" = false && "$JETKVM_INSIDE_DOCKER" != 1 ]]; then +if [[ "$SKIP_UI_BUILD" = false && "$JETKVM_INSIDE_DOCKER" != 1 ]]; then msg_info "▶ Building frontend" make frontend SKIP_UI_BUILD=0 SKIP_UI_BUILD_RELEASE=1 +elif [[ "$SKIP_UI_BUILD" = true ]]; then + # User explicitly requested to skip UI build and static files exist + SKIP_UI_BUILD_RELEASE=1 fi if [[ "$SKIP_UI_BUILD_RELEASE" = 0 && "$BUILD_IN_DOCKER" = true ]]; then @@ -204,16 +213,16 @@ fi if [ "$INSTALL_APP" = true ] then msg_info "▶ Building release binary" - do_make build_release SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} - + do_make build_release SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} SKIP_AUDIO_BINARIES_IF_EXISTS=${SKIP_AUDIO_BINARIES} + # Copy the binary to the remote host as if we were the OTA updater. ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app - + # Reboot the device, the new app will be deployed by the startup process. ssh "${REMOTE_USER}@${REMOTE_HOST}" "reboot" else msg_info "▶ Building development binary" - do_make build_dev SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} + do_make build_dev SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} SKIP_AUDIO_BINARIES_IF_EXISTS=${SKIP_AUDIO_BINARIES} # Kill any existing instances of the application ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"