[WIP] Updates: use native C binaries for audio

This commit is contained in:
Alex P 2025-10-01 20:13:13 +03:00
parent 74f73d9496
commit 6ccd9fdf19
12 changed files with 1491 additions and 57 deletions

View File

@ -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

View File

@ -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

1
.gitignore vendored
View File

@ -19,3 +19,4 @@ node_modules
# generated during the build process
#internal/native/include
#internal/native/lib
internal/audio/bin/

View File

@ -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 \

View File

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/uio.h>
#include <endian.h>
// ============================================================================
// 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;
}

View File

@ -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 <stdint.h>
#include <sys/types.h>
// ============================================================================
// 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

View File

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>
// 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;
}

View File

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>
// 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;
}

123
internal/audio/embed.go Normal file
View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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
@ -152,6 +158,9 @@ 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,7 +213,7 @@ 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
@ -213,7 +222,7 @@ then
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"