This commit is contained in:
BuGu
2025-03-21 11:03:03 +08:00
24 changed files with 307 additions and 349 deletions

View File

@@ -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()

View File

@@ -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...")
}

View File

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

Binary file not shown.

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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_
#endif // KVM_VISION_H_

View File

@@ -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)

View File

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

View File

@@ -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

View File

@@ -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,

View File

@@ -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()

View File

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

View File

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

View File

@@ -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))

View File

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

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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 ? (
<LoaderCircleIcon className="animate-spin" size={18} />
) : isEnabled ? (
<div>
<Tally4Icon color="#22c55e" size={18} className="block group-hover:hidden" />
<Tally5Icon color="#ef4444" size={18} className="hidden group-hover:block" />
</div>
) : (
<div>
<Tally5Icon size={18} className="block group-hover:hidden" />
<Tally4Icon color="#22c55e" size={18} className="hidden group-hover:block" />
</div>
<>
{isEnabled ? <Tally4Icon color="#22c55e" size={18} /> : <Tally5Icon size={18} />}
<span
className={clsx(
'select-none text-sm',
isEnabled ? 'group-hover:text-red-500' : 'group-hover:text-green-500'
)}
>
{t('screen.frameDetect')}
</span>
</>
)}
<span className="select-none text-sm">{t('screen.frameDetect')}</span>
</div>
</Tooltip>
);

View File

@@ -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) => (
<div
key={item.key}
className="flex cursor-pointer select-none items-center rounded py-1 pl-1 pr-6 hover:bg-neutral-700/70"
onClick={() => update(item.key)}
>
<div className="flex h-[14px] w-[20px] items-end text-blue-500">
{item.key === gop && <CheckIcon size={14} />}
</div>
<span>{item.label}</span>
</div>
))}
</>
);
return (
<Popover content={content} placement="rightTop">
<div className="flex h-[30px] cursor-pointer items-center space-x-2 rounded px-3 text-neutral-300 hover:bg-neutral-700/70">
<SquareKanbanIcon size={18} />
<span className="select-none text-sm">GOP</span>
</div>
</Popover>
);
};

View File

@@ -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 = () => {
<Resolution />
<Quality quality={quality} setQuality={setQuality} />
<Fps fps={fps} setFps={setFps} />
{videoMode === 'h264' && <Gop gop={gop} setGop={setGop} />}
{videoMode === 'mjpeg' && <FrameDetect />}
<Reset />
</div>

View File

@@ -55,9 +55,9 @@ export const Resolution = () => {
</div>
) : (
<>
<span className="flex w-[32px]">{res.width}</span>
<span>x</span>
<span className="w-[36px]">{res.height}</span>
<span>{res.width}</span>
<span className="px-1">x</span>
<span>{res.height}</span>
</>
)}
</div>

View File

@@ -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();
};

View File

@@ -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 (