From ba831dc682cac10fff14ed2f3c14294275f80d14 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Mon, 17 Nov 2025 15:23:40 -0600 Subject: [PATCH 1/6] Ran npm run i18n to sort the message strings. --- ui/localization/messages/da.json | 44 ++++++++++++++++++++++-- ui/localization/messages/de.json | 44 ++++++++++++++++++++++-- ui/localization/messages/en.json | 59 +++++++++++++++++++++++++++----- ui/localization/messages/es.json | 44 ++++++++++++++++++++++-- ui/localization/messages/fr.json | 44 ++++++++++++++++++++++-- ui/localization/messages/it.json | 44 ++++++++++++++++++++++-- ui/localization/messages/nb.json | 44 ++++++++++++++++++++++-- ui/localization/messages/sv.json | 45 ++++++++++++++++++++++-- ui/localization/messages/zh.json | 44 ++++++++++++++++++++++-- 9 files changed, 386 insertions(+), 26 deletions(-) diff --git a/ui/localization/messages/da.json b/ui/localization/messages/da.json index bf63ac8a..a474cf33 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,48 @@ "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_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_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", diff --git a/ui/localization/messages/de.json b/ui/localization/messages/de.json index 476214c8..fef9bf2d 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,48 @@ "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_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_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", diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json index 865bb9f1..6b33baf2 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,48 @@ "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_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_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 +263,10 @@ "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_remote_ip_address_description": "The IP address of the remote device.", "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 +792,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 +994,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..d50e817c 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,48 @@ "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_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_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", diff --git a/ui/localization/messages/fr.json b/ui/localization/messages/fr.json index 1175c624..ed79322e 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,48 @@ "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_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_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", diff --git a/ui/localization/messages/it.json b/ui/localization/messages/it.json index 6bb694e4..969c57bf 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,48 @@ "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_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_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", diff --git a/ui/localization/messages/nb.json b/ui/localization/messages/nb.json index e144bb2c..f2b60117 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,48 @@ "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_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_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", diff --git a/ui/localization/messages/sv.json b/ui/localization/messages/sv.json index 6ec317a9..68fa483f 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,48 @@ "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_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_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", diff --git a/ui/localization/messages/zh.json b/ui/localization/messages/zh.json index 965d593f..8ddcdbee 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,48 @@ "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_disabled": "音频输入已禁用", + "audio_input_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": "身份验证模式无效", From cbba7f255a42c726174e0add31f625345707218a Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Mon, 17 Nov 2025 15:30:27 -0600 Subject: [PATCH 2/6] Removed unused translations. --- ui/localization/messages/da.json | 2 -- ui/localization/messages/de.json | 2 -- ui/localization/messages/en.json | 3 --- ui/localization/messages/es.json | 2 -- ui/localization/messages/fr.json | 2 -- ui/localization/messages/it.json | 2 -- ui/localization/messages/nb.json | 2 -- ui/localization/messages/sv.json | 2 -- ui/localization/messages/zh.json | 2 -- 9 files changed, 19 deletions(-) diff --git a/ui/localization/messages/da.json b/ui/localization/messages/da.json index a474cf33..06edb1d0 100644 --- a/ui/localization/messages/da.json +++ b/ui/localization/messages/da.json @@ -168,8 +168,6 @@ "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_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_microphone_description": "Mikrofonindgang til mål", diff --git a/ui/localization/messages/de.json b/ui/localization/messages/de.json index fef9bf2d..b346b444 100644 --- a/ui/localization/messages/de.json +++ b/ui/localization/messages/de.json @@ -168,8 +168,6 @@ "audio_https_only": "Nur HTTPS", "audio_input_auto_enable_disabled": "Automatische Mikrofonaktivierung deaktiviert", "audio_input_auto_enable_enabled": "Automatische Mikrofonaktivierung aktiviert", - "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_microphone_description": "Mikrofoneingang zum Ziel", diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json index 6b33baf2..78513a26 100644 --- a/ui/localization/messages/en.json +++ b/ui/localization/messages/en.json @@ -168,8 +168,6 @@ "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_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_microphone_description": "Microphone input to target", @@ -266,7 +264,6 @@ "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_remote_ip_address_description": "The IP address of the remote device.", "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", diff --git a/ui/localization/messages/es.json b/ui/localization/messages/es.json index d50e817c..acc1ff0e 100644 --- a/ui/localization/messages/es.json +++ b/ui/localization/messages/es.json @@ -168,8 +168,6 @@ "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_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_microphone_description": "Entrada de micrófono al objetivo", diff --git a/ui/localization/messages/fr.json b/ui/localization/messages/fr.json index ed79322e..c17d78dc 100644 --- a/ui/localization/messages/fr.json +++ b/ui/localization/messages/fr.json @@ -168,8 +168,6 @@ "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_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_microphone_description": "Entrée microphone vers la cible", diff --git a/ui/localization/messages/it.json b/ui/localization/messages/it.json index 969c57bf..478ef5d5 100644 --- a/ui/localization/messages/it.json +++ b/ui/localization/messages/it.json @@ -168,8 +168,6 @@ "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_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_microphone_description": "Ingresso microfono al target", diff --git a/ui/localization/messages/nb.json b/ui/localization/messages/nb.json index f2b60117..7f7061ad 100644 --- a/ui/localization/messages/nb.json +++ b/ui/localization/messages/nb.json @@ -168,8 +168,6 @@ "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_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_microphone_description": "Mikrofoninngang til mål", diff --git a/ui/localization/messages/sv.json b/ui/localization/messages/sv.json index 68fa483f..de662a6e 100644 --- a/ui/localization/messages/sv.json +++ b/ui/localization/messages/sv.json @@ -167,8 +167,6 @@ "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_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_microphone_description": "Mikrofoningång till mål", diff --git a/ui/localization/messages/zh.json b/ui/localization/messages/zh.json index 8ddcdbee..6d4bf613 100644 --- a/ui/localization/messages/zh.json +++ b/ui/localization/messages/zh.json @@ -168,8 +168,6 @@ "audio_https_only": "仅限 HTTPS", "audio_input_auto_enable_disabled": "自动启用麦克风已禁用", "audio_input_auto_enable_enabled": "自动启用麦克风已启用", - "audio_input_disabled": "音频输入已禁用", - "audio_input_enabled": "音频输入已启用", "audio_input_failed_disable": "禁用音频输入失败:{error}", "audio_input_failed_enable": "启用音频输入失败:{error}", "audio_microphone_description": "麦克风输入到目标设备", From 7f930e01b3905aac81d0948187e2d93b66fe5f87 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Mon, 17 Nov 2025 15:31:27 -0600 Subject: [PATCH 3/6] Add missing translations for new connection stats --- ui/localization/messages/da.json | 6 ++++++ ui/localization/messages/de.json | 6 ++++++ ui/localization/messages/es.json | 6 ++++++ ui/localization/messages/fr.json | 6 ++++++ ui/localization/messages/it.json | 6 ++++++ ui/localization/messages/nb.json | 6 ++++++ ui/localization/messages/sv.json | 6 ++++++ ui/localization/messages/zh.json | 6 ++++++ 8 files changed, 48 insertions(+) diff --git a/ui/localization/messages/da.json b/ui/localization/messages/da.json index 06edb1d0..f6bb076f 100644 --- a/ui/localization/messages/da.json +++ b/ui/localization/messages/da.json @@ -261,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", @@ -786,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 b346b444..de20729b 100644 --- a/ui/localization/messages/de.json +++ b/ui/localization/messages/de.json @@ -261,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", @@ -786,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/es.json b/ui/localization/messages/es.json index acc1ff0e..10d74573 100644 --- a/ui/localization/messages/es.json +++ b/ui/localization/messages/es.json @@ -261,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", @@ -786,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 c17d78dc..ef142755 100644 --- a/ui/localization/messages/fr.json +++ b/ui/localization/messages/fr.json @@ -261,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", @@ -786,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 478ef5d5..ebc3215e 100644 --- a/ui/localization/messages/it.json +++ b/ui/localization/messages/it.json @@ -261,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", @@ -786,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 7f7061ad..aba9e0af 100644 --- a/ui/localization/messages/nb.json +++ b/ui/localization/messages/nb.json @@ -261,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", @@ -786,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 de662a6e..20ecc940 100644 --- a/ui/localization/messages/sv.json +++ b/ui/localization/messages/sv.json @@ -260,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", @@ -785,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 6d4bf613..4f90a95a 100644 --- a/ui/localization/messages/zh.json +++ b/ui/localization/messages/zh.json @@ -261,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": "连接统计", @@ -786,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": "为您的设备命名,以便日后轻松识别。您可以随时更改此名称。", From 9b2500b2df06517387fb09b1276de60c2b1803f3 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Mon, 17 Nov 2025 15:46:00 -0600 Subject: [PATCH 4/6] Extract alsaDevice configuration to helper --- audio.go | 37 +++++++++++++++---------------------- jsonrpc.go | 3 ++- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/audio.go b/audio.go index 88f16c96..a5305dd2 100644 --- a/audio.go +++ b/audio.go @@ -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) @@ -73,11 +81,7 @@ func startAudio() error { if outputSource == nil && audioOutputEnabled.Load() && currentAudioTrack != nil { ensureConfigLoaded() - alsaDevice := "hw:1,0" - if config.AudioOutputSource == "hdmi" { - alsaDevice = "hw:0,0" - } - + alsaDevice := getAlsaDevice(config.AudioOutputSource) source := audio.NewCgoOutputSource(alsaDevice) source.SetConfig(getAudioConfig()) outputSource = source @@ -89,8 +93,7 @@ func startAudio() error { ensureConfigLoaded() if inputSource.Load() == nil && audioInputEnabled.Load() && config.UsbDevices != nil && config.UsbDevices.Audio { - alsaPlaybackDevice := "hw:1,0" - + alsaPlaybackDevice := getAlsaDevice("usb") source := audio.NewCgoInputSource(alsaPlaybackDevice) source.SetConfig(getAudioConfig()) var audioSource audio.AudioSource = source @@ -175,11 +178,8 @@ func setAudioTrack(audioTrack *webrtc.TrackLocalStaticSample) { 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) + alsaDevice := getAlsaDevice(config.AudioOutputSource) + newSource := audio.NewCgoOutputSource(alsaDevice) newSource.SetConfig(getAudioConfig()) newRelay = audio.NewOutputRelay(newSource, currentAudioTrack) outputSource = newSource @@ -255,11 +255,7 @@ func SetAudioOutputSource(source string) error { stopOutputAudio() if audioOutputEnabled.Load() && activeConnections.Load() > 0 && currentAudioTrack != nil { - alsaDevice := "hw:1,0" - if source == "hdmi" { - alsaDevice = "hw:0,0" - } - + alsaDevice := getAlsaDevice(source) newSource := audio.NewCgoOutputSource(alsaDevice) newSource.SetConfig(getAudioConfig()) newRelay := audio.NewOutputRelay(newSource, currentAudioTrack) @@ -291,10 +287,7 @@ func RestartAudioOutput() { stopOutputAudio() ensureConfigLoaded() - alsaDevice := "hw:1,0" - if config.AudioOutputSource == "hdmi" { - alsaDevice = "hw:0,0" - } + alsaDevice := getAlsaDevice(config.AudioOutputSource) newSource := audio.NewCgoOutputSource(alsaDevice) newSource.SetConfig(getAudioConfig()) diff --git a/jsonrpc.go b/jsonrpc.go index 69235805..1d596eaf 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -926,7 +926,8 @@ func updateUsbRelatedConfig(wasAudioEnabled bool) error { config.AudioOutputSource = "hdmi" stopOutputAudio() if audioOutputEnabled.Load() && activeConnections.Load() > 0 && currentAudioTrack != nil { - newSource := audio.NewCgoOutputSource("hw:0,0") + alsaDevice := getAlsaDevice("hdmi") + newSource := audio.NewCgoOutputSource(alsaDevice) newSource.SetConfig(getAudioConfig()) newRelay := audio.NewOutputRelay(newSource, currentAudioTrack) From 8c7764a663de6bf16834c654e86a7aa11248a0f9 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Mon, 17 Nov 2025 19:44:11 -0600 Subject: [PATCH 5/6] Ensure the stopAudio() always runs --- main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 { From 1ec994110393be731f6f3626fd02cecdc1a2cbb2 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Mon, 17 Nov 2025 19:57:41 -0600 Subject: [PATCH 6/6] Simplify audio management Moved all start/stop of sources into audio (out of jsonrpc) Clean up duplicated code, made direction a bool, more logging, made all source/relay atomics. Eliminate SetConfig since we always set it during start. Eliminate the extra initialized flag. Properly detect when USB audio was previously active. Relay has the pointer to the source, not a copy. CgoSource (and stub) expose the AudioSource interface. --- audio.go | 180 ++++++++++++------------------ internal/audio/cgo_source.go | 69 ++++++------ internal/audio/cgo_source_stub.go | 10 +- internal/audio/relay.go | 16 +-- internal/audio/source.go | 1 - jsonrpc.go | 69 ++++-------- 6 files changed, 144 insertions(+), 201 deletions(-) diff --git a/audio.go b/audio.go index a5305dd2..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 @@ -79,58 +79,81 @@ func startAudio() error { return nil } - if outputSource == nil && audioOutputEnabled.Load() && currentAudioTrack != nil { - ensureConfigLoaded() - alsaDevice := getAlsaDevice(config.AudioOutputSource) - 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 := getAlsaDevice("usb") - source := audio.NewCgoInputSource(alsaPlaybackDevice) - source.SetConfig(getAudioConfig()) - var audioSource audio.AudioSource = source - inputSource.Store(&audioSource) - inputRelay = audio.NewInputRelay(audioSource) - if err := inputRelay.Start(); err != nil { - audioLogger.Error().Err(err).Msg("Failed to start input relay") - } + if audioOutputEnabled.Load() && currentAudioTrack != nil { + startOutputAudioUnderMutex(getAlsaDevice(config.AudioOutputSource)) + } + + 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() @@ -156,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() } @@ -166,39 +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 := getAlsaDevice(config.AudioOutputSource) - 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,72 +246,44 @@ func SetAudioOutputSource(source string) error { return nil } + stopOutputAudio() config.AudioOutputSource = source - stopOutputAudio() - - if audioOutputEnabled.Load() && activeConnections.Load() > 0 && currentAudioTrack != nil { - alsaDevice := getAlsaDevice(source) - 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 := getAlsaDevice(config.AudioOutputSource) - - 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 @@ -324,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 1d596eaf..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,43 +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 { - alsaDevice := getAlsaDevice("hdmi") - 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 { - 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 { @@ -950,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 @@ -973,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 { @@ -1002,7 +980,7 @@ func rpcSetUsbDeviceState(device string, enabled bool) error { } gadget.SetGadgetDevices(config.UsbDevices) - return updateUsbRelatedConfig(wasAudioEnabled) + return updateUsbRelatedConfig(wasUsbAudioEnabled) } func rpcGetAudioOutputEnabled() (bool, error) { @@ -1105,8 +1083,7 @@ func rpcSetAudioConfig(bitrate int, complexity int, dtxEnabled bool, fecEnabled } func rpcRestartAudioOutput() error { - RestartAudioOutput() - return nil + return RestartAudioOutput() } func rpcGetAudioInputAutoEnable() (bool, error) {