diff --git a/kvmapp/system/update-nanokvm.py b/kvmapp/system/update-nanokvm.py index af0e490..2a2f15f 100755 --- a/kvmapp/system/update-nanokvm.py +++ b/kvmapp/system/update-nanokvm.py @@ -46,32 +46,6 @@ def download_firmware() -> None: print("Completed downloading firmware.") -def download_lib(): - print("download lib...") - - device_key = read("/device_key") - - url = f"https://maixvision.sipeed.com/api/v1/nanokvm/encryption?uid={device_key}" - headers = {"token": "MaixVision2024"} - response = requests.get(url, headers=headers, stream=True) - - if response.status_code != 200: - raise Exception(f"download lib failed, status: {response.status_code}") - - content_type = response.headers.get("content-type") - if content_type != "application/octet-stream": - raise Exception(f"download lib failed, content_type: {content_type}") - - lib_file = f"{temporary}/libmaixcam_lib.so" - with open(lib_file, "wb") as f: - f.write(response.content) - - lib_dir = f"{temporary}/latest/kvm_system/dl_lib/" - shutil.copy(lib_file, lib_dir) - - print("download lib done") - - def update() -> None: backup_dir = pathlib.Path("/root/old") firmware_dir = pathlib.Path("/kvmapp") @@ -94,7 +68,7 @@ def change_permissions() -> None: os.chmod(file_path, 0o755) print("change permissions done") - + def main() -> None: try: @@ -105,7 +79,6 @@ def main() -> None: mkdir() download_firmware() - # download_lib() update() change_permissions() diff --git a/server/common/cgo.go b/server/common/cgo.go index 6289d30..0a040fd 100644 --- a/server/common/cgo.go +++ b/server/common/cgo.go @@ -20,9 +20,7 @@ var ( kvmVisionOnce sync.Once ) -type KvmVision struct { - mutex sync.Mutex -} +type KvmVision struct{} func GetKvmVision() *KvmVision { kvmVisionOnce.Do(func() { @@ -44,9 +42,6 @@ func GetKvmVision() *KvmVision { } func (k *KvmVision) ReadMjpeg(width uint16, height uint16, quality uint16) (data []byte, result int) { - k.mutex.Lock() - defer k.mutex.Unlock() - var ( kvmData *C.uint8_t dataSize C.uint32_t @@ -72,10 +67,7 @@ func (k *KvmVision) ReadMjpeg(width uint16, height uint16, quality uint16) (data return } -func (k *KvmVision) ReadH264(width uint16, height uint16, bitRate uint16) (data []byte, sps []byte, pps []byte, result int) { - k.mutex.Lock() - defer k.mutex.Unlock() - +func (k *KvmVision) ReadH264(width uint16, height uint16, bitRate uint16) (data []byte, result int) { var ( kvmData *C.uint8_t dataSize C.uint32_t @@ -97,48 +89,10 @@ func (k *KvmVision) ReadH264(width uint16, height uint16, bitRate uint16) (data data = C.GoBytes(unsafe.Pointer(kvmData), C.int(dataSize)) - if result == 3 { - sps, _ = k.ReadH264SPS() - pps, _ = k.ReadH264PPS() - } - log.Debugf("read kvm image: %v", result) return } -func (k *KvmVision) ReadH264SPS() ([]byte, int) { - var ( - kvmData *C.uint8_t - dataSize C.uint32_t - ) - - result := int(C.kvmv_get_sps_frame(&kvmData, &dataSize)) - if result < 0 { - log.Errorf("failed to read sps: %v", result) - return nil, result - } - - data := C.GoBytes(unsafe.Pointer(kvmData), C.int(dataSize)) - - return data, result -} - -func (k *KvmVision) ReadH264PPS() ([]byte, int) { - var ( - kvmData *C.uint8_t - dataSize C.uint32_t - ) - - result := int(C.kvmv_get_pps_frame(&kvmData, &dataSize)) - if result < 0 { - log.Errorf("failed to read pps: %v", result) - return nil, result - } - - data := C.GoBytes(unsafe.Pointer(kvmData), C.int(dataSize)) - return data, result -} - func (k *KvmVision) SetHDMI(enable bool) int { hdmiEnable := C.uint8_t(0) if enable { @@ -154,10 +108,17 @@ func (k *KvmVision) SetHDMI(enable bool) int { return result } -func (k *KvmVision) Close() { - k.mutex.Lock() - defer k.mutex.Unlock() +func (k *KvmVision) SetGop(gop uint8) { + _gop := C.uint8_t(gop) + C.set_h264_gop(_gop) +} +func (k *KvmVision) SetFrameDetect(frame uint8) { + _frame := C.uint8_t(frame) + C.set_frame_detact(_frame) +} + +func (k *KvmVision) Close() { C.kvmv_deinit() log.Debugf("stop kvm vision...") } diff --git a/server/common/screen.go b/server/common/screen.go index 8059bd5..8ed486a 100644 --- a/server/common/screen.go +++ b/server/common/screen.go @@ -8,6 +8,7 @@ type Screen struct { FPS int Quality uint16 BitRate uint16 + GOP uint8 } var ( @@ -46,6 +47,7 @@ func GetScreen() *Screen { Quality: 80, FPS: 30, BitRate: 3000, + GOP: 30, } }) @@ -70,6 +72,9 @@ func SetScreen(key string, value int) { case "fps": screen.FPS = validateFPS(value) + + case "gop": + screen.GOP = uint8(value) } } diff --git a/server/dl_lib/libkvm.so b/server/dl_lib/libkvm.so index 256f24a..bf5bbbe 100644 Binary files a/server/dl_lib/libkvm.so and b/server/dl_lib/libkvm.so differ diff --git a/server/go.mod b/server/go.mod index b123016..58ffbce 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,6 +1,8 @@ module NanoKVM-Server -go 1.22.1 +go 1.23.0 + +toolchain go1.23.2 require ( github.com/creack/pty v1.1.24 @@ -15,7 +17,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/viper v1.19.0 github.com/unrolled/secure v1.15.0 - golang.org/x/crypto v0.31.0 + golang.org/x/crypto v0.36.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -71,9 +73,9 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) diff --git a/server/go.sum b/server/go.sum index 271cc14..61ff70e 100644 --- a/server/go.sum +++ b/server/go.sum @@ -160,17 +160,25 @@ golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/server/include/kvm_vision.h b/server/include/kvm_vision.h index cacd036..4f7af4f 100644 --- a/server/include/kvm_vision.h +++ b/server/include/kvm_vision.h @@ -50,20 +50,20 @@ void set_venc_auto_recyc(uint8_t _enable); -5: Retrieving image, please wait -4: Modifying image resolution, please wait -3: img buffer full - -2: VENC Errorl + -2: VENC Error -1: No images were acquired 0: Acquire MJPEG encoded images - 1: Acquire H264 encoded images(SPS) - 2: Acquire H264 encoded images(PPS) + 1: Acquire H264 encoded images(SPS)[Deprecated] + 2: Acquire H264 encoded images(PPS)[Deprecated] 3: Acquire H264 encoded images(I) 4: Acquire H264 encoded images(P) + 5: IMG not changed **********************************************************************************/ int kvmv_read_img(uint16_t _width, uint16_t _height, uint8_t _type, uint16_t _qlty, uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size); -int kvmv_get_sps_frame(uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size); -int kvmv_get_pps_frame(uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size); int free_kvmv_data(uint8_t ** _pp_kvm_data); void free_all_kvmv_data(); void set_h264_gop(uint8_t _gop); +void set_frame_detact(uint8_t _frame_detact); void kvmv_deinit(); uint8_t kvmv_hdmi_control(uint8_t _en); @@ -71,4 +71,4 @@ uint8_t kvmv_hdmi_control(uint8_t _en); } #endif -#endif // KVM_VISION_H_ \ No newline at end of file +#endif // KVM_VISION_H_ diff --git a/server/logger/formatter.go b/server/logger/formatter.go index a995d1a..b1dca36 100644 --- a/server/logger/formatter.go +++ b/server/logger/formatter.go @@ -22,7 +22,7 @@ func (f *formatter) Format(entry *logrus.Entry) ([]byte, error) { buffer = &bytes.Buffer{} } - now := entry.Time.Format("2006-01-02 15:04:05") + now := entry.Time.Format("2006-01-02 15:04:05.000") if entry.HasCaller() { fileName := filepath.Base(entry.Caller.File) diff --git a/server/proto/stream.go b/server/proto/stream.go index 52687db..f0bb202 100644 --- a/server/proto/stream.go +++ b/server/proto/stream.go @@ -1,9 +1,9 @@ package proto -type GetFrameDetectRsp struct { - Enabled bool `json:"enabled"` +type UpdateFrameDetectReq struct { + Enabled bool `validate:"omitempty"` } -type UpdateFrameDetectRsp struct { - Enabled bool `json:"enabled"` +type StopFrameDetectReq struct { + Duration int `validate:"omitempty"` } diff --git a/server/router/stream.go b/server/router/stream.go index a59308e..bcc13d3 100644 --- a/server/router/stream.go +++ b/server/router/stream.go @@ -12,8 +12,7 @@ func streamRouter(r *gin.Engine) { api := r.Group("/api").Use(middleware.CheckToken()) api.GET("/stream/mjpeg", mjpeg.Connect) // mjpeg stream - api.GET("/stream/mjpeg/detect", mjpeg.GetFrameDetect) // get frame detect state - api.POST("/stream/mjpeg/detect", mjpeg.UpdateFrameDetect) // update frame detect state + api.POST("/stream/mjpeg/detect", mjpeg.UpdateFrameDetect) // update frame detect api.POST("/stream/mjpeg/detect/stop", mjpeg.StopFrameDetect) // temporary stop frame detect api.GET("/stream/h264", h264.Connect) // h264 stream diff --git a/server/service/stream/h264/client.go b/server/service/stream/h264/client.go index 4714ac6..1db5d5b 100644 --- a/server/service/stream/h264/client.go +++ b/server/service/stream/h264/client.go @@ -2,6 +2,7 @@ package h264 import ( "encoding/json" + "sync" "github.com/gorilla/websocket" "github.com/pion/webrtc/v4" @@ -9,8 +10,9 @@ import ( ) type Client struct { - ws *websocket.Conn - pc *webrtc.PeerConnection + ws *websocket.Conn + pc *webrtc.PeerConnection + mutex sync.Mutex } type Message struct { @@ -36,9 +38,7 @@ func (c *Client) addTrack() { return } - mutex.Lock() trackMap[c.ws] = videoTrack - mutex.Unlock() } // register callback events @@ -61,13 +61,9 @@ func (c *Client) register() { // ICE connection state has changed c.pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) { if state == webrtc.ICEConnectionStateConnected && !isSending { - mutex.Lock() - if !isSending { - // start sending h264 data - go send() - isSending = true - } - mutex.Unlock() + // start sending h264 data + go send() + isSending = true } log.Debugf("ice connection state has changed to %s", state.String()) @@ -81,13 +77,11 @@ func (c *Client) readMessage() { for { _, raw, err := c.ws.ReadMessage() if err != nil { - mutex.Lock() delete(trackMap, c.ws) - if len(trackMap) == 0 && isSending { + if isSending && len(trackMap) == 0 { // stop sending when all websocket connections are closed - exitSig <- true + isSending = false } - mutex.Unlock() log.Debugf("failed to read message: %s", err) return @@ -144,9 +138,6 @@ func (c *Client) readMessage() { return } - case "heartbeat": - _ = c.sendMessage("heartbeat", "") - default: log.Debugf("unhandled message event: %s", message.Event) } @@ -155,8 +146,8 @@ func (c *Client) readMessage() { // send websocket message func (c *Client) sendMessage(event string, data string) error { - mutex.RLock() - defer mutex.RUnlock() + c.mutex.Lock() + defer c.mutex.Unlock() message := &Message{ Event: event, diff --git a/server/service/stream/h264/h264.go b/server/service/stream/h264/h264.go index cd9b691..67358de 100644 --- a/server/service/stream/h264/h264.go +++ b/server/service/stream/h264/h264.go @@ -4,6 +4,7 @@ import ( "NanoKVM-Server/config" "net/http" "sync" + "time" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" @@ -18,9 +19,7 @@ var ( }, } trackMap = make(map[*websocket.Conn]*webrtc.TrackLocalStaticSample) - mutex = sync.RWMutex{} isSending = false - exitSig = make(chan bool, 1) ) func Connect(c *gin.Context) { @@ -35,6 +34,9 @@ func Connect(c *gin.Context) { log.Debugf("h264 websocket disconnected") }() + var zeroTime time.Time + _ = wsConn.SetReadDeadline(zeroTime) + conf := config.GetInstance() var iceServers []webrtc.ICEServer @@ -67,8 +69,9 @@ func Connect(c *gin.Context) { }() client := &Client{ - ws: wsConn, - pc: peerConn, + ws: wsConn, + pc: peerConn, + mutex: sync.Mutex{}, } client.addTrack() diff --git a/server/service/stream/h264/sender.go b/server/service/stream/h264/sender.go index da16c9c..2510c16 100644 --- a/server/service/stream/h264/sender.go +++ b/server/service/stream/h264/sender.go @@ -2,7 +2,6 @@ package h264 import ( "NanoKVM-Server/common" - "NanoKVM-Server/service/stream" "time" "github.com/pion/webrtc/v4/pkg/media" @@ -18,63 +17,45 @@ func send() { defer ticker.Stop() vision := common.GetKvmVision() - for { - select { - case <-ticker.C: - height := screen.Height - width, ok := common.ResolutionMap[height] - if !ok { - width = 0 - height = 0 - } - - bitRate := screen.BitRate - if _, ok := common.BitRateMap[bitRate]; !ok { - bitRate = 3000 - } - - data, sps, pps, result := vision.ReadH264(width, height, bitRate) - if result < 0 { - continue - } - - if result == 3 { - writeSample(sps, duration) - writeSample(pps, duration) - } - writeSample(data, duration) - - stream.GetFrameRateCounter().Update() - - if screen.FPS != fps { - fps = screen.FPS - duration = time.Second / time.Duration(fps) - ticker.Reset(duration) - } - - case <-exitSig: - mutex.Lock() - isSending = false - mutex.Unlock() + for range ticker.C { + if !isSending && len(trackMap) == 0 { return } - } -} -func writeSample(data []byte, duration time.Duration) { - sample := media.Sample{ - Data: data, - Duration: duration, - } + height := screen.Height + width, ok := common.ResolutionMap[height] + if !ok { + width = 0 + height = 0 + } - mutex.RLock() - defer mutex.RUnlock() + bitRate := screen.BitRate + if _, ok := common.BitRateMap[bitRate]; !ok { + bitRate = 3000 + } - for _, track := range trackMap { - if err := track.WriteSample(sample); err != nil { - log.Errorf("failed to send h264 data: %s", err) + data, result := vision.ReadH264(width, height, bitRate) + if result < 0 { + continue + } + + sample := media.Sample{ + Data: data, + Duration: duration, + } + + for _, track := range trackMap { + if err := track.WriteSample(sample); err != nil { + log.Errorf("failed to send h264 data: %s", err) + } + } + + log.Debugf("send h264 data: %d", len(data)) + + if screen.FPS != fps { + fps = screen.FPS + duration = time.Second / time.Duration(fps) + ticker.Reset(duration) } } - - log.Debugf("send h264 data: %d", len(data)) } diff --git a/server/service/stream/mjpeg/frame-detect.go b/server/service/stream/mjpeg/frame-detect.go index fa89358..ac2d8df 100644 --- a/server/service/stream/mjpeg/frame-detect.go +++ b/server/service/stream/mjpeg/frame-detect.go @@ -1,132 +1,55 @@ package mjpeg import ( - "errors" - "os" + "NanoKVM-Server/common" + "NanoKVM-Server/proto" "time" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" - - "NanoKVM-Server/proto" ) -const ( - frameDetect = "/etc/kvm/frame_detact" - frameDetectTmp = "/etc/kvm/frame_detact_tmp" -) - -type UpdateFrameDetectRsp struct { - Enabled bool `json:"enabled"` -} - -func GetFrameDetect(c *gin.Context) { - var rsp proto.Response - - isEnabled, err := isFrameDetectEnabled() - if err != nil { - rsp.ErrRsp(c, -1, "unknown frame detect status") - return - } - - rsp.OkRspWithData(c, &proto.GetFrameDetectRsp{ - Enabled: isEnabled, - }) - log.Debugf("get frame detect success, enabled: %t", isEnabled) -} +const FrameDetectInterval uint8 = 60 func UpdateFrameDetect(c *gin.Context) { + var req proto.UpdateFrameDetectReq var rsp proto.Response - isEnabled, err := isFrameDetectEnabled() - if err != nil { - rsp.ErrRsp(c, -1, "unknown frame status") + if err := proto.ParseFormRequest(c, &req); err != nil { + rsp.ErrRsp(c, -1, "invalid parameters") return } - if isEnabled { - _ = os.Remove(frameDetect) - _ = os.Remove(frameDetectTmp) - } else { - file, err2 := os.OpenFile(frameDetect, os.O_CREATE|os.O_TRUNC, 0o644) - if err2 != nil { - rsp.ErrRsp(c, -3, "enable frame detect failed") - return - } - defer func() { - _ = file.Close() - }() + var frame uint8 = 0 + if req.Enabled { + frame = FrameDetectInterval } - isEnabled, err = isFrameDetectEnabled() - if err != nil { - rsp.ErrRsp(c, -4, "unknown frame status") - return - } + common.GetKvmVision().SetFrameDetect(frame) - rsp.OkRspWithData(c, &UpdateFrameDetectRsp{ - Enabled: isEnabled, - }) - log.Debugf("update frame detect success, enabled: %t", isEnabled) + rsp.OkRsp(c) + log.Debugf("update frame detect: %t", req.Enabled) } func StopFrameDetect(c *gin.Context) { + var req proto.StopFrameDetectReq var rsp proto.Response - exist, err := isFileExist(frameDetect) - if err != nil { - rsp.ErrRsp(c, -1, "unknown frame status") + if err := proto.ParseFormRequest(c, &req); err != nil { + rsp.ErrRsp(c, -1, "invalid parameters") return } - if !exist { - rsp.OkRsp(c) - return + duration := 10 * time.Second + if req.Duration > 0 { + duration = time.Duration(req.Duration) * time.Second } - err = os.Rename(frameDetect, frameDetectTmp) - if err != nil { - rsp.ErrRsp(c, -2, "stop operation failed") - return - } + vision := common.GetKvmVision() - go func() { - time.Sleep(20 * time.Second) - _ = os.Rename(frameDetectTmp, frameDetect) - log.Debug("frame detect started") - }() + vision.SetFrameDetect(0) + time.Sleep(duration) + vision.SetFrameDetect(FrameDetectInterval) rsp.OkRsp(c) - log.Debug("frame detect stopped") -} - -func isFrameDetectEnabled() (bool, error) { - exist, err := isFileExist(frameDetect) - if err != nil { - return false, err - } - - if exist { - return true, nil - } - - exist, err = isFileExist(frameDetectTmp) - if err != nil { - return false, err - } - - return exist, nil -} - -func isFileExist(name string) (bool, error) { - _, err := os.Stat(name) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return false, nil - } - - return false, nil - } - - return true, nil } diff --git a/server/service/stream/mjpeg/mjpeg.go b/server/service/stream/mjpeg/mjpeg.go index 32ba210..5f77e8d 100644 --- a/server/service/stream/mjpeg/mjpeg.go +++ b/server/service/stream/mjpeg/mjpeg.go @@ -11,8 +11,8 @@ import ( ) var ( - chanMap = make(map[*gin.Context]chan struct{}) - mutex = sync.RWMutex{} + chanMap = make(map[*gin.Context]bool) + mutex = sync.Mutex{} exitSig = make(chan bool, 1) ) @@ -23,13 +23,13 @@ func Connect(c *gin.Context) { c.Header("Pragma", "no-cache") mutex.Lock() - chanMap[c] = make(chan struct{}, 1) + chanMap[c] = true if len(chanMap) == 1 { go send() } mutex.Unlock() - <-chanMap[c] + <-c.Request.Context().Done() mutex.Lock() delete(chanMap, c) @@ -63,14 +63,13 @@ func send() { } data, result := vision.ReadMjpeg(width, height, quality) - if result < 0 { + if result < 0 || result == 5 { continue } - for c, ch := range chanMap { + for c := range chanMap { if err := write(c, data); err != nil { log.Debugf("failed to write mjpeg data: %s", err) - close(ch) } } log.Debugf("send mjpeg data: %d", len(data)) diff --git a/server/service/vm/screen.go b/server/service/vm/screen.go index a42a395..a5b4c39 100644 --- a/server/service/vm/screen.go +++ b/server/service/vm/screen.go @@ -4,6 +4,7 @@ import ( "NanoKVM-Server/common" "fmt" "os" + "strconv" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" @@ -22,30 +23,34 @@ func (s *Service) SetScreen(c *gin.Context) { var req proto.SetScreenReq var rsp proto.Response - if err := proto.ParseFormRequest(c, &req); err != nil { + err := proto.ParseFormRequest(c, &req) + if err != nil { rsp.ErrRsp(c, -1, "invalid arguments") return } - file, ok := screenFileMap[req.Type] - if !ok { - rsp.ErrRsp(c, -2, "invalid arguments") - return - } - - data := fmt.Sprintf("%d", req.Value) - if req.Type == "type" { + switch req.Type { + case "type": + data := "h264" if req.Value == 0 { data = "mjpeg" - } else { - data = "h264" } + err = writeScreen("type", data) + + case "gop": + gop := 30 + if req.Value >= 1 && req.Value <= 100 { + gop = req.Value + } + common.GetKvmVision().SetGop(uint8(gop)) + + default: + data := strconv.Itoa(req.Value) + err = writeScreen(req.Type, data) } - err := os.WriteFile(file, []byte(data), 0o666) if err != nil { - log.Errorf("write kvm %s failed: %s", file, err) - rsp.ErrRsp(c, -3, "update screen failed") + rsp.ErrRsp(c, -2, "update screen failed") return } @@ -54,3 +59,18 @@ func (s *Service) SetScreen(c *gin.Context) { log.Debugf("update screen: %+v", req) rsp.OkRsp(c) } + +func writeScreen(key string, value string) error { + file, ok := screenFileMap[key] + if !ok { + return fmt.Errorf("invalid argument %s", key) + } + + err := os.WriteFile(file, []byte(value), 0o666) + if err != nil { + log.Errorf("write kvm %s failed: %s", file, err) + return err + } + + return nil +} diff --git a/web/src/api/stream.ts b/web/src/api/stream.ts index 132fb55..227e55f 100644 --- a/web/src/api/stream.ts +++ b/web/src/api/stream.ts @@ -1,16 +1,17 @@ import { http } from '@/lib/http.ts'; -// get whether the frame detect is enabled -export function getFrameDetect() { - return http.get('/api/stream/mjpeg/detect'); -} - // enable/disable frame detect -export function updateFrameDetect() { - return http.post('/api/stream/mjpeg/detect'); +export function updateFrameDetect(enabled: boolean) { + const data = { + enabled + }; + return http.post('/api/stream/mjpeg/detect', data); } // pause frame detect for a while (prevent a black screen when opening the page for the first time) -export function stopFrameDetect() { - return http.post('/api/stream/mjpeg/detect/stop'); +export function stopFrameDetect(duration: number) { + const data = { + duration + }; + return http.post('/api/stream/mjpeg/detect/stop', data); } diff --git a/web/src/lib/localstorage.ts b/web/src/lib/localstorage.ts index d59c011..07cbc1e 100644 --- a/web/src/lib/localstorage.ts +++ b/web/src/lib/localstorage.ts @@ -5,6 +5,8 @@ const VIDEO_MODE_KEY = 'nano-kvm-vide-mode'; const WEB_RESOLUTION_KEY = 'nano-kvm-web-resolution'; const FPS_KEY = 'nano-kvm-fps'; const QUALITY_KEY = 'nano-kvm-quality'; +const GOP_KEY = 'nano-kvm-gop'; +const FRAME_DETECT_KEY = 'nano-kvm-frame-detect'; const MOUSE_STYLE_KEY = 'nano-kvm-mouse-style'; const MOUSE_MODE_KEY = 'nano-kvm-mouse-mode'; const SKIP_UPDATE_KEY = 'nano-kvm-check-update'; @@ -92,6 +94,24 @@ export function setQuality(quality: number) { localStorage.setItem(QUALITY_KEY, String(quality)); } +export function getGop() { + const gop = localStorage.getItem(GOP_KEY); + return gop ? Number(gop) : null; +} + +export function setGop(gop: number) { + localStorage.setItem(GOP_KEY, String(gop)); +} + +export function getFrameDetect(): boolean { + const enabled = localStorage.getItem(FRAME_DETECT_KEY); + return enabled ? Boolean(enabled) : true; +} + +export function setFrameDetect(enabled: boolean) { + localStorage.setItem(FRAME_DETECT_KEY, String(enabled)); +} + export function getMouseStyle() { return localStorage.getItem(MOUSE_STYLE_KEY); } diff --git a/web/src/pages/desktop/menu/screen/frame-detect.tsx b/web/src/pages/desktop/menu/screen/frame-detect.tsx index a750e21..f640817 100644 --- a/web/src/pages/desktop/menu/screen/frame-detect.tsx +++ b/web/src/pages/desktop/menu/screen/frame-detect.tsx @@ -1,31 +1,39 @@ import { useEffect, useState } from 'react'; import { Tooltip } from 'antd'; +import clsx from 'clsx'; import { LoaderCircleIcon, Tally4Icon, Tally5Icon } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { getFrameDetect, updateFrameDetect } from '@/api/stream.ts'; +import * as api from '@/api/stream.ts'; +import * as ls from '@/lib/localstorage.ts'; export const FrameDetect = () => { const { t } = useTranslation(); - const [isEnabled, setIsEnabled] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isEnabled, setIsEnabled] = useState(false); useEffect(() => { - getFrameDetect().then((rsp) => { - if (rsp.code === 0) { - setIsEnabled(rsp.data.enabled); - } - }); + const enabled = ls.getFrameDetect(); + if (enabled) { + setIsEnabled(true); + } else { + api.updateFrameDetect(false); + } }, []); function update() { if (isLoading) return; setIsLoading(true); - updateFrameDetect() + const enabled = !isEnabled; + + api + .updateFrameDetect(enabled) .then((rsp) => { if (rsp.code === 0) { - setIsEnabled(rsp.data.enabled); + setIsEnabled(enabled); + ls.setFrameDetect(enabled); } }) .finally(() => { @@ -41,18 +49,20 @@ export const FrameDetect = () => { > {isLoading ? ( - ) : isEnabled ? ( -
- - -
) : ( -
- - -
+ <> + {isEnabled ? : } + + + {t('screen.frameDetect')} + + )} - {t('screen.frameDetect')} ); diff --git a/web/src/pages/desktop/menu/screen/gop.tsx b/web/src/pages/desktop/menu/screen/gop.tsx new file mode 100644 index 0000000..1103d96 --- /dev/null +++ b/web/src/pages/desktop/menu/screen/gop.tsx @@ -0,0 +1,57 @@ +import { Popover } from 'antd'; +import { CheckIcon, SquareKanbanIcon } from 'lucide-react'; + +import { updateScreen } from '@/api/vm.ts'; +import { setGop as setCookie } from '@/lib/localstorage'; + +type GopProps = { + gop: number; + setGop: (gop: number) => void; +}; + +const gopList = [ + { key: 10, label: '10' }, + { key: 30, label: '30' }, + { key: 50, label: '50' }, + { key: 100, label: '100' } +]; + +export const Gop = ({ gop, setGop }: GopProps) => { + async function update(value: number) { + if (value === gop) return; + + const rsp = await updateScreen('gop', value); + if (rsp.code !== 0) { + return; + } + + setGop(value); + setCookie(value); + } + + const content = ( + <> + {gopList.map((item) => ( +
update(item.key)} + > +
+ {item.key === gop && } +
+ {item.label} +
+ ))} + + ); + + return ( + +
+ + GOP +
+
+ ); +}; diff --git a/web/src/pages/desktop/menu/screen/index.tsx b/web/src/pages/desktop/menu/screen/index.tsx index 99c632a..451c42a 100644 --- a/web/src/pages/desktop/menu/screen/index.tsx +++ b/web/src/pages/desktop/menu/screen/index.tsx @@ -11,6 +11,7 @@ import { MenuItem } from '@/components/menu-item.tsx'; import { BitRateMap, QualityMap } from './constants.ts'; import { Fps } from './fps'; import { FrameDetect } from './frame-detect'; +import { Gop } from './gop.tsx'; import { Quality } from './quality'; import { Reset } from './reset.tsx'; import { Resolution } from './resolution'; @@ -24,37 +25,47 @@ export const Screen = () => { const [fps, setFps] = useState(30); const [quality, setQuality] = useState(2); + const [gop, setGop] = useState(30); useEffect(() => { updateScreen('type', videoMode === 'mjpeg' ? 0 : 1); updateScreen('resolution', resolution!.height); updateQuality(); updateFps(); + updateGop(); }, []); - function updateQuality() { + async function updateQuality() { const cookieQuality = ls.getQuality(); if (!cookieQuality) return; const key = cookieQuality >= 1 && cookieQuality <= 4 ? cookieQuality : 2; const value = videoMode === 'mjpeg' ? QualityMap.get(key)! : BitRateMap.get(key)!; - updateScreen('quality', value).then((rsp) => { - if (rsp.code === 0) { - setQuality(key); - } - }); + const rsp = await updateScreen('quality', value); + if (rsp.code === 0) { + setQuality(key); + } } - function updateFps() { + async function updateFps() { const cookieFps = ls.getFps(); if (!cookieFps) return; - updateScreen('fps', cookieFps).then((rsp) => { - if (rsp.code === 0) { - setFps(cookieFps); - } - }); + const rsp = await updateScreen('fps', cookieFps); + if (rsp.code === 0) { + setFps(cookieFps); + } + } + + async function updateGop() { + const cookieGop = ls.getGop(); + if (!cookieGop) return; + + const rsp = await updateScreen('gop', cookieGop); + if (rsp.code === 0) { + setGop(cookieGop); + } } const content = ( @@ -63,6 +74,7 @@ export const Screen = () => { + {videoMode === 'h264' && } {videoMode === 'mjpeg' && } diff --git a/web/src/pages/desktop/menu/screen/resolution.tsx b/web/src/pages/desktop/menu/screen/resolution.tsx index 66b9161..37661dc 100644 --- a/web/src/pages/desktop/menu/screen/resolution.tsx +++ b/web/src/pages/desktop/menu/screen/resolution.tsx @@ -55,9 +55,9 @@ export const Resolution = () => { ) : ( <> - {res.width} - x - {res.height} + {res.width} + x + {res.height} )} diff --git a/web/src/pages/desktop/screen/h264.tsx b/web/src/pages/desktop/screen/h264.tsx index e3ce1d7..d0d57d4 100644 --- a/web/src/pages/desktop/screen/h264.tsx +++ b/web/src/pages/desktop/screen/h264.tsx @@ -15,7 +15,6 @@ export const H264 = () => { const [isLoading, setIsLoading] = useState(true); useEffect(() => { - let heartbeatTimer: any; const videoElement = document.getElementById('screen') as HTMLVideoElement; const url = `${getBaseUrl('ws')}/api/stream/h264`; @@ -52,10 +51,6 @@ export const H264 = () => { ws.send(JSON.stringify({ event: 'offer', data: JSON.stringify(offer) })); }) .catch(console.log); - - heartbeatTimer = setInterval(() => { - ws.send(JSON.stringify({ event: 'heartbeat', data: '' })); - }, 60 * 1000); }; ws.onmessage = (event) => { @@ -75,9 +70,6 @@ export const H264 = () => { pc.addIceCandidate(data).catch(console.log); break; - case 'heartbeat': - break; - default: console.log('unhandled event: ', msg.event); } @@ -91,9 +83,6 @@ export const H264 = () => { }, 15 * 1000); return () => { - if (heartbeatTimer) { - clearInterval(heartbeatTimer); - } ws.close(); pc.close(); }; diff --git a/web/src/pages/desktop/screen/mjpeg.tsx b/web/src/pages/desktop/screen/mjpeg.tsx index 743b17d..8dec868 100644 --- a/web/src/pages/desktop/screen/mjpeg.tsx +++ b/web/src/pages/desktop/screen/mjpeg.tsx @@ -5,6 +5,7 @@ import { useAtomValue } from 'jotai'; import MonitorXIcon from '@/assets/images/monitor-x.svg'; import { stopFrameDetect } from '@/api/stream.ts'; +import { getFrameDetect } from '@/lib/localstorage.ts'; import { getBaseUrl } from '@/lib/service.ts'; import { mouseStyleAtom } from '@/jotai/mouse.ts'; import { resolutionAtom } from '@/jotai/screen.ts'; @@ -15,7 +16,10 @@ export const Mjpeg = () => { useEffect(() => { // stop frame detect for a while - stopFrameDetect(); + const enabled = getFrameDetect(); + if (enabled) { + stopFrameDetect(10); + } }, [resolution]); return (