mirror of https://github.com/jetkvm/kvm.git
Add runtime configurable audio parameters with UI controls
- Add config fields for bitrate, complexity, DTX, FEC, buffer periods - Add RPC methods for get/set audio config and restart - Add UI settings page with controls for all audio parameters - Add Apply Settings button to restart audio with new config - Add config migration for backwards compatibility - Add translations for all 9 languages - Clean up redundant comments and optimize log levels
This commit is contained in:
parent
e79c6f730e
commit
922a7158e7
106
audio.go
106
audio.go
|
|
@ -39,7 +39,23 @@ func initAudio() {
|
|||
audioInitialized = true
|
||||
}
|
||||
|
||||
// startAudio starts audio sources and relays (skips already running ones)
|
||||
func getAudioConfig() audio.AudioConfig {
|
||||
ensureConfigLoaded()
|
||||
cfg := audio.DefaultAudioConfig()
|
||||
if config.AudioBitrate >= 64 && config.AudioBitrate <= 256 {
|
||||
cfg.Bitrate = uint16(config.AudioBitrate)
|
||||
}
|
||||
if config.AudioComplexity >= 0 && config.AudioComplexity <= 10 {
|
||||
cfg.Complexity = uint8(config.AudioComplexity)
|
||||
}
|
||||
cfg.DTXEnabled = config.AudioDTXEnabled
|
||||
cfg.FECEnabled = config.AudioFECEnabled
|
||||
if config.AudioBufferPeriods >= 2 && config.AudioBufferPeriods <= 24 {
|
||||
cfg.BufferPeriods = uint8(config.AudioBufferPeriods)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func startAudio() error {
|
||||
audioMutex.Lock()
|
||||
defer audioMutex.Unlock()
|
||||
|
|
@ -49,30 +65,32 @@ func startAudio() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Start output audio if not running, enabled, and we have a track
|
||||
if outputSource == nil && audioOutputEnabled.Load() && currentAudioTrack != nil {
|
||||
ensureConfigLoaded()
|
||||
alsaDevice := "hw:1,0" // USB audio (default)
|
||||
alsaDevice := "hw:1,0"
|
||||
if config.AudioOutputSource == "hdmi" {
|
||||
alsaDevice = "hw:0,0" // HDMI audio
|
||||
alsaDevice = "hw:0,0"
|
||||
}
|
||||
|
||||
outputSource = audio.NewCgoOutputSource(alsaDevice)
|
||||
source := audio.NewCgoOutputSource(alsaDevice)
|
||||
source.SetConfig(getAudioConfig())
|
||||
outputSource = source
|
||||
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.Load() == nil && audioInputEnabled.Load() && config.UsbDevices != nil && config.UsbDevices.Audio {
|
||||
alsaPlaybackDevice := "hw:1,0" // USB speakers
|
||||
alsaPlaybackDevice := "hw:1,0"
|
||||
|
||||
var source audio.AudioSource = audio.NewCgoInputSource(alsaPlaybackDevice)
|
||||
inputSource.Store(&source)
|
||||
source := audio.NewCgoInputSource(alsaPlaybackDevice)
|
||||
source.SetConfig(getAudioConfig())
|
||||
var audioSource audio.AudioSource = source
|
||||
inputSource.Store(&audioSource)
|
||||
|
||||
inputRelay = audio.NewInputRelay(source)
|
||||
inputRelay = audio.NewInputRelay(audioSource)
|
||||
if err := inputRelay.Start(); err != nil {
|
||||
audioLogger.Error().Err(err).Msg("Failed to start input relay")
|
||||
}
|
||||
|
|
@ -158,11 +176,12 @@ func setAudioTrack(audioTrack *webrtc.TrackLocalStaticSample) {
|
|||
audioMutex.Lock()
|
||||
if currentAudioTrack != nil && audioOutputEnabled.Load() {
|
||||
ensureConfigLoaded()
|
||||
alsaDevice := "hw:1,0" // USB audio (default)
|
||||
alsaDevice := "hw:1,0"
|
||||
if config.AudioOutputSource == "hdmi" {
|
||||
alsaDevice = "hw:0,0" // HDMI audio
|
||||
alsaDevice = "hw:0,0"
|
||||
}
|
||||
newSource := audio.NewCgoOutputSource(alsaDevice)
|
||||
newSource.SetConfig(getAudioConfig())
|
||||
newRelay := audio.NewOutputRelay(newSource, currentAudioTrack)
|
||||
outputSource = newSource
|
||||
outputRelay = newRelay
|
||||
|
|
@ -187,10 +206,9 @@ func setPendingInputTrack(track *webrtc.TrackRemote) {
|
|||
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
|
||||
return nil
|
||||
}
|
||||
|
||||
if enabled {
|
||||
|
|
@ -204,10 +222,9 @@ func SetAudioOutputEnabled(enabled bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// SetAudioInputEnabled enables or disables audio input
|
||||
func SetAudioInputEnabled(enabled bool) error {
|
||||
if audioInputEnabled.Swap(enabled) == enabled {
|
||||
return nil // Already in desired state
|
||||
return nil
|
||||
}
|
||||
|
||||
if enabled {
|
||||
|
|
@ -221,7 +238,6 @@ func SetAudioInputEnabled(enabled bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// SetAudioOutputSource switches between HDMI and USB audio sources
|
||||
func SetAudioOutputSource(source string) error {
|
||||
if source != "hdmi" && source != "usb" {
|
||||
return nil
|
||||
|
|
@ -237,12 +253,13 @@ func SetAudioOutputSource(source string) error {
|
|||
stopOutputAudio()
|
||||
|
||||
if audioOutputEnabled.Load() && activeConnections.Load() > 0 && currentAudioTrack != nil {
|
||||
alsaDevice := "hw:1,0" // USB
|
||||
alsaDevice := "hw:1,0"
|
||||
if source == "hdmi" {
|
||||
alsaDevice = "hw:0,0" // HDMI
|
||||
alsaDevice = "hw:0,0"
|
||||
}
|
||||
|
||||
newSource := audio.NewCgoOutputSource(alsaDevice)
|
||||
newSource.SetConfig(getAudioConfig())
|
||||
newRelay := audio.NewOutputRelay(newSource, currentAudioTrack)
|
||||
|
||||
audioMutex.Lock()
|
||||
|
|
@ -258,54 +275,79 @@ func SetAudioOutputSource(source string) error {
|
|||
return SaveConfig()
|
||||
}
|
||||
|
||||
// handleInputTrackForSession runs for the entire WebRTC session lifetime
|
||||
// It continuously reads from the track and sends to whatever relay is currently active
|
||||
func RestartAudioOutput() {
|
||||
audioMutex.Lock()
|
||||
hasActiveOutput := outputSource != nil && currentAudioTrack != nil && audioOutputEnabled.Load()
|
||||
audioMutex.Unlock()
|
||||
|
||||
if !hasActiveOutput {
|
||||
return
|
||||
}
|
||||
|
||||
audioLogger.Info().Msg("Restarting audio output")
|
||||
|
||||
stopOutputAudio()
|
||||
|
||||
ensureConfigLoaded()
|
||||
alsaDevice := "hw:1,0"
|
||||
if config.AudioOutputSource == "hdmi" {
|
||||
alsaDevice = "hw:0,0"
|
||||
}
|
||||
|
||||
newSource := audio.NewCgoOutputSource(alsaDevice)
|
||||
newSource.SetConfig(getAudioConfig())
|
||||
newRelay := audio.NewOutputRelay(newSource, currentAudioTrack)
|
||||
|
||||
audioMutex.Lock()
|
||||
outputSource = newSource
|
||||
outputRelay = newRelay
|
||||
audioMutex.Unlock()
|
||||
|
||||
if err := newRelay.Start(); err != nil {
|
||||
audioLogger.Error().Err(err).Msg("Failed to restart audio output")
|
||||
}
|
||||
}
|
||||
|
||||
func handleInputTrackForSession(track *webrtc.TrackRemote) {
|
||||
myTrackID := track.ID()
|
||||
|
||||
audioLogger.Debug().
|
||||
Str("codec", track.Codec().MimeType).
|
||||
Str("track_id", myTrackID).
|
||||
Msg("starting session-lifetime track handler")
|
||||
Msg("starting input track handler")
|
||||
|
||||
for {
|
||||
// Check if we've been superseded by a new track
|
||||
currentTrackID := currentInputTrack.Load()
|
||||
if currentTrackID != nil && *currentTrackID != myTrackID {
|
||||
audioLogger.Debug().
|
||||
Str("my_track_id", myTrackID).
|
||||
Str("current_track_id", *currentTrackID).
|
||||
Msg("audio track handler exiting - superseded by new track")
|
||||
Msg("input track handler exiting - superseded")
|
||||
return
|
||||
}
|
||||
|
||||
// Read RTP packet (must always read to keep track alive)
|
||||
rtpPacket, _, err := track.ReadRTP()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
audioLogger.Debug().Str("track_id", myTrackID).Msg("audio track ended")
|
||||
audioLogger.Debug().Str("track_id", myTrackID).Msg("input track ended")
|
||||
return
|
||||
}
|
||||
audioLogger.Warn().Err(err).Str("track_id", myTrackID).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
|
||||
continue
|
||||
}
|
||||
|
||||
// Lock-free source access (hot path optimization)
|
||||
source := inputSource.Load()
|
||||
|
||||
if source == nil {
|
||||
continue // No relay, drop frame but keep reading
|
||||
continue
|
||||
}
|
||||
|
||||
inputSourceMutex.Lock()
|
||||
|
|
|
|||
19
config.go
19
config.go
|
|
@ -110,6 +110,11 @@ type Config struct {
|
|||
AudioInputAutoEnable bool `json:"audio_input_auto_enable"`
|
||||
AudioOutputEnabled bool `json:"audio_output_enabled"`
|
||||
AudioOutputSource string `json:"audio_output_source"` // "hdmi" or "usb"
|
||||
AudioBitrate int `json:"audio_bitrate"` // kbps (64-256)
|
||||
AudioComplexity int `json:"audio_complexity"` // 0-10
|
||||
AudioDTXEnabled bool `json:"audio_dtx_enabled"`
|
||||
AudioFECEnabled bool `json:"audio_fec_enabled"`
|
||||
AudioBufferPeriods int `json:"audio_buffer_periods"` // 2-24
|
||||
}
|
||||
|
||||
func (c *Config) GetDisplayRotation() uint16 {
|
||||
|
|
@ -186,6 +191,11 @@ func getDefaultConfig() Config {
|
|||
AudioInputAutoEnable: false,
|
||||
AudioOutputEnabled: true,
|
||||
AudioOutputSource: "usb",
|
||||
AudioBitrate: 128,
|
||||
AudioComplexity: 5,
|
||||
AudioDTXEnabled: true,
|
||||
AudioFECEnabled: true,
|
||||
AudioBufferPeriods: 12,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -256,6 +266,15 @@ func LoadConfig() {
|
|||
loadedConfig.JigglerConfig = getDefaultConfig().JigglerConfig
|
||||
}
|
||||
|
||||
if loadedConfig.AudioBitrate == 0 {
|
||||
defaults := getDefaultConfig()
|
||||
loadedConfig.AudioBitrate = defaults.AudioBitrate
|
||||
loadedConfig.AudioComplexity = defaults.AudioComplexity
|
||||
loadedConfig.AudioDTXEnabled = defaults.AudioDTXEnabled
|
||||
loadedConfig.AudioFECEnabled = defaults.AudioFECEnabled
|
||||
loadedConfig.AudioBufferPeriods = defaults.AudioBufferPeriods
|
||||
}
|
||||
|
||||
// fixup old keyboard layout value
|
||||
if loadedConfig.KeyboardLayout == "en_US" {
|
||||
loadedConfig.KeyboardLayout = "en-US"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
* 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)
|
||||
*
|
||||
|
|
@ -56,18 +55,20 @@ 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 uint8_t opus_complexity = 5;
|
||||
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
|
||||
#define OPUS_VBR 1
|
||||
#define OPUS_VBR_CONSTRAINT 1
|
||||
#define OPUS_SIGNAL_TYPE 3002
|
||||
#define OPUS_BANDWIDTH 1104
|
||||
#define OPUS_LSB_DEPTH 16
|
||||
|
||||
static uint8_t opus_dtx_enabled = 1;
|
||||
static uint8_t opus_fec_enabled = 1;
|
||||
static uint8_t opus_packet_loss_perc = 20;
|
||||
static uint8_t buffer_period_count = 12;
|
||||
|
||||
// ALSA retry configuration
|
||||
static uint32_t sleep_microseconds = 1000;
|
||||
static uint32_t sleep_milliseconds = 1;
|
||||
static uint8_t max_attempts_global = 5;
|
||||
|
|
@ -86,43 +87,45 @@ 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);
|
||||
uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff,
|
||||
uint8_t dtx_enabled, uint8_t fec_enabled, uint8_t buf_periods);
|
||||
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);
|
||||
uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff,
|
||||
uint8_t buf_periods);
|
||||
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;
|
||||
uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff,
|
||||
uint8_t dtx_enabled, uint8_t fec_enabled, uint8_t buf_periods) {
|
||||
opus_bitrate = (bitrate >= 64000 && bitrate <= 256000) ? bitrate : 128000;
|
||||
opus_complexity = (complexity <= 10) ? complexity : 5;
|
||||
sample_rate = sr > 0 ? sr : 48000;
|
||||
channels = (ch == 1 || ch == 2) ? ch : 2;
|
||||
frame_size = fs > 0 ? fs : 960;
|
||||
max_packet_size = max_pkt > 0 ? max_pkt : 1500;
|
||||
sleep_microseconds = sleep_us > 0 ? sleep_us : 1000;
|
||||
sleep_milliseconds = sleep_microseconds / 1000;
|
||||
max_attempts_global = max_attempts > 0 ? max_attempts : 5;
|
||||
max_backoff_us_global = max_backoff > 0 ? max_backoff : 500000;
|
||||
opus_dtx_enabled = dtx_enabled ? 1 : 0;
|
||||
opus_fec_enabled = fec_enabled ? 1 : 0;
|
||||
buffer_period_count = (buf_periods >= 2 && buf_periods <= 24) ? buf_periods : 12;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff,
|
||||
uint8_t buf_periods) {
|
||||
sample_rate = sr > 0 ? sr : 48000;
|
||||
channels = (ch == 1 || ch == 2) ? ch : 2;
|
||||
frame_size = fs > 0 ? fs : 960;
|
||||
max_packet_size = max_pkt > 0 ? max_pkt : 1500;
|
||||
sleep_microseconds = sleep_us > 0 ? sleep_us : 1000;
|
||||
sleep_milliseconds = sleep_microseconds / 1000;
|
||||
max_attempts_global = max_attempts > 0 ? max_attempts : 5;
|
||||
max_backoff_us_global = max_backoff > 0 ? max_backoff : 500000;
|
||||
buffer_period_count = (buf_periods >= 2 && buf_periods <= 24) ? buf_periods : 12;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -233,18 +236,17 @@ static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream
|
|||
|
||||
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;
|
||||
backoff_us = (backoff_us < 50000) ? (backoff_us << 1) : 50000;
|
||||
} 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;
|
||||
precise_sleep_us(backoff_us);
|
||||
backoff_us = (backoff_us < 50000) ? (backoff_us << 1) : 50000;
|
||||
} 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;
|
||||
backoff_us = (backoff_us < 50000) ? (backoff_us << 1) : 50000;
|
||||
}
|
||||
}
|
||||
return err;
|
||||
|
|
@ -285,13 +287,13 @@ static int configure_alsa_device(snd_pcm_t *handle, const char *device_name) {
|
|||
if (err < 0) return err;
|
||||
}
|
||||
|
||||
snd_pcm_uframes_t period_size = frame_size; // Optimized: use full frame as period
|
||||
snd_pcm_uframes_t period_size = frame_size;
|
||||
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 * 12; // 12 periods = 240ms buffer for better jitter tolerance
|
||||
snd_pcm_uframes_t buffer_size = period_size * buffer_period_count;
|
||||
err = snd_pcm_hw_params_set_buffer_size_near(handle, params, &buffer_size);
|
||||
if (err < 0) return err;
|
||||
|
||||
|
|
@ -378,11 +380,11 @@ int jetkvm_audio_capture_init() {
|
|||
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_DTX(opus_dtx_enabled));
|
||||
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));
|
||||
opus_encoder_ctl(encoder, OPUS_SET_INBAND_FEC(opus_fec_enabled));
|
||||
opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(opus_packet_loss_perc));
|
||||
|
||||
capture_initialized = 1;
|
||||
capture_initializing = 0;
|
||||
|
|
|
|||
|
|
@ -21,21 +21,20 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
ipcMaxFrameSize = 1024 // Max Opus frame size: 128kbps @ 20ms = ~600 bytes
|
||||
ipcMaxFrameSize = 1024
|
||||
)
|
||||
|
||||
// CgoSource implements AudioSource via direct CGO calls to C audio functions (in-process)
|
||||
type CgoSource struct {
|
||||
direction string // "output" or "input"
|
||||
direction string
|
||||
alsaDevice string
|
||||
initialized bool
|
||||
connected bool
|
||||
mu sync.Mutex
|
||||
logger zerolog.Logger
|
||||
opusBuf []byte // Reusable buffer for Opus packets
|
||||
opusBuf []byte
|
||||
config AudioConfig
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
|
|
@ -44,10 +43,10 @@ func NewCgoOutputSource(alsaDevice string) *CgoSource {
|
|||
alsaDevice: alsaDevice,
|
||||
logger: logger,
|
||||
opusBuf: make([]byte, ipcMaxFrameSize),
|
||||
config: DefaultAudioConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
|
|
@ -56,10 +55,16 @@ func NewCgoInputSource(alsaDevice string) *CgoSource {
|
|||
alsaDevice: alsaDevice,
|
||||
logger: logger,
|
||||
opusBuf: make([]byte, ipcMaxFrameSize),
|
||||
config: DefaultAudioConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// Connect initializes the C audio subsystem
|
||||
func (c *CgoSource) SetConfig(cfg AudioConfig) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.config = cfg
|
||||
}
|
||||
|
||||
func (c *CgoSource) Connect() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
|
@ -68,46 +73,61 @@ func (c *CgoSource) Connect() error {
|
|||
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
|
||||
dtx := C.uchar(0)
|
||||
if c.config.DTXEnabled {
|
||||
dtx = C.uchar(1)
|
||||
}
|
||||
fec := C.uchar(0)
|
||||
if c.config.FECEnabled {
|
||||
fec = C.uchar(1)
|
||||
}
|
||||
|
||||
c.logger.Debug().
|
||||
Uint16("bitrate_kbps", c.config.Bitrate).
|
||||
Uint8("complexity", c.config.Complexity).
|
||||
Bool("dtx", c.config.DTXEnabled).
|
||||
Bool("fec", c.config.FECEnabled).
|
||||
Uint8("buffer_periods", c.config.BufferPeriods).
|
||||
Str("alsa_device", c.alsaDevice).
|
||||
Msg("Initializing audio capture")
|
||||
|
||||
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
|
||||
C.uint(uint32(c.config.Bitrate)*1000),
|
||||
C.uchar(c.config.Complexity),
|
||||
C.uint(48000),
|
||||
C.uchar(2),
|
||||
C.ushort(960),
|
||||
C.ushort(1500),
|
||||
C.uint(1000),
|
||||
C.uchar(5),
|
||||
C.uint(500000),
|
||||
dtx,
|
||||
fec,
|
||||
C.uchar(c.config.BufferPeriods),
|
||||
)
|
||||
|
||||
// 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
|
||||
C.uint(48000),
|
||||
C.uchar(2),
|
||||
C.ushort(960),
|
||||
C.ushort(1500),
|
||||
C.uint(1000),
|
||||
C.uchar(5),
|
||||
C.uint(500000),
|
||||
C.uchar(c.config.BufferPeriods),
|
||||
)
|
||||
|
||||
// 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")
|
||||
|
|
@ -120,7 +140,6 @@ func (c *CgoSource) Connect() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Disconnect closes the C audio subsystem
|
||||
func (c *CgoSource) Disconnect() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
|
@ -138,17 +157,12 @@ func (c *CgoSource) Disconnect() {
|
|||
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()
|
||||
|
|
@ -161,25 +175,18 @@ func (c *CgoSource) ReadMessage() (uint8, []byte, error) {
|
|||
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()
|
||||
|
|
@ -193,7 +200,6 @@ func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error {
|
|||
}
|
||||
|
||||
if msgType != ipcMsgTypeOpus {
|
||||
// Ignore non-Opus messages
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -201,9 +207,7 @@ func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error {
|
|||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,3 +33,7 @@ func (c *CgoSource) ReadMessage() (uint8, []byte, error) {
|
|||
func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error {
|
||||
panic("audio CGO source not supported on this platform")
|
||||
}
|
||||
|
||||
func (c *CgoSource) SetConfig(cfg AudioConfig) {
|
||||
panic("audio CGO source not supported on this platform")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import (
|
|||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// OutputRelay forwards audio from AudioSource (CGO) to WebRTC (browser)
|
||||
type OutputRelay struct {
|
||||
source AudioSource
|
||||
audioTrack *webrtc.TrackLocalStaticSample
|
||||
|
|
@ -23,12 +22,10 @@ type OutputRelay struct {
|
|||
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()
|
||||
|
|
@ -46,7 +43,6 @@ func NewOutputRelay(source AudioSource, audioTrack *webrtc.TrackLocalStaticSampl
|
|||
}
|
||||
}
|
||||
|
||||
// Start begins relaying audio frames
|
||||
func (r *OutputRelay) Start() error {
|
||||
if r.running.Swap(true) {
|
||||
return fmt.Errorf("output relay already running")
|
||||
|
|
@ -57,7 +53,6 @@ func (r *OutputRelay) Start() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the relay and waits for goroutine to exit
|
||||
func (r *OutputRelay) Stop() {
|
||||
if !r.running.Swap(false) {
|
||||
return
|
||||
|
|
@ -72,14 +67,12 @@ func (r *OutputRelay) Stop() {
|
|||
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")
|
||||
|
|
@ -88,10 +81,8 @@ func (r *OutputRelay) relayLoop() {
|
|||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
|
@ -100,11 +91,8 @@ func (r *OutputRelay) relayLoop() {
|
|||
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")
|
||||
|
|
@ -115,7 +103,6 @@ func (r *OutputRelay) relayLoop() {
|
|||
}
|
||||
}
|
||||
|
||||
// InputRelay forwards audio from WebRTC (browser microphone) to AudioSource (USB audio)
|
||||
type InputRelay struct {
|
||||
source AudioSource
|
||||
ctx context.Context
|
||||
|
|
@ -124,7 +111,6 @@ type InputRelay struct {
|
|||
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()
|
||||
|
|
@ -137,7 +123,6 @@ func NewInputRelay(source AudioSource) *InputRelay {
|
|||
}
|
||||
}
|
||||
|
||||
// Start begins relaying audio frames
|
||||
func (r *InputRelay) Start() error {
|
||||
if r.running.Swap(true) {
|
||||
return fmt.Errorf("input relay already running")
|
||||
|
|
@ -147,7 +132,6 @@ func (r *InputRelay) Start() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the relay
|
||||
func (r *InputRelay) Stop() {
|
||||
if !r.running.Swap(false) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,28 +1,32 @@
|
|||
package audio
|
||||
|
||||
// IPC message types
|
||||
const (
|
||||
ipcMsgTypeOpus = 0 // Message type for Opus audio data
|
||||
ipcMsgTypeOpus = 0
|
||||
)
|
||||
|
||||
// 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()
|
||||
type AudioConfig struct {
|
||||
Bitrate uint16
|
||||
Complexity uint8
|
||||
BufferPeriods uint8
|
||||
DTXEnabled bool
|
||||
FECEnabled bool
|
||||
}
|
||||
|
||||
func DefaultAudioConfig() AudioConfig {
|
||||
return AudioConfig{
|
||||
Bitrate: 128,
|
||||
Complexity: 5,
|
||||
BufferPeriods: 12,
|
||||
DTXEnabled: true,
|
||||
FECEnabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
type AudioSource interface {
|
||||
ReadMessage() (msgType uint8, payload []byte, err error)
|
||||
WriteMessage(msgType uint8, payload []byte) error
|
||||
IsConnected() bool
|
||||
Connect() error
|
||||
Disconnect()
|
||||
SetConfig(cfg AudioConfig)
|
||||
}
|
||||
|
|
|
|||
57
jsonrpc.go
57
jsonrpc.go
|
|
@ -1000,6 +1000,60 @@ func rpcSetAudioOutputSource(source string) error {
|
|||
return SetAudioOutputSource(source)
|
||||
}
|
||||
|
||||
type AudioConfigResponse struct {
|
||||
Bitrate int `json:"bitrate"`
|
||||
Complexity int `json:"complexity"`
|
||||
DTXEnabled bool `json:"dtx_enabled"`
|
||||
FECEnabled bool `json:"fec_enabled"`
|
||||
BufferPeriods int `json:"buffer_periods"`
|
||||
}
|
||||
|
||||
func rpcGetAudioConfig() (AudioConfigResponse, error) {
|
||||
ensureConfigLoaded()
|
||||
bitrate := config.AudioBitrate
|
||||
if bitrate < 64 || bitrate > 256 {
|
||||
bitrate = 128
|
||||
}
|
||||
bufferPeriods := config.AudioBufferPeriods
|
||||
if bufferPeriods < 2 || bufferPeriods > 24 {
|
||||
bufferPeriods = 12
|
||||
}
|
||||
return AudioConfigResponse{
|
||||
Bitrate: bitrate,
|
||||
Complexity: config.AudioComplexity,
|
||||
DTXEnabled: config.AudioDTXEnabled,
|
||||
FECEnabled: config.AudioFECEnabled,
|
||||
BufferPeriods: bufferPeriods,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func rpcSetAudioConfig(bitrate int, complexity int, dtxEnabled bool, fecEnabled bool, bufferPeriods int) error {
|
||||
ensureConfigLoaded()
|
||||
|
||||
if bitrate < 64 || bitrate > 256 {
|
||||
return fmt.Errorf("bitrate must be between 64 and 256 kbps")
|
||||
}
|
||||
if complexity < 0 || complexity > 10 {
|
||||
return fmt.Errorf("complexity must be between 0 and 10")
|
||||
}
|
||||
if bufferPeriods < 2 || bufferPeriods > 24 {
|
||||
return fmt.Errorf("buffer periods must be between 2 and 24")
|
||||
}
|
||||
|
||||
config.AudioBitrate = bitrate
|
||||
config.AudioComplexity = complexity
|
||||
config.AudioDTXEnabled = dtxEnabled
|
||||
config.AudioFECEnabled = fecEnabled
|
||||
config.AudioBufferPeriods = bufferPeriods
|
||||
|
||||
return SaveConfig()
|
||||
}
|
||||
|
||||
func rpcRestartAudioOutput() error {
|
||||
RestartAudioOutput()
|
||||
return nil
|
||||
}
|
||||
|
||||
func rpcGetAudioInputAutoEnable() (bool, error) {
|
||||
ensureConfigLoaded()
|
||||
return config.AudioInputAutoEnable, nil
|
||||
|
|
@ -1340,6 +1394,9 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"getAudioOutputSource": {Func: rpcGetAudioOutputSource},
|
||||
"setAudioOutputSource": {Func: rpcSetAudioOutputSource, Params: []string{"source"}},
|
||||
"refreshHdmiConnection": {Func: rpcRefreshHdmiConnection},
|
||||
"getAudioConfig": {Func: rpcGetAudioConfig},
|
||||
"setAudioConfig": {Func: rpcSetAudioConfig, Params: []string{"bitrate", "complexity", "dtxEnabled", "fecEnabled", "bufferPeriods"}},
|
||||
"restartAudioOutput": {Func: rpcRestartAudioOutput},
|
||||
"getAudioInputAutoEnable": {Func: rpcGetAudioInputAutoEnable},
|
||||
"setAudioInputAutoEnable": {Func: rpcSetAudioInputAutoEnable, Params: []string{"enabled"}},
|
||||
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
|
||||
|
|
|
|||
|
|
@ -86,6 +86,19 @@
|
|||
"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)",
|
||||
"audio_settings_bitrate_title": "Opus Bitrate",
|
||||
"audio_settings_bitrate_description": "Lydkodningsbitrate (højere = bedre kvalitet, mere båndbredde)",
|
||||
"audio_settings_complexity_title": "Opus Kompleksitet",
|
||||
"audio_settings_complexity_description": "Encoder-kompleksitet (0-10, højere = bedre kvalitet, mere CPU)",
|
||||
"audio_settings_dtx_title": "DTX (Diskontinuerlig Transmission)",
|
||||
"audio_settings_dtx_description": "Spar båndbredde under stilhed",
|
||||
"audio_settings_fec_title": "FEC (Fremadrettet Fejlkorrektion)",
|
||||
"audio_settings_fec_description": "Forbedre lydkvaliteten på tabende forbindelser",
|
||||
"audio_settings_buffer_title": "Bufferperioder",
|
||||
"audio_settings_buffer_description": "ALSA bufferstørrelse (højere = mere stabil, mere latens)",
|
||||
"audio_settings_config_updated": "Lydkonfiguration opdateret",
|
||||
"audio_settings_apply_button": "Anvend indstillinger",
|
||||
"audio_settings_applied": "Lydindstillinger anvendt",
|
||||
"action_bar_extension": "Udvidelse",
|
||||
"action_bar_fullscreen": "Fuldskærm",
|
||||
"action_bar_settings": "Indstillinger",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,19 @@
|
|||
"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)",
|
||||
"audio_settings_bitrate_title": "Opus Bitrate",
|
||||
"audio_settings_bitrate_description": "Audio-Codierungsbitrate (höher = bessere Qualität, mehr Bandbreite)",
|
||||
"audio_settings_complexity_title": "Opus Komplexität",
|
||||
"audio_settings_complexity_description": "Encoder-Komplexität (0-10, höher = bessere Qualität, mehr CPU)",
|
||||
"audio_settings_dtx_title": "DTX (Discontinuous Transmission)",
|
||||
"audio_settings_dtx_description": "Bandbreite während Stille sparen",
|
||||
"audio_settings_fec_title": "FEC (Forward Error Correction)",
|
||||
"audio_settings_fec_description": "Audioqualität bei verlustbehafteten Verbindungen verbessern",
|
||||
"audio_settings_buffer_title": "Pufferperioden",
|
||||
"audio_settings_buffer_description": "ALSA-Puffergröße (höher = stabiler, mehr Latenz)",
|
||||
"audio_settings_config_updated": "Audiokonfiguration aktualisiert",
|
||||
"audio_settings_apply_button": "Einstellungen anwenden",
|
||||
"audio_settings_applied": "Audioeinstellungen angewendet",
|
||||
"action_bar_extension": "Erweiterung",
|
||||
"action_bar_fullscreen": "Vollbild",
|
||||
"action_bar_settings": "Einstellungen",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,19 @@
|
|||
"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)",
|
||||
"audio_settings_bitrate_title": "Opus Bitrate",
|
||||
"audio_settings_bitrate_description": "Audio encoding bitrate (higher = better quality, more bandwidth)",
|
||||
"audio_settings_complexity_title": "Opus Complexity",
|
||||
"audio_settings_complexity_description": "Encoder complexity (0-10, higher = better quality, more CPU)",
|
||||
"audio_settings_dtx_title": "DTX (Discontinuous Transmission)",
|
||||
"audio_settings_dtx_description": "Save bandwidth during silence",
|
||||
"audio_settings_fec_title": "FEC (Forward Error Correction)",
|
||||
"audio_settings_fec_description": "Improve audio quality on lossy connections",
|
||||
"audio_settings_buffer_title": "Buffer Periods",
|
||||
"audio_settings_buffer_description": "ALSA buffer size (higher = more stable, more latency)",
|
||||
"audio_settings_config_updated": "Audio configuration updated",
|
||||
"audio_settings_apply_button": "Apply Settings",
|
||||
"audio_settings_applied": "Audio settings applied",
|
||||
"action_bar_extension": "Extension",
|
||||
"action_bar_fullscreen": "Fullscreen",
|
||||
"action_bar_settings": "Settings",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,19 @@
|
|||
"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)",
|
||||
"audio_settings_bitrate_title": "Bitrate Opus",
|
||||
"audio_settings_bitrate_description": "Tasa de bits de codificación de audio (mayor = mejor calidad, más ancho de banda)",
|
||||
"audio_settings_complexity_title": "Complejidad Opus",
|
||||
"audio_settings_complexity_description": "Complejidad del codificador (0-10, mayor = mejor calidad, más CPU)",
|
||||
"audio_settings_dtx_title": "DTX (Transmisión Discontinua)",
|
||||
"audio_settings_dtx_description": "Ahorrar ancho de banda durante el silencio",
|
||||
"audio_settings_fec_title": "FEC (Corrección de Errores)",
|
||||
"audio_settings_fec_description": "Mejorar la calidad de audio en conexiones con pérdida",
|
||||
"audio_settings_buffer_title": "Períodos de Buffer",
|
||||
"audio_settings_buffer_description": "Tamaño del buffer ALSA (mayor = más estable, más latencia)",
|
||||
"audio_settings_config_updated": "Configuración de audio actualizada",
|
||||
"audio_settings_apply_button": "Aplicar configuración",
|
||||
"audio_settings_applied": "Configuración de audio aplicada",
|
||||
"action_bar_extension": "Extensión",
|
||||
"action_bar_fullscreen": "Pantalla completa",
|
||||
"action_bar_settings": "Ajustes",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,19 @@
|
|||
"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)",
|
||||
"audio_settings_bitrate_title": "Débit Opus",
|
||||
"audio_settings_bitrate_description": "Débit d'encodage audio (plus élevé = meilleure qualité, plus de bande passante)",
|
||||
"audio_settings_complexity_title": "Complexité Opus",
|
||||
"audio_settings_complexity_description": "Complexité de l'encodeur (0-10, plus élevé = meilleure qualité, plus de CPU)",
|
||||
"audio_settings_dtx_title": "DTX (Transmission Discontinue)",
|
||||
"audio_settings_dtx_description": "Économiser la bande passante pendant le silence",
|
||||
"audio_settings_fec_title": "FEC (Correction d'Erreur)",
|
||||
"audio_settings_fec_description": "Améliorer la qualité audio sur les connexions avec perte",
|
||||
"audio_settings_buffer_title": "Périodes de Tampon",
|
||||
"audio_settings_buffer_description": "Taille du tampon ALSA (plus élevé = plus stable, plus de latence)",
|
||||
"audio_settings_config_updated": "Configuration audio mise à jour",
|
||||
"audio_settings_apply_button": "Appliquer les paramètres",
|
||||
"audio_settings_applied": "Paramètres audio appliqués",
|
||||
"action_bar_extension": "Extension",
|
||||
"action_bar_fullscreen": "Plein écran",
|
||||
"action_bar_settings": "Paramètres",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,19 @@
|
|||
"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)",
|
||||
"audio_settings_bitrate_title": "Bitrate Opus",
|
||||
"audio_settings_bitrate_description": "Bitrate di codifica audio (più alto = migliore qualità, più banda)",
|
||||
"audio_settings_complexity_title": "Complessità Opus",
|
||||
"audio_settings_complexity_description": "Complessità dell'encoder (0-10, più alto = migliore qualità, più CPU)",
|
||||
"audio_settings_dtx_title": "DTX (Trasmissione Discontinua)",
|
||||
"audio_settings_dtx_description": "Risparmia banda durante il silenzio",
|
||||
"audio_settings_fec_title": "FEC (Correzione Errori)",
|
||||
"audio_settings_fec_description": "Migliora la qualità audio su connessioni con perdita",
|
||||
"audio_settings_buffer_title": "Periodi Buffer",
|
||||
"audio_settings_buffer_description": "Dimensione buffer ALSA (più alto = più stabile, più latenza)",
|
||||
"audio_settings_config_updated": "Configurazione audio aggiornata",
|
||||
"audio_settings_apply_button": "Applica impostazioni",
|
||||
"audio_settings_applied": "Impostazioni audio applicate",
|
||||
"action_bar_extension": "Estensione",
|
||||
"action_bar_fullscreen": "A schermo intero",
|
||||
"action_bar_settings": "Impostazioni",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,19 @@
|
|||
"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)",
|
||||
"audio_settings_bitrate_title": "Opus Bitrate",
|
||||
"audio_settings_bitrate_description": "Lydkodingsbitrate (høyere = bedre kvalitet, mer båndbredde)",
|
||||
"audio_settings_complexity_title": "Opus Kompleksitet",
|
||||
"audio_settings_complexity_description": "Encoder-kompleksitet (0-10, høyere = bedre kvalitet, mer CPU)",
|
||||
"audio_settings_dtx_title": "DTX (Diskontinuerlig Overføring)",
|
||||
"audio_settings_dtx_description": "Spar båndbredde under stillhet",
|
||||
"audio_settings_fec_title": "FEC (Fremadrettet Feilkorreksjon)",
|
||||
"audio_settings_fec_description": "Forbedre lydkvaliteten på tapende tilkoblinger",
|
||||
"audio_settings_buffer_title": "Bufferperioder",
|
||||
"audio_settings_buffer_description": "ALSA bufferstørrelse (høyere = mer stabil, mer latens)",
|
||||
"audio_settings_config_updated": "Lydkonfigurasjon oppdatert",
|
||||
"audio_settings_apply_button": "Bruk innstillinger",
|
||||
"audio_settings_applied": "Lydinnstillinger brukt",
|
||||
"action_bar_extension": "Forlengelse",
|
||||
"action_bar_fullscreen": "Fullskjerm",
|
||||
"action_bar_settings": "Innstillinger",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,19 @@
|
|||
"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)",
|
||||
"audio_settings_bitrate_title": "Opus Bitrate",
|
||||
"audio_settings_bitrate_description": "Ljudkodningsbitrate (högre = bättre kvalitet, mer bandbredd)",
|
||||
"audio_settings_complexity_title": "Opus Komplexitet",
|
||||
"audio_settings_complexity_description": "Encoder-komplexitet (0-10, högre = bättre kvalitet, mer CPU)",
|
||||
"audio_settings_dtx_title": "DTX (Diskontinuerlig Överföring)",
|
||||
"audio_settings_dtx_description": "Spara bandbredd under tystnad",
|
||||
"audio_settings_fec_title": "FEC (Framåtriktad Felkorrigering)",
|
||||
"audio_settings_fec_description": "Förbättra ljudkvaliteten på förlustdrabbade anslutningar",
|
||||
"audio_settings_buffer_title": "Bufferperioder",
|
||||
"audio_settings_buffer_description": "ALSA bufferstorlek (högre = mer stabil, mer latens)",
|
||||
"audio_settings_config_updated": "Ljudkonfiguration uppdaterad",
|
||||
"audio_settings_apply_button": "Tillämpa inställningar",
|
||||
"audio_settings_applied": "Ljudinställningar tillämpade",
|
||||
"action_bar_extension": "Förlängning",
|
||||
"action_bar_fullscreen": "Helskärm",
|
||||
"action_bar_settings": "Inställningar",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,19 @@
|
|||
"audio_settings_usb_label": "USB",
|
||||
"audio_settings_auto_enable_microphone_title": "自动启用麦克风",
|
||||
"audio_settings_auto_enable_microphone_description": "连接时自动启用浏览器麦克风(否则您必须在每次会话中手动启用)",
|
||||
"audio_settings_bitrate_title": "Opus 比特率",
|
||||
"audio_settings_bitrate_description": "音频编码比特率(越高 = 质量越好,带宽越大)",
|
||||
"audio_settings_complexity_title": "Opus 复杂度",
|
||||
"audio_settings_complexity_description": "编码器复杂度(0-10,越高 = 质量越好,CPU 使用越多)",
|
||||
"audio_settings_dtx_title": "DTX(不连续传输)",
|
||||
"audio_settings_dtx_description": "在静音时节省带宽",
|
||||
"audio_settings_fec_title": "FEC(前向纠错)",
|
||||
"audio_settings_fec_description": "改善有损连接上的音频质量",
|
||||
"audio_settings_buffer_title": "缓冲周期",
|
||||
"audio_settings_buffer_description": "ALSA 缓冲大小(越高 = 越稳定,延迟越高)",
|
||||
"audio_settings_config_updated": "音频配置已更新",
|
||||
"audio_settings_apply_button": "应用设置",
|
||||
"audio_settings_applied": "音频设置已应用",
|
||||
"action_bar_extension": "扩展",
|
||||
"action_bar_fullscreen": "全屏",
|
||||
"action_bar_settings": "设置",
|
||||
|
|
|
|||
|
|
@ -392,6 +392,18 @@ export interface SettingsState {
|
|||
audioInputAutoEnable: boolean;
|
||||
setAudioInputAutoEnable: (enabled: boolean) => void;
|
||||
|
||||
// Audio codec settings
|
||||
audioBitrate: number;
|
||||
setAudioBitrate: (value: number) => void;
|
||||
audioComplexity: number;
|
||||
setAudioComplexity: (value: number) => void;
|
||||
audioDTXEnabled: boolean;
|
||||
setAudioDTXEnabled: (enabled: boolean) => void;
|
||||
audioFECEnabled: boolean;
|
||||
setAudioFECEnabled: (enabled: boolean) => void;
|
||||
audioBufferPeriods: number;
|
||||
setAudioBufferPeriods: (value: number) => void;
|
||||
|
||||
resetMicrophoneState: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -450,6 +462,17 @@ export const useSettingsStore = create(
|
|||
audioInputAutoEnable: false,
|
||||
setAudioInputAutoEnable: (enabled: boolean) => set({ audioInputAutoEnable: enabled }),
|
||||
|
||||
audioBitrate: 128,
|
||||
setAudioBitrate: (value: number) => set({ audioBitrate: value }),
|
||||
audioComplexity: 5,
|
||||
setAudioComplexity: (value: number) => set({ audioComplexity: value }),
|
||||
audioDTXEnabled: true,
|
||||
setAudioDTXEnabled: (enabled: boolean) => set({ audioDTXEnabled: enabled }),
|
||||
audioFECEnabled: true,
|
||||
setAudioFECEnabled: (enabled: boolean) => set({ audioFECEnabled: enabled }),
|
||||
audioBufferPeriods: 12,
|
||||
setAudioBufferPeriods: (value: number) => set({ audioBufferPeriods: value }),
|
||||
|
||||
resetMicrophoneState: () => set({ microphoneEnabled: false }),
|
||||
}),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,9 +10,34 @@ import { m } from "@localizations/messages.js";
|
|||
|
||||
import notifications from "../notifications";
|
||||
|
||||
interface AudioConfigResult {
|
||||
bitrate: number;
|
||||
complexity: number;
|
||||
dtx_enabled: boolean;
|
||||
fec_enabled: boolean;
|
||||
buffer_periods: number;
|
||||
}
|
||||
|
||||
export default function SettingsAudioRoute() {
|
||||
const { send } = useJsonRpc();
|
||||
const { setAudioOutputEnabled, setAudioInputAutoEnable, setAudioOutputSource, audioOutputEnabled, audioInputAutoEnable, audioOutputSource } = useSettingsStore();
|
||||
const {
|
||||
setAudioOutputEnabled,
|
||||
setAudioInputAutoEnable,
|
||||
setAudioOutputSource,
|
||||
audioOutputEnabled,
|
||||
audioInputAutoEnable,
|
||||
audioOutputSource,
|
||||
audioBitrate,
|
||||
setAudioBitrate,
|
||||
audioComplexity,
|
||||
setAudioComplexity,
|
||||
audioDTXEnabled,
|
||||
setAudioDTXEnabled,
|
||||
audioFECEnabled,
|
||||
setAudioFECEnabled,
|
||||
audioBufferPeriods,
|
||||
setAudioBufferPeriods,
|
||||
} = useSettingsStore();
|
||||
|
||||
useEffect(() => {
|
||||
send("getAudioOutputEnabled", {}, (resp: JsonRpcResponse) => {
|
||||
|
|
@ -29,7 +54,17 @@ export default function SettingsAudioRoute() {
|
|||
if ("error" in resp) return;
|
||||
setAudioOutputSource(resp.result as string);
|
||||
});
|
||||
}, [send, setAudioOutputEnabled, setAudioInputAutoEnable, setAudioOutputSource]);
|
||||
|
||||
send("getAudioConfig", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
const config = resp.result as AudioConfigResult;
|
||||
setAudioBitrate(config.bitrate);
|
||||
setAudioComplexity(config.complexity);
|
||||
setAudioDTXEnabled(config.dtx_enabled);
|
||||
setAudioFECEnabled(config.fec_enabled);
|
||||
setAudioBufferPeriods(config.buffer_periods);
|
||||
});
|
||||
}, [send, setAudioOutputEnabled, setAudioInputAutoEnable, setAudioOutputSource, setAudioBitrate, setAudioComplexity, setAudioDTXEnabled, setAudioFECEnabled, setAudioBufferPeriods]);
|
||||
|
||||
const handleAudioOutputEnabledChange = (enabled: boolean) => {
|
||||
send("setAudioOutputEnabled", { enabled }, (resp: JsonRpcResponse) => {
|
||||
|
|
@ -71,6 +106,41 @@ export default function SettingsAudioRoute() {
|
|||
});
|
||||
};
|
||||
|
||||
const handleAudioConfigChange = (
|
||||
bitrate: number,
|
||||
complexity: number,
|
||||
dtxEnabled: boolean,
|
||||
fecEnabled: boolean,
|
||||
bufferPeriods: number
|
||||
) => {
|
||||
send(
|
||||
"setAudioConfig",
|
||||
{ bitrate, complexity, dtxEnabled, fecEnabled, bufferPeriods },
|
||||
(resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(String(resp.error.data || m.unknown_error()));
|
||||
return;
|
||||
}
|
||||
setAudioBitrate(bitrate);
|
||||
setAudioComplexity(complexity);
|
||||
setAudioDTXEnabled(dtxEnabled);
|
||||
setAudioFECEnabled(fecEnabled);
|
||||
setAudioBufferPeriods(bufferPeriods);
|
||||
notifications.success(m.audio_settings_config_updated());
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleRestartAudio = () => {
|
||||
send("restartAudioOutput", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(String(resp.error.data || m.unknown_error()));
|
||||
return;
|
||||
}
|
||||
notifications.success(m.audio_settings_applied());
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
|
|
@ -112,6 +182,130 @@ export default function SettingsAudioRoute() {
|
|||
onChange={(e) => handleAudioInputAutoEnableChange(e.target.checked)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title={m.audio_settings_bitrate_title()}
|
||||
description={m.audio_settings_bitrate_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={String(audioBitrate)}
|
||||
options={[
|
||||
{ value: "64", label: "64 kbps" },
|
||||
{ value: "96", label: "96 kbps" },
|
||||
{ value: "128", label: "128 kbps" },
|
||||
{ value: "160", label: "160 kbps" },
|
||||
{ value: "192", label: "192 kbps" },
|
||||
{ value: "256", label: "256 kbps" },
|
||||
]}
|
||||
onChange={(e) =>
|
||||
handleAudioConfigChange(
|
||||
parseInt(e.target.value),
|
||||
audioComplexity,
|
||||
audioDTXEnabled,
|
||||
audioFECEnabled,
|
||||
audioBufferPeriods
|
||||
)
|
||||
}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title={m.audio_settings_complexity_title()}
|
||||
description={m.audio_settings_complexity_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={String(audioComplexity)}
|
||||
options={[
|
||||
{ value: "0", label: "0 (fastest)" },
|
||||
{ value: "2", label: "2" },
|
||||
{ value: "5", label: "5 (balanced)" },
|
||||
{ value: "8", label: "8" },
|
||||
{ value: "10", label: "10 (best)" },
|
||||
]}
|
||||
onChange={(e) =>
|
||||
handleAudioConfigChange(
|
||||
audioBitrate,
|
||||
parseInt(e.target.value),
|
||||
audioDTXEnabled,
|
||||
audioFECEnabled,
|
||||
audioBufferPeriods
|
||||
)
|
||||
}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title={m.audio_settings_dtx_title()}
|
||||
description={m.audio_settings_dtx_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={audioDTXEnabled}
|
||||
onChange={(e) =>
|
||||
handleAudioConfigChange(
|
||||
audioBitrate,
|
||||
audioComplexity,
|
||||
e.target.checked,
|
||||
audioFECEnabled,
|
||||
audioBufferPeriods
|
||||
)
|
||||
}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title={m.audio_settings_fec_title()}
|
||||
description={m.audio_settings_fec_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={audioFECEnabled}
|
||||
onChange={(e) =>
|
||||
handleAudioConfigChange(
|
||||
audioBitrate,
|
||||
audioComplexity,
|
||||
audioDTXEnabled,
|
||||
e.target.checked,
|
||||
audioBufferPeriods
|
||||
)
|
||||
}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title={m.audio_settings_buffer_title()}
|
||||
description={m.audio_settings_buffer_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={String(audioBufferPeriods)}
|
||||
options={[
|
||||
{ value: "4", label: "4 (80ms)" },
|
||||
{ value: "8", label: "8 (160ms)" },
|
||||
{ value: "12", label: "12 (240ms)" },
|
||||
{ value: "16", label: "16 (320ms)" },
|
||||
{ value: "24", label: "24 (480ms)" },
|
||||
]}
|
||||
onChange={(e) =>
|
||||
handleAudioConfigChange(
|
||||
audioBitrate,
|
||||
audioComplexity,
|
||||
audioDTXEnabled,
|
||||
audioFECEnabled,
|
||||
parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<div className="pt-4">
|
||||
<button
|
||||
onClick={handleRestartAudio}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
{m.audio_settings_apply_button()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue