Merge pull request #1 from IDisposable/small-tweaks

Some cleanup to eliminate duplicate code ensure we don't carry around multiple copies of state
This commit is contained in:
Alex 2025-11-18 09:49:10 +02:00 committed by GitHub
commit bc2a5f88e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 570 additions and 242 deletions

203
audio.go
View File

@ -15,10 +15,10 @@ var (
audioMutex sync.Mutex audioMutex sync.Mutex
setAudioTrackMutex sync.Mutex // Prevents concurrent setAudioTrack() calls setAudioTrackMutex sync.Mutex // Prevents concurrent setAudioTrack() calls
inputSourceMutex sync.Mutex // Serializes Connect() and WriteMessage() calls to input source inputSourceMutex sync.Mutex // Serializes Connect() and WriteMessage() calls to input source
outputSource audio.AudioSource outputSource atomic.Pointer[audio.AudioSource]
inputSource atomic.Pointer[audio.AudioSource] inputSource atomic.Pointer[audio.AudioSource]
outputRelay *audio.OutputRelay outputRelay atomic.Pointer[audio.OutputRelay]
inputRelay *audio.InputRelay inputRelay atomic.Pointer[audio.InputRelay]
audioInitialized bool audioInitialized bool
activeConnections atomic.Int32 activeConnections atomic.Int32
audioLogger zerolog.Logger audioLogger zerolog.Logger
@ -28,6 +28,14 @@ var (
audioInputEnabled atomic.Bool audioInputEnabled atomic.Bool
) )
func getAlsaDevice(source string) string {
if source == "hdmi" {
return "hw:0,0"
} else {
return "hw:1,0"
}
}
func initAudio() { func initAudio() {
audioLogger = logging.GetDefaultLogger().With().Str("component", "audio-manager").Logger() audioLogger = logging.GetDefaultLogger().With().Str("component", "audio-manager").Logger()
@ -40,7 +48,7 @@ func initAudio() {
} }
func getAudioConfig() audio.AudioConfig { func getAudioConfig() audio.AudioConfig {
ensureConfigLoaded() // config is already loaded
cfg := audio.DefaultAudioConfig() cfg := audio.DefaultAudioConfig()
if config.AudioBitrate >= 64 && config.AudioBitrate <= 256 { if config.AudioBitrate >= 64 && config.AudioBitrate <= 256 {
cfg.Bitrate = uint16(config.AudioBitrate) cfg.Bitrate = uint16(config.AudioBitrate)
@ -71,63 +79,81 @@ func startAudio() error {
return nil return nil
} }
if outputSource == nil && audioOutputEnabled.Load() && currentAudioTrack != nil { if activeConnections.Load() <= 0 {
ensureConfigLoaded() audioLogger.Debug().Msg("No active connections, skipping audio start")
alsaDevice := "hw:1,0" return nil
if config.AudioOutputSource == "hdmi" {
alsaDevice = "hw:0,0"
}
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")
}
} }
ensureConfigLoaded() ensureConfigLoaded()
if inputSource.Load() == nil && audioInputEnabled.Load() && config.UsbDevices != nil && config.UsbDevices.Audio {
alsaPlaybackDevice := "hw:1,0"
source := audio.NewCgoInputSource(alsaPlaybackDevice) if audioOutputEnabled.Load() && currentAudioTrack != nil {
source.SetConfig(getAudioConfig()) startOutputAudioUnderMutex(getAlsaDevice(config.AudioOutputSource))
var audioSource audio.AudioSource = source }
inputSource.Store(&audioSource)
inputRelay = audio.NewInputRelay(audioSource) if audioInputEnabled.Load() && config.UsbDevices != nil && config.UsbDevices.Audio {
if err := inputRelay.Start(); err != nil { startInputAudioUnderMutex(getAlsaDevice("usb"))
audioLogger.Error().Err(err).Msg("Failed to start input relay")
}
} }
return nil return nil
} }
func startOutputAudioUnderMutex(alsaOutputDevice string) {
newSource := audio.NewCgoOutputSource(alsaOutputDevice, getAudioConfig())
oldSource := outputSource.Swap(&newSource)
newRelay := audio.NewOutputRelay(&newSource, currentAudioTrack)
oldRelay := outputRelay.Swap(newRelay)
if oldRelay != nil {
oldRelay.Stop()
}
if oldSource != nil {
(*oldSource).Disconnect()
}
if err := newRelay.Start(); err != nil {
audioLogger.Error().Err(err).Str("alsaOutputDevice", alsaOutputDevice).Msg("Failed to start audio output relay")
}
}
func startInputAudioUnderMutex(alsaPlaybackDevice string) {
newSource := audio.NewCgoInputSource(alsaPlaybackDevice, getAudioConfig())
oldSource := outputSource.Swap(&newSource)
newRelay := audio.NewInputRelay(&newSource)
oldRelay := inputRelay.Swap(newRelay)
if oldRelay != nil {
oldRelay.Stop()
}
if oldSource != nil {
(*oldSource).Disconnect()
}
if err := newRelay.Start(); err != nil {
audioLogger.Error().Err(err).Str("alsaPlaybackDevice", alsaPlaybackDevice).Msg("Failed to start input relay")
}
}
func stopOutputAudio() { func stopOutputAudio() {
audioMutex.Lock() audioMutex.Lock()
outRelay := outputRelay outRelay := outputRelay.Swap(nil)
outSource := outputSource outSource := outputSource.Swap(nil)
outputRelay = nil
outputSource = nil
audioMutex.Unlock() audioMutex.Unlock()
if outRelay != nil { if outRelay != nil {
outRelay.Stop() outRelay.Stop()
} }
if outSource != nil { if outSource != nil {
outSource.Disconnect() (*outSource).Disconnect()
} }
} }
func stopInputAudio() { func stopInputAudio() {
audioMutex.Lock() audioMutex.Lock()
inRelay := inputRelay inRelay := inputRelay.Swap(nil)
inputRelay = nil
audioMutex.Unlock()
inSource := inputSource.Swap(nil) inSource := inputSource.Swap(nil)
audioMutex.Unlock()
if inRelay != nil { if inRelay != nil {
inRelay.Stop() inRelay.Stop()
@ -153,7 +179,7 @@ func onWebRTCConnect() {
func onWebRTCDisconnect() { func onWebRTCDisconnect() {
count := activeConnections.Add(-1) count := activeConnections.Add(-1)
if count == 0 { if count <= 0 {
// Stop audio immediately to release HDMI audio device which shares hardware with video device // Stop audio immediately to release HDMI audio device which shares hardware with video device
stopAudio() stopAudio()
} }
@ -163,42 +189,12 @@ func setAudioTrack(audioTrack *webrtc.TrackLocalStaticSample) {
setAudioTrackMutex.Lock() setAudioTrackMutex.Lock()
defer setAudioTrackMutex.Unlock() defer setAudioTrackMutex.Unlock()
// Capture old resources and update state in single critical section stopOutputAudio()
audioMutex.Lock()
currentAudioTrack = audioTrack currentAudioTrack = audioTrack
oldRelay := outputRelay
oldSource := outputSource
outputRelay = nil
outputSource = nil
var newRelay *audio.OutputRelay if err := startAudio(); err != nil {
var newSource audio.AudioSource audioLogger.Error().Err(err).Msg("Failed to start with new audio track")
if currentAudioTrack != nil && audioOutputEnabled.Load() {
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)
outputSource = newSource
outputRelay = newRelay
}
audioMutex.Unlock()
// Stop/start resources outside mutex to avoid blocking on CGO calls
if oldRelay != nil {
oldRelay.Stop()
}
if oldSource != nil {
oldSource.Disconnect()
}
if newRelay != nil {
if err := newRelay.Start(); err != nil {
audioLogger.Error().Err(err).Msg("Failed to start output relay")
}
} }
} }
@ -250,79 +246,44 @@ func SetAudioOutputSource(source string) error {
return nil return nil
} }
stopOutputAudio()
config.AudioOutputSource = source config.AudioOutputSource = source
stopOutputAudio() if err := startAudio(); err != nil {
audioLogger.Error().Err(err).Str("source", source).Msg("Failed to start audio output after source change")
if audioOutputEnabled.Load() && activeConnections.Load() > 0 && currentAudioTrack != nil {
alsaDevice := "hw:1,0"
if source == "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).Str("source", source).Msg("Failed to start audio relay with new source")
}
} }
return SaveConfig() return SaveConfig()
} }
func RestartAudioOutput() { func RestartAudioOutput() error {
audioMutex.Lock() audioMutex.Lock()
hasActiveOutput := outputSource != nil && currentAudioTrack != nil && audioOutputEnabled.Load() hasActiveOutput := audioOutputEnabled.Load() && currentAudioTrack != nil && outputSource.Load() != nil
audioMutex.Unlock() audioMutex.Unlock()
if !hasActiveOutput { if !hasActiveOutput {
return return nil
} }
audioLogger.Info().Msg("Restarting audio output") audioLogger.Info().Msg("Restarting audio output")
stopOutputAudio() stopOutputAudio()
return startAudio()
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) { func handleInputTrackForSession(track *webrtc.TrackRemote) {
myTrackID := track.ID() myTrackID := track.ID()
audioLogger.Debug(). trackLogger := audioLogger.With().
Str("codec", track.Codec().MimeType). Str("codec", track.Codec().MimeType).
Str("track_id", myTrackID). Str("track_id", myTrackID).
Msg("starting input track handler") Logger()
trackLogger.Debug().Msg("starting input track handler")
for { for {
currentTrackID := currentInputTrack.Load() currentTrackID := currentInputTrack.Load()
if currentTrackID != nil && *currentTrackID != myTrackID { if currentTrackID != nil && *currentTrackID != myTrackID {
audioLogger.Debug(). trackLogger.Debug().
Str("my_track_id", myTrackID).
Str("current_track_id", *currentTrackID). Str("current_track_id", *currentTrackID).
Msg("input track handler exiting - superseded") Msg("input track handler exiting - superseded")
return return
@ -331,10 +292,10 @@ func handleInputTrackForSession(track *webrtc.TrackRemote) {
rtpPacket, _, err := track.ReadRTP() rtpPacket, _, err := track.ReadRTP()
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
audioLogger.Debug().Str("track_id", myTrackID).Msg("input track ended") trackLogger.Debug().Msg("input track ended")
return return
} }
audioLogger.Warn().Err(err).Str("track_id", myTrackID).Msg("failed to read RTP packet") trackLogger.Warn().Err(err).Msg("failed to read RTP packet")
continue continue
} }

View File

@ -25,46 +25,47 @@ const (
) )
type CgoSource struct { type CgoSource struct {
direction string outputDevice bool
alsaDevice string alsaDevice string
initialized bool connected bool
connected bool mu sync.Mutex
mu sync.Mutex logger zerolog.Logger
logger zerolog.Logger opusBuf []byte
opusBuf []byte config AudioConfig
config AudioConfig
} }
func NewCgoOutputSource(alsaDevice string) *CgoSource { var _ AudioSource = (*CgoSource)(nil)
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-cgo").Logger()
func NewCgoOutputSource(alsaDevice string, cfg AudioConfig) AudioSource {
logger := logging.GetDefaultLogger().With().
Str("component", "audio-output-cgo").
Str("alsa_device", alsaDevice).
Logger()
return &CgoSource{ return &CgoSource{
direction: "output", outputDevice: true,
alsaDevice: alsaDevice, alsaDevice: alsaDevice,
logger: logger, logger: logger,
opusBuf: make([]byte, ipcMaxFrameSize), opusBuf: make([]byte, ipcMaxFrameSize),
config: DefaultAudioConfig(), config: cfg,
} }
} }
func NewCgoInputSource(alsaDevice string) *CgoSource { func NewCgoInputSource(alsaDevice string, cfg AudioConfig) AudioSource {
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-cgo").Logger() logger := logging.GetDefaultLogger().With().
Str("component", "audio-input-cgo").
Str("alsa_device", alsaDevice).
Logger()
return &CgoSource{ return &CgoSource{
direction: "input", outputDevice: false,
alsaDevice: alsaDevice, alsaDevice: alsaDevice,
logger: logger, logger: logger,
opusBuf: make([]byte, ipcMaxFrameSize), opusBuf: make([]byte, ipcMaxFrameSize),
config: DefaultAudioConfig(), config: cfg,
} }
} }
func (c *CgoSource) SetConfig(cfg AudioConfig) {
c.mu.Lock()
defer c.mu.Unlock()
c.config = cfg
}
func (c *CgoSource) Connect() error { func (c *CgoSource) Connect() error {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -73,7 +74,7 @@ func (c *CgoSource) Connect() error {
return nil return nil
} }
if c.direction == "output" { if c.outputDevice {
os.Setenv("ALSA_CAPTURE_DEVICE", c.alsaDevice) os.Setenv("ALSA_CAPTURE_DEVICE", c.alsaDevice)
dtx := C.uchar(0) dtx := C.uchar(0)
@ -93,7 +94,6 @@ func (c *CgoSource) Connect() error {
Uint8("buffer_periods", c.config.BufferPeriods). Uint8("buffer_periods", c.config.BufferPeriods).
Uint32("sample_rate", c.config.SampleRate). Uint32("sample_rate", c.config.SampleRate).
Uint8("packet_loss_perc", c.config.PacketLossPerc). Uint8("packet_loss_perc", c.config.PacketLossPerc).
Str("alsa_device", c.alsaDevice).
Msg("Initializing audio capture") Msg("Initializing audio capture")
C.update_audio_constants( C.update_audio_constants(
@ -139,7 +139,6 @@ func (c *CgoSource) Connect() error {
} }
c.connected = true c.connected = true
c.initialized = true
return nil return nil
} }
@ -151,10 +150,12 @@ func (c *CgoSource) Disconnect() {
return return
} }
if c.direction == "output" { if c.outputDevice {
C.jetkvm_audio_capture_close() C.jetkvm_audio_capture_close()
os.Unsetenv("ALSA_CAPTURE_DEVICE")
} else { } else {
C.jetkvm_audio_playback_close() C.jetkvm_audio_playback_close()
os.Unsetenv("ALSA_PLAYBACK_DEVICE")
} }
c.connected = false c.connected = false
@ -173,7 +174,7 @@ func (c *CgoSource) ReadMessage() (uint8, []byte, error) {
return 0, nil, fmt.Errorf("not connected") return 0, nil, fmt.Errorf("not connected")
} }
if c.direction != "output" { if !c.outputDevice {
c.mu.Unlock() c.mu.Unlock()
return 0, nil, fmt.Errorf("ReadMessage only supported for output direction") return 0, nil, fmt.Errorf("ReadMessage only supported for output direction")
} }
@ -203,7 +204,7 @@ func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error {
return fmt.Errorf("not connected") return fmt.Errorf("not connected")
} }
if c.direction != "input" { if c.outputDevice {
c.mu.Unlock() c.mu.Unlock()
return fmt.Errorf("WriteMessage only supported for input direction") return fmt.Errorf("WriteMessage only supported for input direction")
} }

View File

@ -6,11 +6,13 @@ package audio
type CgoSource struct{} type CgoSource struct{}
func NewCgoOutputSource(alsaDevice string) *CgoSource { var _ AudioSource = (*CgoSource)(nil)
func NewCgoOutputSource(alsaDevice string, audioConfig AudioConfig) AudioSource {
panic("audio CGO source not supported on this platform") panic("audio CGO source not supported on this platform")
} }
func NewCgoInputSource(alsaDevice string) *CgoSource { func NewCgoInputSource(alsaDevice string, audioConfig AudioConfig) AudioSource {
panic("audio CGO source not supported on this platform") panic("audio CGO source not supported on this platform")
} }
@ -33,7 +35,3 @@ func (c *CgoSource) ReadMessage() (uint8, []byte, error) {
func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error { func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error {
panic("audio CGO source not supported on this platform") panic("audio CGO source not supported on this platform")
} }
func (c *CgoSource) SetConfig(cfg AudioConfig) {
panic("audio CGO source not supported on this platform")
}

View File

@ -13,7 +13,7 @@ import (
) )
type OutputRelay struct { type OutputRelay struct {
source AudioSource source *AudioSource
audioTrack *webrtc.TrackLocalStaticSample audioTrack *webrtc.TrackLocalStaticSample
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
@ -26,7 +26,7 @@ type OutputRelay struct {
framesDropped atomic.Uint32 framesDropped atomic.Uint32
} }
func NewOutputRelay(source AudioSource, audioTrack *webrtc.TrackLocalStaticSample) *OutputRelay { func NewOutputRelay(source *AudioSource, audioTrack *webrtc.TrackLocalStaticSample) *OutputRelay {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-relay").Logger() logger := logging.GetDefaultLogger().With().Str("component", "audio-output-relay").Logger()
@ -73,19 +73,19 @@ func (r *OutputRelay) relayLoop() {
const reconnectDelay = 1 * time.Second const reconnectDelay = 1 * time.Second
for r.running.Load() { for r.running.Load() {
if !r.source.IsConnected() { if !(*r.source).IsConnected() {
if err := r.source.Connect(); err != nil { if err := (*r.source).Connect(); err != nil {
r.logger.Debug().Err(err).Msg("failed to connect, will retry") r.logger.Debug().Err(err).Msg("failed to connect, will retry")
time.Sleep(reconnectDelay) time.Sleep(reconnectDelay)
continue continue
} }
} }
msgType, payload, err := r.source.ReadMessage() msgType, payload, err := (*r.source).ReadMessage()
if err != nil { if err != nil {
if r.running.Load() { if r.running.Load() {
r.logger.Warn().Err(err).Msg("read error, reconnecting") r.logger.Warn().Err(err).Msg("read error, reconnecting")
r.source.Disconnect() (*r.source).Disconnect()
time.Sleep(reconnectDelay) time.Sleep(reconnectDelay)
} }
continue continue
@ -104,14 +104,14 @@ func (r *OutputRelay) relayLoop() {
} }
type InputRelay struct { type InputRelay struct {
source AudioSource source *AudioSource
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
logger zerolog.Logger logger zerolog.Logger
running atomic.Bool running atomic.Bool
} }
func NewInputRelay(source AudioSource) *InputRelay { func NewInputRelay(source *AudioSource) *InputRelay {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-relay").Logger() logger := logging.GetDefaultLogger().With().Str("component", "audio-input-relay").Logger()

View File

@ -32,5 +32,4 @@ type AudioSource interface {
IsConnected() bool IsConnected() bool
Connect() error Connect() error
Disconnect() Disconnect()
SetConfig(cfg AudioConfig)
} }

View File

@ -18,7 +18,6 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.bug.st/serial" "go.bug.st/serial"
"github.com/jetkvm/kvm/internal/audio"
"github.com/jetkvm/kvm/internal/hidrpc" "github.com/jetkvm/kvm/internal/hidrpc"
"github.com/jetkvm/kvm/internal/usbgadget" "github.com/jetkvm/kvm/internal/usbgadget"
"github.com/jetkvm/kvm/internal/utils" "github.com/jetkvm/kvm/internal/utils"
@ -688,10 +687,12 @@ func rpcGetUsbConfig() (usbgadget.Config, error) {
func rpcSetUsbConfig(usbConfig usbgadget.Config) error { func rpcSetUsbConfig(usbConfig usbgadget.Config) error {
LoadConfig() LoadConfig()
wasUsbAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio
config.UsbConfig = &usbConfig config.UsbConfig = &usbConfig
gadget.SetGadgetConfig(config.UsbConfig) gadget.SetGadgetConfig(config.UsbConfig)
wasAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio
return updateUsbRelatedConfig(wasAudioEnabled) return updateUsbRelatedConfig(wasUsbAudioEnabled)
} }
func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) { func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) {
@ -903,42 +904,23 @@ func rpcGetUsbDevices() (usbgadget.Devices, error) {
return *config.UsbDevices, nil return *config.UsbDevices, nil
} }
func updateUsbRelatedConfig(wasAudioEnabled bool) error { func updateUsbRelatedConfig(wasUsbAudioEnabled bool) error {
ensureConfigLoaded() ensureConfigLoaded()
nowHasUsbAudio := config.UsbDevices != nil && config.UsbDevices.Audio
outputSourceIsUsb := config.AudioOutputSource == "usb"
audioMutex.Lock() // must stop input audio before reconfiguring
inRelay := inputRelay stopInputAudio()
inputRelay = nil
audioMutex.Unlock()
inSource := inputSource.Swap(nil) // if we're currently sourcing audio from USB, stop the output audio before reconfiguring
if outputSourceIsUsb {
if inRelay != nil {
inRelay.Stop()
}
if inSource != nil {
(*inSource).Disconnect()
}
// Auto-switch to HDMI audio output when USB audio is disabled
audioNowEnabled := config.UsbDevices != nil && config.UsbDevices.Audio
if wasAudioEnabled && !audioNowEnabled && config.AudioOutputSource == "usb" {
config.AudioOutputSource = "hdmi"
stopOutputAudio() stopOutputAudio()
if audioOutputEnabled.Load() && activeConnections.Load() > 0 && currentAudioTrack != nil { }
newSource := audio.NewCgoOutputSource("hw:0,0")
newSource.SetConfig(getAudioConfig())
newRelay := audio.NewOutputRelay(newSource, currentAudioTrack)
audioMutex.Lock() // Auto-switch to HDMI audio output when USB audio was selected and is now disabled
outputSource = newSource if wasUsbAudioEnabled && !nowHasUsbAudio && config.AudioOutputSource == "usb" {
outputRelay = newRelay logger.Info().Msg("USB audio just disabled, automatic switch audio output source to HDMI")
audioMutex.Unlock() config.AudioOutputSource = "hdmi"
if err := newRelay.Start(); err != nil {
logger.Warn().Err(err).Msg("Failed to start HDMI audio after USB audio disabled")
}
}
} }
if err := gadget.UpdateGadgetConfig(); err != nil { if err := gadget.UpdateGadgetConfig(); err != nil {
@ -949,18 +931,15 @@ func updateUsbRelatedConfig(wasAudioEnabled bool) error {
return fmt.Errorf("failed to save config: %w", err) return fmt.Errorf("failed to save config: %w", err)
} }
// Restart audio if USB audio is enabled with active connections if err := startAudio(); err != nil {
if activeConnections.Load() > 0 && config.UsbDevices != nil && config.UsbDevices.Audio { logger.Warn().Err(err).Msg("Failed to restart audio after USB reconfiguration")
if err := startAudio(); err != nil {
logger.Warn().Err(err).Msg("Failed to restart audio after USB reconfiguration")
}
} }
return nil return nil
} }
func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { func rpcSetUsbDevices(usbDevices usbgadget.Devices) error {
wasAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio wasUsbAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio
currentDevices := gadget.GetGadgetDevices() currentDevices := gadget.GetGadgetDevices()
// Skip reconfiguration if devices haven't changed to avoid HID disruption // Skip reconfiguration if devices haven't changed to avoid HID disruption
@ -972,11 +951,11 @@ func rpcSetUsbDevices(usbDevices usbgadget.Devices) error {
config.UsbDevices = &usbDevices config.UsbDevices = &usbDevices
gadget.SetGadgetDevices(config.UsbDevices) gadget.SetGadgetDevices(config.UsbDevices)
return updateUsbRelatedConfig(wasAudioEnabled) return updateUsbRelatedConfig(wasUsbAudioEnabled)
} }
func rpcSetUsbDeviceState(device string, enabled bool) error { func rpcSetUsbDeviceState(device string, enabled bool) error {
wasAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio wasUsbAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio
currentDevices := gadget.GetGadgetDevices() currentDevices := gadget.GetGadgetDevices()
switch device { switch device {
@ -1001,7 +980,7 @@ func rpcSetUsbDeviceState(device string, enabled bool) error {
} }
gadget.SetGadgetDevices(config.UsbDevices) gadget.SetGadgetDevices(config.UsbDevices)
return updateUsbRelatedConfig(wasAudioEnabled) return updateUsbRelatedConfig(wasUsbAudioEnabled)
} }
func rpcGetAudioOutputEnabled() (bool, error) { func rpcGetAudioOutputEnabled() (bool, error) {
@ -1104,8 +1083,7 @@ func rpcSetAudioConfig(bitrate int, complexity int, dtxEnabled bool, fecEnabled
} }
func rpcRestartAudioOutput() error { func rpcRestartAudioOutput() error {
RestartAudioOutput() return RestartAudioOutput()
return nil
} }
func rpcGetAudioInputAutoEnable() (bool, error) { func rpcGetAudioInputAutoEnable() (bool, error) {

View File

@ -42,7 +42,9 @@ func Main() {
initDisplay() initDisplay()
initNative(systemVersionLocal, appVersionLocal) initNative(systemVersionLocal, appVersionLocal)
initAudio() initAudio()
defer stopAudio()
http.DefaultClient.Timeout = 1 * time.Minute http.DefaultClient.Timeout = 1 * time.Minute
@ -81,6 +83,7 @@ func Main() {
if err := initImagesFolder(); err != nil { if err := initImagesFolder(); err != nil {
logger.Warn().Err(err).Msg("failed to init images folder") logger.Warn().Err(err).Msg("failed to init images folder")
} }
initJiggler() initJiggler()
// start video sleep mode timer // start video sleep mode timer
@ -140,7 +143,6 @@ func Main() {
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs <-sigs
stopAudio()
logger.Log().Msg("JetKVM Shutting Down") logger.Log().Msg("JetKVM Shutting Down")
//if fuseServer != nil { //if fuseServer != nil {

View File

@ -49,8 +49,6 @@
"access_update_tls_settings": "Opdater TLS-indstillinger", "access_update_tls_settings": "Opdater TLS-indstillinger",
"action_bar_audio": "Lyd", "action_bar_audio": "Lyd",
"action_bar_connection_stats": "Forbindelsesstatistik", "action_bar_connection_stats": "Forbindelsesstatistik",
"audio_input_disabled": "Lydindgang deaktiveret",
"audio_input_enabled": "Lydindgang aktiveret",
"audio_input_failed_disable": "Kunne ikke deaktivere lydindgang: {error}", "audio_input_failed_disable": "Kunne ikke deaktivere lydindgang: {error}",
"audio_input_failed_enable": "Kunne ikke aktivere lydindgang: {error}", "audio_input_failed_enable": "Kunne ikke aktivere lydindgang: {error}",
"audio_input_auto_enable_disabled": "Automatisk aktivering af mikrofon deaktiveret", "audio_input_auto_enable_disabled": "Automatisk aktivering af mikrofon deaktiveret",
@ -167,6 +165,46 @@
"atx_power_control_reset_button": "Nulstil", "atx_power_control_reset_button": "Nulstil",
"atx_power_control_send_action_error": "Kunne ikke sende ATX-strømfunktion {action} : {error}", "atx_power_control_send_action_error": "Kunne ikke sende ATX-strømfunktion {action} : {error}",
"atx_power_control_short_power_button": "Kort tryk", "atx_power_control_short_power_button": "Kort tryk",
"audio_https_only": "Kun HTTPS",
"audio_input_auto_enable_disabled": "Automatisk aktivering af mikrofon deaktiveret",
"audio_input_auto_enable_enabled": "Automatisk aktivering af mikrofon aktiveret",
"audio_input_failed_disable": "Kunne ikke deaktivere lydindgang: {error}",
"audio_input_failed_enable": "Kunne ikke aktivere lydindgang: {error}",
"audio_microphone_description": "Mikrofonindgang til mål",
"audio_microphone_title": "Mikrofon",
"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_popover_description": "Hurtige lydkontroller til højttalere og mikrofon",
"audio_popover_title": "Lyd",
"audio_settings_applied": "Lydindstillinger anvendt",
"audio_settings_apply_button": "Anvend indstillinger",
"audio_settings_auto_enable_microphone_description": "Aktiver automatisk browsermikrofon ved tilslutning (ellers skal du aktivere det manuelt ved hver session)",
"audio_settings_auto_enable_microphone_title": "Aktiver mikrofon automatisk",
"audio_settings_bitrate_description": "Lydkodningsbitrate (højere = bedre kvalitet, mere båndbredde)",
"audio_settings_bitrate_title": "Opus Bitrate",
"audio_settings_buffer_description": "ALSA bufferstørrelse (højere = mere stabil, mere latens)",
"audio_settings_buffer_title": "Bufferperioder",
"audio_settings_complexity_description": "Encoder-kompleksitet (0-10, højere = bedre kvalitet, mere CPU)",
"audio_settings_complexity_title": "Opus Kompleksitet",
"audio_settings_config_updated": "Lydkonfiguration opdateret",
"audio_settings_description": "Konfigurer lydindgangs- og lydudgangsindstillinger for din JetKVM-enhed",
"audio_settings_dtx_description": "Spar båndbredde under stilhed",
"audio_settings_dtx_title": "DTX (Diskontinuerlig Transmission)",
"audio_settings_fec_description": "Forbedre lydkvaliteten på tabende forbindelser",
"audio_settings_fec_title": "FEC (Fremadrettet Fejlkorrektion)",
"audio_settings_hdmi_label": "HDMI",
"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_speakers_description": "Lyd fra mål til højttalere",
"audio_speakers_title": "Højttalere",
"auth_authentication_mode": "Vælg venligst en godkendelsestilstand", "auth_authentication_mode": "Vælg venligst en godkendelsestilstand",
"auth_authentication_mode_error": "Der opstod en fejl under indstilling af godkendelsestilstanden", "auth_authentication_mode_error": "Der opstod en fejl under indstilling af godkendelsestilstanden",
"auth_authentication_mode_invalid": "Ugyldig godkendelsestilstand", "auth_authentication_mode_invalid": "Ugyldig godkendelsestilstand",
@ -223,6 +261,9 @@
"connection_stats_packets_lost_description": "Antal mistede indgående video-RTP-pakker.", "connection_stats_packets_lost_description": "Antal mistede indgående video-RTP-pakker.",
"connection_stats_playback_delay": "Afspilningsforsinkelse", "connection_stats_playback_delay": "Afspilningsforsinkelse",
"connection_stats_playback_delay_description": "Forsinkelse tilføjet af jitterbufferen for at jævne afspilningen, når billeder ankommer ujævnt.", "connection_stats_playback_delay_description": "Forsinkelse tilføjet af jitterbufferen for at jævne afspilningen, når billeder ankommer ujævnt.",
"connection_stats_remote_ip_address": "Fjern IP-adresse",
"connection_stats_remote_ip_address_copy_error": "Kunne ikke kopiere fjern-IP-adresse",
"connection_stats_remote_ip_address_copy_success": "Fjern IP-adresse { ip } kopieret til udklipsholder",
"connection_stats_round_trip_time": "Rundturstid", "connection_stats_round_trip_time": "Rundturstid",
"connection_stats_round_trip_time_description": "Rundrejsetid for det aktive ICE-kandidatpar mellem peers.", "connection_stats_round_trip_time_description": "Rundrejsetid for det aktive ICE-kandidatpar mellem peers.",
"connection_stats_sidebar": "Forbindelsesstatistik", "connection_stats_sidebar": "Forbindelsesstatistik",
@ -748,6 +789,9 @@
"peer_connection_failed": "Forbindelsen mislykkedes", "peer_connection_failed": "Forbindelsen mislykkedes",
"peer_connection_new": "Forbinder", "peer_connection_new": "Forbinder",
"previous": "Tidligere", "previous": "Tidligere",
"public_ip_card_header": "Offentlige IP-adresser",
"public_ip_card_refresh": "Opfriske",
"public_ip_card_refresh_error": "Kunne ikke opdatere offentlige IP-adresser: {error}",
"register_device_error": "Der opstod en fejl {error} under registrering af din enhed.", "register_device_error": "Der opstod en fejl {error} under registrering af din enhed.",
"register_device_finish_button": "Afslut opsætning", "register_device_finish_button": "Afslut opsætning",
"register_device_name_description": "Navngiv din enhed, så du nemt kan identificere den senere. Du kan til enhver tid ændre dette navn.", "register_device_name_description": "Navngiv din enhed, så du nemt kan identificere den senere. Du kan til enhver tid ændre dette navn.",

View File

@ -49,8 +49,6 @@
"access_update_tls_settings": "TLS-Einstellungen aktualisieren", "access_update_tls_settings": "TLS-Einstellungen aktualisieren",
"action_bar_audio": "Audio", "action_bar_audio": "Audio",
"action_bar_connection_stats": "Verbindungsstatistiken", "action_bar_connection_stats": "Verbindungsstatistiken",
"audio_input_disabled": "Audioeingang deaktiviert",
"audio_input_enabled": "Audioeingang aktiviert",
"audio_input_failed_disable": "Fehler beim Deaktivieren des Audioeingangs: {error}", "audio_input_failed_disable": "Fehler beim Deaktivieren des Audioeingangs: {error}",
"audio_input_failed_enable": "Fehler beim Aktivieren des Audioeingangs: {error}", "audio_input_failed_enable": "Fehler beim Aktivieren des Audioeingangs: {error}",
"audio_input_auto_enable_disabled": "Automatische Mikrofonaktivierung deaktiviert", "audio_input_auto_enable_disabled": "Automatische Mikrofonaktivierung deaktiviert",
@ -167,6 +165,46 @@
"atx_power_control_reset_button": "Reset-Taste", "atx_power_control_reset_button": "Reset-Taste",
"atx_power_control_send_action_error": "ATX-Stromversorgungsaktion {action} konnte nicht gesendet werden: {error}", "atx_power_control_send_action_error": "ATX-Stromversorgungsaktion {action} konnte nicht gesendet werden: {error}",
"atx_power_control_short_power_button": "Kurzes Drücken", "atx_power_control_short_power_button": "Kurzes Drücken",
"audio_https_only": "Nur HTTPS",
"audio_input_auto_enable_disabled": "Automatische Mikrofonaktivierung deaktiviert",
"audio_input_auto_enable_enabled": "Automatische Mikrofonaktivierung aktiviert",
"audio_input_failed_disable": "Fehler beim Deaktivieren des Audioeingangs: {error}",
"audio_input_failed_enable": "Fehler beim Aktivieren des Audioeingangs: {error}",
"audio_microphone_description": "Mikrofoneingang zum Ziel",
"audio_microphone_title": "Mikrofon",
"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_popover_description": "Schnelle Audiosteuerung für Lautsprecher und Mikrofon",
"audio_popover_title": "Audio",
"audio_settings_applied": "Audioeinstellungen angewendet",
"audio_settings_apply_button": "Einstellungen anwenden",
"audio_settings_auto_enable_microphone_description": "Browser-Mikrofon beim Verbinden automatisch aktivieren (andernfalls müssen Sie es in jeder Sitzung manuell aktivieren)",
"audio_settings_auto_enable_microphone_title": "Mikrofon automatisch aktivieren",
"audio_settings_bitrate_description": "Audio-Codierungsbitrate (höher = bessere Qualität, mehr Bandbreite)",
"audio_settings_bitrate_title": "Opus Bitrate",
"audio_settings_buffer_description": "ALSA-Puffergröße (höher = stabiler, mehr Latenz)",
"audio_settings_buffer_title": "Pufferperioden",
"audio_settings_complexity_description": "Encoder-Komplexität (0-10, höher = bessere Qualität, mehr CPU)",
"audio_settings_complexity_title": "Opus Komplexität",
"audio_settings_config_updated": "Audiokonfiguration aktualisiert",
"audio_settings_description": "Konfigurieren Sie Audio-Eingangs- und Ausgangseinstellungen für Ihr JetKVM-Gerät",
"audio_settings_dtx_description": "Bandbreite während Stille sparen",
"audio_settings_dtx_title": "DTX (Discontinuous Transmission)",
"audio_settings_fec_description": "Audioqualität bei verlustbehafteten Verbindungen verbessern",
"audio_settings_fec_title": "FEC (Forward Error Correction)",
"audio_settings_hdmi_label": "HDMI",
"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_speakers_description": "Audio vom Ziel zu Lautsprechern",
"audio_speakers_title": "Lautsprecher",
"auth_authentication_mode": "Bitte wählen Sie einen Authentifizierungsmodus", "auth_authentication_mode": "Bitte wählen Sie einen Authentifizierungsmodus",
"auth_authentication_mode_error": "Beim Einstellen des Authentifizierungsmodus ist ein Fehler aufgetreten", "auth_authentication_mode_error": "Beim Einstellen des Authentifizierungsmodus ist ein Fehler aufgetreten",
"auth_authentication_mode_invalid": "Ungültiger Authentifizierungsmodus", "auth_authentication_mode_invalid": "Ungültiger Authentifizierungsmodus",
@ -223,6 +261,9 @@
"connection_stats_packets_lost_description": "Anzahl der verlorenen eingehenden Video-RTP-Pakete.", "connection_stats_packets_lost_description": "Anzahl der verlorenen eingehenden Video-RTP-Pakete.",
"connection_stats_playback_delay": "Wiedergabeverzögerung", "connection_stats_playback_delay": "Wiedergabeverzögerung",
"connection_stats_playback_delay_description": "Durch den Jitter-Puffer hinzugefügte Verzögerung, um die Wiedergabe zu glätten, wenn die Frames ungleichmäßig ankommen.", "connection_stats_playback_delay_description": "Durch den Jitter-Puffer hinzugefügte Verzögerung, um die Wiedergabe zu glätten, wenn die Frames ungleichmäßig ankommen.",
"connection_stats_remote_ip_address": "Remote-IP-Adresse",
"connection_stats_remote_ip_address_copy_error": "Fehler beim Kopieren der Remote-IP-Adresse",
"connection_stats_remote_ip_address_copy_success": "Remote-IP-Adresse { ip } in die Zwischenablage kopiert",
"connection_stats_round_trip_time": "Round-Trip-Zeit", "connection_stats_round_trip_time": "Round-Trip-Zeit",
"connection_stats_round_trip_time_description": "Roundtrip-Zeit für das aktive ICE-Kandidatenpaar zwischen Peers.", "connection_stats_round_trip_time_description": "Roundtrip-Zeit für das aktive ICE-Kandidatenpaar zwischen Peers.",
"connection_stats_sidebar": "Verbindungsstatistiken", "connection_stats_sidebar": "Verbindungsstatistiken",
@ -748,6 +789,9 @@
"peer_connection_failed": "Verbindung fehlgeschlagen", "peer_connection_failed": "Verbindung fehlgeschlagen",
"peer_connection_new": "Verbinden", "peer_connection_new": "Verbinden",
"previous": "Vorherige", "previous": "Vorherige",
"public_ip_card_header": "Öffentliche IP-Adressen",
"public_ip_card_refresh": "Aktualisieren",
"public_ip_card_refresh_error": "Aktualisierung der öffentlichen IP-Adressen fehlgeschlagen: {error}",
"register_device_error": "Beim Registrieren Ihres Geräts ist ein Fehler {error} aufgetreten.", "register_device_error": "Beim Registrieren Ihres Geräts ist ein Fehler {error} aufgetreten.",
"register_device_finish_button": "Einrichtung abschließen", "register_device_finish_button": "Einrichtung abschließen",
"register_device_name_description": "Geben Sie Ihrem Gerät einen Namen, damit Sie es später leicht identifizieren können. Sie können diesen Namen jederzeit ändern.", "register_device_name_description": "Geben Sie Ihrem Gerät einen Namen, damit Sie es später leicht identifizieren können. Sie können diesen Namen jederzeit ändern.",

View File

@ -49,8 +49,6 @@
"access_update_tls_settings": "Update TLS Settings", "access_update_tls_settings": "Update TLS Settings",
"action_bar_audio": "Audio", "action_bar_audio": "Audio",
"action_bar_connection_stats": "Connection Stats", "action_bar_connection_stats": "Connection Stats",
"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_disable": "Failed to disable audio input: {error}",
"audio_input_failed_enable": "Failed to enable audio input: {error}", "audio_input_failed_enable": "Failed to enable audio input: {error}",
"audio_input_auto_enable_disabled": "Auto-enable microphone disabled", "audio_input_auto_enable_disabled": "Auto-enable microphone disabled",
@ -167,6 +165,46 @@
"atx_power_control_reset_button": "Reset", "atx_power_control_reset_button": "Reset",
"atx_power_control_send_action_error": "Failed to send ATX power action {action}: {error}", "atx_power_control_send_action_error": "Failed to send ATX power action {action}: {error}",
"atx_power_control_short_power_button": "Short Press", "atx_power_control_short_power_button": "Short Press",
"audio_https_only": "HTTPS only",
"audio_input_auto_enable_disabled": "Auto-enable microphone disabled",
"audio_input_auto_enable_enabled": "Auto-enable microphone enabled",
"audio_input_failed_disable": "Failed to disable audio input: {error}",
"audio_input_failed_enable": "Failed to enable audio input: {error}",
"audio_microphone_description": "Microphone input to target",
"audio_microphone_title": "Microphone",
"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_popover_description": "Quick audio controls for speakers and microphone",
"audio_popover_title": "Audio",
"audio_settings_applied": "Audio settings applied",
"audio_settings_apply_button": "Apply Settings",
"audio_settings_auto_enable_microphone_description": "Automatically enable browser microphone when connecting (otherwise you must manually enable each session)",
"audio_settings_auto_enable_microphone_title": "Auto-enable Microphone",
"audio_settings_bitrate_description": "Audio encoding bitrate (higher = better quality, more bandwidth)",
"audio_settings_bitrate_title": "Opus Bitrate",
"audio_settings_buffer_description": "ALSA buffer size (higher = more stable, more latency)",
"audio_settings_buffer_title": "Buffer Periods",
"audio_settings_complexity_description": "Encoder complexity (0-10, higher = better quality, more CPU)",
"audio_settings_complexity_title": "Opus Complexity",
"audio_settings_config_updated": "Audio configuration updated",
"audio_settings_description": "Configure audio input and output settings for your JetKVM device",
"audio_settings_dtx_description": "Save bandwidth during silence",
"audio_settings_dtx_title": "DTX (Discontinuous Transmission)",
"audio_settings_fec_description": "Improve audio quality on lossy connections",
"audio_settings_fec_title": "FEC (Forward Error Correction)",
"audio_settings_hdmi_label": "HDMI",
"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_speakers_description": "Audio from target to speakers",
"audio_speakers_title": "Speakers",
"auth_authentication_mode": "Please select an authentication mode", "auth_authentication_mode": "Please select an authentication mode",
"auth_authentication_mode_error": "An error occurred while setting the authentication mode", "auth_authentication_mode_error": "An error occurred while setting the authentication mode",
"auth_authentication_mode_invalid": "Invalid authentication mode", "auth_authentication_mode_invalid": "Invalid authentication mode",
@ -223,6 +261,9 @@
"connection_stats_packets_lost_description": "Count of lost inbound video RTP packets.", "connection_stats_packets_lost_description": "Count of lost inbound video RTP packets.",
"connection_stats_playback_delay": "Playback Delay", "connection_stats_playback_delay": "Playback Delay",
"connection_stats_playback_delay_description": "Delay added by the jitter buffer to smooth playback when frames arrive unevenly.", "connection_stats_playback_delay_description": "Delay added by the jitter buffer to smooth playback when frames arrive unevenly.",
"connection_stats_remote_ip_address": "Remote IP Address",
"connection_stats_remote_ip_address_copy_error": "Failed to copy remote IP address",
"connection_stats_remote_ip_address_copy_success": "Remote IP address { ip } copied to clipboard",
"connection_stats_round_trip_time": "Round-Trip Time", "connection_stats_round_trip_time": "Round-Trip Time",
"connection_stats_round_trip_time_description": "Round-trip time for the active ICE candidate pair between peers.", "connection_stats_round_trip_time_description": "Round-trip time for the active ICE candidate pair between peers.",
"connection_stats_sidebar": "Connection Stats", "connection_stats_sidebar": "Connection Stats",
@ -748,6 +789,9 @@
"peer_connection_failed": "Connection failed", "peer_connection_failed": "Connection failed",
"peer_connection_new": "Connecting", "peer_connection_new": "Connecting",
"previous": "Previous", "previous": "Previous",
"public_ip_card_header": "Public IP addresses",
"public_ip_card_refresh": "Refresh",
"public_ip_card_refresh_error": "Failed to refresh public IP addresses: {error}",
"register_device_error": "There was an error {error} registering your device.", "register_device_error": "There was an error {error} registering your device.",
"register_device_finish_button": "Finish Setup", "register_device_finish_button": "Finish Setup",
"register_device_name_description": "Name your device so you can easily identify it later. You can change this name at any time.", "register_device_name_description": "Name your device so you can easily identify it later. You can change this name at any time.",
@ -947,11 +991,5 @@
"wake_on_lan_invalid_mac": "Invalid MAC address", "wake_on_lan_invalid_mac": "Invalid MAC address",
"wake_on_lan_magic_sent_success": "Magic Packet sent successfully", "wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
"welcome_to_jetkvm": "Welcome to JetKVM", "welcome_to_jetkvm": "Welcome to JetKVM",
"welcome_to_jetkvm_description": "Control any computer remotely","connection_stats_remote_ip_address": "Remote IP Address", "welcome_to_jetkvm_description": "Control any computer remotely"
"connection_stats_remote_ip_address_description": "The IP address of the remote device.",
"connection_stats_remote_ip_address_copy_error": "Failed to copy remote IP address",
"connection_stats_remote_ip_address_copy_success": "Remote IP address { ip } copied to clipboard",
"public_ip_card_header": "Public IP addresses",
"public_ip_card_refresh": "Refresh",
"public_ip_card_refresh_error": "Failed to refresh public IP addresses: {error}"
} }

View File

@ -49,8 +49,6 @@
"access_update_tls_settings": "Actualizar la configuración de TLS", "access_update_tls_settings": "Actualizar la configuración de TLS",
"action_bar_audio": "Audio", "action_bar_audio": "Audio",
"action_bar_connection_stats": "Estadísticas de conexión", "action_bar_connection_stats": "Estadísticas de conexión",
"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_disable": "Error al desactivar la entrada de audio: {error}",
"audio_input_failed_enable": "Error al activar la entrada de audio: {error}", "audio_input_failed_enable": "Error al activar la entrada de audio: {error}",
"audio_input_auto_enable_disabled": "Habilitación automática de micrófono desactivada", "audio_input_auto_enable_disabled": "Habilitación automática de micrófono desactivada",
@ -167,6 +165,46 @@
"atx_power_control_reset_button": "Reiniciar", "atx_power_control_reset_button": "Reiniciar",
"atx_power_control_send_action_error": "No se pudo enviar la acción de alimentación ATX {action} : {error}", "atx_power_control_send_action_error": "No se pudo enviar la acción de alimentación ATX {action} : {error}",
"atx_power_control_short_power_button": "Prensa corta", "atx_power_control_short_power_button": "Prensa corta",
"audio_https_only": "Solo HTTPS",
"audio_input_auto_enable_disabled": "Habilitación automática de micrófono desactivada",
"audio_input_auto_enable_enabled": "Habilitación automática de micrófono 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_microphone_description": "Entrada de micrófono al objetivo",
"audio_microphone_title": "Micrófono",
"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_popover_description": "Controles de audio rápidos para altavoces y micrófono",
"audio_popover_title": "Audio",
"audio_settings_applied": "Configuración de audio aplicada",
"audio_settings_apply_button": "Aplicar configuración",
"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_auto_enable_microphone_title": "Habilitar micrófono automáticamente",
"audio_settings_bitrate_description": "Tasa de bits de codificación de audio (mayor = mejor calidad, más ancho de banda)",
"audio_settings_bitrate_title": "Bitrate Opus",
"audio_settings_buffer_description": "Tamaño del buffer ALSA (mayor = más estable, más latencia)",
"audio_settings_buffer_title": "Períodos de Buffer",
"audio_settings_complexity_description": "Complejidad del codificador (0-10, mayor = mejor calidad, más CPU)",
"audio_settings_complexity_title": "Complejidad Opus",
"audio_settings_config_updated": "Configuración de audio actualizada",
"audio_settings_description": "Configure los ajustes de entrada y salida de audio para su dispositivo JetKVM",
"audio_settings_dtx_description": "Ahorrar ancho de banda durante el silencio",
"audio_settings_dtx_title": "DTX (Transmisión Discontinua)",
"audio_settings_fec_description": "Mejorar la calidad de audio en conexiones con pérdida",
"audio_settings_fec_title": "FEC (Corrección de Errores)",
"audio_settings_hdmi_label": "HDMI",
"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_speakers_description": "Audio del objetivo a los altavoces",
"audio_speakers_title": "Altavoces",
"auth_authentication_mode": "Por favor seleccione un modo de autenticación", "auth_authentication_mode": "Por favor seleccione un modo de autenticación",
"auth_authentication_mode_error": "Se produjo un error al configurar el modo de autenticación", "auth_authentication_mode_error": "Se produjo un error al configurar el modo de autenticación",
"auth_authentication_mode_invalid": "Modo de autenticación no válido", "auth_authentication_mode_invalid": "Modo de autenticación no válido",
@ -223,6 +261,9 @@
"connection_stats_packets_lost_description": "Recuento de paquetes de vídeo RTP entrantes perdidos.", "connection_stats_packets_lost_description": "Recuento de paquetes de vídeo RTP entrantes perdidos.",
"connection_stats_playback_delay": "Retraso de reproducción", "connection_stats_playback_delay": "Retraso de reproducción",
"connection_stats_playback_delay_description": "Retraso agregado por el buffer de fluctuación para suavizar la reproducción cuando los cuadros llegan de manera desigual.", "connection_stats_playback_delay_description": "Retraso agregado por el buffer de fluctuación para suavizar la reproducción cuando los cuadros llegan de manera desigual.",
"connection_stats_remote_ip_address": "Dirección IP remota",
"connection_stats_remote_ip_address_copy_error": "No se pudo copiar la dirección IP remota",
"connection_stats_remote_ip_address_copy_success": "Dirección IP remota { ip } copiada al portapapeles",
"connection_stats_round_trip_time": "Tiempo de ida y vuelta", "connection_stats_round_trip_time": "Tiempo de ida y vuelta",
"connection_stats_round_trip_time_description": "Tiempo de ida y vuelta para el par de candidatos ICE activos entre pares.", "connection_stats_round_trip_time_description": "Tiempo de ida y vuelta para el par de candidatos ICE activos entre pares.",
"connection_stats_sidebar": "Estadísticas de conexión", "connection_stats_sidebar": "Estadísticas de conexión",
@ -748,6 +789,9 @@
"peer_connection_failed": "La conexión falló", "peer_connection_failed": "La conexión falló",
"peer_connection_new": "Conectando", "peer_connection_new": "Conectando",
"previous": "Anterior", "previous": "Anterior",
"public_ip_card_header": "Direcciones IP públicas",
"public_ip_card_refresh": "Refrescar",
"public_ip_card_refresh_error": "Error al actualizar las direcciones IP públicas: {error}",
"register_device_error": "Se produjo un error {error} al registrar su dispositivo.", "register_device_error": "Se produjo un error {error} al registrar su dispositivo.",
"register_device_finish_button": "Finalizar configuración", "register_device_finish_button": "Finalizar configuración",
"register_device_name_description": "Ponle un nombre a tu dispositivo para que puedas identificarlo fácilmente más tarde. Puedes cambiarlo en cualquier momento.", "register_device_name_description": "Ponle un nombre a tu dispositivo para que puedas identificarlo fácilmente más tarde. Puedes cambiarlo en cualquier momento.",

View File

@ -49,8 +49,6 @@
"access_update_tls_settings": "Mettre à jour les paramètres TLS", "access_update_tls_settings": "Mettre à jour les paramètres TLS",
"action_bar_audio": "Audio", "action_bar_audio": "Audio",
"action_bar_connection_stats": "Statistiques de connexion", "action_bar_connection_stats": "Statistiques de connexion",
"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_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_failed_enable": "Échec de l'activation de l'entrée audio : {error}",
"audio_input_auto_enable_disabled": "Activation automatique du microphone désactivée", "audio_input_auto_enable_disabled": "Activation automatique du microphone désactivée",
@ -167,6 +165,46 @@
"atx_power_control_reset_button": "Réinitialiser", "atx_power_control_reset_button": "Réinitialiser",
"atx_power_control_send_action_error": "Échec de l'envoi de l'action d'alimentation ATX {action} : {error}", "atx_power_control_send_action_error": "Échec de l'envoi de l'action d'alimentation ATX {action} : {error}",
"atx_power_control_short_power_button": "Appui court", "atx_power_control_short_power_button": "Appui court",
"audio_https_only": "HTTPS uniquement",
"audio_input_auto_enable_disabled": "Activation automatique du microphone désactivée",
"audio_input_auto_enable_enabled": "Activation automatique du microphone 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_microphone_description": "Entrée microphone vers la cible",
"audio_microphone_title": "Microphone",
"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_popover_description": "Contrôles audio rapides pour haut-parleurs et microphone",
"audio_popover_title": "Audio",
"audio_settings_applied": "Paramètres audio appliqués",
"audio_settings_apply_button": "Appliquer les paramètres",
"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_auto_enable_microphone_title": "Activer automatiquement le microphone",
"audio_settings_bitrate_description": "Débit d'encodage audio (plus élevé = meilleure qualité, plus de bande passante)",
"audio_settings_bitrate_title": "Débit Opus",
"audio_settings_buffer_description": "Taille du tampon ALSA (plus élevé = plus stable, plus de latence)",
"audio_settings_buffer_title": "Périodes de Tampon",
"audio_settings_complexity_description": "Complexité de l'encodeur (0-10, plus élevé = meilleure qualité, plus de CPU)",
"audio_settings_complexity_title": "Complexité Opus",
"audio_settings_config_updated": "Configuration audio mise à jour",
"audio_settings_description": "Configurez les paramètres d'entrée et de sortie audio pour votre appareil JetKVM",
"audio_settings_dtx_description": "Économiser la bande passante pendant le silence",
"audio_settings_dtx_title": "DTX (Transmission Discontinue)",
"audio_settings_fec_description": "Améliorer la qualité audio sur les connexions avec perte",
"audio_settings_fec_title": "FEC (Correction d'Erreur)",
"audio_settings_hdmi_label": "HDMI",
"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_speakers_description": "Audio de la cible vers les haut-parleurs",
"audio_speakers_title": "Haut-parleurs",
"auth_authentication_mode": "Veuillez sélectionner un mode d'authentification", "auth_authentication_mode": "Veuillez sélectionner un mode d'authentification",
"auth_authentication_mode_error": "Une erreur s'est produite lors de la définition du mode d'authentification", "auth_authentication_mode_error": "Une erreur s'est produite lors de la définition du mode d'authentification",
"auth_authentication_mode_invalid": "Mode d'authentification non valide", "auth_authentication_mode_invalid": "Mode d'authentification non valide",
@ -223,6 +261,9 @@
"connection_stats_packets_lost_description": "Nombre de paquets vidéo RTP entrants perdus.", "connection_stats_packets_lost_description": "Nombre de paquets vidéo RTP entrants perdus.",
"connection_stats_playback_delay": "Délai de lecture", "connection_stats_playback_delay": "Délai de lecture",
"connection_stats_playback_delay_description": "Retard ajouté par le tampon de gigue pour fluidifier la lecture lorsque les images arrivent de manière inégale.", "connection_stats_playback_delay_description": "Retard ajouté par le tampon de gigue pour fluidifier la lecture lorsque les images arrivent de manière inégale.",
"connection_stats_remote_ip_address": "Adresse IP distante",
"connection_stats_remote_ip_address_copy_error": "Échec de la copie de l'adresse IP distante",
"connection_stats_remote_ip_address_copy_success": "Adresse IP distante { ip } copiée dans le presse-papiers",
"connection_stats_round_trip_time": "Temps de trajet aller-retour", "connection_stats_round_trip_time": "Temps de trajet aller-retour",
"connection_stats_round_trip_time_description": "Temps de trajet aller-retour pour la paire de candidats ICE actifs entre pairs.", "connection_stats_round_trip_time_description": "Temps de trajet aller-retour pour la paire de candidats ICE actifs entre pairs.",
"connection_stats_sidebar": "Statistiques de connexion", "connection_stats_sidebar": "Statistiques de connexion",
@ -748,6 +789,9 @@
"peer_connection_failed": "La connexion a échoué", "peer_connection_failed": "La connexion a échoué",
"peer_connection_new": "Nouveau", "peer_connection_new": "Nouveau",
"previous": "Précédent", "previous": "Précédent",
"public_ip_card_header": "Adresses IP publiques",
"public_ip_card_refresh": "Rafraîchir",
"public_ip_card_refresh_error": "Échec de l'actualisation des adresses IP publiques : {error}",
"register_device_error": "Une erreur {error} s'est produite lors de l'enregistrement de votre appareil.", "register_device_error": "Une erreur {error} s'est produite lors de l'enregistrement de votre appareil.",
"register_device_finish_button": "Terminer la configuration", "register_device_finish_button": "Terminer la configuration",
"register_device_name_description": "Nommez votre appareil pour pouvoir l'identifier facilement plus tard. Vous pouvez modifier ce nom à tout moment.", "register_device_name_description": "Nommez votre appareil pour pouvoir l'identifier facilement plus tard. Vous pouvez modifier ce nom à tout moment.",

View File

@ -49,8 +49,6 @@
"access_update_tls_settings": "Aggiorna le impostazioni TLS", "access_update_tls_settings": "Aggiorna le impostazioni TLS",
"action_bar_audio": "Audio", "action_bar_audio": "Audio",
"action_bar_connection_stats": "Statistiche di connessione", "action_bar_connection_stats": "Statistiche di connessione",
"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_disable": "Impossibile disabilitare l'ingresso audio: {error}",
"audio_input_failed_enable": "Impossibile abilitare l'ingresso audio: {error}", "audio_input_failed_enable": "Impossibile abilitare l'ingresso audio: {error}",
"audio_input_auto_enable_disabled": "Abilitazione automatica microfono disabilitata", "audio_input_auto_enable_disabled": "Abilitazione automatica microfono disabilitata",
@ -167,6 +165,46 @@
"atx_power_control_reset_button": "Reset", "atx_power_control_reset_button": "Reset",
"atx_power_control_send_action_error": "Impossibile inviare l'azione di alimentazione ATX {action} : {error}", "atx_power_control_send_action_error": "Impossibile inviare l'azione di alimentazione ATX {action} : {error}",
"atx_power_control_short_power_button": "Pressione breve", "atx_power_control_short_power_button": "Pressione breve",
"audio_https_only": "Solo HTTPS",
"audio_input_auto_enable_disabled": "Abilitazione automatica microfono disabilitata",
"audio_input_auto_enable_enabled": "Abilitazione automatica microfono abilitata",
"audio_input_failed_disable": "Impossibile disabilitare l'ingresso audio: {error}",
"audio_input_failed_enable": "Impossibile abilitare l'ingresso audio: {error}",
"audio_microphone_description": "Ingresso microfono al target",
"audio_microphone_title": "Microfono",
"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_popover_description": "Controlli audio rapidi per altoparlanti e microfono",
"audio_popover_title": "Audio",
"audio_settings_applied": "Impostazioni audio applicate",
"audio_settings_apply_button": "Applica impostazioni",
"audio_settings_auto_enable_microphone_description": "Abilita automaticamente il microfono del browser durante la connessione (altrimenti devi abilitarlo manualmente ad ogni sessione)",
"audio_settings_auto_enable_microphone_title": "Abilita automaticamente il microfono",
"audio_settings_bitrate_description": "Bitrate di codifica audio (più alto = migliore qualità, più banda)",
"audio_settings_bitrate_title": "Bitrate Opus",
"audio_settings_buffer_description": "Dimensione buffer ALSA (più alto = più stabile, più latenza)",
"audio_settings_buffer_title": "Periodi Buffer",
"audio_settings_complexity_description": "Complessità dell'encoder (0-10, più alto = migliore qualità, più CPU)",
"audio_settings_complexity_title": "Complessità Opus",
"audio_settings_config_updated": "Configurazione audio aggiornata",
"audio_settings_description": "Configura le impostazioni di ingresso e uscita audio per il tuo dispositivo JetKVM",
"audio_settings_dtx_description": "Risparmia banda durante il silenzio",
"audio_settings_dtx_title": "DTX (Trasmissione Discontinua)",
"audio_settings_fec_description": "Migliora la qualità audio su connessioni con perdita",
"audio_settings_fec_title": "FEC (Correzione Errori)",
"audio_settings_hdmi_label": "HDMI",
"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_speakers_description": "Audio dal target agli altoparlanti",
"audio_speakers_title": "Altoparlanti",
"auth_authentication_mode": "Seleziona una modalità di autenticazione", "auth_authentication_mode": "Seleziona una modalità di autenticazione",
"auth_authentication_mode_error": "Si è verificato un errore durante l'impostazione della modalità di autenticazione", "auth_authentication_mode_error": "Si è verificato un errore durante l'impostazione della modalità di autenticazione",
"auth_authentication_mode_invalid": "Modalità di autenticazione non valida", "auth_authentication_mode_invalid": "Modalità di autenticazione non valida",
@ -223,6 +261,9 @@
"connection_stats_packets_lost_description": "Conteggio dei pacchetti video RTP in entrata persi.", "connection_stats_packets_lost_description": "Conteggio dei pacchetti video RTP in entrata persi.",
"connection_stats_playback_delay": "Ritardo di riproduzione", "connection_stats_playback_delay": "Ritardo di riproduzione",
"connection_stats_playback_delay_description": "Ritardo aggiunto dal buffer jitter per rendere più fluida la riproduzione quando i fotogrammi arrivano in modo non uniforme.", "connection_stats_playback_delay_description": "Ritardo aggiunto dal buffer jitter per rendere più fluida la riproduzione quando i fotogrammi arrivano in modo non uniforme.",
"connection_stats_remote_ip_address": "Indirizzo IP remoto",
"connection_stats_remote_ip_address_copy_error": "Impossibile copiare l'indirizzo IP remoto",
"connection_stats_remote_ip_address_copy_success": "Indirizzo IP remoto { ip } copiato negli appunti",
"connection_stats_round_trip_time": "Tempo di andata e ritorno", "connection_stats_round_trip_time": "Tempo di andata e ritorno",
"connection_stats_round_trip_time_description": "Tempo di andata e ritorno per la coppia di candidati ICE attivi tra pari.", "connection_stats_round_trip_time_description": "Tempo di andata e ritorno per la coppia di candidati ICE attivi tra pari.",
"connection_stats_sidebar": "Statistiche di connessione", "connection_stats_sidebar": "Statistiche di connessione",
@ -748,6 +789,9 @@
"peer_connection_failed": "Connessione fallita", "peer_connection_failed": "Connessione fallita",
"peer_connection_new": "Collegamento", "peer_connection_new": "Collegamento",
"previous": "Precedente", "previous": "Precedente",
"public_ip_card_header": "Indirizzi IP pubblici",
"public_ip_card_refresh": "Aggiorna",
"public_ip_card_refresh_error": "Impossibile aggiornare gli indirizzi IP pubblici: {error}",
"register_device_error": "Si è verificato un errore {error} durante la registrazione del dispositivo.", "register_device_error": "Si è verificato un errore {error} durante la registrazione del dispositivo.",
"register_device_finish_button": "Completa l'installazione", "register_device_finish_button": "Completa l'installazione",
"register_device_name_description": "Assegna un nome al tuo dispositivo per poterlo identificare facilmente in seguito. Puoi cambiare questo nome in qualsiasi momento.", "register_device_name_description": "Assegna un nome al tuo dispositivo per poterlo identificare facilmente in seguito. Puoi cambiare questo nome in qualsiasi momento.",

View File

@ -49,8 +49,6 @@
"access_update_tls_settings": "Oppdater TLS-innstillinger", "access_update_tls_settings": "Oppdater TLS-innstillinger",
"action_bar_audio": "Lyd", "action_bar_audio": "Lyd",
"action_bar_connection_stats": "Tilkoblingsstatistikk", "action_bar_connection_stats": "Tilkoblingsstatistikk",
"audio_input_disabled": "Lydinngang deaktivert",
"audio_input_enabled": "Lydinngang aktivert",
"audio_input_failed_disable": "Kunne ikke deaktivere lydinngang: {error}", "audio_input_failed_disable": "Kunne ikke deaktivere lydinngang: {error}",
"audio_input_failed_enable": "Kunne ikke aktivere lydinngang: {error}", "audio_input_failed_enable": "Kunne ikke aktivere lydinngang: {error}",
"audio_input_auto_enable_disabled": "Automatisk aktivering av mikrofon deaktivert", "audio_input_auto_enable_disabled": "Automatisk aktivering av mikrofon deaktivert",
@ -167,6 +165,46 @@
"atx_power_control_reset_button": "Tilbakestill", "atx_power_control_reset_button": "Tilbakestill",
"atx_power_control_send_action_error": "Kunne ikke sende ATX-strømhandling {action} : {error}", "atx_power_control_send_action_error": "Kunne ikke sende ATX-strømhandling {action} : {error}",
"atx_power_control_short_power_button": "Kort trykk", "atx_power_control_short_power_button": "Kort trykk",
"audio_https_only": "Kun HTTPS",
"audio_input_auto_enable_disabled": "Automatisk aktivering av mikrofon deaktivert",
"audio_input_auto_enable_enabled": "Automatisk aktivering av mikrofon aktivert",
"audio_input_failed_disable": "Kunne ikke deaktivere lydinngang: {error}",
"audio_input_failed_enable": "Kunne ikke aktivere lydinngang: {error}",
"audio_microphone_description": "Mikrofoninngang til mål",
"audio_microphone_title": "Mikrofon",
"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_popover_description": "Raske lydkontroller for høyttalere og mikrofon",
"audio_popover_title": "Lyd",
"audio_settings_applied": "Lydinnstillinger brukt",
"audio_settings_apply_button": "Bruk innstillinger",
"audio_settings_auto_enable_microphone_description": "Aktiver automatisk nettlesermikrofon ved tilkobling (ellers må du aktivere det manuelt hver økt)",
"audio_settings_auto_enable_microphone_title": "Aktiver mikrofon automatisk",
"audio_settings_bitrate_description": "Lydkodingsbitrate (høyere = bedre kvalitet, mer båndbredde)",
"audio_settings_bitrate_title": "Opus Bitrate",
"audio_settings_buffer_description": "ALSA bufferstørrelse (høyere = mer stabil, mer latens)",
"audio_settings_buffer_title": "Bufferperioder",
"audio_settings_complexity_description": "Encoder-kompleksitet (0-10, høyere = bedre kvalitet, mer CPU)",
"audio_settings_complexity_title": "Opus Kompleksitet",
"audio_settings_config_updated": "Lydkonfigurasjon oppdatert",
"audio_settings_description": "Konfigurer lydinngangs- og lydutgangsinnstillinger for JetKVM-enheten din",
"audio_settings_dtx_description": "Spar båndbredde under stillhet",
"audio_settings_dtx_title": "DTX (Diskontinuerlig Overføring)",
"audio_settings_fec_description": "Forbedre lydkvaliteten på tapende tilkoblinger",
"audio_settings_fec_title": "FEC (Fremadrettet Feilkorreksjon)",
"audio_settings_hdmi_label": "HDMI",
"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_speakers_description": "Lyd fra mål til høyttalere",
"audio_speakers_title": "Høyttalere",
"auth_authentication_mode": "Vennligst velg en autentiseringsmodus", "auth_authentication_mode": "Vennligst velg en autentiseringsmodus",
"auth_authentication_mode_error": "Det oppsto en feil under angivelse av autentiseringsmodus", "auth_authentication_mode_error": "Det oppsto en feil under angivelse av autentiseringsmodus",
"auth_authentication_mode_invalid": "Ugyldig autentiseringsmodus", "auth_authentication_mode_invalid": "Ugyldig autentiseringsmodus",
@ -223,6 +261,9 @@
"connection_stats_packets_lost_description": "Antall tapte innkommende RTP-videopakker.", "connection_stats_packets_lost_description": "Antall tapte innkommende RTP-videopakker.",
"connection_stats_playback_delay": "Avspillingsforsinkelse", "connection_stats_playback_delay": "Avspillingsforsinkelse",
"connection_stats_playback_delay_description": "Forsinkelse lagt til av jitterbufferen for jevn avspilling når bilder ankommer ujevnt.", "connection_stats_playback_delay_description": "Forsinkelse lagt til av jitterbufferen for jevn avspilling når bilder ankommer ujevnt.",
"connection_stats_remote_ip_address": "Ekstern IP-adresse",
"connection_stats_remote_ip_address_copy_error": "Kunne ikke kopiere den eksterne IP-adressen",
"connection_stats_remote_ip_address_copy_success": "Ekstern IP-adresse { ip } kopiert til utklippstavlen",
"connection_stats_round_trip_time": "Tur-retur-tid", "connection_stats_round_trip_time": "Tur-retur-tid",
"connection_stats_round_trip_time_description": "Rundturstid for det aktive ICE-kandidatparet mellom jevnaldrende.", "connection_stats_round_trip_time_description": "Rundturstid for det aktive ICE-kandidatparet mellom jevnaldrende.",
"connection_stats_sidebar": "Tilkoblingsstatistikk", "connection_stats_sidebar": "Tilkoblingsstatistikk",
@ -748,6 +789,9 @@
"peer_connection_failed": "Tilkoblingen mislyktes", "peer_connection_failed": "Tilkoblingen mislyktes",
"peer_connection_new": "Tilkobling", "peer_connection_new": "Tilkobling",
"previous": "Tidligere", "previous": "Tidligere",
"public_ip_card_header": "Offentlige IP-adresser",
"public_ip_card_refresh": "Forfriske",
"public_ip_card_refresh_error": "Kunne ikke oppdatere offentlige IP-adresser: {error}",
"register_device_error": "Det oppsto en feil {error} under registrering av enheten din.", "register_device_error": "Det oppsto en feil {error} under registrering av enheten din.",
"register_device_finish_button": "Fullfør oppsettet", "register_device_finish_button": "Fullfør oppsettet",
"register_device_name_description": "Gi enheten din et navn slik at du enkelt kan identifisere den senere. Du kan endre dette navnet når som helst.", "register_device_name_description": "Gi enheten din et navn slik at du enkelt kan identifisere den senere. Du kan endre dette navnet når som helst.",

View File

@ -48,9 +48,6 @@
"access_tls_updated": "TLS-inställningarna har uppdaterats", "access_tls_updated": "TLS-inställningarna har uppdaterats",
"access_update_tls_settings": "Uppdatera TLS-inställningar", "access_update_tls_settings": "Uppdatera TLS-inställningar",
"action_bar_audio": "Ljud", "action_bar_audio": "Ljud",
"action_bar_connection_stats": "Anslutningsstatistik",
"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_disable": "Det gick inte att inaktivera ljudingången: {error}",
"audio_input_failed_enable": "Det gick inte att aktivera ljudingången: {error}", "audio_input_failed_enable": "Det gick inte att aktivera ljudingången: {error}",
"audio_input_auto_enable_disabled": "Automatisk aktivering av mikrofon inaktiverad", "audio_input_auto_enable_disabled": "Automatisk aktivering av mikrofon inaktiverad",
@ -167,6 +164,46 @@
"atx_power_control_reset_button": "Starta om", "atx_power_control_reset_button": "Starta om",
"atx_power_control_send_action_error": "Misslyckades med att skicka ATX-strömåtgärd {action} : {error}", "atx_power_control_send_action_error": "Misslyckades med att skicka ATX-strömåtgärd {action} : {error}",
"atx_power_control_short_power_button": "Kort tryck", "atx_power_control_short_power_button": "Kort tryck",
"audio_https_only": "Endast HTTPS",
"audio_input_auto_enable_disabled": "Automatisk aktivering av mikrofon inaktiverad",
"audio_input_auto_enable_enabled": "Automatisk aktivering av mikrofon 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_microphone_description": "Mikrofoningång till mål",
"audio_microphone_title": "Mikrofon",
"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_popover_description": "Snabba ljudkontroller för högtalare och mikrofon",
"audio_popover_title": "Ljud",
"audio_settings_applied": "Ljudinställningar tillämpade",
"audio_settings_apply_button": "Tillämpa inställningar",
"audio_settings_auto_enable_microphone_description": "Aktivera automatiskt webbläsarmikrofon vid anslutning (annars måste du aktivera den manuellt varje session)",
"audio_settings_auto_enable_microphone_title": "Aktivera mikrofon automatiskt",
"audio_settings_bitrate_description": "Ljudkodningsbitrate (högre = bättre kvalitet, mer bandbredd)",
"audio_settings_bitrate_title": "Opus Bitrate",
"audio_settings_buffer_description": "ALSA bufferstorlek (högre = mer stabil, mer latens)",
"audio_settings_buffer_title": "Bufferperioder",
"audio_settings_complexity_description": "Encoder-komplexitet (0-10, högre = bättre kvalitet, mer CPU)",
"audio_settings_complexity_title": "Opus Komplexitet",
"audio_settings_config_updated": "Ljudkonfiguration uppdaterad",
"audio_settings_description": "Konfigurera ljudinmatnings- och ljudutgångsinställningar för din JetKVM-enhet",
"audio_settings_dtx_description": "Spara bandbredd under tystnad",
"audio_settings_dtx_title": "DTX (Diskontinuerlig Överföring)",
"audio_settings_fec_description": "Förbättra ljudkvaliteten på förlustdrabbade anslutningar",
"audio_settings_fec_title": "FEC (Framåtriktad Felkorrigering)",
"audio_settings_hdmi_label": "HDMI",
"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_speakers_description": "Ljud från mål till högtalare",
"audio_speakers_title": "Högtalare",
"auth_authentication_mode": "Välj ett autentiseringsläge", "auth_authentication_mode": "Välj ett autentiseringsläge",
"auth_authentication_mode_error": "Ett fel uppstod när autentiseringsläget ställdes in", "auth_authentication_mode_error": "Ett fel uppstod när autentiseringsläget ställdes in",
"auth_authentication_mode_invalid": "Ogiltigt autentiseringsläge", "auth_authentication_mode_invalid": "Ogiltigt autentiseringsläge",
@ -223,6 +260,9 @@
"connection_stats_packets_lost_description": "Antal förlorade inkommande RTP-videopaket.", "connection_stats_packets_lost_description": "Antal förlorade inkommande RTP-videopaket.",
"connection_stats_playback_delay": "Uppspelningsfördröjning", "connection_stats_playback_delay": "Uppspelningsfördröjning",
"connection_stats_playback_delay_description": "Fördröjning som läggs till av jitterbufferten för att jämna ut uppspelningen när bildrutor anländer ojämnt.", "connection_stats_playback_delay_description": "Fördröjning som läggs till av jitterbufferten för att jämna ut uppspelningen när bildrutor anländer ojämnt.",
"connection_stats_remote_ip_address": "Fjärr-IP-adress",
"connection_stats_remote_ip_address_copy_error": "Misslyckades med att kopiera fjärr-IP-adressen",
"connection_stats_remote_ip_address_copy_success": "Fjärr-IP-adress { ip } kopierad till urklipp",
"connection_stats_round_trip_time": "Tur- och returtid", "connection_stats_round_trip_time": "Tur- och returtid",
"connection_stats_round_trip_time_description": "Tur- och returtid för det aktiva ICE-kandidatparet mellan peers.", "connection_stats_round_trip_time_description": "Tur- och returtid för det aktiva ICE-kandidatparet mellan peers.",
"connection_stats_sidebar": "Anslutningsstatistik", "connection_stats_sidebar": "Anslutningsstatistik",
@ -748,6 +788,9 @@
"peer_connection_failed": "Anslutningen misslyckades", "peer_connection_failed": "Anslutningen misslyckades",
"peer_connection_new": "Ansluter", "peer_connection_new": "Ansluter",
"previous": "Föregående", "previous": "Föregående",
"public_ip_card_header": "Offentliga IP-adresser",
"public_ip_card_refresh": "Uppdatera",
"public_ip_card_refresh_error": "Misslyckades med att uppdatera offentliga IP-adresser: {error}",
"register_device_error": "Det uppstod ett fel {error} din enhet registrerades.", "register_device_error": "Det uppstod ett fel {error} din enhet registrerades.",
"register_device_finish_button": "Slutför installationen", "register_device_finish_button": "Slutför installationen",
"register_device_name_description": "Namnge din enhet så att du enkelt kan identifiera den senare. Du kan ändra namnet när som helst.", "register_device_name_description": "Namnge din enhet så att du enkelt kan identifiera den senare. Du kan ändra namnet när som helst.",

View File

@ -49,8 +49,6 @@
"access_update_tls_settings": "更新 TLS 设置", "access_update_tls_settings": "更新 TLS 设置",
"action_bar_audio": "音频", "action_bar_audio": "音频",
"action_bar_connection_stats": "连接统计", "action_bar_connection_stats": "连接统计",
"audio_input_disabled": "音频输入已禁用",
"audio_input_enabled": "音频输入已启用",
"audio_input_failed_disable": "禁用音频输入失败:{error}", "audio_input_failed_disable": "禁用音频输入失败:{error}",
"audio_input_failed_enable": "启用音频输入失败:{error}", "audio_input_failed_enable": "启用音频输入失败:{error}",
"audio_input_auto_enable_disabled": "自动启用麦克风已禁用", "audio_input_auto_enable_disabled": "自动启用麦克风已禁用",
@ -167,6 +165,46 @@
"atx_power_control_reset_button": "重置", "atx_power_control_reset_button": "重置",
"atx_power_control_send_action_error": "无法发送 ATX 电源操作 {action} : {error}", "atx_power_control_send_action_error": "无法发送 ATX 电源操作 {action} : {error}",
"atx_power_control_short_power_button": "短按", "atx_power_control_short_power_button": "短按",
"audio_https_only": "仅限 HTTPS",
"audio_input_auto_enable_disabled": "自动启用麦克风已禁用",
"audio_input_auto_enable_enabled": "自动启用麦克风已启用",
"audio_input_failed_disable": "禁用音频输入失败:{error}",
"audio_input_failed_enable": "启用音频输入失败:{error}",
"audio_microphone_description": "麦克风输入到目标设备",
"audio_microphone_title": "麦克风",
"audio_output_disabled": "音频输出已禁用",
"audio_output_enabled": "音频输出已启用",
"audio_output_failed_disable": "禁用音频输出失败:{error}",
"audio_output_failed_enable": "启用音频输出失败:{error}",
"audio_popover_description": "扬声器和麦克风的快速音频控制",
"audio_popover_title": "音频",
"audio_settings_applied": "音频设置已应用",
"audio_settings_apply_button": "应用设置",
"audio_settings_auto_enable_microphone_description": "连接时自动启用浏览器麦克风(否则您必须在每次会话中手动启用)",
"audio_settings_auto_enable_microphone_title": "自动启用麦克风",
"audio_settings_bitrate_description": "音频编码比特率(越高 = 质量越好,带宽越大)",
"audio_settings_bitrate_title": "Opus 比特率",
"audio_settings_buffer_description": "ALSA 缓冲大小(越高 = 越稳定,延迟越高)",
"audio_settings_buffer_title": "缓冲周期",
"audio_settings_complexity_description": "编码器复杂度0-10越高 = 质量越好CPU 使用越多)",
"audio_settings_complexity_title": "Opus 复杂度",
"audio_settings_config_updated": "音频配置已更新",
"audio_settings_description": "配置 JetKVM 设备的音频输入和输出设置",
"audio_settings_dtx_description": "在静音时节省带宽",
"audio_settings_dtx_title": "DTX不连续传输",
"audio_settings_fec_description": "改善有损连接上的音频质量",
"audio_settings_fec_title": "FEC前向纠错",
"audio_settings_hdmi_label": "HDMI",
"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_speakers_description": "从目标设备到扬声器的音频",
"audio_speakers_title": "扬声器",
"auth_authentication_mode": "请选择身份验证方式", "auth_authentication_mode": "请选择身份验证方式",
"auth_authentication_mode_error": "设置身份验证模式时发生错误", "auth_authentication_mode_error": "设置身份验证模式时发生错误",
"auth_authentication_mode_invalid": "身份验证模式无效", "auth_authentication_mode_invalid": "身份验证模式无效",
@ -223,6 +261,9 @@
"connection_stats_packets_lost_description": "丢失的入站视频 RTP 数据包的数量。", "connection_stats_packets_lost_description": "丢失的入站视频 RTP 数据包的数量。",
"connection_stats_playback_delay": "播放延迟", "connection_stats_playback_delay": "播放延迟",
"connection_stats_playback_delay_description": "当帧不均匀到达时,抖动缓冲区添加延迟以平滑播放。", "connection_stats_playback_delay_description": "当帧不均匀到达时,抖动缓冲区添加延迟以平滑播放。",
"connection_stats_remote_ip_address": "远程IP地址",
"connection_stats_remote_ip_address_copy_error": "复制远程 IP 地址失败",
"connection_stats_remote_ip_address_copy_success": "远程 IP 地址{ ip }已复制到剪贴板",
"connection_stats_round_trip_time": "往返时间", "connection_stats_round_trip_time": "往返时间",
"connection_stats_round_trip_time_description": "对等体之间活跃 ICE 候选对的往返时间。", "connection_stats_round_trip_time_description": "对等体之间活跃 ICE 候选对的往返时间。",
"connection_stats_sidebar": "连接统计", "connection_stats_sidebar": "连接统计",
@ -748,6 +789,9 @@
"peer_connection_failed": "连接失败", "peer_connection_failed": "连接失败",
"peer_connection_new": "正在连接", "peer_connection_new": "正在连接",
"previous": "上一步", "previous": "上一步",
"public_ip_card_header": "公共 IP 地址",
"public_ip_card_refresh": "刷新",
"public_ip_card_refresh_error": "刷新公网 IP 地址失败: {error}",
"register_device_error": "注册您的设备时出现错误{error} 。", "register_device_error": "注册您的设备时出现错误{error} 。",
"register_device_finish_button": "完成设置", "register_device_finish_button": "完成设置",
"register_device_name_description": "为您的设备命名,以便日后轻松识别。您可以随时更改此名称。", "register_device_name_description": "为您的设备命名,以便日后轻松识别。您可以随时更改此名称。",