diff --git a/audio.go b/audio.go index 88f16c96..368b2f7e 100644 --- a/audio.go +++ b/audio.go @@ -15,10 +15,10 @@ var ( audioMutex sync.Mutex setAudioTrackMutex sync.Mutex // Prevents concurrent setAudioTrack() calls inputSourceMutex sync.Mutex // Serializes Connect() and WriteMessage() calls to input source - outputSource audio.AudioSource + outputSource atomic.Pointer[audio.AudioSource] inputSource atomic.Pointer[audio.AudioSource] - outputRelay *audio.OutputRelay - inputRelay *audio.InputRelay + outputRelay atomic.Pointer[audio.OutputRelay] + inputRelay atomic.Pointer[audio.InputRelay] audioInitialized bool activeConnections atomic.Int32 audioLogger zerolog.Logger @@ -28,6 +28,14 @@ var ( audioInputEnabled atomic.Bool ) +func getAlsaDevice(source string) string { + if source == "hdmi" { + return "hw:0,0" + } else { + return "hw:1,0" + } +} + func initAudio() { audioLogger = logging.GetDefaultLogger().With().Str("component", "audio-manager").Logger() @@ -40,7 +48,7 @@ func initAudio() { } func getAudioConfig() audio.AudioConfig { - ensureConfigLoaded() + // config is already loaded cfg := audio.DefaultAudioConfig() if config.AudioBitrate >= 64 && config.AudioBitrate <= 256 { cfg.Bitrate = uint16(config.AudioBitrate) @@ -71,63 +79,81 @@ func startAudio() error { return nil } - if outputSource == nil && audioOutputEnabled.Load() && currentAudioTrack != nil { - ensureConfigLoaded() - alsaDevice := "hw:1,0" - 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") - } + if activeConnections.Load() <= 0 { + audioLogger.Debug().Msg("No active connections, skipping audio start") + return nil } ensureConfigLoaded() - if inputSource.Load() == nil && audioInputEnabled.Load() && config.UsbDevices != nil && config.UsbDevices.Audio { - alsaPlaybackDevice := "hw:1,0" - source := audio.NewCgoInputSource(alsaPlaybackDevice) - source.SetConfig(getAudioConfig()) - var audioSource audio.AudioSource = source - inputSource.Store(&audioSource) + if audioOutputEnabled.Load() && currentAudioTrack != nil { + startOutputAudioUnderMutex(getAlsaDevice(config.AudioOutputSource)) + } - inputRelay = audio.NewInputRelay(audioSource) - if err := inputRelay.Start(); err != nil { - audioLogger.Error().Err(err).Msg("Failed to start input relay") - } + if audioInputEnabled.Load() && config.UsbDevices != nil && config.UsbDevices.Audio { + startInputAudioUnderMutex(getAlsaDevice("usb")) } 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() { audioMutex.Lock() - outRelay := outputRelay - outSource := outputSource - outputRelay = nil - outputSource = nil + outRelay := outputRelay.Swap(nil) + outSource := outputSource.Swap(nil) audioMutex.Unlock() if outRelay != nil { outRelay.Stop() } if outSource != nil { - outSource.Disconnect() + (*outSource).Disconnect() } } func stopInputAudio() { audioMutex.Lock() - inRelay := inputRelay - inputRelay = nil - audioMutex.Unlock() - + inRelay := inputRelay.Swap(nil) inSource := inputSource.Swap(nil) + audioMutex.Unlock() if inRelay != nil { inRelay.Stop() @@ -153,7 +179,7 @@ func onWebRTCConnect() { func onWebRTCDisconnect() { count := activeConnections.Add(-1) - if count == 0 { + if count <= 0 { // Stop audio immediately to release HDMI audio device which shares hardware with video device stopAudio() } @@ -163,42 +189,12 @@ func setAudioTrack(audioTrack *webrtc.TrackLocalStaticSample) { setAudioTrackMutex.Lock() defer setAudioTrackMutex.Unlock() - // Capture old resources and update state in single critical section - audioMutex.Lock() + stopOutputAudio() + currentAudioTrack = audioTrack - oldRelay := outputRelay - oldSource := outputSource - outputRelay = nil - outputSource = nil - var newRelay *audio.OutputRelay - var newSource audio.AudioSource - 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") - } + if err := startAudio(); err != nil { + audioLogger.Error().Err(err).Msg("Failed to start with new audio track") } } @@ -250,79 +246,44 @@ func SetAudioOutputSource(source string) error { return nil } + stopOutputAudio() config.AudioOutputSource = source - stopOutputAudio() - - 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") - } + if err := startAudio(); err != nil { + audioLogger.Error().Err(err).Str("source", source).Msg("Failed to start audio output after source change") } return SaveConfig() } -func RestartAudioOutput() { +func RestartAudioOutput() error { audioMutex.Lock() - hasActiveOutput := outputSource != nil && currentAudioTrack != nil && audioOutputEnabled.Load() + hasActiveOutput := audioOutputEnabled.Load() && currentAudioTrack != nil && outputSource.Load() != nil audioMutex.Unlock() if !hasActiveOutput { - return + return nil } 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") - } + return startAudio() } func handleInputTrackForSession(track *webrtc.TrackRemote) { myTrackID := track.ID() - audioLogger.Debug(). + trackLogger := audioLogger.With(). Str("codec", track.Codec().MimeType). Str("track_id", myTrackID). - Msg("starting input track handler") + Logger() + + trackLogger.Debug().Msg("starting input track handler") for { currentTrackID := currentInputTrack.Load() if currentTrackID != nil && *currentTrackID != myTrackID { - audioLogger.Debug(). - Str("my_track_id", myTrackID). + trackLogger.Debug(). Str("current_track_id", *currentTrackID). Msg("input track handler exiting - superseded") return @@ -331,10 +292,10 @@ func handleInputTrackForSession(track *webrtc.TrackRemote) { rtpPacket, _, err := track.ReadRTP() if err != nil { if err == io.EOF { - audioLogger.Debug().Str("track_id", myTrackID).Msg("input track ended") + trackLogger.Debug().Msg("input track ended") 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 } diff --git a/internal/audio/cgo_source.go b/internal/audio/cgo_source.go index 45f08be7..81f9b70b 100644 --- a/internal/audio/cgo_source.go +++ b/internal/audio/cgo_source.go @@ -25,46 +25,47 @@ const ( ) type CgoSource struct { - direction string - alsaDevice string - initialized bool - connected bool - mu sync.Mutex - logger zerolog.Logger - opusBuf []byte - config AudioConfig + outputDevice bool + alsaDevice string + connected bool + mu sync.Mutex + logger zerolog.Logger + opusBuf []byte + config AudioConfig } -func NewCgoOutputSource(alsaDevice string) *CgoSource { - logger := logging.GetDefaultLogger().With().Str("component", "audio-output-cgo").Logger() +var _ AudioSource = (*CgoSource)(nil) + +func NewCgoOutputSource(alsaDevice string, cfg AudioConfig) AudioSource { + logger := logging.GetDefaultLogger().With(). + Str("component", "audio-output-cgo"). + Str("alsa_device", alsaDevice). + Logger() return &CgoSource{ - direction: "output", - alsaDevice: alsaDevice, - logger: logger, - opusBuf: make([]byte, ipcMaxFrameSize), - config: DefaultAudioConfig(), + outputDevice: true, + alsaDevice: alsaDevice, + logger: logger, + opusBuf: make([]byte, ipcMaxFrameSize), + config: cfg, } } -func NewCgoInputSource(alsaDevice string) *CgoSource { - logger := logging.GetDefaultLogger().With().Str("component", "audio-input-cgo").Logger() +func NewCgoInputSource(alsaDevice string, cfg AudioConfig) AudioSource { + logger := logging.GetDefaultLogger().With(). + Str("component", "audio-input-cgo"). + Str("alsa_device", alsaDevice). + Logger() return &CgoSource{ - direction: "input", - alsaDevice: alsaDevice, - logger: logger, - opusBuf: make([]byte, ipcMaxFrameSize), - config: DefaultAudioConfig(), + outputDevice: false, + alsaDevice: alsaDevice, + logger: logger, + opusBuf: make([]byte, ipcMaxFrameSize), + config: cfg, } } -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() @@ -73,7 +74,7 @@ func (c *CgoSource) Connect() error { return nil } - if c.direction == "output" { + if c.outputDevice { os.Setenv("ALSA_CAPTURE_DEVICE", c.alsaDevice) dtx := C.uchar(0) @@ -93,7 +94,6 @@ func (c *CgoSource) Connect() error { Uint8("buffer_periods", c.config.BufferPeriods). Uint32("sample_rate", c.config.SampleRate). Uint8("packet_loss_perc", c.config.PacketLossPerc). - Str("alsa_device", c.alsaDevice). Msg("Initializing audio capture") C.update_audio_constants( @@ -139,7 +139,6 @@ func (c *CgoSource) Connect() error { } c.connected = true - c.initialized = true return nil } @@ -151,10 +150,12 @@ func (c *CgoSource) Disconnect() { return } - if c.direction == "output" { + if c.outputDevice { C.jetkvm_audio_capture_close() + os.Unsetenv("ALSA_CAPTURE_DEVICE") } else { C.jetkvm_audio_playback_close() + os.Unsetenv("ALSA_PLAYBACK_DEVICE") } c.connected = false @@ -173,7 +174,7 @@ func (c *CgoSource) ReadMessage() (uint8, []byte, error) { return 0, nil, fmt.Errorf("not connected") } - if c.direction != "output" { + if !c.outputDevice { c.mu.Unlock() 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") } - if c.direction != "input" { + if c.outputDevice { c.mu.Unlock() return fmt.Errorf("WriteMessage only supported for input direction") } diff --git a/internal/audio/cgo_source_stub.go b/internal/audio/cgo_source_stub.go index 3658877d..22cf499b 100644 --- a/internal/audio/cgo_source_stub.go +++ b/internal/audio/cgo_source_stub.go @@ -6,11 +6,13 @@ package audio 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") } -func NewCgoInputSource(alsaDevice string) *CgoSource { +func NewCgoInputSource(alsaDevice string, audioConfig AudioConfig) AudioSource { 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 { panic("audio CGO source not supported on this platform") } - -func (c *CgoSource) SetConfig(cfg AudioConfig) { - panic("audio CGO source not supported on this platform") -} diff --git a/internal/audio/relay.go b/internal/audio/relay.go index e836482d..e877697c 100644 --- a/internal/audio/relay.go +++ b/internal/audio/relay.go @@ -13,7 +13,7 @@ import ( ) type OutputRelay struct { - source AudioSource + source *AudioSource audioTrack *webrtc.TrackLocalStaticSample ctx context.Context cancel context.CancelFunc @@ -26,7 +26,7 @@ type OutputRelay struct { 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()) logger := logging.GetDefaultLogger().With().Str("component", "audio-output-relay").Logger() @@ -73,19 +73,19 @@ func (r *OutputRelay) relayLoop() { const reconnectDelay = 1 * time.Second for r.running.Load() { - if !r.source.IsConnected() { - if err := r.source.Connect(); err != nil { + if !(*r.source).IsConnected() { + if err := (*r.source).Connect(); err != nil { r.logger.Debug().Err(err).Msg("failed to connect, will retry") time.Sleep(reconnectDelay) continue } } - msgType, payload, err := r.source.ReadMessage() + msgType, payload, err := (*r.source).ReadMessage() if err != nil { if r.running.Load() { r.logger.Warn().Err(err).Msg("read error, reconnecting") - r.source.Disconnect() + (*r.source).Disconnect() time.Sleep(reconnectDelay) } continue @@ -104,14 +104,14 @@ func (r *OutputRelay) relayLoop() { } type InputRelay struct { - source AudioSource + source *AudioSource ctx context.Context cancel context.CancelFunc logger zerolog.Logger running atomic.Bool } -func NewInputRelay(source AudioSource) *InputRelay { +func NewInputRelay(source *AudioSource) *InputRelay { ctx, cancel := context.WithCancel(context.Background()) logger := logging.GetDefaultLogger().With().Str("component", "audio-input-relay").Logger() diff --git a/internal/audio/source.go b/internal/audio/source.go index bdb953d4..b490b31d 100644 --- a/internal/audio/source.go +++ b/internal/audio/source.go @@ -32,5 +32,4 @@ type AudioSource interface { IsConnected() bool Connect() error Disconnect() - SetConfig(cfg AudioConfig) } diff --git a/jsonrpc.go b/jsonrpc.go index 69235805..bbbb65e5 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -18,7 +18,6 @@ import ( "github.com/rs/zerolog" "go.bug.st/serial" - "github.com/jetkvm/kvm/internal/audio" "github.com/jetkvm/kvm/internal/hidrpc" "github.com/jetkvm/kvm/internal/usbgadget" "github.com/jetkvm/kvm/internal/utils" @@ -688,10 +687,12 @@ func rpcGetUsbConfig() (usbgadget.Config, error) { func rpcSetUsbConfig(usbConfig usbgadget.Config) error { LoadConfig() + wasUsbAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio + config.UsbConfig = &usbConfig gadget.SetGadgetConfig(config.UsbConfig) - wasAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio - return updateUsbRelatedConfig(wasAudioEnabled) + + return updateUsbRelatedConfig(wasUsbAudioEnabled) } func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) { @@ -903,42 +904,23 @@ func rpcGetUsbDevices() (usbgadget.Devices, error) { return *config.UsbDevices, nil } -func updateUsbRelatedConfig(wasAudioEnabled bool) error { +func updateUsbRelatedConfig(wasUsbAudioEnabled bool) error { ensureConfigLoaded() + nowHasUsbAudio := config.UsbDevices != nil && config.UsbDevices.Audio + outputSourceIsUsb := config.AudioOutputSource == "usb" - audioMutex.Lock() - inRelay := inputRelay - inputRelay = nil - audioMutex.Unlock() + // must stop input audio before reconfiguring + stopInputAudio() - inSource := inputSource.Swap(nil) - - 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" + // if we're currently sourcing audio from USB, stop the output audio before reconfiguring + if outputSourceIsUsb { 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() - outputSource = newSource - outputRelay = newRelay - audioMutex.Unlock() - - if err := newRelay.Start(); err != nil { - logger.Warn().Err(err).Msg("Failed to start HDMI audio after USB audio disabled") - } - } + // Auto-switch to HDMI audio output when USB audio was selected and is now disabled + if wasUsbAudioEnabled && !nowHasUsbAudio && config.AudioOutputSource == "usb" { + logger.Info().Msg("USB audio just disabled, automatic switch audio output source to HDMI") + config.AudioOutputSource = "hdmi" } if err := gadget.UpdateGadgetConfig(); err != nil { @@ -949,18 +931,15 @@ func updateUsbRelatedConfig(wasAudioEnabled bool) error { return fmt.Errorf("failed to save config: %w", err) } - // Restart audio if USB audio is enabled with active connections - if activeConnections.Load() > 0 && config.UsbDevices != nil && config.UsbDevices.Audio { - if err := startAudio(); err != nil { - logger.Warn().Err(err).Msg("Failed to restart audio after USB reconfiguration") - } + if err := startAudio(); err != nil { + logger.Warn().Err(err).Msg("Failed to restart audio after USB reconfiguration") } return nil } func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { - wasAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio + wasUsbAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio currentDevices := gadget.GetGadgetDevices() // Skip reconfiguration if devices haven't changed to avoid HID disruption @@ -972,11 +951,11 @@ func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { config.UsbDevices = &usbDevices gadget.SetGadgetDevices(config.UsbDevices) - return updateUsbRelatedConfig(wasAudioEnabled) + return updateUsbRelatedConfig(wasUsbAudioEnabled) } func rpcSetUsbDeviceState(device string, enabled bool) error { - wasAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio + wasUsbAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio currentDevices := gadget.GetGadgetDevices() switch device { @@ -1001,7 +980,7 @@ func rpcSetUsbDeviceState(device string, enabled bool) error { } gadget.SetGadgetDevices(config.UsbDevices) - return updateUsbRelatedConfig(wasAudioEnabled) + return updateUsbRelatedConfig(wasUsbAudioEnabled) } func rpcGetAudioOutputEnabled() (bool, error) { @@ -1104,8 +1083,7 @@ func rpcSetAudioConfig(bitrate int, complexity int, dtxEnabled bool, fecEnabled } func rpcRestartAudioOutput() error { - RestartAudioOutput() - return nil + return RestartAudioOutput() } func rpcGetAudioInputAutoEnable() (bool, error) { diff --git a/main.go b/main.go index 0b5de7dc..081b039b 100644 --- a/main.go +++ b/main.go @@ -42,7 +42,9 @@ func Main() { initDisplay() initNative(systemVersionLocal, appVersionLocal) + initAudio() + defer stopAudio() http.DefaultClient.Timeout = 1 * time.Minute @@ -81,6 +83,7 @@ func Main() { if err := initImagesFolder(); err != nil { logger.Warn().Err(err).Msg("failed to init images folder") } + initJiggler() // start video sleep mode timer @@ -140,7 +143,6 @@ func Main() { signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs - stopAudio() logger.Log().Msg("JetKVM Shutting Down") //if fuseServer != nil { diff --git a/ui/localization/messages/da.json b/ui/localization/messages/da.json index bf63ac8a..f6bb076f 100644 --- a/ui/localization/messages/da.json +++ b/ui/localization/messages/da.json @@ -49,8 +49,6 @@ "access_update_tls_settings": "Opdater TLS-indstillinger", "action_bar_audio": "Lyd", "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_enable": "Kunne ikke aktivere lydindgang: {error}", "audio_input_auto_enable_disabled": "Automatisk aktivering af mikrofon deaktiveret", @@ -167,6 +165,46 @@ "atx_power_control_reset_button": "Nulstil", "atx_power_control_send_action_error": "Kunne ikke sende ATX-strømfunktion {action} : {error}", "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_error": "Der opstod en fejl under indstilling af godkendelsestilstanden", "auth_authentication_mode_invalid": "Ugyldig godkendelsestilstand", @@ -223,6 +261,9 @@ "connection_stats_packets_lost_description": "Antal mistede indgående video-RTP-pakker.", "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_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_description": "Rundrejsetid for det aktive ICE-kandidatpar mellem peers.", "connection_stats_sidebar": "Forbindelsesstatistik", @@ -748,6 +789,9 @@ "peer_connection_failed": "Forbindelsen mislykkedes", "peer_connection_new": "Forbinder", "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_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.", diff --git a/ui/localization/messages/de.json b/ui/localization/messages/de.json index 476214c8..de20729b 100644 --- a/ui/localization/messages/de.json +++ b/ui/localization/messages/de.json @@ -49,8 +49,6 @@ "access_update_tls_settings": "TLS-Einstellungen aktualisieren", "action_bar_audio": "Audio", "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_enable": "Fehler beim Aktivieren des Audioeingangs: {error}", "audio_input_auto_enable_disabled": "Automatische Mikrofonaktivierung deaktiviert", @@ -167,6 +165,46 @@ "atx_power_control_reset_button": "Reset-Taste", "atx_power_control_send_action_error": "ATX-Stromversorgungsaktion {action} konnte nicht gesendet werden: {error}", "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_error": "Beim Einstellen des Authentifizierungsmodus ist ein Fehler aufgetreten", "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_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_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_description": "Roundtrip-Zeit für das aktive ICE-Kandidatenpaar zwischen Peers.", "connection_stats_sidebar": "Verbindungsstatistiken", @@ -748,6 +789,9 @@ "peer_connection_failed": "Verbindung fehlgeschlagen", "peer_connection_new": "Verbinden", "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_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.", diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json index 865bb9f1..78513a26 100644 --- a/ui/localization/messages/en.json +++ b/ui/localization/messages/en.json @@ -49,8 +49,6 @@ "access_update_tls_settings": "Update TLS Settings", "action_bar_audio": "Audio", "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_enable": "Failed to enable audio input: {error}", "audio_input_auto_enable_disabled": "Auto-enable microphone disabled", @@ -167,6 +165,46 @@ "atx_power_control_reset_button": "Reset", "atx_power_control_send_action_error": "Failed to send ATX power action {action}: {error}", "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_error": "An error occurred while setting the 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_playback_delay": "Playback Delay", "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_description": "Round-trip time for the active ICE candidate pair between peers.", "connection_stats_sidebar": "Connection Stats", @@ -748,6 +789,9 @@ "peer_connection_failed": "Connection failed", "peer_connection_new": "Connecting", "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_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.", @@ -947,11 +991,5 @@ "wake_on_lan_invalid_mac": "Invalid MAC address", "wake_on_lan_magic_sent_success": "Magic Packet sent successfully", "welcome_to_jetkvm": "Welcome to JetKVM", - "welcome_to_jetkvm_description": "Control any computer remotely","connection_stats_remote_ip_address": "Remote IP Address", - "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}" + "welcome_to_jetkvm_description": "Control any computer remotely" } diff --git a/ui/localization/messages/es.json b/ui/localization/messages/es.json index 450290bd..10d74573 100644 --- a/ui/localization/messages/es.json +++ b/ui/localization/messages/es.json @@ -49,8 +49,6 @@ "access_update_tls_settings": "Actualizar la configuración de TLS", "action_bar_audio": "Audio", "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_enable": "Error al activar la entrada de audio: {error}", "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_send_action_error": "No se pudo enviar la acción de alimentación ATX {action} : {error}", "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_error": "Se produjo un error al configurar el modo de autenticación", "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_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_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_description": "Tiempo de ida y vuelta para el par de candidatos ICE activos entre pares.", "connection_stats_sidebar": "Estadísticas de conexión", @@ -748,6 +789,9 @@ "peer_connection_failed": "La conexión falló", "peer_connection_new": "Conectando", "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_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.", diff --git a/ui/localization/messages/fr.json b/ui/localization/messages/fr.json index 1175c624..ef142755 100644 --- a/ui/localization/messages/fr.json +++ b/ui/localization/messages/fr.json @@ -49,8 +49,6 @@ "access_update_tls_settings": "Mettre à jour les paramètres TLS", "action_bar_audio": "Audio", "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_enable": "Échec de l'activation de l'entrée audio : {error}", "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_send_action_error": "Échec de l'envoi de l'action d'alimentation ATX {action} : {error}", "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_error": "Une erreur s'est produite lors de la définition du mode d'authentification", "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_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_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_description": "Temps de trajet aller-retour pour la paire de candidats ICE actifs entre pairs.", "connection_stats_sidebar": "Statistiques de connexion", @@ -748,6 +789,9 @@ "peer_connection_failed": "La connexion a échoué", "peer_connection_new": "Nouveau", "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_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.", diff --git a/ui/localization/messages/it.json b/ui/localization/messages/it.json index 6bb694e4..ebc3215e 100644 --- a/ui/localization/messages/it.json +++ b/ui/localization/messages/it.json @@ -49,8 +49,6 @@ "access_update_tls_settings": "Aggiorna le impostazioni TLS", "action_bar_audio": "Audio", "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_enable": "Impossibile abilitare l'ingresso audio: {error}", "audio_input_auto_enable_disabled": "Abilitazione automatica microfono disabilitata", @@ -167,6 +165,46 @@ "atx_power_control_reset_button": "Reset", "atx_power_control_send_action_error": "Impossibile inviare l'azione di alimentazione ATX {action} : {error}", "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_error": "Si è verificato un errore durante l'impostazione della modalità di autenticazione", "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_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_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_description": "Tempo di andata e ritorno per la coppia di candidati ICE attivi tra pari.", "connection_stats_sidebar": "Statistiche di connessione", @@ -748,6 +789,9 @@ "peer_connection_failed": "Connessione fallita", "peer_connection_new": "Collegamento", "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_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.", diff --git a/ui/localization/messages/nb.json b/ui/localization/messages/nb.json index e144bb2c..aba9e0af 100644 --- a/ui/localization/messages/nb.json +++ b/ui/localization/messages/nb.json @@ -49,8 +49,6 @@ "access_update_tls_settings": "Oppdater TLS-innstillinger", "action_bar_audio": "Lyd", "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_enable": "Kunne ikke aktivere lydinngang: {error}", "audio_input_auto_enable_disabled": "Automatisk aktivering av mikrofon deaktivert", @@ -167,6 +165,46 @@ "atx_power_control_reset_button": "Tilbakestill", "atx_power_control_send_action_error": "Kunne ikke sende ATX-strømhandling {action} : {error}", "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_error": "Det oppsto en feil under angivelse av autentiseringsmodus", "auth_authentication_mode_invalid": "Ugyldig autentiseringsmodus", @@ -223,6 +261,9 @@ "connection_stats_packets_lost_description": "Antall tapte innkommende RTP-videopakker.", "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_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_description": "Rundturstid for det aktive ICE-kandidatparet mellom jevnaldrende.", "connection_stats_sidebar": "Tilkoblingsstatistikk", @@ -748,6 +789,9 @@ "peer_connection_failed": "Tilkoblingen mislyktes", "peer_connection_new": "Tilkobling", "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_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.", diff --git a/ui/localization/messages/sv.json b/ui/localization/messages/sv.json index 6ec317a9..20ecc940 100644 --- a/ui/localization/messages/sv.json +++ b/ui/localization/messages/sv.json @@ -48,9 +48,6 @@ "access_tls_updated": "TLS-inställningarna har uppdaterats", "access_update_tls_settings": "Uppdatera TLS-inställningar", "action_bar_audio": "Ljud", - "action_bar_connection_stats": "Anslutningsstatistik", - "audio_input_disabled": "Ljudingång inaktiverad", - "audio_input_enabled": "Ljudingång aktiverad", "audio_input_failed_disable": "Det gick inte att inaktivera ljudingången: {error}", "audio_input_failed_enable": "Det gick inte att aktivera ljudingången: {error}", "audio_input_auto_enable_disabled": "Automatisk aktivering av mikrofon inaktiverad", @@ -167,6 +164,46 @@ "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_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_error": "Ett fel uppstod när autentiseringsläget ställdes in", "auth_authentication_mode_invalid": "Ogiltigt autentiseringsläge", @@ -223,6 +260,9 @@ "connection_stats_packets_lost_description": "Antal förlorade inkommande RTP-videopaket.", "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_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_description": "Tur- och returtid för det aktiva ICE-kandidatparet mellan peers.", "connection_stats_sidebar": "Anslutningsstatistik", @@ -748,6 +788,9 @@ "peer_connection_failed": "Anslutningen misslyckades", "peer_connection_new": "Ansluter", "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_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.", diff --git a/ui/localization/messages/zh.json b/ui/localization/messages/zh.json index 965d593f..4f90a95a 100644 --- a/ui/localization/messages/zh.json +++ b/ui/localization/messages/zh.json @@ -49,8 +49,6 @@ "access_update_tls_settings": "更新 TLS 设置", "action_bar_audio": "音频", "action_bar_connection_stats": "连接统计", - "audio_input_disabled": "音频输入已禁用", - "audio_input_enabled": "音频输入已启用", "audio_input_failed_disable": "禁用音频输入失败:{error}", "audio_input_failed_enable": "启用音频输入失败:{error}", "audio_input_auto_enable_disabled": "自动启用麦克风已禁用", @@ -167,6 +165,46 @@ "atx_power_control_reset_button": "重置", "atx_power_control_send_action_error": "无法发送 ATX 电源操作 {action} : {error}", "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_error": "设置身份验证模式时发生错误", "auth_authentication_mode_invalid": "身份验证模式无效", @@ -223,6 +261,9 @@ "connection_stats_packets_lost_description": "丢失的入站视频 RTP 数据包的数量。", "connection_stats_playback_delay": "播放延迟", "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_description": "对等体之间活跃 ICE 候选对的往返时间。", "connection_stats_sidebar": "连接统计", @@ -748,6 +789,9 @@ "peer_connection_failed": "连接失败", "peer_connection_new": "正在连接", "previous": "上一步", + "public_ip_card_header": "公共 IP 地址", + "public_ip_card_refresh": "刷新", + "public_ip_card_refresh_error": "刷新公网 IP 地址失败: {error}", "register_device_error": "注册您的设备时出现错误{error} 。", "register_device_finish_button": "完成设置", "register_device_name_description": "为您的设备命名,以便日后轻松识别。您可以随时更改此名称。",