Files
NanoKVM-MIRROR/server/service/stream/direct/streamer.go
watermeko b587b9f912 fix: stop HDMI capture when logged out (#841)
* fix: stop HDMI capture when logged out

* feat: stop HDMI capture when viewers are idle

* fix: keep HDMI capture active for consumers
2026-07-30 17:29:07 +08:00

160 lines
3.5 KiB
Go

package direct
import (
"NanoKVM-Server/common"
"NanoKVM-Server/service/stream"
"NanoKVM-Server/service/vm"
"bytes"
"encoding/binary"
"sync"
"sync/atomic"
"time"
"github.com/gorilla/websocket"
log "github.com/sirupsen/logrus"
)
type Streamer struct {
mutex sync.Mutex
clients map[*websocket.Conn]bool
clientSnapshot atomic.Pointer[[]*websocket.Conn]
running int32
viewerVersion uint64
}
func newStreamer() *Streamer {
s := &Streamer{
clients: make(map[*websocket.Conn]bool),
}
s.updateClientSnapshotLocked()
return s
}
func (s *Streamer) addClient(ws *websocket.Conn) {
s.mutex.Lock()
s.clients[ws] = true
count := s.updateClientSnapshotLocked()
s.viewerVersion++
version := s.viewerVersion
s.mutex.Unlock()
vm.UpdateHdmiViewerSnapshot("direct", count, version)
if atomic.CompareAndSwapInt32(&s.running, 0, 1) {
go s.run()
log.Debug("h264 stream started")
}
}
func (s *Streamer) removeClient(ws *websocket.Conn) {
s.mutex.Lock()
delete(s.clients, ws)
count := s.updateClientSnapshotLocked()
s.viewerVersion++
version := s.viewerVersion
s.mutex.Unlock()
vm.UpdateHdmiViewerSnapshot("direct", count, version)
log.Debugf("h264 websocket disconnected, remaining clients: %d", count)
}
func (s *Streamer) updateClientSnapshotLocked() int {
clients := make([]*websocket.Conn, 0, len(s.clients))
for client := range s.clients {
clients = append(clients, client)
}
s.clientSnapshot.Store(&clients)
return len(clients)
}
func (s *Streamer) getClients() []*websocket.Conn {
clients := s.clientSnapshot.Load()
if clients == nil {
return nil
}
return *clients
}
func (s *Streamer) run() {
defer atomic.StoreInt32(&s.running, 0)
screen := common.GetScreen()
common.CheckScreen()
fps := screen.FPS
ticker := time.NewTicker(time.Second / time.Duration(fps))
defer ticker.Stop()
vision := common.GetKvmVision()
startTime := time.Now()
for range ticker.C {
clients := s.getClients()
if len(clients) == 0 {
log.Debug("h264 stream stopped due to no clients")
return
}
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate)
stream.UpdateCaptureStatus(stream.CaptureModeDirect, result)
if result < 0 || len(data) == 0 {
continue
}
isKeyFrame := byte(0)
if result == 3 {
isKeyFrame = byte(1)
}
timestamp := time.Since(startTime).Microseconds()
if err := s.send(clients, isKeyFrame, timestamp, data); err != nil {
continue
}
if screen.FPS != fps && screen.FPS != 0 {
fps = screen.FPS
ticker.Reset(time.Second / time.Duration(fps))
}
stream.GetFrameRateCounter().Update()
}
}
func (s *Streamer) send(clients []*websocket.Conn, isKeyFrame byte, timestamp int64, data []byte) error {
buf := BufferPool.Get().(*bytes.Buffer)
defer BufferPool.Put(buf)
buf.Reset()
buf.Grow(1 + 8 + len(data))
if err := buf.WriteByte(isKeyFrame); err != nil {
log.Errorf("failed to write keyframe flag: %s", err)
return err
}
var tsBytes [8]byte
binary.LittleEndian.PutUint64(tsBytes[:], uint64(timestamp))
if _, err := buf.Write(tsBytes[:]); err != nil {
log.Errorf("failed to write timestamp: %s", err)
return err
}
if _, err := buf.Write(data); err != nil {
log.Errorf("failed to write h264 data: %s", err)
return err
}
for _, client := range clients {
if err := client.WriteMessage(websocket.BinaryMessage, buf.Bytes()); err != nil {
log.Errorf("failed to write message to client %s: %s.", client.RemoteAddr(), err)
s.removeClient(client)
}
}
return nil
}