Files
NanoKVM-MIRROR/server/service/stream/webrtc/h264.go
wenjie b653f905e4 perf: improve H264 streaming client handling
Use client snapshots for stream fanout, reduce frame queue latency, expose backend ICE server configuration to the WebRTC client, and clean up disconnected WebRTC clients more aggressively.
2026-05-19 17:04:57 +08:00

189 lines
4.5 KiB
Go

package webrtc
import (
"NanoKVM-Server/config"
"encoding/json"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/pion/dtls/v3"
"github.com/pion/webrtc/v4"
log "github.com/sirupsen/logrus"
)
var (
upgrader = websocket.Upgrader{
WriteBufferSize: 256 * 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
globalManager *WebRTCManager
managerOnce sync.Once
)
func getManager() *WebRTCManager {
managerOnce.Do(func() {
globalManager = NewWebRTCManager()
})
return globalManager
}
func Connect(c *gin.Context) {
// create WebSocket connection
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Errorf("failed to create h264 websocket: %s", err)
return
}
defer func() {
_ = wsConn.Close()
log.Debugf("h264 websocket disconnected: %s", c.ClientIP())
}()
log.Debugf("h264 websocket connected: %s", c.ClientIP())
var zeroTime time.Time
_ = wsConn.SetReadDeadline(zeroTime)
// create video connection
iceServers := createICEServers()
mediaEngine, err := createMediaEngine()
if err != nil {
log.Errorf("failed to create h264 media engine: %s", err)
return
}
videoConn, err := createPeerConnection(iceServers, mediaEngine)
if err != nil {
log.Errorf("failed to create h264 video peer connection: %s", err)
return
}
defer func() {
_ = videoConn.Close()
log.Debugf("h264 video peer disconnected: %s", c.ClientIP())
}()
// create client
client := NewClient(wsConn, videoConn)
if err := client.AddTrack(); err != nil {
log.Errorf("failed to add track: %s", err)
return
}
manager := getManager()
manager.AddClient(wsConn, client)
defer manager.RemoveClient(wsConn)
// handle signaling
signalingHandler := NewSignalingHandler(client)
signalingHandler.RegisterCallbacks()
if err := sendICEServers(client, iceServers); err != nil {
log.Errorf("failed to send ICE servers: %s", err)
return
}
// read and wait
for {
message, err := client.ReadMessage()
if err != nil {
return
}
if message != nil {
if err := signalingHandler.HandleMessage(message); err != nil {
log.Errorf("failed to handle signaling message: %s", err)
}
}
}
}
func createICEServers() []webrtc.ICEServer {
var iceServers []webrtc.ICEServer
conf := config.GetInstance()
if conf.Stun != "" && conf.Stun != "disable" {
iceServers = append(iceServers, webrtc.ICEServer{
URLs: []string{"stun:" + conf.Stun},
})
}
if conf.Turn.TurnAddr != "" && conf.Turn.TurnUser != "" && conf.Turn.TurnCred != "" {
iceServers = append(iceServers, webrtc.ICEServer{
URLs: []string{"turn:" + conf.Turn.TurnAddr},
Username: conf.Turn.TurnUser,
Credential: conf.Turn.TurnCred,
})
}
return iceServers
}
type clientICEServer struct {
URLs []string `json:"urls"`
Username string `json:"username,omitempty"`
Credential interface{} `json:"credential,omitempty"`
}
func sendICEServers(client *Client, iceServers []webrtc.ICEServer) error {
clientServers := make([]clientICEServer, 0, len(iceServers))
for _, server := range iceServers {
clientServers = append(clientServers, clientICEServer{
URLs: server.URLs,
Username: server.Username,
Credential: server.Credential,
})
}
data, err := json.Marshal(clientServers)
if err != nil {
return err
}
return client.WriteMessage("ice-servers", string(data))
}
func createMediaEngine() (*webrtc.MediaEngine, error) {
mediaEngine := &webrtc.MediaEngine{}
if err := mediaEngine.RegisterDefaultCodecs(); err != nil {
log.Errorf("failed to register default codecs: %s", 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
}
func createPeerConnection(iceServers []webrtc.ICEServer, mediaEngine *webrtc.MediaEngine) (*webrtc.PeerConnection, error) {
settingEngine := webrtc.SettingEngine{}
settingEngine.SetSRTPProtectionProfiles(
dtls.SRTP_AEAD_AES_128_GCM,
dtls.SRTP_AES128_CM_HMAC_SHA1_80,
)
apiOptions := []func(api *webrtc.API){
webrtc.WithSettingEngine(settingEngine),
}
if mediaEngine != nil {
apiOptions = append(apiOptions, webrtc.WithMediaEngine(mediaEngine))
}
api := webrtc.NewAPI(apiOptions...)
return api.NewPeerConnection(webrtc.Configuration{
ICEServers: iceServers,
SDPSemantics: webrtc.SDPSemanticsUnifiedPlan,
})
}