fix: warn about mixed H264 stream modes (#843)

This commit is contained in:
watermeko
2026-07-31 10:48:52 +08:00
committed by GitHub
parent b587b9f912
commit a7d7574e70
35 changed files with 588 additions and 45 deletions

View File

@@ -1,6 +1,7 @@
package direct
import (
"NanoKVM-Server/service/stream"
"net/http"
"time"
@@ -36,6 +37,9 @@ func Connect(c *gin.Context) {
streamer.addClient(ws)
defer streamer.removeClient(ws)
unregisterMode := stream.RegisterH264Mode(stream.H264ModeDirect)
defer unregisterMode()
for {
if _, _, err := ws.NextReader(); err != nil {
log.Debugf("failed to read message (client disconnected): %s", err)

View File

@@ -152,6 +152,7 @@ func (s *Streamer) send(clients []*websocket.Conn, isKeyFrame byte, timestamp in
log.Errorf("failed to write message to client %s: %s.", client.RemoteAddr(), err)
s.removeClient(client)
_ = client.Close()
}
}

View File

@@ -0,0 +1,172 @@
package stream
import (
"sync"
"github.com/google/uuid"
)
const (
H264ModeStatusEvent = "h264-mode-status"
H264ModeDirect = "direct"
H264ModeWebRTC = "webrtc"
)
type H264ModeStatus struct {
Generation string `json:"generation"`
Revision uint64 `json:"revision"`
Direct int `json:"direct"`
WebRTC int `json:"webrtc"`
Mixed bool `json:"mixed"`
}
type H264ModeStatusSubscriber func(H264ModeStatus)
var defaultH264ModeStore = newH264ModeStore()
func RegisterH264Mode(mode string) func() {
return defaultH264ModeStore.register(mode)
}
func SubscribeH264ModeStatus(subscriber H264ModeStatusSubscriber) func() {
return defaultH264ModeStore.subscribe(subscriber)
}
func CurrentH264ModeStatus() H264ModeStatus {
return defaultH264ModeStore.current()
}
type h264ModeStore struct {
mutex sync.Mutex
counts map[string]int
status H264ModeStatus
subscribers map[int]H264ModeStatusSubscriber
nextID int
pending []h264ModeStatusNotification
notify chan struct{}
}
type h264ModeStatusNotification struct {
status H264ModeStatus
subscribers []H264ModeStatusSubscriber
}
func newH264ModeStore() *h264ModeStore {
store := &h264ModeStore{
counts: make(map[string]int),
subscribers: make(map[int]H264ModeStatusSubscriber),
status: H264ModeStatus{
Generation: uuid.NewString(),
},
notify: make(chan struct{}, 1),
}
go store.run()
return store
}
func (s *h264ModeStore) register(mode string) func() {
if !isH264Mode(mode) {
return func() {}
}
s.update(mode, 1)
var once sync.Once
return func() {
once.Do(func() {
s.update(mode, -1)
})
}
}
func (s *h264ModeStore) subscribe(subscriber H264ModeStatusSubscriber) func() {
s.mutex.Lock()
id := s.nextID
s.nextID++
s.subscribers[id] = subscriber
s.mutex.Unlock()
return func() {
s.mutex.Lock()
delete(s.subscribers, id)
s.mutex.Unlock()
}
}
func (s *h264ModeStore) current() H264ModeStatus {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.status
}
func (s *h264ModeStore) update(mode string, delta int) {
s.mutex.Lock()
previous := s.status
s.counts[mode] += delta
if s.counts[mode] < 0 {
s.counts[mode] = 0
}
next := H264ModeStatus{
Generation: previous.Generation,
Revision: previous.Revision + 1,
Direct: s.counts[H264ModeDirect],
WebRTC: s.counts[H264ModeWebRTC],
}
next.Mixed = next.Direct > 0 && next.WebRTC > 0
s.status = next
if previous.Mixed == next.Mixed {
s.mutex.Unlock()
return
}
subscribers := make([]H264ModeStatusSubscriber, 0, len(s.subscribers))
for _, subscriber := range s.subscribers {
subscribers = append(subscribers, subscriber)
}
s.pending = append(s.pending, h264ModeStatusNotification{
status: next,
subscribers: subscribers,
})
s.mutex.Unlock()
select {
case s.notify <- struct{}{}:
default:
}
}
func (s *h264ModeStore) run() {
for range s.notify {
for {
notification, ok := s.takePending()
if !ok {
break
}
for _, subscriber := range notification.subscribers {
subscriber(notification.status)
}
}
}
}
func (s *h264ModeStore) takePending() (h264ModeStatusNotification, bool) {
s.mutex.Lock()
defer s.mutex.Unlock()
if len(s.pending) == 0 {
return h264ModeStatusNotification{}, false
}
notification := s.pending[0]
s.pending = s.pending[1:]
return notification, true
}
func isH264Mode(mode string) bool {
return mode == H264ModeDirect || mode == H264ModeWebRTC
}

View File

@@ -74,12 +74,9 @@ func Connect(c *gin.Context) {
return
}
manager := getManager()
manager.AddClient(wsConn, client)
defer manager.RemoveClient(wsConn)
// handle signaling
signalingHandler := NewSignalingHandler(client)
defer signalingHandler.Close()
signalingHandler.RegisterCallbacks()
if err := sendICEServers(client, iceServers); err != nil {
log.Errorf("failed to send ICE servers: %s", err)

View File

@@ -4,7 +4,6 @@ import (
"NanoKVM-Server/common"
"NanoKVM-Server/service/stream"
"NanoKVM-Server/service/vm"
"sync/atomic"
"time"
"github.com/gorilla/websocket"
@@ -15,7 +14,7 @@ import (
func NewWebRTCManager() *WebRTCManager {
m := &WebRTCManager{
clients: make(map[*websocket.Conn]*Client),
videoSending: 0,
videoSending: false,
}
m.updateClientSnapshotLocked()
@@ -72,15 +71,31 @@ func (m *WebRTCManager) getClients() []*Client {
}
func (m *WebRTCManager) StartVideoStream() {
if atomic.CompareAndSwapInt32(&m.videoSending, 0, 1) {
go m.sendVideoStream()
log.Debugf("start sending h264 stream")
m.mutex.Lock()
if m.videoSending || len(m.clients) == 0 {
m.mutex.Unlock()
return
}
m.videoSending = true
m.mutex.Unlock()
go m.sendVideoStream()
log.Debugf("start sending h264 stream")
}
func (m *WebRTCManager) stopVideoStreamIfIdle() bool {
m.mutex.Lock()
defer m.mutex.Unlock()
if len(m.clients) > 0 {
return false
}
m.videoSending = false
return true
}
func (m *WebRTCManager) sendVideoStream() {
defer atomic.StoreInt32(&m.videoSending, 0)
screen := common.GetScreen()
common.CheckScreen()
fps := screen.FPS
@@ -94,8 +109,12 @@ func (m *WebRTCManager) sendVideoStream() {
for range ticker.C {
clients := m.getClients()
if len(clients) == 0 {
log.Debugf("stop sending h264 stream")
return
if m.stopVideoStreamIfIdle() {
log.Debugf("stop sending h264 stream")
return
}
continue
}
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate)

View File

@@ -1,6 +1,7 @@
package webrtc
import (
"NanoKVM-Server/service/stream"
"encoding/json"
"errors"
@@ -33,18 +34,85 @@ func (s *SignalingHandler) RegisterCallbacks() {
}
})
manager := getManager()
// video connection state change
s.client.video.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
if state == webrtc.ICEConnectionStateConnected {
manager.StartVideoStream()
}
s.updateVideoStreamState(state)
log.Debugf("video connection state changed to %s", state.String())
})
}
func (s *SignalingHandler) Close() {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.closed {
return
}
s.closed = true
s.unregisterH264ModeLocked()
if s.client != nil && s.client.WsConn() != nil {
getManager().RemoveClient(s.client.WsConn())
}
}
func (s *SignalingHandler) updateVideoStreamState(state webrtc.ICEConnectionState) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.closed {
return
}
manager := getManager()
if s.updateH264ModeLocked(state) {
manager.AddClient(s.client.WsConn(), s.client)
manager.StartVideoStream()
return
}
manager.RemoveClient(s.client.WsConn())
}
func (s *SignalingHandler) updateH264Mode(state webrtc.ICEConnectionState) bool {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.closed {
return false
}
return s.updateH264ModeLocked(state)
}
func (s *SignalingHandler) updateH264ModeLocked(state webrtc.ICEConnectionState) bool {
if state == webrtc.ICEConnectionStateConnected || state == webrtc.ICEConnectionStateCompleted {
s.registerH264ModeLocked()
return true
}
s.unregisterH264ModeLocked()
return false
}
func (s *SignalingHandler) registerH264ModeLocked() {
if s.unregisterMode != nil {
return
}
s.unregisterMode = stream.RegisterH264Mode(stream.H264ModeWebRTC)
}
func (s *SignalingHandler) unregisterH264ModeLocked() {
unregisterMode := s.unregisterMode
s.unregisterMode = nil
if unregisterMode != nil {
unregisterMode()
}
}
// HandleMessage handle the received message
func (s *SignalingHandler) HandleMessage(message *Message) error {
switch message.Event {

View File

@@ -12,7 +12,7 @@ import (
type WebRTCManager struct {
clients map[*websocket.Conn]*Client
clientSnapshot atomic.Pointer[[]*Client]
videoSending int32
videoSending bool
mutex sync.Mutex
viewerVersion uint64
}
@@ -29,7 +29,10 @@ func (c *Client) WsConn() *websocket.Conn {
}
type SignalingHandler struct {
client *Client
client *Client
mutex sync.Mutex
unregisterMode func()
closed bool
}
type Track struct {

View File

@@ -0,0 +1,93 @@
package ws
import (
"encoding/json"
"sync"
"NanoKVM-Server/service/stream"
log "github.com/sirupsen/logrus"
)
var h264ModeStatusBroadcasterInstance = newH264ModeStatusBroadcaster(broadcastH264ModeStatus)
func init() {
go h264ModeStatusBroadcasterInstance.Run()
stream.SubscribeH264ModeStatus(func(status stream.H264ModeStatus) {
h264ModeStatusBroadcasterInstance.Enqueue(status)
})
}
type h264ModeStatusBroadcaster struct {
mutex sync.Mutex
pending *stream.H264ModeStatus
notify chan struct{}
broadcast func(stream.H264ModeStatus)
}
func newH264ModeStatusBroadcaster(broadcast func(stream.H264ModeStatus)) *h264ModeStatusBroadcaster {
return &h264ModeStatusBroadcaster{
notify: make(chan struct{}, 1),
broadcast: broadcast,
}
}
func (b *h264ModeStatusBroadcaster) Enqueue(status stream.H264ModeStatus) {
b.mutex.Lock()
b.pending = &status
b.mutex.Unlock()
select {
case b.notify <- struct{}{}:
default:
}
}
func (b *h264ModeStatusBroadcaster) Run() {
for range b.notify {
for {
status, ok := b.takePending()
if !ok {
break
}
b.broadcast(status)
}
}
}
func (b *h264ModeStatusBroadcaster) takePending() (stream.H264ModeStatus, bool) {
b.mutex.Lock()
defer b.mutex.Unlock()
if b.pending == nil {
return stream.H264ModeStatus{}, false
}
status := *b.pending
b.pending = nil
return status, true
}
func sendH264ModeStatusSnapshot(client *Client) {
if err := sendH264ModeStatus(client, stream.CurrentH264ModeStatus()); err != nil {
log.Errorf("failed to send h264 mode status snapshot: %s", err)
}
}
func broadcastH264ModeStatus(status stream.H264ModeStatus) {
for _, client := range GetManager().GetClients() {
if err := sendH264ModeStatus(client, status); err != nil {
log.Errorf("failed to send h264 mode status: %s", err)
}
}
}
func sendH264ModeStatus(client *Client, status stream.H264ModeStatus) error {
payload, err := json.Marshal(status)
if err != nil {
return err
}
return client.Write(stream.H264ModeStatusEvent, string(payload))
}

View File

@@ -38,6 +38,7 @@ func (s *Service) Connect(c *gin.Context) {
defer manager.RemoveClient(ws)
sendCaptureStatusSnapshot(client)
sendH264ModeStatusSnapshot(client)
client.Start()
}

View File

@@ -78,6 +78,11 @@ const ca = {
frameDetectTip:
"Calcula la diferència entre fotogrames. S'atura la transmissió si no hi ha canvis a la pantalla de l'amfitrió remot.",
resetHdmi: 'Restablir HDMI',
mixedH264: {
title: 'Conflicte de flux H.264',
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."
},
captureStatus: {
hdmiError: 'Error a la pantalla HDMI',
unsupportedResolution: 'La resolució actual no és compatible',

View File

@@ -79,6 +79,11 @@ const cz = {
frameDetectTip:
'Vypočítá rozdíl mezi snímky. Přenos video streamu se zastaví, pokud nejsou detekovány změny na obrazovce vzdáleného hostitele.',
resetHdmi: 'Resetovat HDMI',
mixedH264: {
title: 'Konflikt streamu H.264',
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.'
},
captureStatus: {
hdmiError: 'Chyba obrazu HDMI',
unsupportedResolution: 'Aktuální rozlišení není podporováno',

View File

@@ -78,6 +78,11 @@ const da = {
frameDetectTip:
'Beregner forskellen mellem hver frame. Stopper med at sende et video stream hvis der ikke registreres ændringer på fjerncomputerens skærm.',
resetHdmi: 'Nulstil HDMI',
mixedH264: {
title: 'H.264-streamkonflikt',
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.'
},
captureStatus: {
hdmiError: 'Fejl i HDMI-billedet',
unsupportedResolution: 'Den aktuelle opløsning understøttes ikke',

View File

@@ -81,6 +81,11 @@ const de = {
frameDetectTip:
'Berechnet den Unterschied zwischen den Einzelbildern. Beendet die Liveübertragung des Videostreams wenn keine Änderungen auf dem Bildschirm des Hosts festgestellt werden kann.',
resetHdmi: 'HDMI zurücksetzen',
mixedH264: {
title: 'H.264-Streamkonflikt',
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.'
},
captureStatus: {
hdmiError: 'HDMI-Bildschirmfehler',
unsupportedResolution: 'Die aktuelle Auflösung wird nicht unterstützt',

View File

@@ -77,6 +77,11 @@ const en = {
frameDetectTip:
"Calculate the difference between frames. Stop transmitting video stream when no changes are detected on the remote host's screen.",
resetHdmi: 'Reset HDMI',
mixedH264: {
title: 'H.264 stream conflict',
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.'
},
captureStatus: {
hdmiError: 'HDMI screen error',
unsupportedResolution: 'Current resolution is not supported',

View File

@@ -79,6 +79,11 @@ const es = {
frameDetectTip:
'Calcula la diferencia entre fotogramas. Para de transmitir vídeo cuando no se detectan cambios en la pantalla del host remoto.',
resetHdmi: 'Reiniciar HDMI',
mixedH264: {
title: 'Conflicto de flujo H.264',
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.'
},
captureStatus: {
hdmiError: 'Error de imagen HDMI',
unsupportedResolution: 'La resolución actual no es compatible',

View File

@@ -81,6 +81,11 @@ const fr = {
frameDetectTip:
"Calcule la différence entre les images. Arrête la transmission du flux vidéo lorsqu'aucun changement n'est détecté sur l'écran de l'hôte distant",
resetHdmi: 'Réinitialiser le HDMI',
mixedH264: {
title: 'Conflit de flux H.264',
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 nutiliser quun seul mode H.264.'
},
captureStatus: {
hdmiError: 'Erreur dimage HDMI',
unsupportedResolution: 'La résolution actuelle nest pas prise en charge',

View File

@@ -80,6 +80,11 @@ const hu = {
frameDetectTip:
'Elemzi a képkockák közötti különbségeket. A videó stream küldése leáll, ha a távoli gép képernyőjén nem történik változás.',
resetHdmi: 'HDMI visszaállítása',
mixedH264: {
title: 'H.264 adatfolyam-ütközés',
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.'
},
captureStatus: {
hdmiError: 'HDMI-képernyőhiba',
unsupportedResolution: 'A jelenlegi felbontás nem támogatott',

View File

@@ -78,6 +78,11 @@ const id = {
frameDetectTip:
'Hitung selisih antar frame. Hentikan transmisi aliran video saat tidak ada perubahan yang terdeteksi di layar host jarak jauh.',
resetHdmi: 'Atur ulang HDMI',
mixedH264: {
title: 'Konflik aliran H.264',
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.'
},
captureStatus: {
hdmiError: 'Kesalahan layar HDMI',
unsupportedResolution: 'Resolusi saat ini tidak didukung',

View File

@@ -80,6 +80,11 @@ const it = {
frameDetectTip:
'Calcola la differenza tra i frame. Interrompe la trasmissione del flusso video quando non vengono rilevate modifiche sullo schermo del dispositivo remoto.',
resetHdmi: 'Reimposta HDMI',
mixedH264: {
title: 'Conflitto del flusso H.264',
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.'
},
captureStatus: {
hdmiError: 'Errore schermata HDMI',
unsupportedResolution: 'La risoluzione attuale non è supportata',

View File

@@ -78,6 +78,11 @@ const ja = {
frameDetectTip:
'フレーム間の差異を計算し、リモートホストの画面が変更されない場合はビデオストリームの送信を停止します',
resetHdmi: 'HDMI をリセット',
mixedH264: {
title: 'H.264 ストリームの競合',
description:
'H.264 Direct と H.264 WebRTC が同時に使用されています。画面のティアリングや映像の破損が発生する可能性があります。H.264 モードは 1 つだけ使用してください。'
},
captureStatus: {
hdmiError: 'HDMI 画面エラー',
unsupportedResolution: '現在の解像度はサポートされていません',

View File

@@ -77,6 +77,11 @@ const ko = {
frameDetectTip:
'프레임 간의 차이를 계산합니다. 원격 호스트 화면에 변경 사항이 감지되지 않으면 비디오 스트림 전송을 중지합니다.',
resetHdmi: 'HDMI 초기화',
mixedH264: {
title: 'H.264 스트림 충돌',
description:
'H.264 Direct와 H.264 WebRTC가 동시에 사용되고 있습니다. 화면 찢어짐이나 손상된 영상이 발생할 수 있습니다. H.264 모드는 하나만 사용하세요.'
},
captureStatus: {
hdmiError: 'HDMI 화면 오류',
unsupportedResolution: '현재 해상도는 지원되지 않습니다',

View File

@@ -79,6 +79,11 @@ const nb = {
frameDetectTip:
'Kalkuler forskjellen mellom bilder. Stopper overføring av video når det ikke oppdages forskjell på den eksterne vertens skjerm.',
resetHdmi: 'Tilbakestill HDMI',
mixedH264: {
title: 'H.264-strømmekonflikt',
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.'
},
captureStatus: {
hdmiError: 'HDMI-skjermfeil',
unsupportedResolution: 'Gjeldende oppløsning støttes ikke',

View File

@@ -80,6 +80,11 @@ const nl = {
frameDetectTip:
'Berekent het verschil tussen frames. Stopt met het verzenden van de videostream wanneer er geen veranderingen worden gedetecteerd op het scherm van de externe host.',
resetHdmi: 'Reset HDMI',
mixedH264: {
title: 'H.264-streamconflict',
description:
'H.264 Direct en H.264 WebRTC worden tegelijkertijd gebruikt. Dit kan tearing of beschadigde video veroorzaken. Gebruik slechts één H.264-modus.'
},
captureStatus: {
hdmiError: 'HDMI-schermfout',
unsupportedResolution: 'De huidige resolutie wordt niet ondersteund',

View File

@@ -79,6 +79,11 @@ const pl = {
frameDetectTip:
'Obliczanie różnicy między klatkami. Zatrzymaj transmisję strumienia wideo, gdy na ekranie zdalnego hosta nie zostaną wykryte żadne zmiany.',
resetHdmi: 'Resetuj HDMI',
mixedH264: {
title: 'Konflikt strumieni H.264',
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.'
},
captureStatus: {
hdmiError: 'Błąd obrazu HDMI',
unsupportedResolution: 'Bieżąca rozdzielczość nie jest obsługiwana',

View File

@@ -78,6 +78,11 @@ const pt_br = {
frameDetectTip:
'Calcular a diferença entre os quadros. Parar a transmissão de vídeo quando nenhuma alteração for detectada na tela do host remoto.',
resetHdmi: 'Redefinir HDMI',
mixedH264: {
title: 'Conflito de transmissão H.264',
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.'
},
captureStatus: {
hdmiError: 'Erro na imagem HDMI',
unsupportedResolution: 'A resolução atual não é compatível',

View File

@@ -78,6 +78,11 @@ const ru = {
frameDetectTip:
'Вычисляет разницу между кадрами и прекращает передачу видеопотока, если на экране удаленного узла не обнаружено никаких изменений.',
resetHdmi: 'Перезагрузить HDMI подсистему',
mixedH264: {
title: 'Конфликт потоков H.264',
description:
'H.264 Direct и H.264 WebRTC используются одновременно. Это может привести к разрывам изображения или повреждению видео. Используйте только один режим H.264.'
},
captureStatus: {
hdmiError: 'Ошибка изображения HDMI',
unsupportedResolution: 'Текущее разрешение не поддерживается',

View File

@@ -76,6 +76,11 @@ const se = {
frameDetectTip:
'Beräkna skillnaden mellan ramar. Sluta skicka videoström när inga förändringar upptäcks på fjärrvärdens skärm.',
resetHdmi: 'Återställ HDMI',
mixedH264: {
title: 'H.264-strömningskonflikt',
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.'
},
captureStatus: {
hdmiError: 'HDMI-skärmfel',
unsupportedResolution: 'Den aktuella upplösningen stöds inte',

View File

@@ -76,6 +76,11 @@ const th = {
frameDetectTip:
'ระบบจะคำนวณความแตกต่างระหว่างเฟรม และหยุดส่งสตรีมวิดีโอเมื่อไม่พบการเปลี่ยนแปลงบนหน้าจอของคอมพิวเตอร์ต้นทาง',
resetHdmi: 'รีเช็ท HDMI',
mixedH264: {
title: 'สตรีม H.264 ขัดแย้งกัน',
description:
'กำลังใช้งาน H.264 Direct และ H.264 WebRTC พร้อมกัน ซึ่งอาจทำให้ภาพฉีกขาดหรือวิดีโอเสียหาย โปรดใช้โหมด H.264 เพียงโหมดเดียว'
},
captureStatus: {
hdmiError: 'ข้อผิดพลาดหน้าจอ HDMI',
unsupportedResolution: 'ไม่รองรับความละเอียดปัจจุบัน',

View File

@@ -79,6 +79,11 @@ const tr = {
frameDetectTip:
'Gönderilen kareler arasındaki farkı hesaplar. Uzak ana bilgisayardan gönderilen yayında bir değişiklik yoksa görüntü yayınını durdurur.',
resetHdmi: 'HDMI sıfırla',
mixedH264: {
title: 'H.264 akış çakışması',
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.'
},
captureStatus: {
hdmiError: 'HDMI ekran hatası',
unsupportedResolution: 'Geçerli çözünürlük desteklenmiyor',

View File

@@ -79,6 +79,11 @@ const uk = {
frameDetectTip:
'Обчислює різницю між кадрами. Зупиняє передачу відеопотоку, коли на екрані віддаленого хоста не виявлено змін.',
resetHdmi: 'Перезавантажити HDMI підсистему',
mixedH264: {
title: 'Конфлікт потоків H.264',
description:
'H.264 Direct і H.264 WebRTC використовуються одночасно. Це може спричинити розриви зображення або пошкодження відео. Використовуйте лише один режим H.264.'
},
captureStatus: {
hdmiError: 'Помилка зображення HDMI',
unsupportedResolution: 'Поточна роздільна здатність не підтримується',

View File

@@ -78,6 +78,11 @@ const vi = {
frameDetectTip:
'Tính toán sự khác biệt giữa các khung hình. Dừng truyền video khi không có thay đổi trên màn hình máy chủ từ xa.',
resetHdmi: 'Đặt lại HDMI',
mixedH264: {
title: 'Xung đột luồng H.264',
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.'
},
captureStatus: {
hdmiError: 'Lỗi màn hình HDMI',
unsupportedResolution: 'Độ phân giải hiện tại không được hỗ trợ',

View File

@@ -74,6 +74,11 @@ const zh = {
frameDetect: '帧差检测',
frameDetectTip: '计算帧之间的差异,当检测到远程主机画面不变时,停止传输视频流',
resetHdmi: '重置 HDMI',
mixedH264: {
title: 'H.264 视频流冲突',
description:
'检测到 H.264 Direct 和 H.264 WebRTC 同时使用,可能导致画面撕裂或花屏。请只保留一种 H.264 模式。'
},
captureStatus: {
hdmiError: 'HDMI 画面异常',
unsupportedResolution: '当前分辨率不支持',

View File

@@ -74,6 +74,11 @@ const zh_tw = {
frameDetect: '影格檢測',
frameDetectTip: '計算影格之間的差異。當遠端主機畫面未偵測到任何變更時,停止視訊傳輸串流。',
resetHdmi: '重置 HDMI',
mixedH264: {
title: 'H.264 串流衝突',
description:
'偵測到 H.264 Direct 和 H.264 WebRTC 同時使用,可能導致畫面撕裂或影片損壞。請只保留一種 H.264 模式。'
},
captureStatus: {
hdmiError: 'HDMI 畫面異常',
unsupportedResolution: '目前解析度不支援',

View File

@@ -14,7 +14,7 @@ import { CaptureStatusOverlay, useCaptureStatus } from './capture-status';
import { Keyboard } from './keyboard';
import { Menu } from './menu';
import { Mouse } from './mouse';
import { Notification } from './notification.tsx';
import { H264ModeNotification, Notification } from './notification.tsx';
import { Sidebar as PicoclawSidebar } from './picoclaw';
import { ActionOverlay } from './picoclaw/action-overlay.tsx';
import { Screen } from './screen';
@@ -70,6 +70,7 @@ export const Desktop = () => {
<Head title={t('head.desktop')} />
{isBigScreen && <Notification />}
<H264ModeNotification />
{videoMode && resolution && (
<div className="relative flex h-full min-h-0 w-full min-w-0">

View File

@@ -1,10 +1,20 @@
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { Button, notification } from 'antd';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { isPasswordUpdated } from '@/api/auth.ts';
import { getSkipModifyPassword, setSkipModifyPassword } from '@/lib/localstorage.ts';
import { client } from '@/lib/websocket.ts';
const H264_MODE_STATUS_EVENT = 'h264-mode-status';
const MIXED_H264_NOTIFICATION_KEY = 'mixed_h264_modes';
type H264ModeStatus = {
generation: string;
revision: number;
mixed: boolean;
};
export const Notification = () => {
const { t } = useTranslation();
@@ -17,31 +27,85 @@ export const Notification = () => {
isPasswordUpdated().then((rsp) => {
if (rsp.code === 0 && !rsp.data.isUpdated) {
openNotification();
api.warning({
key: 'no_change_password',
message: t('auth.changePassword'),
description: t('auth.changePasswordDesc'),
placement: 'topRight',
btn: (
<Button
type="primary"
onClick={() => {
api.destroy();
navigate('/auth/password');
}}
>
{t('auth.ok')}
</Button>
),
duration: null,
onClose: () => setSkipModifyPassword(true)
});
}
});
}, []);
function openNotification() {
api.warning({
key: 'no_change_password',
message: t('auth.changePassword'),
description: t('auth.changePasswordDesc'),
placement: 'topRight',
btn: (
<Button type="primary" onClick={changePassword}>
{t('auth.ok')}
</Button>
),
duration: null,
onClose: () => setSkipModifyPassword(true)
});
}
function changePassword() {
api.destroy();
navigate('/auth/password');
}
}, [api, navigate, t]);
return <>{contextHolder}</>;
};
export const H264ModeNotification = () => {
const { t } = useTranslation();
const [api, contextHolder] = notification.useNotification();
const latestStatusRef = useRef<H264ModeStatus | null>(null);
useEffect(() => {
return client.on(H264_MODE_STATUS_EVENT, (message) => {
const status = parseH264ModeStatus(message.data);
if (!status) return;
const latestStatus = latestStatusRef.current;
if (latestStatus && status.generation === latestStatus.generation && status.revision < latestStatus.revision) {
return;
}
latestStatusRef.current = status;
if (status.mixed) {
api.warning({
key: MIXED_H264_NOTIFICATION_KEY,
message: t('screen.mixedH264.title'),
description: t('screen.mixedH264.description'),
placement: 'topRight',
duration: null
});
} else {
api.destroy(MIXED_H264_NOTIFICATION_KEY);
}
});
}, [api, t]);
return <>{contextHolder}</>;
};
function parseH264ModeStatus(data: unknown): H264ModeStatus | null {
if (typeof data !== 'string') return null;
try {
const envelope = JSON.parse(data) as { data?: unknown };
if (typeof envelope.data !== 'string') return null;
const status = JSON.parse(envelope.data) as Partial<H264ModeStatus>;
if (
typeof status.generation !== 'string' ||
typeof status.revision !== 'number' ||
!Number.isSafeInteger(status.revision) ||
status.revision < 0 ||
typeof status.mixed !== 'boolean'
) {
return null;
}
return { generation: status.generation, revision: status.revision, mixed: status.mixed };
} catch {
return null;
}
}