mirror of https://github.com/jetkvm/kvm.git
[WIP] Updates: use native C binaries for audio
This commit is contained in:
parent
74f73d9496
commit
6ccd9fdf19
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -19,3 +19,4 @@ node_modules
|
|||
# generated during the build process
|
||||
#internal/native/include
|
||||
#internal/native/lib
|
||||
internal/audio/bin/
|
||||
|
|
44
Makefile
44
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 \
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue