Refactor: Rename NanoKVM to BatchuKVM and update server URL
This commit is contained in:
112
server/service/stream/webrtc/client.go
Normal file
112
server/service/stream/webrtc/client.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/rtp/codecs"
|
||||
"github.com/pion/webrtc/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"sync"
|
||||
)
|
||||
|
||||
func NewClient(ws *websocket.Conn, videoConn *webrtc.PeerConnection) *Client {
|
||||
return &Client{
|
||||
ws: ws,
|
||||
video: videoConn,
|
||||
mutex: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) WriteMessage(event string, data string) error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
message := &Message{
|
||||
Event: event,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
if err := c.ws.WriteJSON(message); err != nil {
|
||||
log.Errorf("failed to send message %s: %v", event, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("sent message %s", event)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ReadMessage() (*Message, error) {
|
||||
_, raw, err := c.ws.ReadMessage()
|
||||
if err != nil {
|
||||
log.Errorf("failed to read message: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var message Message
|
||||
if err := json.Unmarshal(raw, &message); err != nil {
|
||||
log.Errorf("failed to unmarshal message: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
func (c *Client) AddTrack() error {
|
||||
// video track
|
||||
videoTrack, err := webrtc.NewTrackLocalStaticRTP(
|
||||
webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264},
|
||||
"video",
|
||||
"pion-video",
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create video track: %s", 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)
|
||||
if err != nil {
|
||||
log.Errorf("failed to add video track: %s", err)
|
||||
return err
|
||||
}
|
||||
go startRTCPReader(videoSender)
|
||||
|
||||
track := &Track{
|
||||
videoPacketizer: videoPacketizer,
|
||||
video: videoTrack,
|
||||
}
|
||||
track.updateExtension()
|
||||
|
||||
c.mutex.Lock()
|
||||
c.track = track
|
||||
c.mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startRTCPReader(sender *webrtc.RTPSender) {
|
||||
rtcpBuf := make([]byte, 1500)
|
||||
for {
|
||||
if _, _, err := sender.Read(rtcpBuf); err != nil {
|
||||
log.Debugf("RTCP reader error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
158
server/service/stream/webrtc/h264.go
Normal file
158
server/service/stream/webrtc/h264.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/config"
|
||||
"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{
|
||||
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()
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
96
server/service/stream/webrtc/manager.go
Normal file
96
server/service/stream/webrtc/manager.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/service/stream"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func NewWebRTCManager() *WebRTCManager {
|
||||
return &WebRTCManager{
|
||||
clients: make(map[*websocket.Conn]*Client),
|
||||
videoSending: 0,
|
||||
mutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) AddClient(ws *websocket.Conn, client *Client) {
|
||||
client.track.updateExtension()
|
||||
|
||||
m.mutex.Lock()
|
||||
m.clients[ws] = client
|
||||
m.mutex.Unlock()
|
||||
|
||||
log.Debugf("added client %s, total clients: %d", ws.RemoteAddr(), len(m.clients))
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) RemoveClient(ws *websocket.Conn) {
|
||||
m.mutex.Lock()
|
||||
delete(m.clients, ws)
|
||||
m.mutex.Unlock()
|
||||
|
||||
log.Debugf("removed client %s, total clients: %d", ws.RemoteAddr(), len(m.clients))
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) GetClientCount() int {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
return len(m.clients)
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) StartVideoStream() {
|
||||
if atomic.CompareAndSwapInt32(&m.videoSending, 0, 1) {
|
||||
go m.sendVideoStream()
|
||||
log.Debugf("start sending h264 stream")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) sendVideoStream() {
|
||||
defer atomic.StoreInt32(&m.videoSending, 0)
|
||||
|
||||
screen := common.GetScreen()
|
||||
common.CheckScreen()
|
||||
fps := screen.FPS
|
||||
duration := time.Second / time.Duration(fps)
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
|
||||
ticker := time.NewTicker(duration)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if m.GetClientCount() == 0 {
|
||||
log.Debugf("stop sending h264 stream")
|
||||
return
|
||||
}
|
||||
|
||||
data, result := vision.ReadH264(screen.Width, screen.Height, screen.BitRate)
|
||||
if result < 0 || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sample := media.Sample{
|
||||
Data: data,
|
||||
Duration: duration,
|
||||
}
|
||||
|
||||
for _, client := range m.clients {
|
||||
client.track.writeVideo(sample)
|
||||
}
|
||||
|
||||
if screen.FPS != fps && screen.FPS != 0 {
|
||||
fps = screen.FPS
|
||||
duration = time.Second / time.Duration(fps)
|
||||
ticker.Reset(duration)
|
||||
}
|
||||
|
||||
stream.GetFrameRateCounter().Update()
|
||||
}
|
||||
}
|
||||
149
server/service/stream/webrtc/signaling.go
Normal file
149
server/service/stream/webrtc/signaling.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func NewSignalingHandler(client *Client) *SignalingHandler {
|
||||
return &SignalingHandler{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCallbacks Register callback functions
|
||||
func (s *SignalingHandler) RegisterCallbacks() {
|
||||
// video ICE candidate
|
||||
s.client.video.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||
if candidate == nil {
|
||||
return
|
||||
}
|
||||
|
||||
candidateByte, err := json.Marshal(candidate.ToJSON())
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal video candidate: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.client.WriteMessage("video-candidate", string(candidateByte)); err != nil {
|
||||
log.Errorf("failed to send video candidate: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
manager := getManager()
|
||||
|
||||
// video connection state change
|
||||
s.client.video.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
||||
if state == webrtc.ICEConnectionStateConnected {
|
||||
manager.StartVideoStream()
|
||||
}
|
||||
|
||||
log.Debugf("video connection state changed to %s", state.String())
|
||||
})
|
||||
}
|
||||
|
||||
// HandleMessage handle the received message
|
||||
func (s *SignalingHandler) HandleMessage(message *Message) error {
|
||||
switch message.Event {
|
||||
case "video-offer":
|
||||
return s.handleVideoOffer(message.Data)
|
||||
case "video-candidate":
|
||||
return s.handleVideoCandidate(message.Data)
|
||||
case "heartbeat":
|
||||
return s.handleHeartbeat()
|
||||
default:
|
||||
log.Debugf("Unhandled message event: %s", message.Event)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalingHandler) handleVideoOffer(data string) error {
|
||||
if s.client.video.SignalingState() != webrtc.SignalingStateStable {
|
||||
err := errors.New("video signaling is not stable")
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
offer := webrtc.SessionDescription{}
|
||||
if err := json.Unmarshal([]byte(data), &offer); err != nil {
|
||||
log.Errorf("failed to unmarshal video offer: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.client.video.SetRemoteDescription(offer); err != nil {
|
||||
log.Errorf("failed to set remote description: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
answer, err := s.client.video.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create answer: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.client.video.SetLocalDescription(answer); err != nil {
|
||||
log.Errorf("failed to set local description: %s", 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)
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal answer: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
func (s *SignalingHandler) handleVideoCandidate(data string) error {
|
||||
candidate := webrtc.ICECandidateInit{}
|
||||
if err := json.Unmarshal([]byte(data), &candidate); err != nil {
|
||||
log.Errorf("failed to unmarshal candidate: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.client.video.AddICECandidate(candidate); err != nil {
|
||||
log.Errorf("failed to add ICECandidate: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handle heartbeat
|
||||
func (s *SignalingHandler) handleHeartbeat() error {
|
||||
return s.client.WriteMessage("heartbeat", "")
|
||||
}
|
||||
53
server/service/stream/webrtc/track.go
Normal file
53
server/service/stream/webrtc/track.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (t *Track) updateExtension() {
|
||||
if t.playoutDelayExtensionID == 0 {
|
||||
t.playoutDelayExtensionID = 5
|
||||
}
|
||||
|
||||
if t.playoutDelayExtensionData == nil || 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 nil
|
||||
}
|
||||
|
||||
func (t *Track) writeVideo(sample media.Sample) {
|
||||
err := t.writeVideoSample(sample)
|
||||
if err != nil {
|
||||
log.Errorf("failed to write h264 video: %s", err)
|
||||
}
|
||||
}
|
||||
38
server/service/stream/webrtc/types.go
Normal file
38
server/service/stream/webrtc/types.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type WebRTCManager struct {
|
||||
clients map[*websocket.Conn]*Client
|
||||
videoSending int32
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ws *websocket.Conn
|
||||
video *webrtc.PeerConnection
|
||||
track *Track
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
type SignalingHandler struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
type Track struct {
|
||||
playoutDelayExtensionID uint8
|
||||
playoutDelayExtensionData []byte
|
||||
videoPacketizer rtp.Packetizer
|
||||
video *webrtc.TrackLocalStaticRTP
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Event string `json:"event"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
Reference in New Issue
Block a user