diff --git a/web/src/i18n/locales/ca.ts b/web/src/i18n/locales/ca.ts index d0f9d0d02..558c73762 100644 --- a/web/src/i18n/locales/ca.ts +++ b/web/src/i18n/locales/ca.ts @@ -83,6 +83,10 @@ const ca = { description: "S'estan utilitzant H.264 Direct i H.264 WebRTC alhora. Això pot provocar esquinçament de pantalla o vídeo corrupte. Utilitzeu només un mode H.264." }, + webrtcConnectionFailed: { + title: 'Error de connexió WebRTC', + description: 'Comproveu la connexió de xarxa o canvieu el mode de vídeo.' + }, captureStatus: { hdmiError: 'Error a la pantalla HDMI', unsupportedResolution: 'La resolució actual no és compatible', diff --git a/web/src/i18n/locales/cz.ts b/web/src/i18n/locales/cz.ts index 6e2f094e1..2016f2b70 100644 --- a/web/src/i18n/locales/cz.ts +++ b/web/src/i18n/locales/cz.ts @@ -84,6 +84,10 @@ const cz = { description: 'H.264 Direct a H.264 WebRTC se používají současně. To může způsobit trhání obrazu nebo poškozené video. Používejte pouze jeden režim H.264.' }, + webrtcConnectionFailed: { + title: 'Připojení WebRTC se nezdařilo', + description: 'Zkontrolujte síťové připojení nebo přepněte režim videa.' + }, captureStatus: { hdmiError: 'Chyba obrazu HDMI', unsupportedResolution: 'Aktuální rozlišení není podporováno', diff --git a/web/src/i18n/locales/da.ts b/web/src/i18n/locales/da.ts index ac4260728..66b8e9659 100644 --- a/web/src/i18n/locales/da.ts +++ b/web/src/i18n/locales/da.ts @@ -83,6 +83,10 @@ const da = { description: 'H.264 Direct og H.264 WebRTC bruges samtidigt. Dette kan forårsage skærmrivning eller beskadiget video. Brug kun én H.264-tilstand.' }, + webrtcConnectionFailed: { + title: 'WebRTC-forbindelse mislykkedes', + description: 'Kontrollér netværksforbindelsen, eller skift videotilstand.' + }, captureStatus: { hdmiError: 'Fejl i HDMI-billedet', unsupportedResolution: 'Den aktuelle opløsning understøttes ikke', diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index f0937647d..49d91ffc6 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -86,6 +86,10 @@ const de = { description: 'H.264 Direct und H.264 WebRTC werden gleichzeitig verwendet. Dies kann zu Bildschirm-Tearing oder beschädigtem Video führen. Bitte verwenden Sie nur einen H.264-Modus.' }, + webrtcConnectionFailed: { + title: 'WebRTC-Verbindung fehlgeschlagen', + description: 'Überprüfen Sie die Netzwerkverbindung oder wechseln Sie den Videomodus.' + }, captureStatus: { hdmiError: 'HDMI-Bildschirmfehler', unsupportedResolution: 'Die aktuelle Auflösung wird nicht unterstützt', diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 087ad344f..45ed21406 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -82,6 +82,10 @@ const en = { description: 'H.264 Direct and H.264 WebRTC are being used at the same time. This may cause screen tearing or corrupted video. Please use only one H.264 mode.' }, + webrtcConnectionFailed: { + title: 'WebRTC connection failed', + description: 'Check the network connection or switch the video mode.' + }, captureStatus: { hdmiError: 'HDMI screen error', unsupportedResolution: 'Current resolution is not supported', diff --git a/web/src/i18n/locales/es.ts b/web/src/i18n/locales/es.ts index 8cc94f500..5e7c4e79a 100644 --- a/web/src/i18n/locales/es.ts +++ b/web/src/i18n/locales/es.ts @@ -84,6 +84,10 @@ const es = { description: 'H.264 Direct y H.264 WebRTC se están utilizando al mismo tiempo. Esto puede causar tearing de pantalla o vídeo corrupto. Utilice solo un modo H.264.' }, + webrtcConnectionFailed: { + title: 'Error de conexión de WebRTC', + description: 'Compruebe la conexión de red o cambie el modo de vídeo.' + }, captureStatus: { hdmiError: 'Error de imagen HDMI', unsupportedResolution: 'La resolución actual no es compatible', diff --git a/web/src/i18n/locales/fr.ts b/web/src/i18n/locales/fr.ts index c7b8e6fca..43a31dc63 100644 --- a/web/src/i18n/locales/fr.ts +++ b/web/src/i18n/locales/fr.ts @@ -86,6 +86,10 @@ const fr = { description: 'Les modes H.264 Direct et H.264 WebRTC sont utilisés simultanément. Cela peut provoquer des déchirures d’écran ou une vidéo corrompue. Veuillez n’utiliser qu’un seul mode H.264.' }, + webrtcConnectionFailed: { + title: 'Échec de la connexion WebRTC', + description: 'Vérifiez la connexion réseau ou changez de mode vidéo.' + }, captureStatus: { hdmiError: 'Erreur d’image HDMI', unsupportedResolution: 'La résolution actuelle n’est pas prise en charge', diff --git a/web/src/i18n/locales/hu.ts b/web/src/i18n/locales/hu.ts index a2ab1d13f..bacdbd71a 100644 --- a/web/src/i18n/locales/hu.ts +++ b/web/src/i18n/locales/hu.ts @@ -85,6 +85,10 @@ const hu = { description: 'Az H.264 Direct és az H.264 WebRTC egyszerre van használatban. Ez képtörést vagy sérült videót okozhat. Csak egy H.264 módot használjon.' }, + webrtcConnectionFailed: { + title: 'A WebRTC-kapcsolat sikertelen', + description: 'Ellenőrizze a hálózati kapcsolatot, vagy váltson videómódot.' + }, captureStatus: { hdmiError: 'HDMI-képernyőhiba', unsupportedResolution: 'A jelenlegi felbontás nem támogatott', diff --git a/web/src/i18n/locales/id.ts b/web/src/i18n/locales/id.ts index f2e63eb7a..01aa0a146 100644 --- a/web/src/i18n/locales/id.ts +++ b/web/src/i18n/locales/id.ts @@ -83,6 +83,10 @@ const id = { description: 'H.264 Direct dan H.264 WebRTC sedang digunakan secara bersamaan. Hal ini dapat menyebabkan layar robek atau video rusak. Harap gunakan hanya satu mode H.264.' }, + webrtcConnectionFailed: { + title: 'Koneksi WebRTC gagal', + description: 'Periksa koneksi jaringan atau ganti mode video.' + }, captureStatus: { hdmiError: 'Kesalahan layar HDMI', unsupportedResolution: 'Resolusi saat ini tidak didukung', diff --git a/web/src/i18n/locales/it.ts b/web/src/i18n/locales/it.ts index 8a65b9f4e..921947a4f 100644 --- a/web/src/i18n/locales/it.ts +++ b/web/src/i18n/locales/it.ts @@ -85,6 +85,10 @@ const it = { description: 'I flussi H.264 Direct e H.264 WebRTC sono utilizzati contemporaneamente. Ciò può causare tearing dello schermo o video danneggiato. Utilizzare una sola modalità H.264.' }, + webrtcConnectionFailed: { + title: 'Connessione WebRTC non riuscita', + description: 'Controlla la connessione di rete o cambia la modalità video.' + }, captureStatus: { hdmiError: 'Errore schermata HDMI', unsupportedResolution: 'La risoluzione attuale non è supportata', diff --git a/web/src/i18n/locales/ja.ts b/web/src/i18n/locales/ja.ts index f3a0594e4..50e11f4a4 100644 --- a/web/src/i18n/locales/ja.ts +++ b/web/src/i18n/locales/ja.ts @@ -83,6 +83,10 @@ const ja = { description: 'H.264 Direct と H.264 WebRTC が同時に使用されています。画面のティアリングや映像の破損が発生する可能性があります。H.264 モードは 1 つだけ使用してください。' }, + webrtcConnectionFailed: { + title: 'WebRTC 接続に失敗しました', + description: 'ネットワーク接続を確認するか、ビデオモードを切り替えてください。' + }, captureStatus: { hdmiError: 'HDMI 画面エラー', unsupportedResolution: '現在の解像度はサポートされていません', diff --git a/web/src/i18n/locales/ko.ts b/web/src/i18n/locales/ko.ts index cf3685125..132b32238 100644 --- a/web/src/i18n/locales/ko.ts +++ b/web/src/i18n/locales/ko.ts @@ -82,6 +82,10 @@ const ko = { description: 'H.264 Direct와 H.264 WebRTC가 동시에 사용되고 있습니다. 화면 찢어짐이나 손상된 영상이 발생할 수 있습니다. H.264 모드는 하나만 사용하세요.' }, + webrtcConnectionFailed: { + title: 'WebRTC 연결에 실패했습니다', + description: '네트워크 연결을 확인하거나 비디오 모드를 전환하세요.' + }, captureStatus: { hdmiError: 'HDMI 화면 오류', unsupportedResolution: '현재 해상도는 지원되지 않습니다', diff --git a/web/src/i18n/locales/nb.ts b/web/src/i18n/locales/nb.ts index 9a2231856..3d6f3b3f3 100644 --- a/web/src/i18n/locales/nb.ts +++ b/web/src/i18n/locales/nb.ts @@ -84,6 +84,10 @@ const nb = { description: 'H.264 Direct og H.264 WebRTC brukes samtidig. Dette kan føre til skjermriving eller ødelagt video. Bruk bare én H.264-modus.' }, + webrtcConnectionFailed: { + title: 'WebRTC-tilkobling mislyktes', + description: 'Kontroller nettverkstilkoblingen eller bytt videomodus.' + }, captureStatus: { hdmiError: 'HDMI-skjermfeil', unsupportedResolution: 'Gjeldende oppløsning støttes ikke', diff --git a/web/src/i18n/locales/nl.ts b/web/src/i18n/locales/nl.ts index 5652acc7e..ab378936f 100644 --- a/web/src/i18n/locales/nl.ts +++ b/web/src/i18n/locales/nl.ts @@ -85,6 +85,10 @@ const nl = { description: 'H.264 Direct en H.264 WebRTC worden tegelijkertijd gebruikt. Dit kan tearing of beschadigde video veroorzaken. Gebruik slechts één H.264-modus.' }, + webrtcConnectionFailed: { + title: 'WebRTC-verbinding mislukt', + description: 'Controleer de netwerkverbinding of wijzig de videomodus.' + }, captureStatus: { hdmiError: 'HDMI-schermfout', unsupportedResolution: 'De huidige resolutie wordt niet ondersteund', diff --git a/web/src/i18n/locales/pl.ts b/web/src/i18n/locales/pl.ts index a13a10d9d..dc848da72 100644 --- a/web/src/i18n/locales/pl.ts +++ b/web/src/i18n/locales/pl.ts @@ -84,6 +84,10 @@ const pl = { description: 'Strumienie H.264 Direct i H.264 WebRTC są używane jednocześnie. Może to powodować rozrywanie obrazu lub uszkodzenie wideo. Używaj tylko jednego trybu H.264.' }, + webrtcConnectionFailed: { + title: 'Połączenie WebRTC nie powiodło się', + description: 'Sprawdź połączenie sieciowe lub zmień tryb wideo.' + }, captureStatus: { hdmiError: 'Błąd obrazu HDMI', unsupportedResolution: 'Bieżąca rozdzielczość nie jest obsługiwana', diff --git a/web/src/i18n/locales/pt_br.ts b/web/src/i18n/locales/pt_br.ts index 161c2930e..00bac05b9 100644 --- a/web/src/i18n/locales/pt_br.ts +++ b/web/src/i18n/locales/pt_br.ts @@ -83,6 +83,10 @@ const pt_br = { description: 'H.264 Direct e H.264 WebRTC estão sendo usados ao mesmo tempo. Isso pode causar rasgos na tela ou vídeo corrompido. Use apenas um modo H.264.' }, + webrtcConnectionFailed: { + title: 'Falha na conexão WebRTC', + description: 'Verifique a conexão de rede ou alterne o modo de vídeo.' + }, captureStatus: { hdmiError: 'Erro na imagem HDMI', unsupportedResolution: 'A resolução atual não é compatível', diff --git a/web/src/i18n/locales/ru.ts b/web/src/i18n/locales/ru.ts index f1928fc6a..a67668313 100644 --- a/web/src/i18n/locales/ru.ts +++ b/web/src/i18n/locales/ru.ts @@ -83,6 +83,10 @@ const ru = { description: 'H.264 Direct и H.264 WebRTC используются одновременно. Это может привести к разрывам изображения или повреждению видео. Используйте только один режим H.264.' }, + webrtcConnectionFailed: { + title: 'Не удалось подключиться через WebRTC', + description: 'Проверьте сетевое подключение или переключите режим видео.' + }, captureStatus: { hdmiError: 'Ошибка изображения HDMI', unsupportedResolution: 'Текущее разрешение не поддерживается', diff --git a/web/src/i18n/locales/se.ts b/web/src/i18n/locales/se.ts index 53a778de7..90d58bac2 100644 --- a/web/src/i18n/locales/se.ts +++ b/web/src/i18n/locales/se.ts @@ -81,6 +81,10 @@ const se = { description: 'H.264 Direct och H.264 WebRTC används samtidigt. Detta kan orsaka skärmrivningar eller skadad video. Använd endast ett H.264-läge.' }, + webrtcConnectionFailed: { + title: 'WebRTC-anslutningen misslyckades', + description: 'Kontrollera nätverksanslutningen eller byt videoläge.' + }, captureStatus: { hdmiError: 'HDMI-skärmfel', unsupportedResolution: 'Den aktuella upplösningen stöds inte', diff --git a/web/src/i18n/locales/th.ts b/web/src/i18n/locales/th.ts index dd7aae89d..7d7a9ec3b 100644 --- a/web/src/i18n/locales/th.ts +++ b/web/src/i18n/locales/th.ts @@ -81,6 +81,10 @@ const th = { description: 'กำลังใช้งาน H.264 Direct และ H.264 WebRTC พร้อมกัน ซึ่งอาจทำให้ภาพฉีกขาดหรือวิดีโอเสียหาย โปรดใช้โหมด H.264 เพียงโหมดเดียว' }, + webrtcConnectionFailed: { + title: 'การเชื่อมต่อ WebRTC ล้มเหลว', + description: 'โปรดตรวจสอบการเชื่อมต่อเครือข่ายหรือสลับโหมดวิดีโอ' + }, captureStatus: { hdmiError: 'ข้อผิดพลาดหน้าจอ HDMI', unsupportedResolution: 'ไม่รองรับความละเอียดปัจจุบัน', diff --git a/web/src/i18n/locales/tr.ts b/web/src/i18n/locales/tr.ts index cee7d3eff..3a8c4cd77 100644 --- a/web/src/i18n/locales/tr.ts +++ b/web/src/i18n/locales/tr.ts @@ -84,6 +84,10 @@ const tr = { description: 'H.264 Direct ve H.264 WebRTC aynı anda kullanılıyor. Bu, ekran yırtılmasına veya bozuk videoya neden olabilir. Lütfen yalnızca bir H.264 modu kullanın.' }, + webrtcConnectionFailed: { + title: 'WebRTC bağlantısı başarısız', + description: 'Ağ bağlantısını kontrol edin veya video modunu değiştirin.' + }, captureStatus: { hdmiError: 'HDMI ekran hatası', unsupportedResolution: 'Geçerli çözünürlük desteklenmiyor', diff --git a/web/src/i18n/locales/uk.ts b/web/src/i18n/locales/uk.ts index f09dd8038..593b6665c 100644 --- a/web/src/i18n/locales/uk.ts +++ b/web/src/i18n/locales/uk.ts @@ -84,6 +84,10 @@ const uk = { description: 'H.264 Direct і H.264 WebRTC використовуються одночасно. Це може спричинити розриви зображення або пошкодження відео. Використовуйте лише один режим H.264.' }, + webrtcConnectionFailed: { + title: 'Не вдалося підключитися через WebRTC', + description: 'Перевірте мережеве з’єднання або змініть режим відео.' + }, captureStatus: { hdmiError: 'Помилка зображення HDMI', unsupportedResolution: 'Поточна роздільна здатність не підтримується', diff --git a/web/src/i18n/locales/vi.ts b/web/src/i18n/locales/vi.ts index f0ec1e762..f2314c77a 100644 --- a/web/src/i18n/locales/vi.ts +++ b/web/src/i18n/locales/vi.ts @@ -83,6 +83,10 @@ const vi = { description: 'H.264 Direct và H.264 WebRTC đang được sử dụng đồng thời. Điều này có thể gây xé hình hoặc video bị hỏng. Vui lòng chỉ sử dụng một chế độ H.264.' }, + webrtcConnectionFailed: { + title: 'Kết nối WebRTC thất bại', + description: 'Kiểm tra kết nối mạng hoặc chuyển đổi chế độ video.' + }, captureStatus: { hdmiError: 'Lỗi màn hình HDMI', unsupportedResolution: 'Độ phân giải hiện tại không được hỗ trợ', diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts index b7a997df4..23d8f640c 100644 --- a/web/src/i18n/locales/zh.ts +++ b/web/src/i18n/locales/zh.ts @@ -79,6 +79,10 @@ const zh = { description: '检测到 H.264 Direct 和 H.264 WebRTC 同时使用,可能导致画面撕裂或花屏。请只保留一种 H.264 模式。' }, + webrtcConnectionFailed: { + title: 'WebRTC 连接失败', + description: '请检查网络连接或切换视频模式。' + }, captureStatus: { hdmiError: 'HDMI 画面异常', unsupportedResolution: '当前分辨率不支持', diff --git a/web/src/i18n/locales/zh_tw.ts b/web/src/i18n/locales/zh_tw.ts index 1d12530ab..c570eefc0 100644 --- a/web/src/i18n/locales/zh_tw.ts +++ b/web/src/i18n/locales/zh_tw.ts @@ -79,6 +79,10 @@ const zh_tw = { description: '偵測到 H.264 Direct 和 H.264 WebRTC 同時使用,可能導致畫面撕裂或影片損壞。請只保留一種 H.264 模式。' }, + webrtcConnectionFailed: { + title: 'WebRTC 連線失敗', + description: '請檢查網路連線或切換影片模式。' + }, captureStatus: { hdmiError: 'HDMI 畫面異常', unsupportedResolution: '目前解析度不支援', diff --git a/web/src/pages/desktop/screen/h264-webrtc.tsx b/web/src/pages/desktop/screen/h264-webrtc.tsx index a54ba791d..6efc609ff 100644 --- a/web/src/pages/desktop/screen/h264-webrtc.tsx +++ b/web/src/pages/desktop/screen/h264-webrtc.tsx @@ -1,7 +1,8 @@ import { useEffect, useRef, useState } from 'react'; -import { Spin } from 'antd'; +import { notification, Spin } from 'antd'; import clsx from 'clsx'; import { useAtom, useAtomValue } from 'jotai'; +import { useTranslation } from 'react-i18next'; import { w3cwebsocket as W3cWebSocket } from 'websocket'; import * as storage from '@/lib/localstorage.ts'; @@ -14,6 +15,10 @@ type SignalingMessage = { data?: string; }; +const WEBRTC_CONNECTION_FAILED_NOTIFICATION_KEY = 'webrtc_connection_failed'; +const WEBRTC_CONNECTION_TIMEOUT = 10 * 1000; +const WEBRTC_RECONNECT_DELAY = 3 * 1000; + const parseSignalingData = (data?: string): T | null => { if (!data) { return null; @@ -23,13 +28,21 @@ const parseSignalingData = (data?: string): T | null => { }; export const H264Webrtc = () => { + const { t } = useTranslation(); const mouseStyle = useAtomValue(mouseStyleAtom); const [videoScale, setVideoScale] = useAtom(videoScaleAtom); const [isLoading, setIsLoading] = useState(true); + const [connectionAttempt, setConnectionAttempt] = useState(0); + const [notificationApi, contextHolder] = notification.useNotification(); const videoRef = useRef(null); const videoOfferSent = useRef(false); const videoIceCandidates = useRef([]); + const translationRef = useRef(t); + + useEffect(() => { + translationRef.current = t; + }, [t]); useEffect(() => { const url = `${getBaseUrl('ws')}/api/stream/h264`; @@ -38,8 +51,48 @@ export const H264Webrtc = () => { let video: RTCPeerConnection | null = null; let heartbeatTimer: ReturnType | null = null; + let reconnectTimer: ReturnType | null = null; let disposed = false; + const showConnectionFailureNotification = () => { + notificationApi.error({ + key: WEBRTC_CONNECTION_FAILED_NOTIFICATION_KEY, + message: translationRef.current('screen.webrtcConnectionFailed.title'), + description: translationRef.current('screen.webrtcConnectionFailed.description'), + placement: 'topRight', + closable: false, + duration: null + }); + }; + + const cancelReconnect = () => { + if (!reconnectTimer) { + return; + } + + clearTimeout(reconnectTimer); + reconnectTimer = null; + }; + + const scheduleReconnect = (reason: string, error?: unknown) => { + if (disposed) { + return; + } + + console.error(`WebRTC connection failed: ${reason}`, error); + showConnectionFailureNotification(); + setIsLoading(false); + + if (reconnectTimer) { + return; + } + + reconnectTimer = setTimeout(() => { + reconnectTimer = null; + setConnectionAttempt((attempt) => attempt + 1); + }, WEBRTC_RECONNECT_DELAY); + }; + const sendMsg = (event: string, data: string) => { if (ws.readyState !== WebSocket.OPEN) { return; @@ -82,6 +135,20 @@ export const H264Webrtc = () => { } catch (error) { videoOfferSent.current = false; console.error('Video negotiation failed:', error); + scheduleReconnect('creating or sending the offer', error); + } + }; + + peer.onconnectionstatechange = () => { + if (disposed || peer !== video) { + return; + } + + if (peer.connectionState === 'failed') { + scheduleReconnect('peer connection state is failed'); + } else if (peer.connectionState === 'connected') { + cancelReconnect(); + notificationApi.destroy(WEBRTC_CONNECTION_FAILED_NOTIFICATION_KEY); } }; @@ -126,6 +193,7 @@ export const H264Webrtc = () => { .catch((error) => { console.error('Video answer set failed:', error); videoOfferSent.current = false; + scheduleReconnect('setting the remote description', error); }); }; @@ -156,6 +224,14 @@ export const H264Webrtc = () => { }, 60 * 1000); }; + ws.onerror = (error) => { + scheduleReconnect('the signaling WebSocket returned an error', error); + }; + + ws.onclose = () => { + scheduleReconnect('the signaling WebSocket closed'); + }; + ws.onmessage = (event) => { try { const msg = JSON.parse(event.data as string) as SignalingMessage; @@ -192,10 +268,23 @@ export const H264Webrtc = () => { const loadingTimer = setTimeout(() => { setIsLoading(false); + + if (!video || video.connectionState !== 'connected') { + showConnectionFailureNotification(); + } }, 5 * 1000); + const connectionTimeoutTimer = setTimeout(() => { + if (!video || video.connectionState !== 'connected') { + scheduleReconnect( + `connection timed out in state ${video?.connectionState ?? 'not initialized'}` + ); + } + }, WEBRTC_CONNECTION_TIMEOUT); + return () => { disposed = true; + cancelReconnect(); if (ws.readyState !== WebSocket.CLOSING && ws.readyState !== WebSocket.CLOSED) { ws.close(); @@ -213,8 +302,15 @@ export const H264Webrtc = () => { clearInterval(heartbeatTimer); } clearTimeout(loadingTimer); + clearTimeout(connectionTimeoutTimer); }; - }, []); + }, [connectionAttempt, notificationApi]); + + useEffect(() => { + return () => { + notificationApi.destroy(WEBRTC_CONNECTION_FAILED_NOTIFICATION_KEY); + }; + }, [notificationApi]); useEffect(() => { const scale = storage.getVideoScale(); @@ -225,6 +321,8 @@ export const H264Webrtc = () => { return (
+ {contextHolder} +