mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
fix: warn about mixed H264 stream modes (#843)
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package direct
|
package direct
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"NanoKVM-Server/service/stream"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -36,6 +37,9 @@ func Connect(c *gin.Context) {
|
|||||||
streamer.addClient(ws)
|
streamer.addClient(ws)
|
||||||
defer streamer.removeClient(ws)
|
defer streamer.removeClient(ws)
|
||||||
|
|
||||||
|
unregisterMode := stream.RegisterH264Mode(stream.H264ModeDirect)
|
||||||
|
defer unregisterMode()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if _, _, err := ws.NextReader(); err != nil {
|
if _, _, err := ws.NextReader(); err != nil {
|
||||||
log.Debugf("failed to read message (client disconnected): %s", err)
|
log.Debugf("failed to read message (client disconnected): %s", err)
|
||||||
|
|||||||
@@ -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)
|
log.Errorf("failed to write message to client %s: %s.", client.RemoteAddr(), err)
|
||||||
|
|
||||||
s.removeClient(client)
|
s.removeClient(client)
|
||||||
|
_ = client.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
172
server/service/stream/h264_mode.go
Normal file
172
server/service/stream/h264_mode.go
Normal 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
|
||||||
|
}
|
||||||
@@ -74,12 +74,9 @@ func Connect(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := getManager()
|
|
||||||
manager.AddClient(wsConn, client)
|
|
||||||
defer manager.RemoveClient(wsConn)
|
|
||||||
|
|
||||||
// handle signaling
|
// handle signaling
|
||||||
signalingHandler := NewSignalingHandler(client)
|
signalingHandler := NewSignalingHandler(client)
|
||||||
|
defer signalingHandler.Close()
|
||||||
signalingHandler.RegisterCallbacks()
|
signalingHandler.RegisterCallbacks()
|
||||||
if err := sendICEServers(client, iceServers); err != nil {
|
if err := sendICEServers(client, iceServers); err != nil {
|
||||||
log.Errorf("failed to send ICE servers: %s", err)
|
log.Errorf("failed to send ICE servers: %s", err)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"NanoKVM-Server/common"
|
"NanoKVM-Server/common"
|
||||||
"NanoKVM-Server/service/stream"
|
"NanoKVM-Server/service/stream"
|
||||||
"NanoKVM-Server/service/vm"
|
"NanoKVM-Server/service/vm"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
@@ -15,7 +14,7 @@ import (
|
|||||||
func NewWebRTCManager() *WebRTCManager {
|
func NewWebRTCManager() *WebRTCManager {
|
||||||
m := &WebRTCManager{
|
m := &WebRTCManager{
|
||||||
clients: make(map[*websocket.Conn]*Client),
|
clients: make(map[*websocket.Conn]*Client),
|
||||||
videoSending: 0,
|
videoSending: false,
|
||||||
}
|
}
|
||||||
m.updateClientSnapshotLocked()
|
m.updateClientSnapshotLocked()
|
||||||
|
|
||||||
@@ -72,15 +71,31 @@ func (m *WebRTCManager) getClients() []*Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *WebRTCManager) StartVideoStream() {
|
func (m *WebRTCManager) StartVideoStream() {
|
||||||
if atomic.CompareAndSwapInt32(&m.videoSending, 0, 1) {
|
m.mutex.Lock()
|
||||||
go m.sendVideoStream()
|
if m.videoSending || len(m.clients) == 0 {
|
||||||
log.Debugf("start sending h264 stream")
|
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() {
|
func (m *WebRTCManager) sendVideoStream() {
|
||||||
defer atomic.StoreInt32(&m.videoSending, 0)
|
|
||||||
|
|
||||||
screen := common.GetScreen()
|
screen := common.GetScreen()
|
||||||
common.CheckScreen()
|
common.CheckScreen()
|
||||||
fps := screen.FPS
|
fps := screen.FPS
|
||||||
@@ -94,8 +109,12 @@ func (m *WebRTCManager) sendVideoStream() {
|
|||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
clients := m.getClients()
|
clients := m.getClients()
|
||||||
if len(clients) == 0 {
|
if len(clients) == 0 {
|
||||||
log.Debugf("stop sending h264 stream")
|
if m.stopVideoStreamIfIdle() {
|
||||||
return
|
log.Debugf("stop sending h264 stream")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate)
|
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package webrtc
|
package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"NanoKVM-Server/service/stream"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
@@ -33,18 +34,85 @@ func (s *SignalingHandler) RegisterCallbacks() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
manager := getManager()
|
|
||||||
|
|
||||||
// video connection state change
|
// video connection state change
|
||||||
s.client.video.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
s.client.video.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
||||||
if state == webrtc.ICEConnectionStateConnected {
|
s.updateVideoStreamState(state)
|
||||||
manager.StartVideoStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("video connection state changed to %s", state.String())
|
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
|
// HandleMessage handle the received message
|
||||||
func (s *SignalingHandler) HandleMessage(message *Message) error {
|
func (s *SignalingHandler) HandleMessage(message *Message) error {
|
||||||
switch message.Event {
|
switch message.Event {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
type WebRTCManager struct {
|
type WebRTCManager struct {
|
||||||
clients map[*websocket.Conn]*Client
|
clients map[*websocket.Conn]*Client
|
||||||
clientSnapshot atomic.Pointer[[]*Client]
|
clientSnapshot atomic.Pointer[[]*Client]
|
||||||
videoSending int32
|
videoSending bool
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
viewerVersion uint64
|
viewerVersion uint64
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,10 @@ func (c *Client) WsConn() *websocket.Conn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SignalingHandler struct {
|
type SignalingHandler struct {
|
||||||
client *Client
|
client *Client
|
||||||
|
mutex sync.Mutex
|
||||||
|
unregisterMode func()
|
||||||
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Track struct {
|
type Track struct {
|
||||||
|
|||||||
93
server/service/ws/h264_mode.go
Normal file
93
server/service/ws/h264_mode.go
Normal 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))
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ func (s *Service) Connect(c *gin.Context) {
|
|||||||
defer manager.RemoveClient(ws)
|
defer manager.RemoveClient(ws)
|
||||||
|
|
||||||
sendCaptureStatusSnapshot(client)
|
sendCaptureStatusSnapshot(client)
|
||||||
|
sendH264ModeStatusSnapshot(client)
|
||||||
|
|
||||||
client.Start()
|
client.Start()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ const ca = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
"Calcula la diferència entre fotogrames. S'atura la transmissió si no hi ha canvis a la pantalla de l'amfitrió remot.",
|
"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',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'Error a la pantalla HDMI',
|
hdmiError: 'Error a la pantalla HDMI',
|
||||||
unsupportedResolution: 'La resolució actual no és compatible',
|
unsupportedResolution: 'La resolució actual no és compatible',
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ const cz = {
|
|||||||
frameDetectTip:
|
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.',
|
'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',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'Chyba obrazu HDMI',
|
hdmiError: 'Chyba obrazu HDMI',
|
||||||
unsupportedResolution: 'Aktuální rozlišení není podporováno',
|
unsupportedResolution: 'Aktuální rozlišení není podporováno',
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ const da = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
'Beregner forskellen mellem hver frame. Stopper med at sende et video stream hvis der ikke registreres ændringer på fjerncomputerens skærm.',
|
'Beregner forskellen mellem hver frame. Stopper med at sende et video stream hvis der ikke registreres ændringer på fjerncomputerens skærm.',
|
||||||
resetHdmi: 'Nulstil HDMI',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'Fejl i HDMI-billedet',
|
hdmiError: 'Fejl i HDMI-billedet',
|
||||||
unsupportedResolution: 'Den aktuelle opløsning understøttes ikke',
|
unsupportedResolution: 'Den aktuelle opløsning understøttes ikke',
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ const de = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
'Berechnet den Unterschied zwischen den Einzelbildern. Beendet die Liveübertragung des Videostreams wenn keine Änderungen auf dem Bildschirm des Hosts festgestellt werden kann.',
|
'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',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'HDMI-Bildschirmfehler',
|
hdmiError: 'HDMI-Bildschirmfehler',
|
||||||
unsupportedResolution: 'Die aktuelle Auflösung wird nicht unterstützt',
|
unsupportedResolution: 'Die aktuelle Auflösung wird nicht unterstützt',
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ const en = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
"Calculate the difference between frames. Stop transmitting video stream when no changes are detected on the remote host's screen.",
|
"Calculate the difference between frames. Stop transmitting video stream when no changes are detected on the remote host's screen.",
|
||||||
resetHdmi: 'Reset HDMI',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'HDMI screen error',
|
hdmiError: 'HDMI screen error',
|
||||||
unsupportedResolution: 'Current resolution is not supported',
|
unsupportedResolution: 'Current resolution is not supported',
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ const es = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
'Calcula la diferencia entre fotogramas. Para de transmitir vídeo cuando no se detectan cambios en la pantalla del host remoto.',
|
'Calcula la diferencia entre fotogramas. Para de transmitir vídeo cuando no se detectan cambios en la pantalla del host remoto.',
|
||||||
resetHdmi: 'Reiniciar HDMI',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'Error de imagen HDMI',
|
hdmiError: 'Error de imagen HDMI',
|
||||||
unsupportedResolution: 'La resolución actual no es compatible',
|
unsupportedResolution: 'La resolución actual no es compatible',
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ const fr = {
|
|||||||
frameDetectTip:
|
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",
|
"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',
|
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 n’utiliser qu’un seul mode H.264.'
|
||||||
|
},
|
||||||
captureStatus: {
|
captureStatus: {
|
||||||
hdmiError: 'Erreur d’image HDMI',
|
hdmiError: 'Erreur d’image HDMI',
|
||||||
unsupportedResolution: 'La résolution actuelle n’est pas prise en charge',
|
unsupportedResolution: 'La résolution actuelle n’est pas prise en charge',
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ const hu = {
|
|||||||
frameDetectTip:
|
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.',
|
'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',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'HDMI-képernyőhiba',
|
hdmiError: 'HDMI-képernyőhiba',
|
||||||
unsupportedResolution: 'A jelenlegi felbontás nem támogatott',
|
unsupportedResolution: 'A jelenlegi felbontás nem támogatott',
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ const id = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
'Hitung selisih antar frame. Hentikan transmisi aliran video saat tidak ada perubahan yang terdeteksi di layar host jarak jauh.',
|
'Hitung selisih antar frame. Hentikan transmisi aliran video saat tidak ada perubahan yang terdeteksi di layar host jarak jauh.',
|
||||||
resetHdmi: 'Atur ulang HDMI',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'Kesalahan layar HDMI',
|
hdmiError: 'Kesalahan layar HDMI',
|
||||||
unsupportedResolution: 'Resolusi saat ini tidak didukung',
|
unsupportedResolution: 'Resolusi saat ini tidak didukung',
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ const it = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
'Calcola la differenza tra i frame. Interrompe la trasmissione del flusso video quando non vengono rilevate modifiche sullo schermo del dispositivo remoto.',
|
'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',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'Errore schermata HDMI',
|
hdmiError: 'Errore schermata HDMI',
|
||||||
unsupportedResolution: 'La risoluzione attuale non è supportata',
|
unsupportedResolution: 'La risoluzione attuale non è supportata',
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ const ja = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
'フレーム間の差異を計算し、リモートホストの画面が変更されない場合はビデオストリームの送信を停止します',
|
'フレーム間の差異を計算し、リモートホストの画面が変更されない場合はビデオストリームの送信を停止します',
|
||||||
resetHdmi: 'HDMI をリセット',
|
resetHdmi: 'HDMI をリセット',
|
||||||
|
mixedH264: {
|
||||||
|
title: 'H.264 ストリームの競合',
|
||||||
|
description:
|
||||||
|
'H.264 Direct と H.264 WebRTC が同時に使用されています。画面のティアリングや映像の破損が発生する可能性があります。H.264 モードは 1 つだけ使用してください。'
|
||||||
|
},
|
||||||
captureStatus: {
|
captureStatus: {
|
||||||
hdmiError: 'HDMI 画面エラー',
|
hdmiError: 'HDMI 画面エラー',
|
||||||
unsupportedResolution: '現在の解像度はサポートされていません',
|
unsupportedResolution: '現在の解像度はサポートされていません',
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ const ko = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
'프레임 간의 차이를 계산합니다. 원격 호스트 화면에 변경 사항이 감지되지 않으면 비디오 스트림 전송을 중지합니다.',
|
'프레임 간의 차이를 계산합니다. 원격 호스트 화면에 변경 사항이 감지되지 않으면 비디오 스트림 전송을 중지합니다.',
|
||||||
resetHdmi: 'HDMI 초기화',
|
resetHdmi: 'HDMI 초기화',
|
||||||
|
mixedH264: {
|
||||||
|
title: 'H.264 스트림 충돌',
|
||||||
|
description:
|
||||||
|
'H.264 Direct와 H.264 WebRTC가 동시에 사용되고 있습니다. 화면 찢어짐이나 손상된 영상이 발생할 수 있습니다. H.264 모드는 하나만 사용하세요.'
|
||||||
|
},
|
||||||
captureStatus: {
|
captureStatus: {
|
||||||
hdmiError: 'HDMI 화면 오류',
|
hdmiError: 'HDMI 화면 오류',
|
||||||
unsupportedResolution: '현재 해상도는 지원되지 않습니다',
|
unsupportedResolution: '현재 해상도는 지원되지 않습니다',
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ const nb = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
'Kalkuler forskjellen mellom bilder. Stopper overføring av video når det ikke oppdages forskjell på den eksterne vertens skjerm.',
|
'Kalkuler forskjellen mellom bilder. Stopper overføring av video når det ikke oppdages forskjell på den eksterne vertens skjerm.',
|
||||||
resetHdmi: 'Tilbakestill HDMI',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'HDMI-skjermfeil',
|
hdmiError: 'HDMI-skjermfeil',
|
||||||
unsupportedResolution: 'Gjeldende oppløsning støttes ikke',
|
unsupportedResolution: 'Gjeldende oppløsning støttes ikke',
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ const nl = {
|
|||||||
frameDetectTip:
|
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.',
|
'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',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'HDMI-schermfout',
|
hdmiError: 'HDMI-schermfout',
|
||||||
unsupportedResolution: 'De huidige resolutie wordt niet ondersteund',
|
unsupportedResolution: 'De huidige resolutie wordt niet ondersteund',
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ const pl = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
'Obliczanie różnicy między klatkami. Zatrzymaj transmisję strumienia wideo, gdy na ekranie zdalnego hosta nie zostaną wykryte żadne zmiany.',
|
'Obliczanie różnicy między klatkami. Zatrzymaj transmisję strumienia wideo, gdy na ekranie zdalnego hosta nie zostaną wykryte żadne zmiany.',
|
||||||
resetHdmi: 'Resetuj HDMI',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'Błąd obrazu HDMI',
|
hdmiError: 'Błąd obrazu HDMI',
|
||||||
unsupportedResolution: 'Bieżąca rozdzielczość nie jest obsługiwana',
|
unsupportedResolution: 'Bieżąca rozdzielczość nie jest obsługiwana',
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ const pt_br = {
|
|||||||
frameDetectTip:
|
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.',
|
'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',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'Erro na imagem HDMI',
|
hdmiError: 'Erro na imagem HDMI',
|
||||||
unsupportedResolution: 'A resolução atual não é compatível',
|
unsupportedResolution: 'A resolução atual não é compatível',
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ const ru = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
'Вычисляет разницу между кадрами и прекращает передачу видеопотока, если на экране удаленного узла не обнаружено никаких изменений.',
|
'Вычисляет разницу между кадрами и прекращает передачу видеопотока, если на экране удаленного узла не обнаружено никаких изменений.',
|
||||||
resetHdmi: 'Перезагрузить HDMI подсистему',
|
resetHdmi: 'Перезагрузить HDMI подсистему',
|
||||||
|
mixedH264: {
|
||||||
|
title: 'Конфликт потоков H.264',
|
||||||
|
description:
|
||||||
|
'H.264 Direct и H.264 WebRTC используются одновременно. Это может привести к разрывам изображения или повреждению видео. Используйте только один режим H.264.'
|
||||||
|
},
|
||||||
captureStatus: {
|
captureStatus: {
|
||||||
hdmiError: 'Ошибка изображения HDMI',
|
hdmiError: 'Ошибка изображения HDMI',
|
||||||
unsupportedResolution: 'Текущее разрешение не поддерживается',
|
unsupportedResolution: 'Текущее разрешение не поддерживается',
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ const se = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
'Beräkna skillnaden mellan ramar. Sluta skicka videoström när inga förändringar upptäcks på fjärrvärdens skärm.',
|
'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',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'HDMI-skärmfel',
|
hdmiError: 'HDMI-skärmfel',
|
||||||
unsupportedResolution: 'Den aktuella upplösningen stöds inte',
|
unsupportedResolution: 'Den aktuella upplösningen stöds inte',
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ const th = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
'ระบบจะคำนวณความแตกต่างระหว่างเฟรม และหยุดส่งสตรีมวิดีโอเมื่อไม่พบการเปลี่ยนแปลงบนหน้าจอของคอมพิวเตอร์ต้นทาง',
|
'ระบบจะคำนวณความแตกต่างระหว่างเฟรม และหยุดส่งสตรีมวิดีโอเมื่อไม่พบการเปลี่ยนแปลงบนหน้าจอของคอมพิวเตอร์ต้นทาง',
|
||||||
resetHdmi: 'รีเช็ท HDMI',
|
resetHdmi: 'รีเช็ท HDMI',
|
||||||
|
mixedH264: {
|
||||||
|
title: 'สตรีม H.264 ขัดแย้งกัน',
|
||||||
|
description:
|
||||||
|
'กำลังใช้งาน H.264 Direct และ H.264 WebRTC พร้อมกัน ซึ่งอาจทำให้ภาพฉีกขาดหรือวิดีโอเสียหาย โปรดใช้โหมด H.264 เพียงโหมดเดียว'
|
||||||
|
},
|
||||||
captureStatus: {
|
captureStatus: {
|
||||||
hdmiError: 'ข้อผิดพลาดหน้าจอ HDMI',
|
hdmiError: 'ข้อผิดพลาดหน้าจอ HDMI',
|
||||||
unsupportedResolution: 'ไม่รองรับความละเอียดปัจจุบัน',
|
unsupportedResolution: 'ไม่รองรับความละเอียดปัจจุบัน',
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ const tr = {
|
|||||||
frameDetectTip:
|
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.',
|
'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',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'HDMI ekran hatası',
|
hdmiError: 'HDMI ekran hatası',
|
||||||
unsupportedResolution: 'Geçerli çözünürlük desteklenmiyor',
|
unsupportedResolution: 'Geçerli çözünürlük desteklenmiyor',
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ const uk = {
|
|||||||
frameDetectTip:
|
frameDetectTip:
|
||||||
'Обчислює різницю між кадрами. Зупиняє передачу відеопотоку, коли на екрані віддаленого хоста не виявлено змін.',
|
'Обчислює різницю між кадрами. Зупиняє передачу відеопотоку, коли на екрані віддаленого хоста не виявлено змін.',
|
||||||
resetHdmi: 'Перезавантажити HDMI підсистему',
|
resetHdmi: 'Перезавантажити HDMI підсистему',
|
||||||
|
mixedH264: {
|
||||||
|
title: 'Конфлікт потоків H.264',
|
||||||
|
description:
|
||||||
|
'H.264 Direct і H.264 WebRTC використовуються одночасно. Це може спричинити розриви зображення або пошкодження відео. Використовуйте лише один режим H.264.'
|
||||||
|
},
|
||||||
captureStatus: {
|
captureStatus: {
|
||||||
hdmiError: 'Помилка зображення HDMI',
|
hdmiError: 'Помилка зображення HDMI',
|
||||||
unsupportedResolution: 'Поточна роздільна здатність не підтримується',
|
unsupportedResolution: 'Поточна роздільна здатність не підтримується',
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ const vi = {
|
|||||||
frameDetectTip:
|
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.',
|
'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',
|
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: {
|
captureStatus: {
|
||||||
hdmiError: 'Lỗi màn hình HDMI',
|
hdmiError: 'Lỗi màn hình HDMI',
|
||||||
unsupportedResolution: 'Độ phân giải hiện tại không được hỗ trợ',
|
unsupportedResolution: 'Độ phân giải hiện tại không được hỗ trợ',
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ const zh = {
|
|||||||
frameDetect: '帧差检测',
|
frameDetect: '帧差检测',
|
||||||
frameDetectTip: '计算帧之间的差异,当检测到远程主机画面不变时,停止传输视频流',
|
frameDetectTip: '计算帧之间的差异,当检测到远程主机画面不变时,停止传输视频流',
|
||||||
resetHdmi: '重置 HDMI',
|
resetHdmi: '重置 HDMI',
|
||||||
|
mixedH264: {
|
||||||
|
title: 'H.264 视频流冲突',
|
||||||
|
description:
|
||||||
|
'检测到 H.264 Direct 和 H.264 WebRTC 同时使用,可能导致画面撕裂或花屏。请只保留一种 H.264 模式。'
|
||||||
|
},
|
||||||
captureStatus: {
|
captureStatus: {
|
||||||
hdmiError: 'HDMI 画面异常',
|
hdmiError: 'HDMI 画面异常',
|
||||||
unsupportedResolution: '当前分辨率不支持',
|
unsupportedResolution: '当前分辨率不支持',
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ const zh_tw = {
|
|||||||
frameDetect: '影格檢測',
|
frameDetect: '影格檢測',
|
||||||
frameDetectTip: '計算影格之間的差異。當遠端主機畫面未偵測到任何變更時,停止視訊傳輸串流。',
|
frameDetectTip: '計算影格之間的差異。當遠端主機畫面未偵測到任何變更時,停止視訊傳輸串流。',
|
||||||
resetHdmi: '重置 HDMI',
|
resetHdmi: '重置 HDMI',
|
||||||
|
mixedH264: {
|
||||||
|
title: 'H.264 串流衝突',
|
||||||
|
description:
|
||||||
|
'偵測到 H.264 Direct 和 H.264 WebRTC 同時使用,可能導致畫面撕裂或影片損壞。請只保留一種 H.264 模式。'
|
||||||
|
},
|
||||||
captureStatus: {
|
captureStatus: {
|
||||||
hdmiError: 'HDMI 畫面異常',
|
hdmiError: 'HDMI 畫面異常',
|
||||||
unsupportedResolution: '目前解析度不支援',
|
unsupportedResolution: '目前解析度不支援',
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { CaptureStatusOverlay, useCaptureStatus } from './capture-status';
|
|||||||
import { Keyboard } from './keyboard';
|
import { Keyboard } from './keyboard';
|
||||||
import { Menu } from './menu';
|
import { Menu } from './menu';
|
||||||
import { Mouse } from './mouse';
|
import { Mouse } from './mouse';
|
||||||
import { Notification } from './notification.tsx';
|
import { H264ModeNotification, Notification } from './notification.tsx';
|
||||||
import { Sidebar as PicoclawSidebar } from './picoclaw';
|
import { Sidebar as PicoclawSidebar } from './picoclaw';
|
||||||
import { ActionOverlay } from './picoclaw/action-overlay.tsx';
|
import { ActionOverlay } from './picoclaw/action-overlay.tsx';
|
||||||
import { Screen } from './screen';
|
import { Screen } from './screen';
|
||||||
@@ -70,6 +70,7 @@ export const Desktop = () => {
|
|||||||
<Head title={t('head.desktop')} />
|
<Head title={t('head.desktop')} />
|
||||||
|
|
||||||
{isBigScreen && <Notification />}
|
{isBigScreen && <Notification />}
|
||||||
|
<H264ModeNotification />
|
||||||
|
|
||||||
{videoMode && resolution && (
|
{videoMode && resolution && (
|
||||||
<div className="relative flex h-full min-h-0 w-full min-w-0">
|
<div className="relative flex h-full min-h-0 w-full min-w-0">
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { Button, notification } from 'antd';
|
import { Button, notification } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { isPasswordUpdated } from '@/api/auth.ts';
|
import { isPasswordUpdated } from '@/api/auth.ts';
|
||||||
import { getSkipModifyPassword, setSkipModifyPassword } from '@/lib/localstorage.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 = () => {
|
export const Notification = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -17,31 +27,85 @@ export const Notification = () => {
|
|||||||
|
|
||||||
isPasswordUpdated().then((rsp) => {
|
isPasswordUpdated().then((rsp) => {
|
||||||
if (rsp.code === 0 && !rsp.data.isUpdated) {
|
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)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, [api, navigate, t]);
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{contextHolder}</>;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user