Refactor: Rename NanoKVM to BatchuKVM and update server URL

This commit is contained in:
2025-12-09 20:35:38 +09:00
commit 8cf674c9e5
396 changed files with 54380 additions and 0 deletions

View 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
}
}
}

View 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,
})
}

View 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()
}
}

View 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", "")
}

View 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)
}
}

View 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"`
}