perf(stream): share H264 capture frames

Route Direct and WebRTC consumers through one H264 capture source to avoid duplicate VENC reads. Overlap WebRTC capture with ordered RTP/SRTP writes and packetize each access unit once for all WebRTC clients.

Add libopencv_video.so.409 into dl_lib.
This commit is contained in:
watermeko
2026-08-27 02:51:45 +00:00
committed by Guoguo
parent 2ba45a2147
commit a9e1ef2459
10 changed files with 240 additions and 162 deletions

Binary file not shown.

View File

@@ -14,6 +14,7 @@ require (
github.com/mervick/aes-everywhere/go/aes256 v0.0.0-20240803013625-6759956693c0 github.com/mervick/aes-everywhere/go/aes256 v0.0.0-20240803013625-6759956693c0
github.com/modelcontextprotocol/go-sdk v1.6.1 github.com/modelcontextprotocol/go-sdk v1.6.1
github.com/pion/dtls/v3 v3.1.2 github.com/pion/dtls/v3 v3.1.2
github.com/pion/interceptor v0.1.39
github.com/pion/rtp v1.8.18 github.com/pion/rtp v1.8.18
github.com/pion/webrtc/v4 v4.0.1 github.com/pion/webrtc/v4 v4.0.1
github.com/rs/cors/wrapper/gin v0.0.0-20240830163046-1084d89a1692 github.com/rs/cors/wrapper/gin v0.0.0-20240830163046-1084d89a1692
@@ -48,7 +49,6 @@ require (
github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pion/datachannel v1.5.9 // indirect github.com/pion/datachannel v1.5.9 // indirect
github.com/pion/ice/v4 v4.0.2 // indirect github.com/pion/ice/v4 v4.0.2 // indirect
github.com/pion/interceptor v0.1.39 // indirect
github.com/pion/logging v0.2.4 // indirect github.com/pion/logging v0.2.4 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect

View File

@@ -1,12 +1,10 @@
package direct package direct
import ( import (
"NanoKVM-Server/common"
"NanoKVM-Server/service/stream" "NanoKVM-Server/service/stream"
"NanoKVM-Server/service/vm" "NanoKVM-Server/service/vm"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -87,17 +85,14 @@ func (s *Streamer) getClients() []*client {
} }
func (s *Streamer) run() { func (s *Streamer) run() {
screen := common.GetScreen() subscription := stream.SubscribeH264()
common.CheckScreen() defer subscription.Close()
fps := screen.FPS
ticker := time.NewTicker(time.Second / time.Duration(fps)) for {
defer ticker.Stop() frame, ok := subscription.Next()
if !ok {
vision := common.GetKvmVision() return
startTime := time.Now() }
for range ticker.C {
clients := s.getClients() clients := s.getClients()
if len(clients) == 0 { if len(clients) == 0 {
if s.stopIfIdle() { if s.stopIfIdle() {
@@ -107,28 +102,19 @@ func (s *Streamer) run() {
continue continue
} }
if screen.FPS != fps && screen.FPS != 0 {
fps = screen.FPS
ticker.Reset(time.Second / time.Duration(fps))
}
if !hasCaptureDemand(clients) { if !hasCaptureDemand(clients) {
continue continue
} }
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate) stream.UpdateCaptureStatus(stream.CaptureModeDirect, frame.Result)
stream.UpdateCaptureStatus(stream.CaptureModeDirect, result) if frame.Result < 0 || len(frame.Data) == 0 {
if result < 0 || len(data) == 0 {
continue continue
} }
timestamp := time.Since(startTime).Microseconds() outbound := newOutboundFrame(frame.Result == 3, frame.Timestamp, frame.Data)
frame := newOutboundFrame(result == 3, timestamp, data)
for _, client := range clients { for _, client := range clients {
client.offer(frame) client.offer(outbound)
} }
stream.GetFrameRateCounter().Update()
} }
} }

View File

@@ -0,0 +1,157 @@
package stream
import (
"NanoKVM-Server/common"
"sync"
"sync/atomic"
"time"
)
type H264Frame struct {
Data []byte
Result int
Duration time.Duration
Timestamp int64
}
type H264Subscription struct {
frames chan H264Frame
done chan struct{}
closed atomic.Bool
once sync.Once
}
type H264Source struct {
mutex sync.Mutex
subscribers map[*H264Subscription]struct{}
running bool
}
var defaultH264Source = &H264Source{
subscribers: make(map[*H264Subscription]struct{}),
}
func SubscribeH264() *H264Subscription {
return defaultH264Source.subscribe()
}
func (s *H264Source) subscribe() *H264Subscription {
subscription := &H264Subscription{
frames: make(chan H264Frame, 4),
done: make(chan struct{}),
}
s.mutex.Lock()
s.subscribers[subscription] = struct{}{}
start := !s.running
if start {
s.running = true
}
s.mutex.Unlock()
if start {
go s.run()
}
return subscription
}
func (s *H264Subscription) Next() (H264Frame, bool) {
select {
case frame := <-s.frames:
return frame, true
case <-s.done:
return H264Frame{}, false
}
}
func (s *H264Subscription) Close() {
s.once.Do(func() {
s.closed.Store(true)
close(s.done)
defaultH264Source.remove(s)
})
}
func (s *H264Source) remove(subscription *H264Subscription) {
s.mutex.Lock()
delete(s.subscribers, subscription)
s.mutex.Unlock()
}
func (s *H264Source) run() {
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 {
subscribers := s.snapshot()
if len(subscribers) == 0 {
s.mutex.Lock()
if len(s.subscribers) == 0 {
s.running = false
s.mutex.Unlock()
return
}
s.mutex.Unlock()
continue
}
if screen.FPS != fps && screen.FPS != 0 {
fps = screen.FPS
ticker.Reset(time.Second / time.Duration(fps))
}
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate)
if result < 0 {
frame := H264Frame{Result: result}
for _, subscription := range subscribers {
subscription.send(frame)
}
continue
}
if len(data) == 0 {
continue
}
frame := H264Frame{
Data: data,
Result: result,
Duration: time.Second / time.Duration(fps),
Timestamp: time.Since(startTime).Microseconds(),
}
for _, subscription := range subscribers {
subscription.send(frame)
}
GetFrameRateCounter().Update()
}
}
func (s *H264Source) snapshot() []*H264Subscription {
s.mutex.Lock()
defer s.mutex.Unlock()
subscribers := make([]*H264Subscription, 0, len(s.subscribers))
for subscription := range s.subscribers {
subscribers = append(subscribers, subscription)
}
return subscribers
}
func (s *H264Subscription) send(frame H264Frame) bool {
if s.closed.Load() {
return false
}
select {
case s.frames <- frame:
return true
case <-s.done:
return false
}
}

View File

@@ -2,17 +2,16 @@ package webrtc
import ( import (
"encoding/json" "encoding/json"
"errors"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"sync" "sync"
) )
const h264SDPFmtpLine = "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f"
func NewClient(ws *websocket.Conn, videoConn *webrtc.PeerConnection) *Client { func NewClient(ws *websocket.Conn, videoConn *webrtc.PeerConnection) *Client {
return &Client{ return &Client{
ws: ws, ws: ws,
@@ -72,7 +71,11 @@ func (c *Client) ReadMessage() (*Message, error) {
func (c *Client) AddTrack() error { func (c *Client) AddTrack() error {
// video track // video track
videoTrack, err := webrtc.NewTrackLocalStaticRTP( videoTrack, err := webrtc.NewTrackLocalStaticRTP(
webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264,
ClockRate: 90000,
SDPFmtpLine: h264SDPFmtpLine,
},
"video", "video",
"pion-video", "pion-video",
) )
@@ -81,20 +84,6 @@ func (c *Client) AddTrack() error {
return err return err
} }
videoPacketizer := rtp.NewPacketizer(
1200,
100,
0x1234ABCD,
&codecs.H264Payloader{},
rtp.NewRandomSequencer(),
90000,
)
if videoPacketizer == nil {
err := errors.New("failed to create rtp packetizer")
log.Error(err)
return err
}
videoSender, err := c.video.AddTrack(videoTrack) videoSender, err := c.video.AddTrack(videoTrack)
if err != nil { if err != nil {
log.Errorf("failed to add video track: %s", err) log.Errorf("failed to add video track: %s", err)
@@ -103,10 +92,8 @@ func (c *Client) AddTrack() error {
go startRTCPReader(videoSender) go startRTCPReader(videoSender)
track := &Track{ track := &Track{
videoPacketizer: videoPacketizer, video: videoTrack,
video: videoTrack,
} }
track.updateExtension()
c.mutex.Lock() c.mutex.Lock()
c.track = track c.track = track

View File

@@ -10,6 +10,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/pion/dtls/v3" "github.com/pion/dtls/v3"
"github.com/pion/interceptor"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -151,14 +152,6 @@ func createMediaEngine() (*webrtc.MediaEngine, error) {
return nil, err return nil, err
} }
if err := mediaEngine.RegisterHeaderExtension(
webrtc.RTPHeaderExtensionCapability{URI: "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay"},
webrtc.RTPCodecTypeVideo,
); err != nil {
log.Errorf("failed to register header extension: %s", err)
return nil, err
}
return mediaEngine, nil return mediaEngine, nil
} }
@@ -171,6 +164,7 @@ func createPeerConnection(iceServers []webrtc.ICEServer, mediaEngine *webrtc.Med
apiOptions := []func(api *webrtc.API){ apiOptions := []func(api *webrtc.API){
webrtc.WithSettingEngine(settingEngine), webrtc.WithSettingEngine(settingEngine),
webrtc.WithInterceptorRegistry(&interceptor.Registry{}),
} }
if mediaEngine != nil { if mediaEngine != nil {
apiOptions = append(apiOptions, webrtc.WithMediaEngine(mediaEngine)) apiOptions = append(apiOptions, webrtc.WithMediaEngine(mediaEngine))

View File

@@ -1,19 +1,27 @@
package webrtc package webrtc
import ( import (
"NanoKVM-Server/common"
"NanoKVM-Server/service/stream" "NanoKVM-Server/service/stream"
"NanoKVM-Server/service/vm" "NanoKVM-Server/service/vm"
"time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
"github.com/pion/webrtc/v4/pkg/media" "github.com/pion/webrtc/v4/pkg/media"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func NewWebRTCManager() *WebRTCManager { func NewWebRTCManager() *WebRTCManager {
m := &WebRTCManager{ m := &WebRTCManager{
clients: make(map[*websocket.Conn]*Client), clients: make(map[*websocket.Conn]*Client),
videoPacketizer: rtp.NewPacketizer(
1450,
100,
0x1234ABCD,
&codecs.H264Payloader{},
rtp.NewRandomSequencer(),
90000,
),
videoSending: false, videoSending: false,
} }
m.updateClientSnapshotLocked() m.updateClientSnapshotLocked()
@@ -22,8 +30,6 @@ func NewWebRTCManager() *WebRTCManager {
} }
func (m *WebRTCManager) AddClient(ws *websocket.Conn, client *Client) { func (m *WebRTCManager) AddClient(ws *websocket.Conn, client *Client) {
client.track.updateExtension()
m.mutex.Lock() m.mutex.Lock()
m.clients[ws] = client m.clients[ws] = client
count := m.updateClientSnapshotLocked() count := m.updateClientSnapshotLocked()
@@ -96,52 +102,62 @@ func (m *WebRTCManager) stopVideoStreamIfIdle() bool {
} }
func (m *WebRTCManager) sendVideoStream() { func (m *WebRTCManager) sendVideoStream() {
screen := common.GetScreen() subscription := stream.SubscribeH264()
common.CheckScreen() defer subscription.Close()
fps := screen.FPS samples, writerDone := m.startVideoWriter()
duration := time.Second / time.Duration(fps)
vision := common.GetKvmVision() for {
frame, ok := subscription.Next()
ticker := time.NewTicker(duration) if !ok {
defer ticker.Stop() close(samples)
<-writerDone
for range ticker.C { return
}
clients := m.getClients() clients := m.getClients()
if len(clients) == 0 { if len(clients) == 0 {
close(samples)
<-writerDone
if m.stopVideoStreamIfIdle() { if m.stopVideoStreamIfIdle() {
log.Debugf("stop sending h264 stream") log.Debugf("stop sending h264 stream")
return return
} }
samples, writerDone = m.startVideoWriter()
continue continue
} }
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate) stream.UpdateCaptureStatus(stream.CaptureModeH264, frame.Result)
stream.UpdateCaptureStatus(stream.CaptureModeH264, result) if frame.Result < 0 || len(frame.Data) == 0 {
if result < 0 || len(data) == 0 {
continue continue
} }
sample := media.Sample{ sample := media.Sample{
Data: data, Data: frame.Data,
Duration: duration, Duration: frame.Duration,
} }
for _, client := range clients { samples <- sample
if err := client.track.writeVideoSample(sample); err != nil {
log.Errorf("failed to write h264 video to client: %s", err)
m.RemoveClient(client.WsConn())
client.Close()
}
}
if screen.FPS != fps && screen.FPS != 0 {
fps = screen.FPS
duration = time.Second / time.Duration(fps)
ticker.Reset(duration)
}
stream.GetFrameRateCounter().Update()
} }
} }
func (m *WebRTCManager) startVideoWriter() (chan media.Sample, <-chan struct{}) {
samples := make(chan media.Sample, 1)
done := make(chan struct{})
go func() {
defer close(done)
for sample := range samples {
packets := m.videoPacketizer.Packetize(sample.Data, uint32(sample.Duration.Seconds()*90000))
for _, client := range m.getClients() {
err := client.track.writeVideoPackets(packets)
if err != nil {
log.Errorf("failed to write h264 video to client: %s", err)
m.RemoveClient(client.WsConn())
client.Close()
}
}
}
}()
return samples, done
}

View File

@@ -157,11 +157,6 @@ func (s *SignalingHandler) handleVideoOffer(data string) error {
return err return err
} }
if err := s.updateHeaderExtensionID(); err != nil {
log.Errorf("could not update header extension ID: %v", err)
return err
}
answerByte, err := json.Marshal(answer) answerByte, err := json.Marshal(answer)
if err != nil { if err != nil {
log.Errorf("failed to marshal answer: %s", err) log.Errorf("failed to marshal answer: %s", err)
@@ -171,30 +166,6 @@ func (s *SignalingHandler) handleVideoOffer(data string) error {
return s.client.WriteMessage("video-answer", string(answerByte)) return s.client.WriteMessage("video-answer", string(answerByte))
} }
// set extension ID
func (s *SignalingHandler) updateHeaderExtensionID() error {
receivers := s.client.video.GetReceivers()
if len(receivers) == 0 {
return errors.New("no RTP receiver found for video")
}
params := receivers[0].GetParameters()
if len(params.HeaderExtensions) == 0 {
return errors.New("no header extensions found in negotiated parameters")
}
for _, ext := range params.HeaderExtensions {
if ext.URI == "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" {
s.client.track.playoutDelayExtensionID = uint8(ext.ID)
log.Debugf("found and set playout delay extension ID to: %d", ext.ID)
return nil
}
}
log.Warnf("no track extension found in negotiated parameters, use default value 5")
return nil
}
// handle video candidate // handle video candidate
func (s *SignalingHandler) handleVideoCandidate(data string) error { func (s *SignalingHandler) handleVideoCandidate(data string) error {
candidate := webrtc.ICECandidateInit{} candidate := webrtc.ICECandidateInit{}

View File

@@ -2,42 +2,11 @@ package webrtc
import ( import (
"github.com/pion/rtp" "github.com/pion/rtp"
"github.com/pion/webrtc/v4/pkg/media"
log "github.com/sirupsen/logrus"
) )
func (t *Track) updateExtension() { func (t *Track) writeVideoPackets(packets []*rtp.Packet) error {
if t.playoutDelayExtensionID == 0 { for _, packet := range packets {
t.playoutDelayExtensionID = 5 if err := t.video.WriteRTP(packet); err != nil {
}
if len(t.playoutDelayExtensionData) == 0 {
playoutDelay := &rtp.PlayoutDelayExtension{
MinDelay: 0,
MaxDelay: 0,
}
playoutDelayExtensionData, err := playoutDelay.Marshal()
if err == nil {
t.playoutDelayExtensionData = playoutDelayExtensionData
}
}
}
func (t *Track) writeVideoSample(sample media.Sample) error {
samples := uint32(sample.Duration.Seconds() * 90000)
packets := t.videoPacketizer.Packetize(sample.Data, samples)
for _, p := range packets {
p.Header.Extension = true
p.Header.ExtensionProfile = 0xBEDE
if err := p.Header.SetExtension(t.playoutDelayExtensionID, t.playoutDelayExtensionData); err != nil {
log.Errorf("Failed to set extension: %v", err)
return err
}
if err := t.video.WriteRTP(p); err != nil {
log.Errorf("failed to write RTP: %v", err)
return err return err
} }
} }

View File

@@ -10,11 +10,12 @@ 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 bool videoPacketizer rtp.Packetizer
mutex sync.Mutex videoSending bool
viewerVersion uint64 mutex sync.Mutex
viewerVersion uint64
} }
type Client struct { type Client struct {
@@ -36,10 +37,7 @@ type SignalingHandler struct {
} }
type Track struct { type Track struct {
playoutDelayExtensionID uint8 video *webrtc.TrackLocalStaticRTP
playoutDelayExtensionData []byte
videoPacketizer rtp.Packetizer
video *webrtc.TrackLocalStaticRTP
} }
type Message struct { type Message struct {