diff --git a/.devcontainer/install-deps.sh b/.devcontainer/install-deps.sh index e2ff43e6..a412c21e 100755 --- a/.devcontainer/install-deps.sh +++ b/.devcontainer/install-deps.sh @@ -5,7 +5,7 @@ function sudo() { if [ "$UID" -eq 0 ]; then "$@" else - ${SUDO_PATH} "$@" + ${SUDO_PATH} -E "$@" fi } @@ -17,7 +17,7 @@ sudo apt-get install -y --no-install-recommends \ iputils-ping \ build-essential \ device-tree-compiler \ - gperf g++-multilib gcc-multilib \ + gperf \ libnl-3-dev libdbus-1-dev libelf-dev libmpc-dev dwarves \ bc openssl flex bison libssl-dev python3 python-is-python3 texinfo kmod cmake \ wget zstd \ @@ -31,7 +31,35 @@ pushd "${BUILDKIT_TMPDIR}" > /dev/null wget https://github.com/jetkvm/rv1106-system/releases/download/${BUILDKIT_VERSION}/buildkit.tar.zst && \ sudo mkdir -p /opt/jetkvm-native-buildkit && \ - sudo tar --use-compress-program="unzstd --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \ + sudo tar --use-compress-program="zstd -d --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \ rm buildkit.tar.zst popd + +# Install audio dependencies (ALSA and Opus) for JetKVM +echo "Installing JetKVM audio dependencies..." +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +PROJECT_ROOT="$(dirname "${SCRIPT_DIR}")" +AUDIO_DEPS_SCRIPT="${PROJECT_ROOT}/install_audio_deps.sh" + +if [ -f "${AUDIO_DEPS_SCRIPT}" ]; then + echo "Running audio dependencies installation..." + # 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" + # 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 + fi +else + echo "Warning: Audio dependencies script not found at ${AUDIO_DEPS_SCRIPT}" + echo "Skipping audio dependencies installation." +fi rm -rf "${BUILDKIT_TMPDIR}" diff --git a/.devcontainer/install_audio_deps.sh b/.devcontainer/install_audio_deps.sh new file mode 100755 index 00000000..8d369db4 --- /dev/null +++ b/.devcontainer/install_audio_deps.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# .devcontainer/install_audio_deps.sh +# 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}" + +AUDIO_LIBS_DIR="/opt/jetkvm-audio-libs" +BUILDKIT_PATH="/opt/jetkvm-native-buildkit" +BUILDKIT_FLAVOR="arm-rockchip830-linux-uclibcgnueabihf" +CROSS_PREFIX="$BUILDKIT_PATH/bin/$BUILDKIT_FLAVOR" + +# 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 +[ -f alsa-lib-${ALSA_VERSION}.tar.bz2 ] || wget -N https://www.alsa-project.org/files/pub/lib/alsa-lib-${ALSA_VERSION}.tar.bz2 +[ -f opus-${OPUS_VERSION}.tar.gz ] || wget -N https://downloads.xiph.org/releases/opus/opus-${OPUS_VERSION}.tar.gz + +# Extract +[ -d alsa-lib-${ALSA_VERSION} ] || tar xf alsa-lib-${ALSA_VERSION}.tar.bz2 +[ -d opus-${OPUS_VERSION} ] || tar xf opus-${OPUS_VERSION}.tar.gz + +# Optimization flags for ARM Cortex-A7 with NEON (simplified to avoid FD_SETSIZE issues) +OPTIM_CFLAGS="-O2 -mfpu=neon -mtune=cortex-a7 -mfloat-abi=hard" + +export CC="${CROSS_PREFIX}-gcc" +export CFLAGS="$OPTIM_CFLAGS" +export CXXFLAGS="$OPTIM_CFLAGS" + +# Build ALSA +cd alsa-lib-${ALSA_VERSION} +if [ ! -f .built ]; then + chown -R $(whoami):$(whoami) . + # Use minimal ALSA configuration to avoid FD_SETSIZE issues in devcontainer + CFLAGS="$OPTIM_CFLAGS" ./configure --host $BUILDKIT_FLAVOR \ + --enable-static=yes --enable-shared=no \ + --with-pcm-plugins=rate,linear \ + --disable-seq --disable-rawmidi --disable-ucm \ + --disable-python --disable-old-symbols \ + --disable-topology --disable-hwdep --disable-mixer \ + --disable-alisp --disable-aload --disable-resmgr + make -j$(nproc) + touch .built +fi +cd .. + +# Build Opus +cd opus-${OPUS_VERSION} +if [ ! -f .built ]; then + chown -R $(whoami):$(whoami) . + CFLAGS="$OPTIM_CFLAGS" ./configure --host $BUILDKIT_FLAVOR --enable-static=yes --enable-shared=no --enable-fixed-point + make -j$(nproc) + touch .built +fi +cd .. + +echo "ALSA and Opus built in $AUDIO_LIBS_DIR" diff --git a/.gitignore b/.gitignore index 1691153c..4969e3e4 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ node_modules #internal/native/include #internal/native/lib -ui/reports \ No newline at end of file +ui/reports diff --git a/Dockerfile.build b/Dockerfile.build index db433b2d..b588da1a 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -6,6 +6,8 @@ ENV GOPATH=/go ENV PATH=$GOPATH/bin:/usr/local/go/bin:$PATH COPY install-deps.sh /install-deps.sh +COPY install_audio_deps.sh /install_audio_deps.sh + RUN /install-deps.sh # Create build directory @@ -21,4 +23,4 @@ RUN go mod download && go mod verify COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh -ENTRYPOINT [ "/entrypoint.sh" ] \ No newline at end of file +ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/Makefile b/Makefile index e519a75a..fc1acb7b 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,52 @@ -BRANCH := $(shell git rev-parse --abbrev-ref HEAD) -BUILDDATE := $(shell date -u +%FT%T%z) -BUILDTS := $(shell date -u +%s) -REVISION := $(shell git rev-parse HEAD) +# Build ALSA and Opus static libs for ARM in /opt/jetkvm-audio-libs +build_audio_deps: + bash .devcontainer/install_audio_deps.sh $(ALSA_VERSION) $(OPUS_VERSION) + +# Prepare everything needed for local development (toolchain + audio deps + Go tools) +dev_env: build_audio_deps + $(CLEAN_GO_CACHE) + @echo "Installing Go development tools..." + go install golang.org/x/tools/cmd/goimports@latest + @echo "Development environment ready." +JETKVM_HOME ?= $(HOME)/.jetkvm +BUILDKIT_PATH ?= /opt/jetkvm-native-buildkit +BUILDKIT_FLAVOR ?= arm-rockchip830-linux-uclibcgnueabihf +AUDIO_LIBS_DIR ?= /opt/jetkvm-audio-libs + +BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) +BUILDDATE ?= $(shell date -u +%FT%T%z) +BUILDTS ?= $(shell date -u +%s) +REVISION ?= $(shell git rev-parse HEAD) VERSION_DEV := 0.4.9-dev$(shell date +%Y%m%d%H%M) VERSION := 0.4.8 + +# Audio library versions +ALSA_VERSION ?= 1.2.14 +OPUS_VERSION ?= 1.5.2 + +# Set PKG_CONFIG_PATH globally for all targets that use CGO with audio libraries +export PKG_CONFIG_PATH := $(AUDIO_LIBS_DIR)/alsa-lib-$(ALSA_VERSION)/utils:$(AUDIO_LIBS_DIR)/opus-$(OPUS_VERSION) + +# Common command to clean Go cache with verbose output for all Go builds +CLEAN_GO_CACHE := @echo "Cleaning Go cache..."; go clean -cache -v + +# Optimization flags for ARM Cortex-A7 with NEON SIMD +OPTIM_CFLAGS := -O3 -mfpu=neon -mtune=cortex-a7 -mfloat-abi=hard -ftree-vectorize -ffast-math -funroll-loops -mvectorize-with-neon-quad -marm -D__ARM_NEON + +# Cross-compilation environment for ARM - exported globally +export GOOS := linux +export GOARCH := arm +export GOARM := 7 +export CC := $(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-gcc +export CGO_ENABLED := 1 +export CGO_CFLAGS := $(OPTIM_CFLAGS) -I$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/include -I$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/sysroot/usr/include +export CGO_LDFLAGS := -L$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/lib -L$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/sysroot/usr/lib -lrockit -lrockchip_mpp -lrga -lpthread -lm -ldl + +# Audio-specific flags (only used for audio C binaries, NOT for main Go app) +AUDIO_CFLAGS := $(CGO_CFLAGS) -I$(AUDIO_LIBS_DIR)/alsa-lib-$(ALSA_VERSION)/include -I$(AUDIO_LIBS_DIR)/opus-$(OPUS_VERSION)/include -I$(AUDIO_LIBS_DIR)/opus-$(OPUS_VERSION)/celt +AUDIO_LDFLAGS := $(AUDIO_LIBS_DIR)/alsa-lib-$(ALSA_VERSION)/src/.libs/libasound.a $(AUDIO_LIBS_DIR)/opus-$(OPUS_VERSION)/.libs/libopus.a -lm -ldl -lpthread + PROMETHEUS_TAG := github.com/prometheus/common/version KVM_PKG_NAME := github.com/jetkvm/kvm @@ -55,22 +97,26 @@ build_native: ./scripts/build_cgo.sh; \ fi -build_dev: build_native +build_dev: build_native build_audio_deps + $(CLEAN_GO_CACHE) @echo "Building..." - $(GO_CMD) build \ + go build \ -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \ $(GO_RELEASE_BUILD_ARGS) \ -o $(BIN_DIR)/jetkvm_app -v cmd/main.go build_test2json: + $(CLEAN_GO_CACHE) $(GO_CMD) build -o $(BIN_DIR)/test2json cmd/test2json build_gotestsum: + $(CLEAN_GO_CACHE) @echo "Building gotestsum..." $(GO_CMD) install gotest.tools/gotestsum@latest cp $(shell $(GO_CMD) env GOPATH)/bin/linux_arm/gotestsum $(BIN_DIR)/gotestsum -build_dev_test: build_test2json build_gotestsum +build_dev_test: build_audio_deps build_test2json build_gotestsum + $(CLEAN_GO_CACHE) # collect all directories that contain tests @echo "Building tests for devices ..." @rm -rf $(BIN_DIR)/tests && mkdir -p $(BIN_DIR)/tests @@ -80,7 +126,7 @@ build_dev_test: build_test2json build_gotestsum test_pkg_name=$$(echo $$test | sed 's/^.\///g'); \ test_pkg_full_name=$(KVM_PKG_NAME)/$$(echo $$test | sed 's/^.\///g'); \ test_filename=$$(echo $$test_pkg_name | sed 's/\//__/g')_test; \ - $(GO_CMD) test -v \ + go test -v \ -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \ $(GO_BUILD_ARGS) \ -c -o $(BIN_DIR)/tests/$$test_filename $$test; \ @@ -117,9 +163,10 @@ 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_release: frontend build_native build_audio_deps + $(CLEAN_GO_CACHE) @echo "Building release..." - $(GO_CMD) build \ + go build \ -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" \ $(GO_RELEASE_BUILD_ARGS) \ -o bin/jetkvm_app cmd/main.go @@ -133,4 +180,39 @@ release: @echo "Uploading release..." @shasum -a 256 bin/jetkvm_app | cut -d ' ' -f 1 > bin/jetkvm_app.sha256 rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION)/jetkvm_app - rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256 \ No newline at end of file + rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256 + +# Run both Go and UI linting +lint: lint-go lint-ui + @echo "All linting completed successfully!" + +# Run golangci-lint locally with the same configuration as CI +lint-go: build_audio_deps + @echo "Running golangci-lint..." + @mkdir -p static && touch static/.gitkeep + golangci-lint run --verbose + +# Run both Go and UI linting with auto-fix +lint-fix: lint-go-fix lint-ui-fix + @echo "All linting with auto-fix completed successfully!" + +# Run golangci-lint with auto-fix +lint-go-fix: build_audio_deps + @echo "Running golangci-lint with auto-fix..." + @mkdir -p static && touch static/.gitkeep + golangci-lint run --fix --verbose + +# Run UI linting locally (mirrors GitHub workflow ui-lint.yml) +lint-ui: + @echo "Running UI lint..." + @cd ui && npm ci + @cd ui && npm run lint + +# Run UI linting with auto-fix +lint-ui-fix: + @echo "Running UI lint with auto-fix..." + @cd ui && npm ci + @cd ui && npm run lint:fix + +# Legacy alias for UI linting (for backward compatibility) +ui-lint: lint-ui diff --git a/audio.go b/audio.go new file mode 100644 index 00000000..4d616261 --- /dev/null +++ b/audio.go @@ -0,0 +1,253 @@ +package kvm + +import ( + "io" + "sync" + "sync/atomic" + + "github.com/jetkvm/kvm/internal/audio" + "github.com/jetkvm/kvm/internal/logging" + "github.com/pion/webrtc/v4" + "github.com/rs/zerolog" +) + +var ( + audioMutex sync.Mutex + outputSource audio.AudioSource + inputSource audio.AudioSource + outputRelay *audio.OutputRelay + inputRelay *audio.InputRelay + audioInitialized bool + activeConnections atomic.Int32 + audioLogger zerolog.Logger + currentAudioTrack *webrtc.TrackLocalStaticSample + inputTrackHandling atomic.Bool + audioOutputEnabled atomic.Bool + audioInputEnabled atomic.Bool +) + +func initAudio() { + audioLogger = logging.GetDefaultLogger().With().Str("component", "audio-manager").Logger() + + audioOutputEnabled.Store(true) + audioInputEnabled.Store(true) + + audioLogger.Debug().Msg("Audio subsystem initialized") + audioInitialized = true +} + +// startAudio starts audio sources and relays (skips already running ones) +func startAudio() error { + audioMutex.Lock() + defer audioMutex.Unlock() + + if !audioInitialized { + audioLogger.Warn().Msg("Audio not initialized, skipping start") + return nil + } + + // Start output audio if not running and enabled + if outputSource == nil && audioOutputEnabled.Load() { + alsaDevice := "hw:1,0" // USB audio + + outputSource = audio.NewCgoOutputSource(alsaDevice) + + if currentAudioTrack != nil { + outputRelay = audio.NewOutputRelay(outputSource, currentAudioTrack) + if err := outputRelay.Start(); err != nil { + audioLogger.Error().Err(err).Msg("Failed to start audio output relay") + } + } + } + + // Start input audio if not running, USB audio enabled, and input enabled + ensureConfigLoaded() + if inputSource == nil && audioInputEnabled.Load() && config.UsbDevices != nil && config.UsbDevices.Audio { + alsaPlaybackDevice := "hw:1,0" // USB speakers + + // Create CGO audio source + inputSource = audio.NewCgoInputSource(alsaPlaybackDevice) + + inputRelay = audio.NewInputRelay(inputSource) + if err := inputRelay.Start(); err != nil { + audioLogger.Error().Err(err).Msg("Failed to start input relay") + } + } + + return nil +} + +// stopOutputLocked stops output audio (assumes mutex is held) +func stopOutputLocked() { + if outputRelay != nil { + outputRelay.Stop() + outputRelay = nil + } + if outputSource != nil { + outputSource.Disconnect() + outputSource = nil + } +} + +// stopInputLocked stops input audio (assumes mutex is held) +func stopInputLocked() { + if inputRelay != nil { + inputRelay.Stop() + inputRelay = nil + } + if inputSource != nil { + inputSource.Disconnect() + inputSource = nil + } +} + +// stopAudioLocked stops all audio (assumes mutex is held) +func stopAudioLocked() { + stopOutputLocked() + stopInputLocked() +} + +// stopAudio stops all audio +func stopAudio() { + audioMutex.Lock() + defer audioMutex.Unlock() + stopAudioLocked() +} + +func onWebRTCConnect() { + count := activeConnections.Add(1) + if count == 1 { + if err := startAudio(); err != nil { + audioLogger.Error().Err(err).Msg("Failed to start audio") + } + } +} + +func onWebRTCDisconnect() { + count := activeConnections.Add(-1) + if count == 0 { + // Stop audio immediately to release HDMI audio device which shares hardware with video device + stopAudio() + } +} + +func setAudioTrack(audioTrack *webrtc.TrackLocalStaticSample) { + audioMutex.Lock() + defer audioMutex.Unlock() + + currentAudioTrack = audioTrack + + if outputRelay != nil { + outputRelay.Stop() + outputRelay = nil + } + + if outputSource != nil { + outputRelay = audio.NewOutputRelay(outputSource, audioTrack) + if err := outputRelay.Start(); err != nil { + audioLogger.Error().Err(err).Msg("Failed to start output relay") + } + } +} + +func setPendingInputTrack(track *webrtc.TrackRemote) { + audioMutex.Lock() + defer audioMutex.Unlock() + + // Start input track handler only once per WebRTC session + if inputTrackHandling.CompareAndSwap(false, true) { + go handleInputTrackForSession(track) + } +} + +// SetAudioOutputEnabled enables or disables audio output +func SetAudioOutputEnabled(enabled bool) error { + if audioOutputEnabled.Swap(enabled) == enabled { + return nil // Already in desired state + } + + if enabled { + if activeConnections.Load() > 0 { + return startAudio() + } + } else { + audioMutex.Lock() + stopOutputLocked() + audioMutex.Unlock() + } + + return nil +} + +// SetAudioInputEnabled enables or disables audio input +func SetAudioInputEnabled(enabled bool) error { + if audioInputEnabled.Swap(enabled) == enabled { + return nil // Already in desired state + } + + if enabled { + if activeConnections.Load() > 0 { + return startAudio() + } + } else { + audioMutex.Lock() + stopInputLocked() + audioMutex.Unlock() + } + + return nil +} + +// handleInputTrackForSession runs for the entire WebRTC session lifetime +// It continuously reads from the track and sends to whatever relay is currently active +func handleInputTrackForSession(track *webrtc.TrackRemote) { + defer inputTrackHandling.Store(false) + + audioLogger.Debug(). + Str("codec", track.Codec().MimeType). + Str("track_id", track.ID()). + Msg("starting session-lifetime track handler") + + for { + // Read RTP packet (must always read to keep track alive) + rtpPacket, _, err := track.ReadRTP() + if err != nil { + if err == io.EOF { + audioLogger.Debug().Msg("audio track ended") + return + } + audioLogger.Warn().Err(err).Msg("failed to read RTP packet") + continue + } + + // Extract Opus payload + opusData := rtpPacket.Payload + if len(opusData) == 0 { + continue + } + + // Only send if input is enabled + if !audioInputEnabled.Load() { + continue // Drop frame but keep reading + } + + // Get source in single mutex operation (hot path optimization) + audioMutex.Lock() + source := inputSource + audioMutex.Unlock() + + if source == nil { + continue // No relay, drop frame but keep reading + } + + if !source.IsConnected() { + if err := source.Connect(); err != nil { + continue + } + } + + if err := source.WriteMessage(0, opusData); err != nil { + source.Disconnect() + } + } +} diff --git a/config.go b/config.go index 5a3e7dc8..d1d0f5b0 100644 --- a/config.go +++ b/config.go @@ -151,6 +151,7 @@ var ( RelativeMouse: true, Keyboard: true, MassStorage: true, + Audio: true, } ) diff --git a/internal/audio/c/audio.c b/internal/audio/c/audio.c new file mode 100644 index 00000000..b32b2e8b --- /dev/null +++ b/internal/audio/c/audio.c @@ -0,0 +1,728 @@ +/* + * JetKVM Audio Processing Module + * + * Bidirectional audio processing optimized for ARM NEON SIMD: +* TODO: Remove USB Gadget audio once new system image release is made available + * - OUTPUT PATH: TC358743 HDMI or USB Gadget audio → Client speakers + * Pipeline: ALSA hw:0,0 or hw:1,0 capture → Opus encode (128kbps, FEC enabled) + * + * - INPUT PATH: Client microphone → Device speakers + * Pipeline: Opus decode (with FEC) → ALSA hw:1,0 playback + * + * Key features: + * - ARM NEON SIMD optimization for all audio operations + * - Opus in-band FEC for packet loss resilience + * - S16_LE @ 48kHz stereo, 20ms frames (960 samples) + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ARM NEON SIMD support (always available on JetKVM's ARM Cortex-A7) +#include + +// RV1106 (Cortex-A7) has 64-byte cache lines +#define CACHE_LINE_SIZE 64 +#define SIMD_ALIGN __attribute__((aligned(16))) +#define CACHE_ALIGN __attribute__((aligned(CACHE_LINE_SIZE))) +#define SIMD_PREFETCH(addr, rw, locality) __builtin_prefetch(addr, rw, locality) + +// Compile-time trace logging - disabled for production (zero overhead) +#define TRACE_LOG(...) ((void)0) + +// ALSA device handles +static snd_pcm_t *pcm_capture_handle = NULL; // OUTPUT: TC358743 HDMI audio → client +static snd_pcm_t *pcm_playback_handle = NULL; // INPUT: Client microphone → device speakers + +// ALSA device names +static const char *alsa_capture_device = NULL; +static const char *alsa_playback_device = NULL; + +// Opus codec instances +static OpusEncoder *encoder = NULL; +static OpusDecoder *decoder = NULL; + +// Audio format (S16_LE @ 48kHz stereo) +static uint32_t sample_rate = 48000; +static uint8_t channels = 2; +static uint16_t frame_size = 960; // 20ms frames at 48kHz + +static uint32_t opus_bitrate = 128000; +static uint8_t opus_complexity = 5; // Higher complexity for better quality +static uint16_t max_packet_size = 1500; + +// Opus encoder constants (hardcoded for production) +#define OPUS_VBR 1 // VBR enabled +#define OPUS_VBR_CONSTRAINT 1 // Constrained VBR (prevents bitrate starvation at low volumes) +#define OPUS_SIGNAL_TYPE 3002 // OPUS_SIGNAL_MUSIC (better transient handling) +#define OPUS_BANDWIDTH 1104 // OPUS_BANDWIDTH_SUPERWIDEBAND (16kHz) +#define OPUS_DTX 1 // DTX enabled (bandwidth optimization) +#define OPUS_LSB_DEPTH 16 // 16-bit depth + +// ALSA retry configuration +static uint32_t sleep_microseconds = 1000; +static uint32_t sleep_milliseconds = 1; +static uint8_t max_attempts_global = 5; +static uint32_t max_backoff_us_global = 500000; + +int jetkvm_audio_capture_init(); +void jetkvm_audio_capture_close(); +int jetkvm_audio_read_encode(void *opus_buf); + +int jetkvm_audio_playback_init(); +void jetkvm_audio_playback_close(); +int jetkvm_audio_decode_write(void *opus_buf, int opus_size); + +void update_audio_constants(uint32_t bitrate, uint8_t complexity, + uint32_t sr, uint8_t ch, uint16_t fs, uint16_t max_pkt, + uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff); +void update_audio_decoder_constants(uint32_t sr, uint8_t ch, uint16_t fs, uint16_t max_pkt, + uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff); +int update_opus_encoder_params(uint32_t bitrate, uint8_t complexity); + + +/** + * Sync encoder configuration from Go to C + */ +void update_audio_constants(uint32_t bitrate, uint8_t complexity, + uint32_t sr, uint8_t ch, uint16_t fs, uint16_t max_pkt, + uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff) { + opus_bitrate = bitrate; + opus_complexity = complexity; + sample_rate = sr; + channels = ch; + frame_size = fs; + max_packet_size = max_pkt; + sleep_microseconds = sleep_us; + sleep_milliseconds = sleep_us / 1000; // Precompute for snd_pcm_wait + max_attempts_global = max_attempts; + max_backoff_us_global = max_backoff; +} + +/** + * Sync decoder configuration from Go to C (no encoder-only params) + */ +void update_audio_decoder_constants(uint32_t sr, uint8_t ch, uint16_t fs, uint16_t max_pkt, + uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff) { + sample_rate = sr; + channels = ch; + frame_size = fs; + max_packet_size = max_pkt; + sleep_microseconds = sleep_us; + sleep_milliseconds = sleep_us / 1000; // Precompute for snd_pcm_wait + max_attempts_global = max_attempts; + max_backoff_us_global = max_backoff; +} + +/** + * Initialize ALSA device names from environment variables + * Must be called before jetkvm_audio_capture_init or jetkvm_audio_playback_init + */ +static void init_alsa_devices_from_env(void) { + // Always read from environment to support device switching + alsa_capture_device = getenv("ALSA_CAPTURE_DEVICE"); + if (alsa_capture_device == NULL || alsa_capture_device[0] == '\0') { + alsa_capture_device = "hw:1,0"; // Default to USB gadget + } + + alsa_playback_device = getenv("ALSA_PLAYBACK_DEVICE"); + if (alsa_playback_device == NULL || alsa_playback_device[0] == '\0') { + alsa_playback_device = "hw:1,0"; // Default to USB gadget + } +} + +// SIMD-OPTIMIZED BUFFER OPERATIONS (ARM NEON) + +/** + * Clear audio buffer using NEON (16 samples/iteration with 2x unrolling) + */ +static inline void simd_clear_samples_s16(short * __restrict__ buffer, uint32_t samples) { + const int16x8_t zero = vdupq_n_s16(0); + uint32_t i = 0; + + // Process 16 samples at a time (2x unrolled for better pipeline utilization) + uint32_t simd_samples = samples & ~15U; + for (; i < simd_samples; i += 16) { + vst1q_s16(&buffer[i], zero); + vst1q_s16(&buffer[i + 8], zero); + } + + // Handle remaining 8 samples + if (i + 8 <= samples) { + vst1q_s16(&buffer[i], zero); + i += 8; + } + + // Scalar: remaining samples + for (; i < samples; i++) { + buffer[i] = 0; + } +} + +// INITIALIZATION STATE TRACKING + +static volatile sig_atomic_t capture_initializing = 0; +static volatile sig_atomic_t capture_initialized = 0; +static volatile sig_atomic_t playback_initializing = 0; +static volatile sig_atomic_t playback_initialized = 0; + +/** + * Update Opus encoder settings at runtime (does NOT modify FEC or hardcoded settings) + * + * NOTE: Currently unused but kept for potential future runtime configuration updates. + * In the current CGO implementation, encoder params are set once via update_audio_constants() + * before initialization. This function would be useful if we add runtime bitrate/complexity + * adjustment without restarting the encoder. + * + * @return 0 on success, -1 if not initialized, >0 if some settings failed + */ +int update_opus_encoder_params(uint32_t bitrate, uint8_t complexity) { + if (!encoder || !capture_initialized) { + return -1; + } + + // Update runtime-configurable parameters + opus_bitrate = bitrate; + opus_complexity = complexity; + + // Apply settings to encoder + int result = 0; + result |= opus_encoder_ctl(encoder, OPUS_SET_BITRATE(opus_bitrate)); + result |= opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(opus_complexity)); + + return result; +} + +// ALSA UTILITY FUNCTIONS + +/** + * Open ALSA device with exponential backoff retry + * @return 0 on success, negative error code on failure + */ +// Helper: High-precision sleep using nanosleep (better than usleep) +static inline void precise_sleep_us(uint32_t microseconds) { + struct timespec ts = { + .tv_sec = microseconds / 1000000, + .tv_nsec = (microseconds % 1000000) * 1000 + }; + nanosleep(&ts, NULL); +} + +static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream_t stream) { + uint8_t attempt = 0; + int err; + uint32_t backoff_us = sleep_microseconds; + + while (attempt < max_attempts_global) { + err = snd_pcm_open(handle, device, stream, SND_PCM_NONBLOCK); + if (err >= 0) { + snd_pcm_nonblock(*handle, 0); + return 0; + } + + attempt++; + + // Exponential backoff with bit shift (faster than multiplication) + if (err == -EBUSY || err == -EAGAIN) { + precise_sleep_us(backoff_us); + backoff_us = (backoff_us << 1 < max_backoff_us_global) ? (backoff_us << 1) : max_backoff_us_global; + } else if (err == -ENODEV || err == -ENOENT) { + precise_sleep_us(backoff_us << 1); + backoff_us = (backoff_us << 1 < max_backoff_us_global) ? (backoff_us << 1) : max_backoff_us_global; + } else if (err == -EPERM || err == -EACCES) { + precise_sleep_us(backoff_us >> 1); + } else { + precise_sleep_us(backoff_us); + backoff_us = (backoff_us << 1 < max_backoff_us_global) ? (backoff_us << 1) : max_backoff_us_global; + } + } + return err; +} + +/** + * Configure ALSA device (S16_LE @ 48kHz stereo with optimized buffering) + * @param handle ALSA PCM handle + * @param device_name Unused (for debugging only) + * @return 0 on success, negative error code on failure + */ +static int configure_alsa_device(snd_pcm_t *handle, const char *device_name) { + snd_pcm_hw_params_t *params; + snd_pcm_sw_params_t *sw_params; + int err; + + if (!handle) return -1; + + snd_pcm_hw_params_alloca(¶ms); + snd_pcm_sw_params_alloca(&sw_params); + + err = snd_pcm_hw_params_any(handle, params); + if (err < 0) return err; + + err = snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED); + if (err < 0) return err; + + err = snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE); + if (err < 0) return err; + + err = snd_pcm_hw_params_set_channels(handle, params, channels); + if (err < 0) return err; + + err = snd_pcm_hw_params_set_rate(handle, params, sample_rate, 0); + if (err < 0) { + unsigned int rate = sample_rate; + err = snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0); + if (err < 0) return err; + } + + snd_pcm_uframes_t period_size = frame_size; // Optimized: use full frame as period + if (period_size < 64) period_size = 64; + + err = snd_pcm_hw_params_set_period_size_near(handle, params, &period_size, 0); + if (err < 0) return err; + + snd_pcm_uframes_t buffer_size = period_size * 4; // 4 periods = 80ms buffer for stability + err = snd_pcm_hw_params_set_buffer_size_near(handle, params, &buffer_size); + if (err < 0) return err; + + err = snd_pcm_hw_params(handle, params); + if (err < 0) return err; + + err = snd_pcm_sw_params_current(handle, sw_params); + if (err < 0) return err; + + err = snd_pcm_sw_params_set_start_threshold(handle, sw_params, period_size); + if (err < 0) return err; + + err = snd_pcm_sw_params_set_avail_min(handle, sw_params, period_size); + if (err < 0) return err; + + err = snd_pcm_sw_params(handle, sw_params); + if (err < 0) return err; + + return snd_pcm_prepare(handle); +} + +// AUDIO OUTPUT PATH FUNCTIONS (TC358743 HDMI Audio → Client Speakers) + +/** + * Initialize OUTPUT path (TC358743 HDMI capture → Opus encoder) + * Opens hw:0,0 (TC358743) and creates Opus encoder with optimized settings + * @return 0 on success, -EBUSY if initializing, -1/-2/-3 on errors + */ +int jetkvm_audio_capture_init() { + int err; + + init_alsa_devices_from_env(); + + if (__sync_bool_compare_and_swap(&capture_initializing, 0, 1) == 0) { + return -EBUSY; + } + + if (capture_initialized) { + capture_initializing = 0; + return 0; + } + + if (encoder) { + opus_encoder_destroy(encoder); + encoder = NULL; + } + if (pcm_capture_handle) { + snd_pcm_close(pcm_capture_handle); + pcm_capture_handle = NULL; + } + + err = safe_alsa_open(&pcm_capture_handle, alsa_capture_device, SND_PCM_STREAM_CAPTURE); + if (err < 0) { + fprintf(stderr, "Failed to open ALSA capture device %s: %s\n", + alsa_capture_device, snd_strerror(err)); + fflush(stderr); + capture_initializing = 0; + return -1; + } + + err = configure_alsa_device(pcm_capture_handle, "capture"); + if (err < 0) { + snd_pcm_close(pcm_capture_handle); + pcm_capture_handle = NULL; + capture_initializing = 0; + return -2; + } + + int opus_err = 0; + encoder = opus_encoder_create(sample_rate, channels, OPUS_APPLICATION_AUDIO, &opus_err); + if (!encoder || opus_err != OPUS_OK) { + if (pcm_capture_handle) { + snd_pcm_close(pcm_capture_handle); + pcm_capture_handle = NULL; + } + capture_initializing = 0; + return -3; + } + + // Configure encoder with optimized settings + opus_encoder_ctl(encoder, OPUS_SET_BITRATE(opus_bitrate)); + opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(opus_complexity)); + opus_encoder_ctl(encoder, OPUS_SET_VBR(OPUS_VBR)); + opus_encoder_ctl(encoder, OPUS_SET_VBR_CONSTRAINT(OPUS_VBR_CONSTRAINT)); + opus_encoder_ctl(encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_TYPE)); + opus_encoder_ctl(encoder, OPUS_SET_BANDWIDTH(OPUS_BANDWIDTH)); + opus_encoder_ctl(encoder, OPUS_SET_DTX(OPUS_DTX)); + opus_encoder_ctl(encoder, OPUS_SET_LSB_DEPTH(OPUS_LSB_DEPTH)); + + opus_encoder_ctl(encoder, OPUS_SET_INBAND_FEC(1)); + opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(20)); + + capture_initialized = 1; + capture_initializing = 0; + return 0; +} + +/** + * Read HDMI audio, encode to Opus (OUTPUT path hot function) + * @param opus_buf Output buffer for encoded Opus packet + * @return >0 = Opus packet size in bytes, -1 = error + */ +__attribute__((hot)) int jetkvm_audio_read_encode(void * __restrict__ opus_buf) { + static short CACHE_ALIGN pcm_buffer[960 * 2]; // Cache-aligned + unsigned char * __restrict__ out = (unsigned char*)opus_buf; + int32_t pcm_rc, nb_bytes; + int32_t err = 0; + uint8_t recovery_attempts = 0; + const uint8_t max_recovery_attempts = 3; + + // Prefetch for write (out) and read (pcm_buffer) - RV1106 has small L1 cache + SIMD_PREFETCH(out, 1, 0); // Write, immediate use + SIMD_PREFETCH(pcm_buffer, 0, 0); // Read, immediate use + SIMD_PREFETCH(pcm_buffer + 64, 0, 1); // Prefetch next cache line + + if (__builtin_expect(!capture_initialized || !pcm_capture_handle || !encoder || !opus_buf, 0)) { + TRACE_LOG("[AUDIO_OUTPUT] jetkvm_audio_read_encode: Failed safety checks - capture_initialized=%d, pcm_capture_handle=%p, encoder=%p, opus_buf=%p\n", + capture_initialized, pcm_capture_handle, encoder, opus_buf); + return -1; + } + +retry_read: + // Read 960 frames (20ms) from ALSA capture device + pcm_rc = snd_pcm_readi(pcm_capture_handle, pcm_buffer, frame_size); + + if (__builtin_expect(pcm_rc < 0, 0)) { + if (pcm_rc == -EPIPE) { + recovery_attempts++; + if (recovery_attempts > max_recovery_attempts) { + return -1; + } + err = snd_pcm_prepare(pcm_capture_handle); + if (err < 0) { + snd_pcm_drop(pcm_capture_handle); + err = snd_pcm_prepare(pcm_capture_handle); + if (err < 0) return -1; + } + goto retry_read; + } else if (pcm_rc == -EAGAIN) { + // Wait for data to be available + snd_pcm_wait(pcm_capture_handle, sleep_milliseconds); + goto retry_read; + } else if (pcm_rc == -ESTRPIPE) { + recovery_attempts++; + if (recovery_attempts > max_recovery_attempts) { + return -1; + } + uint8_t resume_attempts = 0; + while ((err = snd_pcm_resume(pcm_capture_handle)) == -EAGAIN && resume_attempts < 10) { + snd_pcm_wait(pcm_capture_handle, sleep_milliseconds); + resume_attempts++; + } + if (err < 0) { + err = snd_pcm_prepare(pcm_capture_handle); + if (err < 0) return -1; + } + return 0; + } else if (pcm_rc == -ENODEV) { + return -1; + } else if (pcm_rc == -EIO) { + recovery_attempts++; + if (recovery_attempts <= max_recovery_attempts) { + snd_pcm_drop(pcm_capture_handle); + err = snd_pcm_prepare(pcm_capture_handle); + if (err >= 0) { + goto retry_read; + } + } + return -1; + } else { + recovery_attempts++; + if (recovery_attempts <= 1 && pcm_rc == -EINTR) { + goto retry_read; + } else if (recovery_attempts <= 1 && pcm_rc == -EBUSY) { + snd_pcm_wait(pcm_capture_handle, 1); // Wait 1ms for device + goto retry_read; + } + return -1; + } + } + + // Zero-pad if we got a short read + if (__builtin_expect(pcm_rc < frame_size, 0)) { + uint32_t remaining_samples = (frame_size - pcm_rc) * channels; + simd_clear_samples_s16(&pcm_buffer[pcm_rc * channels], remaining_samples); + } + + nb_bytes = opus_encode(encoder, pcm_buffer, frame_size, out, max_packet_size); + return nb_bytes; +} + +// AUDIO INPUT PATH FUNCTIONS (Client Microphone → Device Speakers) + +/** + * Initialize INPUT path (Opus decoder → device speakers) + * Opens hw:1,0 (USB gadget) or "default" and creates Opus decoder + * @return 0 on success, -EBUSY if initializing, -1/-2 on errors + */ +int jetkvm_audio_playback_init() { + int err; + + init_alsa_devices_from_env(); + + if (__sync_bool_compare_and_swap(&playback_initializing, 0, 1) == 0) { + return -EBUSY; + } + + if (playback_initialized) { + playback_initializing = 0; + return 0; + } + + if (decoder) { + opus_decoder_destroy(decoder); + decoder = NULL; + } + if (pcm_playback_handle) { + snd_pcm_close(pcm_playback_handle); + pcm_playback_handle = NULL; + } + + err = safe_alsa_open(&pcm_playback_handle, alsa_playback_device, SND_PCM_STREAM_PLAYBACK); + if (err < 0) { + fprintf(stderr, "Failed to open ALSA playback device %s: %s\n", + alsa_playback_device, snd_strerror(err)); + fflush(stderr); + err = safe_alsa_open(&pcm_playback_handle, "default", SND_PCM_STREAM_PLAYBACK); + if (err < 0) { + playback_initializing = 0; + return -1; + } + } + + err = configure_alsa_device(pcm_playback_handle, "playback"); + if (err < 0) { + snd_pcm_close(pcm_playback_handle); + pcm_playback_handle = NULL; + playback_initializing = 0; + return -1; + } + + int opus_err = 0; + decoder = opus_decoder_create(sample_rate, channels, &opus_err); + if (!decoder || opus_err != OPUS_OK) { + snd_pcm_close(pcm_playback_handle); + pcm_playback_handle = NULL; + playback_initializing = 0; + return -2; + } + + playback_initialized = 1; + playback_initializing = 0; + return 0; +} + +/** + * Decode Opus, write to device speakers (INPUT path hot function) + * Processing pipeline: Opus decode (with FEC) → ALSA playback with error recovery + * @param opus_buf Encoded Opus packet from client + * @param opus_size Size of Opus packet in bytes + * @return >0 = PCM frames written, 0 = frame skipped, -1/-2 = error + */ +__attribute__((hot)) int jetkvm_audio_decode_write(void * __restrict__ opus_buf, int32_t opus_size) { + static short CACHE_ALIGN pcm_buffer[960 * 2]; // Cache-aligned + unsigned char * __restrict__ in = (unsigned char*)opus_buf; + int32_t pcm_frames, pcm_rc, err = 0; + uint8_t recovery_attempts = 0; + const uint8_t max_recovery_attempts = 3; + + // Prefetch input buffer - locality 0 for immediate use + SIMD_PREFETCH(in, 0, 0); + + if (__builtin_expect(!playback_initialized || !pcm_playback_handle || !decoder || !opus_buf || opus_size <= 0, 0)) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Failed safety checks - playback_initialized=%d, pcm_playback_handle=%p, decoder=%p, opus_buf=%p, opus_size=%d\n", + playback_initialized, pcm_playback_handle, decoder, opus_buf, opus_size); + return -1; + } + + if (opus_size > max_packet_size) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Opus packet too large - size=%d, max=%d\n", opus_size, max_packet_size); + return -1; + } + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Processing Opus packet - size=%d bytes\n", opus_size); + + // Decode Opus packet to PCM (FEC automatically applied if embedded in packet) + // decode_fec=0 means normal decode (FEC data is used automatically when present) + pcm_frames = opus_decode(decoder, in, opus_size, pcm_buffer, frame_size, 0); + + if (__builtin_expect(pcm_frames < 0, 0)) { + // Decode failed - attempt packet loss concealment using FEC from previous packet + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Opus decode failed with error %d, attempting packet loss concealment\n", pcm_frames); + + // decode_fec=1 means use FEC data from the NEXT packet to reconstruct THIS lost packet + pcm_frames = opus_decode(decoder, NULL, 0, pcm_buffer, frame_size, 1); + if (pcm_frames < 0) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Packet loss concealment also failed with error %d\n", pcm_frames); + return -1; + } + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Packet loss concealment succeeded, recovered %d frames\n", pcm_frames); + } else + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Opus decode successful - decoded %d PCM frames\n", pcm_frames); + +retry_write: + // Write decoded PCM to ALSA playback device + pcm_rc = snd_pcm_writei(pcm_playback_handle, pcm_buffer, pcm_frames); + if (__builtin_expect(pcm_rc < 0, 0)) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: ALSA write failed with error %d (%s), attempt %d/%d\n", + pcm_rc, snd_strerror(pcm_rc), recovery_attempts + 1, max_recovery_attempts); + + if (pcm_rc == -EPIPE) { + recovery_attempts++; + if (recovery_attempts > max_recovery_attempts) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Buffer underrun recovery failed after %d attempts\n", max_recovery_attempts); + return -2; + } + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Buffer underrun detected, attempting recovery (attempt %d)\n", recovery_attempts); + err = snd_pcm_prepare(pcm_playback_handle); + if (err < 0) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: snd_pcm_prepare failed (%s), trying drop+prepare\n", snd_strerror(err)); + snd_pcm_drop(pcm_playback_handle); + err = snd_pcm_prepare(pcm_playback_handle); + if (err < 0) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: drop+prepare recovery failed (%s)\n", snd_strerror(err)); + return -2; + } + } + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Buffer underrun recovery successful, retrying write\n"); + goto retry_write; + } else if (pcm_rc == -ESTRPIPE) { + recovery_attempts++; + if (recovery_attempts > max_recovery_attempts) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device suspend recovery failed after %d attempts\n", max_recovery_attempts); + return -2; + } + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device suspended, attempting resume (attempt %d)\n", recovery_attempts); + uint8_t resume_attempts = 0; + while ((err = snd_pcm_resume(pcm_playback_handle)) == -EAGAIN && resume_attempts < 10) { + snd_pcm_wait(pcm_playback_handle, sleep_milliseconds); + resume_attempts++; + } + if (err < 0) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device resume failed (%s), trying prepare fallback\n", snd_strerror(err)); + err = snd_pcm_prepare(pcm_playback_handle); + if (err < 0) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Prepare fallback failed (%s)\n", snd_strerror(err)); + return -2; + } + } + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device suspend recovery successful, skipping frame\n"); + return 0; + } else if (pcm_rc == -ENODEV) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device disconnected (ENODEV) - critical error\n"); + return -2; + } else if (pcm_rc == -EIO) { + recovery_attempts++; + if (recovery_attempts <= max_recovery_attempts) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: I/O error detected, attempting recovery\n"); + snd_pcm_drop(pcm_playback_handle); + err = snd_pcm_prepare(pcm_playback_handle); + if (err >= 0) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: I/O error recovery successful, retrying write\n"); + goto retry_write; + } + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: I/O error recovery failed (%s)\n", snd_strerror(err)); + } + return -2; + } else if (pcm_rc == -EAGAIN) { + recovery_attempts++; + if (recovery_attempts <= max_recovery_attempts) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device not ready (EAGAIN), waiting and retrying\n"); + snd_pcm_wait(pcm_playback_handle, 1); // Wait 1ms + goto retry_write; + } + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device not ready recovery failed after %d attempts\n", max_recovery_attempts); + return -2; + } else { + recovery_attempts++; + if (recovery_attempts <= 1 && (pcm_rc == -EINTR || pcm_rc == -EBUSY)) { + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Transient error %d (%s), retrying once\n", pcm_rc, snd_strerror(pcm_rc)); + snd_pcm_wait(pcm_playback_handle, 1); // Wait 1ms + goto retry_write; + } + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Unrecoverable error %d (%s)\n", pcm_rc, snd_strerror(pcm_rc)); + return -2; + } + } + TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Successfully wrote %d PCM frames to device\n", pcm_frames); + return pcm_frames; +} + +// CLEANUP FUNCTIONS + +/** + * Close INPUT path (thread-safe with drain) + */ +void jetkvm_audio_playback_close() { + while (playback_initializing) { + sched_yield(); + } + + if (__sync_bool_compare_and_swap(&playback_initialized, 1, 0) == 0) { + return; + } + + if (decoder) { + opus_decoder_destroy(decoder); + decoder = NULL; + } + if (pcm_playback_handle) { + snd_pcm_drain(pcm_playback_handle); + snd_pcm_close(pcm_playback_handle); + pcm_playback_handle = NULL; + } +} + +/** + * Close OUTPUT path (thread-safe with drain) + */ +void jetkvm_audio_capture_close() { + while (capture_initializing) { + sched_yield(); + } + + if (__sync_bool_compare_and_swap(&capture_initialized, 1, 0) == 0) { + return; + } + + if (encoder) { + opus_encoder_destroy(encoder); + encoder = NULL; + } + if (pcm_capture_handle) { + snd_pcm_drain(pcm_capture_handle); + snd_pcm_close(pcm_capture_handle); + pcm_capture_handle = NULL; + } +} diff --git a/internal/audio/c/audio_common.c b/internal/audio/c/audio_common.c new file mode 100644 index 00000000..93609624 --- /dev/null +++ b/internal/audio/c/audio_common.c @@ -0,0 +1,101 @@ +/* + * JetKVM Audio Common Utilities + * + * Shared functions for audio processing + */ + +#include "audio_common.h" +#include +#include +#include +#include +#include +#include + +// GLOBAL STATE FOR SIGNAL HANDLER + +// Pointer to the running flag that will be set to 0 on shutdown +static volatile sig_atomic_t *g_running_ptr = NULL; + +// SIGNAL HANDLERS + +static void signal_handler(int signo) { + if (signo == SIGTERM || signo == SIGINT) { + printf("Audio server: Received signal %d, shutting down...\n", signo); + if (g_running_ptr != NULL) { + *g_running_ptr = 0; + } + } +} + +void audio_common_setup_signal_handlers(volatile sig_atomic_t *running) { + g_running_ptr = running; + + 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); +} + + +int32_t audio_common_parse_env_int(const char *name, int32_t default_value) { + const char *str = getenv(name); + if (str == NULL || str[0] == '\0') { + return default_value; + } + return (int32_t)atoi(str); +} + +const char* audio_common_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; +} + +// COMMON CONFIGURATION + +void audio_common_load_config(audio_config_t *config, int is_output) { + // ALSA device configuration + if (is_output) { + config->alsa_device = audio_common_parse_env_string("ALSA_CAPTURE_DEVICE", "hw:1,0"); + } else { + config->alsa_device = audio_common_parse_env_string("ALSA_PLAYBACK_DEVICE", "hw:1,0"); + } + + // Common Opus configuration + config->opus_bitrate = audio_common_parse_env_int("OPUS_BITRATE", 128000); + config->opus_complexity = audio_common_parse_env_int("OPUS_COMPLEXITY", 2); + + // Audio format + config->sample_rate = audio_common_parse_env_int("AUDIO_SAMPLE_RATE", 48000); + config->channels = audio_common_parse_env_int("AUDIO_CHANNELS", 2); + config->frame_size = audio_common_parse_env_int("AUDIO_FRAME_SIZE", 960); + + // Log configuration + printf("Audio %s Server Configuration:\n", is_output ? "Output" : "Input"); + 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); + if (is_output) { + printf(" Opus Bitrate: %d bps\n", config->opus_bitrate); + printf(" Opus Complexity: %d\n", config->opus_complexity); + } +} + +void audio_common_print_startup(const char *server_name) { + printf("JetKVM %s Starting...\n", server_name); +} + +void audio_common_print_shutdown(const char *server_name) { + printf("Shutting down %s...\n", server_name); +} diff --git a/internal/audio/c/audio_common.h b/internal/audio/c/audio_common.h new file mode 100644 index 00000000..362c1594 --- /dev/null +++ b/internal/audio/c/audio_common.h @@ -0,0 +1,135 @@ +/* + * JetKVM Audio Common Utilities + * + * Shared functions used by both audio input and output servers + */ + +#ifndef JETKVM_AUDIO_COMMON_H +#define JETKVM_AUDIO_COMMON_H + +#include +#include + +// SHARED CONSTANTS + +// Audio processing parameters +#define AUDIO_MAX_PACKET_SIZE 1500 // Maximum Opus packet size +#define AUDIO_SLEEP_MICROSECONDS 1000 // Default sleep time in microseconds +#define AUDIO_MAX_ATTEMPTS 5 // Maximum retry attempts +#define AUDIO_MAX_BACKOFF_US 500000 // Maximum backoff in microseconds + +// Error handling +#define AUDIO_MAX_CONSECUTIVE_ERRORS 10 // Maximum consecutive errors before giving up + +// Performance monitoring +#define AUDIO_TRACE_MASK 0x3FF // Log every 1024th frame (bit mask for efficiency) + +// SIGNAL HANDLERS + +/** + * Setup signal handlers for graceful shutdown. + * Handles SIGTERM and SIGINT by setting the running flag to 0. + * Ignores SIGPIPE to prevent crashes on broken pipe writes. + * + * @param running Pointer to the volatile running flag to set on shutdown + */ +void audio_common_setup_signal_handlers(volatile sig_atomic_t *running); + + +/** + * Parse integer from environment variable. + * Returns default_value if variable is not set or empty. + * + * @param name Environment variable name + * @param default_value Default value if not set + * @return Parsed integer value or default + */ +int32_t audio_common_parse_env_int(const char *name, int32_t default_value); + +/** + * Parse string from environment variable. + * Returns default_value if variable is not set or empty. + * + * @param name Environment variable name + * @param default_value Default value if not set + * @return Environment variable value or default (not duplicated) + */ +const char* audio_common_parse_env_string(const char *name, const char *default_value); + + +// COMMON CONFIGURATION + +/** + * Common audio configuration structure + */ +typedef struct { + const char *alsa_device; // ALSA device path + int opus_bitrate; // Opus bitrate + int opus_complexity; // Opus complexity + int sample_rate; // Sample rate + int channels; // Number of channels + int frame_size; // Frame size in samples +} audio_config_t; + +/** + * Load common audio configuration from environment + * @param config Output configuration + * @param is_output true for output server, false for input + */ +void audio_common_load_config(audio_config_t *config, int is_output); + +/** + * Print server startup message + * @param server_name Name of the server (e.g., "Audio Output Server") + */ +void audio_common_print_startup(const char *server_name); + +/** + * Print server shutdown message + * @param server_name Name of the server + */ +void audio_common_print_shutdown(const char *server_name); + +// ERROR TRACKING + +/** + * Error tracking state for audio processing loops + */ +typedef struct { + uint8_t consecutive_errors; // Current consecutive error count + uint32_t frame_count; // Total frames processed +} audio_error_tracker_t; + +/** + * Initialize error tracker + */ +static inline void audio_error_tracker_init(audio_error_tracker_t *tracker) { + tracker->consecutive_errors = 0; + tracker->frame_count = 0; +} + +/** + * Record an error and check if we should give up + * Returns 1 if too many errors, 0 to continue + */ +static inline uint8_t audio_error_tracker_record_error(audio_error_tracker_t *tracker) { + tracker->consecutive_errors++; + return (tracker->consecutive_errors >= AUDIO_MAX_CONSECUTIVE_ERRORS) ? 1 : 0; +} + +/** + * Record success and increment frame count + */ +static inline void audio_error_tracker_record_success(audio_error_tracker_t *tracker) { + tracker->consecutive_errors = 0; + tracker->frame_count++; +} + +/** + * Check if we should log trace info for this frame + */ +static inline uint8_t audio_error_tracker_should_trace(audio_error_tracker_t *tracker) { + return ((tracker->frame_count & AUDIO_TRACE_MASK) == 1) ? 1 : 0; +} + +#endif // JETKVM_AUDIO_COMMON_H diff --git a/internal/audio/cgo_source.go b/internal/audio/cgo_source.go new file mode 100644 index 00000000..d985e507 --- /dev/null +++ b/internal/audio/cgo_source.go @@ -0,0 +1,212 @@ +//go:build linux && (arm || arm64) + +package audio + +/* +#cgo CFLAGS: -O3 -ffast-math -I/opt/jetkvm-audio-libs/alsa-lib-1.2.14/include -I/opt/jetkvm-audio-libs/opus-1.5.2/include +#cgo LDFLAGS: /opt/jetkvm-audio-libs/alsa-lib-1.2.14/src/.libs/libasound.a /opt/jetkvm-audio-libs/opus-1.5.2/.libs/libopus.a -lm -ldl -lpthread + +#include +#include "c/audio.c" +*/ +import "C" +import ( + "fmt" + "os" + "sync" + "unsafe" + + "github.com/jetkvm/kvm/internal/logging" + "github.com/rs/zerolog" +) + +const ( + ipcMaxFrameSize = 1024 // Max Opus frame size: 128kbps @ 20ms = ~600 bytes +) + +// CgoSource implements AudioSource via direct CGO calls to C audio functions (in-process) +type CgoSource struct { + direction string // "output" or "input" + alsaDevice string + initialized bool + connected bool + mu sync.Mutex + logger zerolog.Logger + opusBuf []byte // Reusable buffer for Opus packets +} + +// NewCgoOutputSource creates a new CGO audio source for output (HDMI/USB → browser) +func NewCgoOutputSource(alsaDevice string) *CgoSource { + logger := logging.GetDefaultLogger().With().Str("component", "audio-output-cgo").Logger() + + return &CgoSource{ + direction: "output", + alsaDevice: alsaDevice, + logger: logger, + opusBuf: make([]byte, ipcMaxFrameSize), + } +} + +// NewCgoInputSource creates a new CGO audio source for input (browser → USB speakers) +func NewCgoInputSource(alsaDevice string) *CgoSource { + logger := logging.GetDefaultLogger().With().Str("component", "audio-input-cgo").Logger() + + return &CgoSource{ + direction: "input", + alsaDevice: alsaDevice, + logger: logger, + opusBuf: make([]byte, ipcMaxFrameSize), + } +} + +// Connect initializes the C audio subsystem +func (c *CgoSource) Connect() error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.connected { + return nil + } + + // Set ALSA device via environment for C code to read via init_alsa_devices_from_env() + if c.direction == "output" { + // Set capture device for output path via environment variable + os.Setenv("ALSA_CAPTURE_DEVICE", c.alsaDevice) + + // Initialize constants + C.update_audio_constants( + C.uint(128000), // bitrate + C.uchar(5), // complexity + C.uint(48000), // sample_rate + C.uchar(2), // channels + C.ushort(960), // frame_size + C.ushort(1500), // max_packet_size + C.uint(1000), // sleep_us + C.uchar(5), // max_attempts + C.uint(500000), // max_backoff_us + ) + + // Initialize capture (HDMI/USB → browser) + rc := C.jetkvm_audio_capture_init() + if rc != 0 { + c.logger.Error().Int("rc", int(rc)).Msg("Failed to initialize audio capture") + return fmt.Errorf("jetkvm_audio_capture_init failed: %d", rc) + } + } else { + // Set playback device for input path via environment variable + os.Setenv("ALSA_PLAYBACK_DEVICE", c.alsaDevice) + + // Initialize decoder constants + C.update_audio_decoder_constants( + C.uint(48000), // sample_rate + C.uchar(2), // channels + C.ushort(960), // frame_size + C.ushort(1500), // max_packet_size + C.uint(1000), // sleep_us + C.uchar(5), // max_attempts + C.uint(500000), // max_backoff_us + ) + + // Initialize playback (browser → USB speakers) + rc := C.jetkvm_audio_playback_init() + if rc != 0 { + c.logger.Error().Int("rc", int(rc)).Msg("Failed to initialize audio playback") + return fmt.Errorf("jetkvm_audio_playback_init failed: %d", rc) + } + } + + c.connected = true + c.initialized = true + return nil +} + +// Disconnect closes the C audio subsystem +func (c *CgoSource) Disconnect() { + c.mu.Lock() + defer c.mu.Unlock() + + if !c.connected { + return + } + + if c.direction == "output" { + C.jetkvm_audio_capture_close() + } else { + C.jetkvm_audio_playback_close() + } + + c.connected = false +} + +// IsConnected returns true if currently connected +func (c *CgoSource) IsConnected() bool { + c.mu.Lock() + defer c.mu.Unlock() + return c.connected +} + +// ReadMessage reads the next audio frame from C audio subsystem +// For output path: reads HDMI/USB audio and encodes to Opus +// For input path: not used (input uses WriteMessage instead) +// Returns message type (0 = Opus), payload data, and error +func (c *CgoSource) ReadMessage() (uint8, []byte, error) { + c.mu.Lock() + defer c.mu.Unlock() + + if !c.connected { + return 0, nil, fmt.Errorf("not connected") + } + + if c.direction != "output" { + return 0, nil, fmt.Errorf("ReadMessage only supported for output direction") + } + + // Call C function to read HDMI/USB audio and encode to Opus + // Returns Opus packet size (>0) or error (<0) + opusSize := C.jetkvm_audio_read_encode(unsafe.Pointer(&c.opusBuf[0])) + + if opusSize < 0 { + return 0, nil, fmt.Errorf("jetkvm_audio_read_encode failed: %d", opusSize) + } + + if opusSize == 0 { + // No data available (silence/DTX) + return 0, nil, nil + } + + // Return slice of opusBuf - caller must use immediately + return ipcMsgTypeOpus, c.opusBuf[:opusSize], nil +} + +// WriteMessage writes an Opus packet to the C audio subsystem for playback +// Only used for input path (browser → USB speakers) +func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error { + c.mu.Lock() + defer c.mu.Unlock() + + if !c.connected { + return fmt.Errorf("not connected") + } + + if c.direction != "input" { + return fmt.Errorf("WriteMessage only supported for input direction") + } + + if msgType != ipcMsgTypeOpus { + // Ignore non-Opus messages + return nil + } + + if len(payload) == 0 { + return nil + } + + // Call C function to decode Opus and write to USB speakers + rc := C.jetkvm_audio_decode_write(unsafe.Pointer(&payload[0]), C.int(len(payload))) + + if rc < 0 { + return fmt.Errorf("jetkvm_audio_decode_write failed: %d", rc) + } + + return nil +} diff --git a/internal/audio/relay.go b/internal/audio/relay.go new file mode 100644 index 00000000..77ec648e --- /dev/null +++ b/internal/audio/relay.go @@ -0,0 +1,158 @@ +package audio + +import ( + "context" + "fmt" + "sync/atomic" + "time" + + "github.com/jetkvm/kvm/internal/logging" + "github.com/pion/webrtc/v4" + "github.com/pion/webrtc/v4/pkg/media" + "github.com/rs/zerolog" +) + +// OutputRelay forwards audio from AudioSource (CGO) to WebRTC (browser) +type OutputRelay struct { + source AudioSource + audioTrack *webrtc.TrackLocalStaticSample + ctx context.Context + cancel context.CancelFunc + logger zerolog.Logger + running atomic.Bool + sample media.Sample + stopped chan struct{} + + // Stats (Uint32: overflows after 2.7 years @ 50fps, faster atomics on 32-bit ARM) + framesRelayed atomic.Uint32 + framesDropped atomic.Uint32 +} + +// NewOutputRelay creates a relay for output audio (device → browser) +func NewOutputRelay(source AudioSource, audioTrack *webrtc.TrackLocalStaticSample) *OutputRelay { + ctx, cancel := context.WithCancel(context.Background()) + logger := logging.GetDefaultLogger().With().Str("component", "audio-output-relay").Logger() + + return &OutputRelay{ + source: source, + audioTrack: audioTrack, + ctx: ctx, + cancel: cancel, + logger: logger, + stopped: make(chan struct{}), + sample: media.Sample{ + Duration: 20 * time.Millisecond, + }, + } +} + +// Start begins relaying audio frames +func (r *OutputRelay) Start() error { + if r.running.Swap(true) { + return fmt.Errorf("output relay already running") + } + + go r.relayLoop() + r.logger.Debug().Msg("output relay started") + return nil +} + +// Stop stops the relay and waits for goroutine to exit +func (r *OutputRelay) Stop() { + if !r.running.Swap(false) { + return + } + + r.cancel() + <-r.stopped + + r.logger.Debug(). + Uint32("frames_relayed", r.framesRelayed.Load()). + Uint32("frames_dropped", r.framesDropped.Load()). + Msg("output relay stopped") +} + +// relayLoop continuously reads from audio source and writes to WebRTC +func (r *OutputRelay) relayLoop() { + defer close(r.stopped) + + const reconnectDelay = 1 * time.Second + + for r.running.Load() { + // Ensure connected + if !r.source.IsConnected() { + if err := r.source.Connect(); err != nil { + r.logger.Debug().Err(err).Msg("failed to connect, will retry") + time.Sleep(reconnectDelay) + continue + } + } + + // Read message from audio source + msgType, payload, err := r.source.ReadMessage() + if err != nil { + // Connection error - reconnect + if r.running.Load() { + r.logger.Warn().Err(err).Msg("read error, reconnecting") + r.source.Disconnect() + time.Sleep(reconnectDelay) + } + continue + } + + // Handle message + if msgType == ipcMsgTypeOpus && len(payload) > 0 { + // Reuse sample struct (zero-allocation hot path) + r.sample.Data = payload + + if err := r.audioTrack.WriteSample(r.sample); err != nil { + r.framesDropped.Add(1) + r.logger.Warn().Err(err).Msg("failed to write sample to WebRTC") + } else { + r.framesRelayed.Add(1) + } + } + } +} + +// InputRelay forwards audio from WebRTC (browser microphone) to AudioSource (USB audio) +type InputRelay struct { + source AudioSource + ctx context.Context + cancel context.CancelFunc + logger zerolog.Logger + running atomic.Bool +} + +// NewInputRelay creates a relay for input audio (browser → device) +func NewInputRelay(source AudioSource) *InputRelay { + ctx, cancel := context.WithCancel(context.Background()) + logger := logging.GetDefaultLogger().With().Str("component", "audio-input-relay").Logger() + + return &InputRelay{ + source: source, + ctx: ctx, + cancel: cancel, + logger: logger, + } +} + +// Start begins relaying audio frames +func (r *InputRelay) Start() error { + if r.running.Swap(true) { + return fmt.Errorf("input relay already running") + } + + r.logger.Debug().Msg("input relay started") + return nil +} + +// Stop stops the relay +func (r *InputRelay) Stop() { + if !r.running.Swap(false) { + return + } + + r.cancel() + r.logger.Debug().Msg("input relay stopped") +} diff --git a/internal/audio/source.go b/internal/audio/source.go new file mode 100644 index 00000000..c7393a04 --- /dev/null +++ b/internal/audio/source.go @@ -0,0 +1,28 @@ +package audio + +// IPC message types +const ( + ipcMsgTypeOpus = 0 // Message type for Opus audio data +) + +// AudioSource provides audio frames via CGO (in-process) C audio functions +type AudioSource interface { + // ReadMessage reads the next audio message + // Returns message type, payload data, and error + // Blocks until data is available or error occurs + // Used for output path (device → browser) + ReadMessage() (msgType uint8, payload []byte, err error) + + // WriteMessage writes an audio message + // Used for input path (browser → device) + WriteMessage(msgType uint8, payload []byte) error + + // IsConnected returns true if the source is connected and ready + IsConnected() bool + + // Connect initializes the C audio subsystem + Connect() error + + // Disconnect closes the connection and releases resources + Disconnect() +} diff --git a/internal/native/cgo/video.c b/internal/native/cgo/video.c index 857acbbb..5deb9f5e 100644 --- a/internal/native/cgo/video.c +++ b/internal/native/cgo/video.c @@ -752,7 +752,6 @@ void *run_detect_format(void *arg) while (!should_exit) { - ensure_sleep_mode_disabled(); memset(&dv_timings, 0, sizeof(dv_timings)); if (ioctl(sub_dev_fd, VIDIOC_QUERY_DV_TIMINGS, &dv_timings) != 0) diff --git a/internal/native/video.go b/internal/native/video.go index c556a938..b8068360 100644 --- a/internal/native/video.go +++ b/internal/native/video.go @@ -8,8 +8,8 @@ import ( const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode" -// DefaultEDID is the default EDID for the video stream. -const DefaultEDID = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b" +// DefaultEDID is the default EDID (identifies as "JetKVM HDMI" with full TC358743 audio/video capabilities). +const DefaultEDID = "00ffffffffffff002a8b01000100000001230104800000782ec9a05747982712484c00000000d1c081c0a9c0b3000101010101010101083a801871382d40582c450000000000001e011d007251d01e206e28550000000000001e000000fc004a65746b564d2048444d490a20000000fd00187801ff1d000a20202020202001e102032e7229097f070d07070f0707509005040302011f132220111214061507831f000068030c0010003021e2050700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000047" var extraLockTimeout = 5 * time.Second diff --git a/internal/usbgadget/changeset_resolver.go b/internal/usbgadget/changeset_resolver.go index 67812e0d..c06fac96 100644 --- a/internal/usbgadget/changeset_resolver.go +++ b/internal/usbgadget/changeset_resolver.go @@ -1,7 +1,9 @@ package usbgadget import ( + "context" "fmt" + "time" "github.com/rs/zerolog" "github.com/sourcegraph/tf-dag/dag" @@ -114,7 +116,20 @@ func (c *ChangeSetResolver) resolveChanges(initial bool) error { } func (c *ChangeSetResolver) applyChanges() error { + return c.applyChangesWithTimeout(45 * time.Second) +} + +func (c *ChangeSetResolver) applyChangesWithTimeout(timeout time.Duration) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + for _, change := range c.resolvedChanges { + select { + case <-ctx.Done(): + return fmt.Errorf("USB gadget reconfiguration timed out after %v: %w", timeout, ctx.Err()) + default: + } + change.ResetActionResolution() action := change.Action() actionStr := FileChangeResolvedActionString[action] @@ -126,7 +141,7 @@ func (c *ChangeSetResolver) applyChanges() error { l.Str("action", actionStr).Str("change", change.String()).Msg("applying change") - err := c.changeset.applyChange(change) + err := c.applyChangeWithTimeout(ctx, change) if err != nil { if change.IgnoreErrors { c.l.Warn().Str("change", change.String()).Err(err).Msg("ignoring error") @@ -139,6 +154,20 @@ func (c *ChangeSetResolver) applyChanges() error { return nil } +func (c *ChangeSetResolver) applyChangeWithTimeout(ctx context.Context, change *FileChange) error { + done := make(chan error, 1) + go func() { + done <- c.changeset.applyChange(change) + }() + + select { + case err := <-done: + return err + case <-ctx.Done(): + return fmt.Errorf("change application timed out for %s: %w", change.String(), ctx.Err()) + } +} + func (c *ChangeSetResolver) GetChanges() ([]*FileChange, error) { localChanges := c.changeset.Changes changesMap := make(map[string]*FileChange) diff --git a/internal/usbgadget/config.go b/internal/usbgadget/config.go index 6d1bd391..0a9b3caa 100644 --- a/internal/usbgadget/config.go +++ b/internal/usbgadget/config.go @@ -59,6 +59,23 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{ // mass storage "mass_storage_base": massStorageBaseConfig, "mass_storage_lun0": massStorageLun0Config, + // audio (UAC1 - USB Audio Class 1) + "audio": { + order: 4000, + device: "uac1.usb0", + path: []string{"functions", "uac1.usb0"}, + configPath: []string{"uac1.usb0"}, + attrs: gadgetAttributes{ + "p_chmask": "3", // Playback: stereo (2 channels) + "p_srate": "48000", // Playback: 48kHz sample rate + "p_ssize": "2", // Playback: 16-bit (2 bytes) + "p_volume_present": "0", // Playback: no volume control + "c_chmask": "3", // Capture: stereo (2 channels) + "c_srate": "48000", // Capture: 48kHz sample rate + "c_ssize": "2", // Capture: 16-bit (2 bytes) + "c_volume_present": "0", // Capture: no volume control + }, + }, } func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool { @@ -73,6 +90,8 @@ func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool { return u.enabledDevices.MassStorage case "mass_storage_lun0": return u.enabledDevices.MassStorage + case "audio": + return u.enabledDevices.Audio default: return true } @@ -182,6 +201,9 @@ func (u *UsbGadget) Init() error { return u.logError("unable to initialize USB stack", err) } + // Pre-open HID files to reduce input latency + u.PreOpenHidFiles() + return nil } @@ -191,11 +213,17 @@ func (u *UsbGadget) UpdateGadgetConfig() error { u.loadGadgetConfig() + // Close HID files before reconfiguration to prevent "file already closed" errors + u.CloseHidFiles() + err := u.configureUsbGadget(true) if err != nil { return u.logError("unable to update gadget config", err) } + // Reopen HID files after reconfiguration + u.PreOpenHidFiles() + return nil } diff --git a/internal/usbgadget/config_tx.go b/internal/usbgadget/config_tx.go index df8a3d1b..d5591b17 100644 --- a/internal/usbgadget/config_tx.go +++ b/internal/usbgadget/config_tx.go @@ -1,10 +1,12 @@ package usbgadget import ( + "context" "fmt" "path" "path/filepath" "sort" + "time" "github.com/rs/zerolog" ) @@ -52,22 +54,49 @@ func (u *UsbGadget) newUsbGadgetTransaction(lock bool) error { } func (u *UsbGadget) WithTransaction(fn func() error) error { - u.txLock.Lock() - defer u.txLock.Unlock() + return u.WithTransactionTimeout(fn, 60*time.Second) +} - err := u.newUsbGadgetTransaction(false) - if err != nil { - u.log.Error().Err(err).Msg("failed to create transaction") - return err - } - if err := fn(); err != nil { - u.log.Error().Err(err).Msg("transaction failed") - return err - } - result := u.tx.Commit() - u.tx = nil +// WithTransactionTimeout executes a USB gadget transaction with a specified timeout +// to prevent indefinite blocking during USB reconfiguration operations +func (u *UsbGadget) WithTransactionTimeout(fn func() error, timeout time.Duration) error { + // Create a context with timeout for the entire transaction + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() - return result + // Channel to signal when the transaction is complete + done := make(chan error, 1) + + // Execute the transaction in a goroutine + go func() { + u.txLock.Lock() + defer u.txLock.Unlock() + + err := u.newUsbGadgetTransaction(false) + if err != nil { + u.log.Error().Err(err).Msg("failed to create transaction") + done <- err + return + } + + if err := fn(); err != nil { + u.log.Error().Err(err).Msg("transaction failed") + done <- err + return + } + + result := u.tx.Commit() + u.tx = nil + done <- result + }() + + // Wait for either completion or timeout + select { + case err := <-done: + return err + case <-ctx.Done(): + return fmt.Errorf("USB gadget transaction timed out after %v: %w", timeout, ctx.Err()) + } } func (tx *UsbGadgetTransaction) addFileChange(component string, change RequestedFileChange) string { diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go index f01ae09d..c7c86d23 100644 --- a/internal/usbgadget/usbgadget.go +++ b/internal/usbgadget/usbgadget.go @@ -19,6 +19,7 @@ type Devices struct { RelativeMouse bool `json:"relative_mouse"` Keyboard bool `json:"keyboard"` MassStorage bool `json:"mass_storage"` + Audio bool `json:"audio"` } // Config is a struct that represents the customizations for a USB gadget. @@ -39,6 +40,7 @@ var defaultUsbGadgetDevices = Devices{ RelativeMouse: true, Keyboard: true, MassStorage: true, + Audio: true, } type KeysDownState struct { @@ -188,3 +190,63 @@ func (u *UsbGadget) Close() error { return nil } + +// CloseHidFiles closes all open HID files +func (u *UsbGadget) CloseHidFiles() { + u.log.Debug().Msg("closing HID files") + + // Close keyboard HID file + if u.keyboardHidFile != nil { + if err := u.keyboardHidFile.Close(); err != nil { + u.log.Debug().Err(err).Msg("failed to close keyboard HID file") + } + u.keyboardHidFile = nil + } + + // Close absolute mouse HID file + if u.absMouseHidFile != nil { + if err := u.absMouseHidFile.Close(); err != nil { + u.log.Debug().Err(err).Msg("failed to close absolute mouse HID file") + } + u.absMouseHidFile = nil + } + + // Close relative mouse HID file + if u.relMouseHidFile != nil { + if err := u.relMouseHidFile.Close(); err != nil { + u.log.Debug().Err(err).Msg("failed to close relative mouse HID file") + } + u.relMouseHidFile = nil + } +} + +// PreOpenHidFiles opens all HID files to reduce input latency +func (u *UsbGadget) PreOpenHidFiles() { + // Add a small delay to allow USB gadget reconfiguration to complete + // This prevents "no such device or address" errors when trying to open HID files + time.Sleep(100 * time.Millisecond) + + if u.enabledDevices.Keyboard { + if err := u.openKeyboardHidFile(); err != nil { + u.log.Debug().Err(err).Msg("failed to pre-open keyboard HID file") + } + } + if u.enabledDevices.AbsoluteMouse { + if u.absMouseHidFile == nil { + var err error + u.absMouseHidFile, err = os.OpenFile("/dev/hidg1", os.O_RDWR, 0666) + if err != nil { + u.log.Debug().Err(err).Msg("failed to pre-open absolute mouse HID file") + } + } + } + if u.enabledDevices.RelativeMouse { + if u.relMouseHidFile == nil { + var err error + u.relMouseHidFile, err = os.OpenFile("/dev/hidg2", os.O_RDWR, 0666) + if err != nil { + u.log.Debug().Err(err).Msg("failed to pre-open relative mouse HID file") + } + } + } +} diff --git a/jsonrpc.go b/jsonrpc.go index 5ed90a7a..f8e07151 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -678,7 +678,8 @@ func rpcSetUsbConfig(usbConfig usbgadget.Config) error { LoadConfig() config.UsbConfig = &usbConfig gadget.SetGadgetConfig(config.UsbConfig) - return updateUsbRelatedConfig() + wasAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio + return updateUsbRelatedConfig(wasAudioEnabled) } func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) { @@ -890,23 +891,42 @@ func rpcGetUsbDevices() (usbgadget.Devices, error) { return *config.UsbDevices, nil } -func updateUsbRelatedConfig() error { +func updateUsbRelatedConfig(wasAudioEnabled bool) error { + ensureConfigLoaded() + + // Stop input audio before USB reconfiguration (input uses USB) + audioMutex.Lock() + stopInputLocked() + audioMutex.Unlock() + if err := gadget.UpdateGadgetConfig(); err != nil { return fmt.Errorf("failed to write gadget config: %w", err) } + if err := SaveConfig(); err != nil { return fmt.Errorf("failed to save config: %w", err) } + + // Restart audio if USB audio is enabled with active connections + if activeConnections.Load() > 0 && config.UsbDevices != nil && config.UsbDevices.Audio { + if err := startAudio(); err != nil { + logger.Warn().Err(err).Msg("Failed to restart audio after USB reconfiguration") + } + } + return nil } func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { + wasAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio config.UsbDevices = &usbDevices gadget.SetGadgetDevices(config.UsbDevices) - return updateUsbRelatedConfig() + return updateUsbRelatedConfig(wasAudioEnabled) } func rpcSetUsbDeviceState(device string, enabled bool) error { + wasAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio + switch device { case "absoluteMouse": config.UsbDevices.AbsoluteMouse = enabled @@ -916,11 +936,29 @@ func rpcSetUsbDeviceState(device string, enabled bool) error { config.UsbDevices.Keyboard = enabled case "massStorage": config.UsbDevices.MassStorage = enabled + case "audio": + config.UsbDevices.Audio = enabled default: return fmt.Errorf("invalid device: %s", device) } gadget.SetGadgetDevices(config.UsbDevices) - return updateUsbRelatedConfig() + return updateUsbRelatedConfig(wasAudioEnabled) +} + +func rpcGetAudioOutputEnabled() (bool, error) { + return audioOutputEnabled.Load(), nil +} + +func rpcSetAudioOutputEnabled(enabled bool) error { + return SetAudioOutputEnabled(enabled) +} + +func rpcGetAudioInputEnabled() (bool, error) { + return audioInputEnabled.Load(), nil +} + +func rpcSetAudioInputEnabled(enabled bool) error { + return SetAudioInputEnabled(enabled) } func rpcSetCloudUrl(apiUrl string, appUrl string) error { @@ -1241,6 +1279,10 @@ var rpcHandlers = map[string]RPCHandler{ "getUsbDevices": {Func: rpcGetUsbDevices}, "setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}}, "setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}}, + "getAudioOutputEnabled": {Func: rpcGetAudioOutputEnabled}, + "setAudioOutputEnabled": {Func: rpcSetAudioOutputEnabled, Params: []string{"enabled"}}, + "getAudioInputEnabled": {Func: rpcGetAudioInputEnabled}, + "setAudioInputEnabled": {Func: rpcSetAudioInputEnabled, Params: []string{"enabled"}}, "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, "getKeyboardLayout": {Func: rpcGetKeyboardLayout}, "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}}, diff --git a/main.go b/main.go index bcc2d73d..4df1a746 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,7 @@ func Main() { initDisplay() initNative(systemVersionLocal, appVersionLocal) + initAudio() http.DefaultClient.Timeout = 1 * time.Minute @@ -132,7 +133,10 @@ func Main() { signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs - logger.Log().Msg("JetKVM Shutting Down") + logger.Info().Msg("JetKVM Shutting Down") + + stopAudio() + //if fuseServer != nil { // err := setMassStorageImage(" ") // if err != nil { diff --git a/scripts/build_utils.sh b/scripts/build_utils.sh index d70b29ae..f924b23a 100644 --- a/scripts/build_utils.sh +++ b/scripts/build_utils.sh @@ -41,6 +41,7 @@ BUILD_IN_DOCKER=${BUILD_IN_DOCKER:-false} function prepare_docker_build_context() { msg_info "▶ Preparing docker build context ..." cp .devcontainer/install-deps.sh \ + .devcontainer/install_audio_deps.sh \ go.mod \ go.sum \ Dockerfile.build \ @@ -103,4 +104,4 @@ function do_make() { make "$@" set +x fi -} \ No newline at end of file +} diff --git a/ui/localization/messages/da.json b/ui/localization/messages/da.json index dae6e906..f4ef424e 100644 --- a/ui/localization/messages/da.json +++ b/ui/localization/messages/da.json @@ -47,7 +47,43 @@ "access_tls_self_signed": "Selvsigneret", "access_tls_updated": "TLS-indstillingerne er blevet opdateret", "access_update_tls_settings": "Opdater TLS-indstillinger", + "action_bar_audio": "Lyd", "action_bar_connection_stats": "Forbindelsesstatistik", + "audio_disable": "Deaktiver", + "audio_enable": "Aktiver", + "audio_input_description": "Aktiver mikrofonindgang til målet", + "audio_input_disabled": "Lydindgang deaktiveret", + "audio_input_enabled": "Lydindgang aktiveret", + "audio_input_failed_disable": "Kunne ikke deaktivere lydindgang: {error}", + "audio_input_failed_enable": "Kunne ikke aktivere lydindgang: {error}", + "audio_input_title": "Lydindgang (Mikrofon)", + "audio_output_description": "Aktiver lyd fra mål til højttalere", + "audio_output_disabled": "Lydudgang deaktiveret", + "audio_output_enabled": "Lydudgang aktiveret", + "audio_output_failed_disable": "Kunne ikke deaktivere lydudgang: {error}", + "audio_output_failed_enable": "Kunne ikke aktivere lydudgang: {error}", + "audio_output_title": "Lydudgang", + "audio_popover_title": "Lyd", + "audio_popover_description": "Hurtige lydkontroller til højttalere og mikrofon", + "audio_speakers_title": "Højttalere", + "audio_speakers_description": "Lyd fra mål til højttalere", + "audio_microphone_title": "Mikrofon", + "audio_microphone_description": "Mikrofonindgang til mål", + "audio_https_only": "Kun HTTPS", + "audio_settings_description": "Konfigurer lydindgangs- og lydudgangsindstillinger for din JetKVM-enhed", + "audio_settings_hdmi_label": "HDMI", + "audio_settings_input_description": "Aktiver eller deaktiver mikrofon lyd til fjerncomputeren", + "audio_settings_input_title": "Lydindgang", + "audio_settings_output_description": "Aktiver eller deaktiver lyd fra fjerncomputeren", + "audio_settings_output_source_description": "Vælg lydoptagelsesenheden (HDMI eller USB)", + "audio_settings_output_source_failed": "Kunne ikke indstille lydudgangskilde: {error}", + "audio_settings_output_source_success": "Lydudgangskilde opdateret med succes", + "audio_settings_output_source_title": "Lydudgangskilde", + "audio_settings_output_title": "Lydudgang", + "audio_settings_title": "Lyd", + "audio_settings_usb_label": "USB", + "audio_settings_auto_enable_microphone_title": "Aktiver mikrofon automatisk", + "audio_settings_auto_enable_microphone_description": "Aktiver automatisk browsermikrofon ved tilslutning (ellers skal du aktivere det manuelt ved hver session)", "action_bar_extension": "Udvidelse", "action_bar_fullscreen": "Fuldskærm", "action_bar_settings": "Indstillinger", @@ -790,6 +826,8 @@ "usb_device_description": "USB-enheder, der skal emuleres på målcomputeren", "usb_device_enable_absolute_mouse_description": "Aktivér absolut mus (markør)", "usb_device_enable_absolute_mouse_title": "Aktivér absolut mus (markør)", + "usb_device_enable_audio_description": "Aktiver tovejs lyd", + "usb_device_enable_audio_title": "Aktiver USB-lyd", "usb_device_enable_keyboard_description": "Aktivér tastatur", "usb_device_enable_keyboard_title": "Aktivér tastatur", "usb_device_enable_mass_storage_description": "Nogle gange skal det muligvis deaktiveres for at forhindre problemer med bestemte enheder.", @@ -799,6 +837,7 @@ "usb_device_failed_load": "Kunne ikke indlæse USB-enheder: {error}", "usb_device_failed_set": "Kunne ikke indstille USB-enheder: {error}", "usb_device_keyboard_mouse_and_mass_storage": "Tastatur, mus og masselagring", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Tastatur, mus, masselager og lyd", "usb_device_keyboard_only": "Kun tastatur", "usb_device_restore_default": "Gendan til standard", "usb_device_title": "USB-enhed", diff --git a/ui/localization/messages/de.json b/ui/localization/messages/de.json index 04e25844..0d69ad13 100644 --- a/ui/localization/messages/de.json +++ b/ui/localization/messages/de.json @@ -47,7 +47,43 @@ "access_tls_self_signed": "Selbstsigniert", "access_tls_updated": "TLS-Einstellungen erfolgreich aktualisiert", "access_update_tls_settings": "TLS-Einstellungen aktualisieren", + "action_bar_audio": "Audio", "action_bar_connection_stats": "Verbindungsstatistiken", + "audio_disable": "Deaktivieren", + "audio_enable": "Aktivieren", + "audio_input_description": "Mikrofoneingang zum Ziel aktivieren", + "audio_input_disabled": "Audioeingang deaktiviert", + "audio_input_enabled": "Audioeingang aktiviert", + "audio_input_failed_disable": "Fehler beim Deaktivieren des Audioeingangs: {error}", + "audio_input_failed_enable": "Fehler beim Aktivieren des Audioeingangs: {error}", + "audio_input_title": "Audioeingang (Mikrofon)", + "audio_output_description": "Audio vom Ziel zu Lautsprechern aktivieren", + "audio_output_disabled": "Audioausgang deaktiviert", + "audio_output_enabled": "Audioausgang aktiviert", + "audio_output_failed_disable": "Fehler beim Deaktivieren des Audioausgangs: {error}", + "audio_output_failed_enable": "Fehler beim Aktivieren des Audioausgangs: {error}", + "audio_output_title": "Audioausgang", + "audio_popover_title": "Audio", + "audio_popover_description": "Schnelle Audiosteuerung für Lautsprecher und Mikrofon", + "audio_speakers_title": "Lautsprecher", + "audio_speakers_description": "Audio vom Ziel zu Lautsprechern", + "audio_microphone_title": "Mikrofon", + "audio_microphone_description": "Mikrofoneingang zum Ziel", + "audio_https_only": "Nur HTTPS", + "audio_settings_description": "Konfigurieren Sie Audio-Eingangs- und Ausgangseinstellungen für Ihr JetKVM-Gerät", + "audio_settings_hdmi_label": "HDMI", + "audio_settings_input_description": "Mikrofonaudio zum entfernten Computer aktivieren oder deaktivieren", + "audio_settings_input_title": "Audioeingang", + "audio_settings_output_description": "Audio vom entfernten Computer aktivieren oder deaktivieren", + "audio_settings_output_source_description": "Wählen Sie das Audioaufnahmegerät (HDMI oder USB)", + "audio_settings_output_source_failed": "Fehler beim Festlegen der Audioausgabequelle: {error}", + "audio_settings_output_source_success": "Audioausgabequelle erfolgreich aktualisiert", + "audio_settings_output_source_title": "Audioausgabequelle", + "audio_settings_output_title": "Audioausgang", + "audio_settings_title": "Audio", + "audio_settings_usb_label": "USB", + "audio_settings_auto_enable_microphone_title": "Mikrofon automatisch aktivieren", + "audio_settings_auto_enable_microphone_description": "Browser-Mikrofon beim Verbinden automatisch aktivieren (andernfalls müssen Sie es in jeder Sitzung manuell aktivieren)", "action_bar_extension": "Erweiterung", "action_bar_fullscreen": "Vollbild", "action_bar_settings": "Einstellungen", @@ -790,6 +826,8 @@ "usb_device_description": "USB-Geräte zum Emulieren auf dem Zielcomputer", "usb_device_enable_absolute_mouse_description": "Absolute Maus (Zeiger) aktivieren", "usb_device_enable_absolute_mouse_title": "Absolute Maus (Zeiger) aktivieren", + "usb_device_enable_audio_description": "Bidirektionales Audio aktivieren", + "usb_device_enable_audio_title": "USB-Audio aktivieren", "usb_device_enable_keyboard_description": "Tastatur aktivieren", "usb_device_enable_keyboard_title": "Tastatur aktivieren", "usb_device_enable_mass_storage_description": "Manchmal muss es möglicherweise deaktiviert werden, um Probleme mit bestimmten Geräten zu vermeiden", @@ -799,6 +837,7 @@ "usb_device_failed_load": "USB-Geräte konnten nicht geladen werden: {error}", "usb_device_failed_set": "Fehler beim Festlegen der USB-Geräte: {error}", "usb_device_keyboard_mouse_and_mass_storage": "Tastatur, Maus und Massenspeicher", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Tastatur, Maus, Massenspeicher und Audio", "usb_device_keyboard_only": "Nur Tastatur", "usb_device_restore_default": "Auf Standard zurücksetzen", "usb_device_title": "USB-Gerät", diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json index 0356e8e5..3813f63f 100644 --- a/ui/localization/messages/en.json +++ b/ui/localization/messages/en.json @@ -47,7 +47,43 @@ "access_tls_self_signed": "Self-signed", "access_tls_updated": "TLS settings updated successfully", "access_update_tls_settings": "Update TLS Settings", + "action_bar_audio": "Audio", "action_bar_connection_stats": "Connection Stats", + "audio_disable": "Disable", + "audio_enable": "Enable", + "audio_input_description": "Enable microphone input to target", + "audio_input_disabled": "Audio input disabled", + "audio_input_enabled": "Audio input enabled", + "audio_input_failed_disable": "Failed to disable audio input: {error}", + "audio_input_failed_enable": "Failed to enable audio input: {error}", + "audio_input_title": "Audio Input (Microphone)", + "audio_output_description": "Enable audio from target to speakers", + "audio_output_disabled": "Audio output disabled", + "audio_output_enabled": "Audio output enabled", + "audio_output_failed_disable": "Failed to disable audio output: {error}", + "audio_output_failed_enable": "Failed to enable audio output: {error}", + "audio_output_title": "Audio Output", + "audio_popover_title": "Audio", + "audio_popover_description": "Quick audio controls for speakers and microphone", + "audio_speakers_title": "Speakers", + "audio_speakers_description": "Audio from target to speakers", + "audio_microphone_title": "Microphone", + "audio_microphone_description": "Microphone input to target", + "audio_https_only": "HTTPS only", + "audio_settings_description": "Configure audio input and output settings for your JetKVM device", + "audio_settings_hdmi_label": "HDMI", + "audio_settings_input_description": "Enable or disable microphone audio to the remote computer", + "audio_settings_input_title": "Audio Input", + "audio_settings_output_description": "Enable or disable audio from the remote computer", + "audio_settings_output_source_description": "Select the audio capture device (HDMI or USB)", + "audio_settings_output_source_failed": "Failed to set audio output source: {error}", + "audio_settings_output_source_success": "Audio output source updated successfully", + "audio_settings_output_source_title": "Audio Output Source", + "audio_settings_output_title": "Audio Output", + "audio_settings_title": "Audio", + "audio_settings_usb_label": "USB", + "audio_settings_auto_enable_microphone_title": "Auto-enable Microphone", + "audio_settings_auto_enable_microphone_description": "Automatically enable browser microphone when connecting (otherwise you must manually enable each session)", "action_bar_extension": "Extension", "action_bar_fullscreen": "Fullscreen", "action_bar_settings": "Settings", @@ -790,6 +826,8 @@ "usb_device_description": "USB devices to emulate on the target computer", "usb_device_enable_absolute_mouse_description": "Enable Absolute Mouse (Pointer)", "usb_device_enable_absolute_mouse_title": "Enable Absolute Mouse (Pointer)", + "usb_device_enable_audio_description": "Enable bidirectional audio", + "usb_device_enable_audio_title": "Enable USB Audio", "usb_device_enable_keyboard_description": "Enable Keyboard", "usb_device_enable_keyboard_title": "Enable Keyboard", "usb_device_enable_mass_storage_description": "Sometimes it might need to be disabled to prevent issues with certain devices", @@ -799,6 +837,7 @@ "usb_device_failed_load": "Failed to load USB devices: {error}", "usb_device_failed_set": "Failed to set USB devices: {error}", "usb_device_keyboard_mouse_and_mass_storage": "Keyboard, Mouse and Mass Storage", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Keyboard, Mouse, Mass Storage and Audio", "usb_device_keyboard_only": "Keyboard Only", "usb_device_restore_default": "Restore to Default", "usb_device_title": "USB Device", diff --git a/ui/localization/messages/es.json b/ui/localization/messages/es.json index a74e35ec..4c4f6c8e 100644 --- a/ui/localization/messages/es.json +++ b/ui/localization/messages/es.json @@ -47,7 +47,43 @@ "access_tls_self_signed": "Autofirmado", "access_tls_updated": "La configuración de TLS se actualizó correctamente", "access_update_tls_settings": "Actualizar la configuración de TLS", + "action_bar_audio": "Audio", "action_bar_connection_stats": "Estadísticas de conexión", + "audio_disable": "Desactivar", + "audio_enable": "Activar", + "audio_input_description": "Habilitar entrada de micrófono al objetivo", + "audio_input_disabled": "Entrada de audio desactivada", + "audio_input_enabled": "Entrada de audio activada", + "audio_input_failed_disable": "Error al desactivar la entrada de audio: {error}", + "audio_input_failed_enable": "Error al activar la entrada de audio: {error}", + "audio_input_title": "Entrada de audio (Micrófono)", + "audio_output_description": "Habilitar audio del objetivo a los altavoces", + "audio_output_disabled": "Salida de audio desactivada", + "audio_output_enabled": "Salida de audio activada", + "audio_output_failed_disable": "Error al desactivar la salida de audio: {error}", + "audio_output_failed_enable": "Error al activar la salida de audio: {error}", + "audio_output_title": "Salida de audio", + "audio_popover_title": "Audio", + "audio_popover_description": "Controles de audio rápidos para altavoces y micrófono", + "audio_speakers_title": "Altavoces", + "audio_speakers_description": "Audio del objetivo a los altavoces", + "audio_microphone_title": "Micrófono", + "audio_microphone_description": "Entrada de micrófono al objetivo", + "audio_https_only": "Solo HTTPS", + "audio_settings_description": "Configure los ajustes de entrada y salida de audio para su dispositivo JetKVM", + "audio_settings_hdmi_label": "HDMI", + "audio_settings_input_description": "Habilitar o deshabilitar el audio del micrófono a la computadora remota", + "audio_settings_input_title": "Entrada de audio", + "audio_settings_output_description": "Habilitar o deshabilitar el audio de la computadora remota", + "audio_settings_output_source_description": "Seleccione el dispositivo de captura de audio (HDMI o USB)", + "audio_settings_output_source_failed": "Error al configurar la fuente de salida de audio: {error}", + "audio_settings_output_source_success": "Fuente de salida de audio actualizada correctamente", + "audio_settings_output_source_title": "Fuente de salida de audio", + "audio_settings_output_title": "Salida de audio", + "audio_settings_title": "Audio", + "audio_settings_usb_label": "USB", + "audio_settings_auto_enable_microphone_title": "Habilitar micrófono automáticamente", + "audio_settings_auto_enable_microphone_description": "Habilitar automáticamente el micrófono del navegador al conectar (de lo contrario, debe habilitarlo manualmente en cada sesión)", "action_bar_extension": "Extensión", "action_bar_fullscreen": "Pantalla completa", "action_bar_settings": "Ajustes", @@ -790,6 +826,8 @@ "usb_device_description": "Dispositivos USB para emular en la computadora de destino", "usb_device_enable_absolute_mouse_description": "Habilitar el puntero absoluto del ratón", "usb_device_enable_absolute_mouse_title": "Habilitar el puntero absoluto del ratón", + "usb_device_enable_audio_description": "Habilitar audio bidireccional", + "usb_device_enable_audio_title": "Habilitar audio USB", "usb_device_enable_keyboard_description": "Habilitar el teclado", "usb_device_enable_keyboard_title": "Habilitar el teclado", "usb_device_enable_mass_storage_description": "A veces puede ser necesario desactivarlo para evitar problemas con ciertos dispositivos.", @@ -799,6 +837,7 @@ "usb_device_failed_load": "No se pudieron cargar los dispositivos USB: {error}", "usb_device_failed_set": "No se pudieron configurar los dispositivos USB: {error}", "usb_device_keyboard_mouse_and_mass_storage": "Teclado, ratón y almacenamiento masivo", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Teclado, ratón, almacenamiento masivo y audio", "usb_device_keyboard_only": "Sólo teclado", "usb_device_restore_default": "Restaurar a valores predeterminados", "usb_device_title": "Dispositivo USB", diff --git a/ui/localization/messages/fr.json b/ui/localization/messages/fr.json index eb7361d1..81a22fdd 100644 --- a/ui/localization/messages/fr.json +++ b/ui/localization/messages/fr.json @@ -47,7 +47,43 @@ "access_tls_self_signed": "Auto-signé", "access_tls_updated": "Les paramètres TLS ont été mis à jour avec succès", "access_update_tls_settings": "Mettre à jour les paramètres TLS", + "action_bar_audio": "Audio", "action_bar_connection_stats": "Statistiques de connexion", + "audio_disable": "Désactiver", + "audio_enable": "Activer", + "audio_input_description": "Activer l'entrée microphone vers la cible", + "audio_input_disabled": "Entrée audio désactivée", + "audio_input_enabled": "Entrée audio activée", + "audio_input_failed_disable": "Échec de la désactivation de l'entrée audio : {error}", + "audio_input_failed_enable": "Échec de l'activation de l'entrée audio : {error}", + "audio_input_title": "Entrée audio (Microphone)", + "audio_output_description": "Activer l'audio de la cible vers les haut-parleurs", + "audio_output_disabled": "Sortie audio désactivée", + "audio_output_enabled": "Sortie audio activée", + "audio_output_failed_disable": "Échec de la désactivation de la sortie audio : {error}", + "audio_output_failed_enable": "Échec de l'activation de la sortie audio : {error}", + "audio_output_title": "Sortie audio", + "audio_popover_title": "Audio", + "audio_popover_description": "Contrôles audio rapides pour haut-parleurs et microphone", + "audio_speakers_title": "Haut-parleurs", + "audio_speakers_description": "Audio de la cible vers les haut-parleurs", + "audio_microphone_title": "Microphone", + "audio_microphone_description": "Entrée microphone vers la cible", + "audio_https_only": "HTTPS uniquement", + "audio_settings_description": "Configurez les paramètres d'entrée et de sortie audio pour votre appareil JetKVM", + "audio_settings_hdmi_label": "HDMI", + "audio_settings_input_description": "Activer ou désactiver l'audio du microphone vers l'ordinateur distant", + "audio_settings_input_title": "Entrée audio", + "audio_settings_output_description": "Activer ou désactiver l'audio de l'ordinateur distant", + "audio_settings_output_source_description": "Sélectionnez le périphérique de capture audio (HDMI ou USB)", + "audio_settings_output_source_failed": "Échec de la configuration de la source de sortie audio : {error}", + "audio_settings_output_source_success": "Source de sortie audio mise à jour avec succès", + "audio_settings_output_source_title": "Source de sortie audio", + "audio_settings_output_title": "Sortie audio", + "audio_settings_title": "Audio", + "audio_settings_usb_label": "USB", + "audio_settings_auto_enable_microphone_title": "Activer automatiquement le microphone", + "audio_settings_auto_enable_microphone_description": "Activer automatiquement le microphone du navigateur lors de la connexion (sinon vous devez l'activer manuellement à chaque session)", "action_bar_extension": "Extension", "action_bar_fullscreen": "Plein écran", "action_bar_settings": "Paramètres", @@ -790,6 +826,8 @@ "usb_device_description": "Périphériques USB à émuler sur l'ordinateur cible", "usb_device_enable_absolute_mouse_description": "Activer la souris absolue (pointeur)", "usb_device_enable_absolute_mouse_title": "Activer la souris absolue (pointeur)", + "usb_device_enable_audio_description": "Activer l'audio bidirectionnel", + "usb_device_enable_audio_title": "Activer l'audio USB", "usb_device_enable_keyboard_description": "Activer le clavier", "usb_device_enable_keyboard_title": "Activer le clavier", "usb_device_enable_mass_storage_description": "Parfois, il peut être nécessaire de le désactiver pour éviter des problèmes avec certains appareils", @@ -799,6 +837,7 @@ "usb_device_failed_load": "Échec du chargement des périphériques USB : {error}", "usb_device_failed_set": "Échec de la configuration des périphériques USB : {error}", "usb_device_keyboard_mouse_and_mass_storage": "Clavier, souris et stockage de masse", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Clavier, souris, stockage de masse et audio", "usb_device_keyboard_only": "Clavier uniquement", "usb_device_restore_default": "Restaurer les paramètres par défaut", "usb_device_title": "périphérique USB", diff --git a/ui/localization/messages/it.json b/ui/localization/messages/it.json index b2aa5529..1bc14cf3 100644 --- a/ui/localization/messages/it.json +++ b/ui/localization/messages/it.json @@ -47,7 +47,43 @@ "access_tls_self_signed": "Autofirmato", "access_tls_updated": "Impostazioni TLS aggiornate correttamente", "access_update_tls_settings": "Aggiorna le impostazioni TLS", + "action_bar_audio": "Audio", "action_bar_connection_stats": "Statistiche di connessione", + "audio_disable": "Disabilita", + "audio_enable": "Abilita", + "audio_input_description": "Abilita l'ingresso del microfono al target", + "audio_input_disabled": "Ingresso audio disabilitato", + "audio_input_enabled": "Ingresso audio abilitato", + "audio_input_failed_disable": "Impossibile disabilitare l'ingresso audio: {error}", + "audio_input_failed_enable": "Impossibile abilitare l'ingresso audio: {error}", + "audio_input_title": "Ingresso audio (Microfono)", + "audio_output_description": "Abilita l'audio dal target agli altoparlanti", + "audio_output_disabled": "Uscita audio disabilitata", + "audio_output_enabled": "Uscita audio abilitata", + "audio_output_failed_disable": "Impossibile disabilitare l'uscita audio: {error}", + "audio_output_failed_enable": "Impossibile abilitare l'uscita audio: {error}", + "audio_output_title": "Uscita audio", + "audio_popover_title": "Audio", + "audio_popover_description": "Controlli audio rapidi per altoparlanti e microfono", + "audio_speakers_title": "Altoparlanti", + "audio_speakers_description": "Audio dal target agli altoparlanti", + "audio_microphone_title": "Microfono", + "audio_microphone_description": "Ingresso microfono al target", + "audio_https_only": "Solo HTTPS", + "audio_settings_description": "Configura le impostazioni di ingresso e uscita audio per il tuo dispositivo JetKVM", + "audio_settings_hdmi_label": "HDMI", + "audio_settings_input_description": "Abilita o disabilita l'audio del microfono al computer remoto", + "audio_settings_input_title": "Ingresso audio", + "audio_settings_output_description": "Abilita o disabilita l'audio dal computer remoto", + "audio_settings_output_source_description": "Seleziona il dispositivo di acquisizione audio (HDMI o USB)", + "audio_settings_output_source_failed": "Impossibile impostare la sorgente di uscita audio: {error}", + "audio_settings_output_source_success": "Sorgente di uscita audio aggiornata con successo", + "audio_settings_output_source_title": "Sorgente di uscita audio", + "audio_settings_output_title": "Uscita audio", + "audio_settings_title": "Audio", + "audio_settings_usb_label": "USB", + "audio_settings_auto_enable_microphone_title": "Abilita automaticamente il microfono", + "audio_settings_auto_enable_microphone_description": "Abilita automaticamente il microfono del browser durante la connessione (altrimenti devi abilitarlo manualmente ad ogni sessione)", "action_bar_extension": "Estensione", "action_bar_fullscreen": "A schermo intero", "action_bar_settings": "Impostazioni", @@ -790,6 +826,8 @@ "usb_device_description": "Dispositivi USB da emulare sul computer di destinazione", "usb_device_enable_absolute_mouse_description": "Abilita mouse assoluto (puntatore)", "usb_device_enable_absolute_mouse_title": "Abilita mouse assoluto (puntatore)", + "usb_device_enable_audio_description": "Abilita audio bidirezionale", + "usb_device_enable_audio_title": "Abilita audio USB", "usb_device_enable_keyboard_description": "Abilita tastiera", "usb_device_enable_keyboard_title": "Abilita tastiera", "usb_device_enable_mass_storage_description": "A volte potrebbe essere necessario disattivarlo per evitare problemi con determinati dispositivi", @@ -799,6 +837,7 @@ "usb_device_failed_load": "Impossibile caricare i dispositivi USB: {error}", "usb_device_failed_set": "Impossibile impostare i dispositivi USB: {error}", "usb_device_keyboard_mouse_and_mass_storage": "Tastiera, mouse e memoria di massa", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Tastiera, mouse, archiviazione di massa e audio", "usb_device_keyboard_only": "Solo tastiera", "usb_device_restore_default": "Ripristina impostazioni predefinite", "usb_device_title": "Dispositivo USB", diff --git a/ui/localization/messages/nb.json b/ui/localization/messages/nb.json index 26b8584e..37ed78b2 100644 --- a/ui/localization/messages/nb.json +++ b/ui/localization/messages/nb.json @@ -47,7 +47,43 @@ "access_tls_self_signed": "Selvsignert", "access_tls_updated": "TLS-innstillingene er oppdatert", "access_update_tls_settings": "Oppdater TLS-innstillinger", + "action_bar_audio": "Lyd", "action_bar_connection_stats": "Tilkoblingsstatistikk", + "audio_disable": "Deaktiver", + "audio_enable": "Aktiver", + "audio_input_description": "Aktiver mikrofoninngang til målet", + "audio_input_disabled": "Lydinngang deaktivert", + "audio_input_enabled": "Lydinngang aktivert", + "audio_input_failed_disable": "Kunne ikke deaktivere lydinngang: {error}", + "audio_input_failed_enable": "Kunne ikke aktivere lydinngang: {error}", + "audio_input_title": "Lydinngang (Mikrofon)", + "audio_output_description": "Aktiver lyd fra mål til høyttalere", + "audio_output_disabled": "Lydutgang deaktivert", + "audio_output_enabled": "Lydutgang aktivert", + "audio_output_failed_disable": "Kunne ikke deaktivere lydutgang: {error}", + "audio_output_failed_enable": "Kunne ikke aktivere lydutgang: {error}", + "audio_output_title": "Lydutgang", + "audio_popover_title": "Lyd", + "audio_popover_description": "Raske lydkontroller for høyttalere og mikrofon", + "audio_speakers_title": "Høyttalere", + "audio_speakers_description": "Lyd fra mål til høyttalere", + "audio_microphone_title": "Mikrofon", + "audio_microphone_description": "Mikrofoninngang til mål", + "audio_https_only": "Kun HTTPS", + "audio_settings_description": "Konfigurer lydinngangs- og lydutgangsinnstillinger for JetKVM-enheten din", + "audio_settings_hdmi_label": "HDMI", + "audio_settings_input_description": "Aktiver eller deaktiver mikrofonlyd til den eksterne datamaskinen", + "audio_settings_input_title": "Lydinngang", + "audio_settings_output_description": "Aktiver eller deaktiver lyd fra den eksterne datamaskinen", + "audio_settings_output_source_description": "Velg lydopptaksenhet (HDMI eller USB)", + "audio_settings_output_source_failed": "Kunne ikke angi lydutgangskilde: {error}", + "audio_settings_output_source_success": "Lydutgangskilde oppdatert vellykket", + "audio_settings_output_source_title": "Lydutgangskilde", + "audio_settings_output_title": "Lydutgang", + "audio_settings_title": "Lyd", + "audio_settings_usb_label": "USB", + "audio_settings_auto_enable_microphone_title": "Aktiver mikrofon automatisk", + "audio_settings_auto_enable_microphone_description": "Aktiver automatisk nettlesermikrofon ved tilkobling (ellers må du aktivere det manuelt hver økt)", "action_bar_extension": "Forlengelse", "action_bar_fullscreen": "Fullskjerm", "action_bar_settings": "Innstillinger", @@ -790,6 +826,8 @@ "usb_device_description": "USB-enheter som skal emuleres på måldatamaskinen", "usb_device_enable_absolute_mouse_description": "Aktiver absolutt mus (peker)", "usb_device_enable_absolute_mouse_title": "Aktiver absolutt mus (peker)", + "usb_device_enable_audio_description": "Aktiver toveis lyd", + "usb_device_enable_audio_title": "Aktiver USB-lyd", "usb_device_enable_keyboard_description": "Aktiver tastatur", "usb_device_enable_keyboard_title": "Aktiver tastatur", "usb_device_enable_mass_storage_description": "Noen ganger må det kanskje deaktiveres for å forhindre problemer med visse enheter.", @@ -799,6 +837,7 @@ "usb_device_failed_load": "Klarte ikke å laste inn USB-enheter: {error}", "usb_device_failed_set": "Kunne ikke angi USB-enheter: {error}", "usb_device_keyboard_mouse_and_mass_storage": "Tastatur, mus og masselagring", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Tastatur, mus, masselagring og lyd", "usb_device_keyboard_only": "Kun tastatur", "usb_device_restore_default": "Gjenopprett til standard", "usb_device_title": "USB-enhet", diff --git a/ui/localization/messages/sv.json b/ui/localization/messages/sv.json index a4df3271..fbb5c6f2 100644 --- a/ui/localization/messages/sv.json +++ b/ui/localization/messages/sv.json @@ -47,7 +47,43 @@ "access_tls_self_signed": "Självsignerad", "access_tls_updated": "TLS-inställningarna har uppdaterats", "access_update_tls_settings": "Uppdatera TLS-inställningar", + "action_bar_audio": "Ljud", "action_bar_connection_stats": "Anslutningsstatistik", + "audio_disable": "Inaktivera", + "audio_enable": "Aktivera", + "audio_input_description": "Aktivera mikrofoningång till målet", + "audio_input_disabled": "Ljudingång inaktiverad", + "audio_input_enabled": "Ljudingång aktiverad", + "audio_input_failed_disable": "Det gick inte att inaktivera ljudingången: {error}", + "audio_input_failed_enable": "Det gick inte att aktivera ljudingången: {error}", + "audio_input_title": "Ljudingång (Mikrofon)", + "audio_output_description": "Aktivera ljud från mål till högtalare", + "audio_output_disabled": "Ljudutgång inaktiverad", + "audio_output_enabled": "Ljudutgång aktiverad", + "audio_output_failed_disable": "Det gick inte att inaktivera ljudutgången: {error}", + "audio_output_failed_enable": "Det gick inte att aktivera ljudutgången: {error}", + "audio_output_title": "Ljudutgång", + "audio_popover_title": "Ljud", + "audio_popover_description": "Snabba ljudkontroller för högtalare och mikrofon", + "audio_speakers_title": "Högtalare", + "audio_speakers_description": "Ljud från mål till högtalare", + "audio_microphone_title": "Mikrofon", + "audio_microphone_description": "Mikrofoningång till mål", + "audio_https_only": "Endast HTTPS", + "audio_settings_description": "Konfigurera ljudinmatnings- och ljudutgångsinställningar för din JetKVM-enhet", + "audio_settings_hdmi_label": "HDMI", + "audio_settings_input_description": "Aktivera eller inaktivera mikrofonljud till fjärrdatorn", + "audio_settings_input_title": "Ljudingång", + "audio_settings_output_description": "Aktivera eller inaktivera ljud från fjärrdatorn", + "audio_settings_output_source_description": "Välj ljudinspelningsenhet (HDMI eller USB)", + "audio_settings_output_source_failed": "Det gick inte att ställa in ljudutgångskälla: {error}", + "audio_settings_output_source_success": "Ljudutgångskälla uppdaterades framgångsrikt", + "audio_settings_output_source_title": "Ljudutgångskälla", + "audio_settings_output_title": "Ljudutgång", + "audio_settings_title": "Ljud", + "audio_settings_usb_label": "USB", + "audio_settings_auto_enable_microphone_title": "Aktivera mikrofon automatiskt", + "audio_settings_auto_enable_microphone_description": "Aktivera automatiskt webbläsarmikrofon vid anslutning (annars måste du aktivera den manuellt varje session)", "action_bar_extension": "Förlängning", "action_bar_fullscreen": "Helskärm", "action_bar_settings": "Inställningar", @@ -790,6 +826,8 @@ "usb_device_description": "USB-enheter att emulera på måldatorn", "usb_device_enable_absolute_mouse_description": "Aktivera absolut mus (pekare)", "usb_device_enable_absolute_mouse_title": "Aktivera absolut mus (pekare)", + "usb_device_enable_audio_description": "Aktivera dubbelriktad ljud", + "usb_device_enable_audio_title": "Aktivera USB-ljud", "usb_device_enable_keyboard_description": "Aktivera tangentbord", "usb_device_enable_keyboard_title": "Aktivera tangentbord", "usb_device_enable_mass_storage_description": "Ibland kan det behöva inaktiveras för att förhindra problem med vissa enheter.", @@ -799,6 +837,7 @@ "usb_device_failed_load": "Misslyckades med att ladda USB-enheter: {error}", "usb_device_failed_set": "Misslyckades med att ställa in USB-enheter: {error}", "usb_device_keyboard_mouse_and_mass_storage": "Tangentbord, mus och masslagring", + "usb_device_keyboard_mouse_mass_storage_and_audio": "Tangentbord, mus, masslagring och ljud", "usb_device_keyboard_only": "Endast tangentbord", "usb_device_restore_default": "Återställ till standard", "usb_device_title": "USB-enhet", diff --git a/ui/localization/messages/zh.json b/ui/localization/messages/zh.json index 14a55883..b89c50e9 100644 --- a/ui/localization/messages/zh.json +++ b/ui/localization/messages/zh.json @@ -47,7 +47,43 @@ "access_tls_self_signed": "自签名", "access_tls_updated": "TLS 设置更新成功", "access_update_tls_settings": "更新 TLS 设置", + "action_bar_audio": "音频", "action_bar_connection_stats": "连接统计", + "audio_disable": "禁用", + "audio_enable": "启用", + "audio_input_description": "启用麦克风输入到目标设备", + "audio_input_disabled": "音频输入已禁用", + "audio_input_enabled": "音频输入已启用", + "audio_input_failed_disable": "禁用音频输入失败:{error}", + "audio_input_failed_enable": "启用音频输入失败:{error}", + "audio_input_title": "音频输入(麦克风)", + "audio_output_description": "启用从目标设备到扬声器的音频", + "audio_output_disabled": "音频输出已禁用", + "audio_output_enabled": "音频输出已启用", + "audio_output_failed_disable": "禁用音频输出失败:{error}", + "audio_output_failed_enable": "启用音频输出失败:{error}", + "audio_output_title": "音频输出", + "audio_popover_title": "音频", + "audio_popover_description": "扬声器和麦克风的快速音频控制", + "audio_speakers_title": "扬声器", + "audio_speakers_description": "从目标设备到扬声器的音频", + "audio_microphone_title": "麦克风", + "audio_microphone_description": "麦克风输入到目标设备", + "audio_https_only": "仅限 HTTPS", + "audio_settings_description": "配置 JetKVM 设备的音频输入和输出设置", + "audio_settings_hdmi_label": "HDMI", + "audio_settings_input_description": "启用或禁用到远程计算机的麦克风音频", + "audio_settings_input_title": "音频输入", + "audio_settings_output_description": "启用或禁用来自远程计算机的音频", + "audio_settings_output_source_description": "选择音频捕获设备(HDMI 或 USB)", + "audio_settings_output_source_failed": "设置音频输出源失败:{error}", + "audio_settings_output_source_success": "音频输出源更新成功", + "audio_settings_output_source_title": "音频输出源", + "audio_settings_output_title": "音频输出", + "audio_settings_title": "音频", + "audio_settings_usb_label": "USB", + "audio_settings_auto_enable_microphone_title": "自动启用麦克风", + "audio_settings_auto_enable_microphone_description": "连接时自动启用浏览器麦克风(否则您必须在每次会话中手动启用)", "action_bar_extension": "扩展", "action_bar_fullscreen": "全屏", "action_bar_settings": "设置", @@ -790,6 +826,8 @@ "usb_device_description": "在目标计算机上仿真的 USB 设备", "usb_device_enable_absolute_mouse_description": "启用绝对鼠标(指针)", "usb_device_enable_absolute_mouse_title": "启用绝对鼠标(指针)", + "usb_device_enable_audio_description": "启用双向音频", + "usb_device_enable_audio_title": "启用 USB 音频", "usb_device_enable_keyboard_description": "启用键盘", "usb_device_enable_keyboard_title": "启用键盘", "usb_device_enable_mass_storage_description": "有时可能需要禁用它以防止某些设备出现问题", @@ -799,6 +837,7 @@ "usb_device_failed_load": "无法加载 USB 设备: {error}", "usb_device_failed_set": "无法设置 USB 设备: {error}", "usb_device_keyboard_mouse_and_mass_storage": "键盘、鼠标和大容量存储器", + "usb_device_keyboard_mouse_mass_storage_and_audio": "键盘、鼠标、大容量存储和音频", "usb_device_keyboard_only": "仅限键盘", "usb_device_restore_default": "恢复默认设置", "usb_device_title": "USB 设备", diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx index 3a6a283d..ed9dc72d 100644 --- a/ui/src/components/ActionBar.tsx +++ b/ui/src/components/ActionBar.tsx @@ -1,6 +1,6 @@ import { Fragment, useCallback, useRef } from "react"; import { MdOutlineContentPasteGo } from "react-icons/md"; -import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu"; +import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal, LuVolume2 } from "react-icons/lu"; import { FaKeyboard } from "react-icons/fa6"; import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import { CommandLineIcon } from "@heroicons/react/20/solid"; @@ -19,6 +19,7 @@ import PasteModal from "@components/popovers/PasteModal"; import WakeOnLanModal from "@components/popovers/WakeOnLan/Index"; import MountPopopover from "@components/popovers/MountPopover"; import ExtensionPopover from "@components/popovers/ExtensionPopover"; +import AudioPopover from "@components/popovers/AudioPopover"; import { m } from "@localizations/messages.js"; export default function Actionbar({ @@ -201,6 +202,36 @@ export default function Actionbar({ onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)} /> + + +