mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
feat: support setting H.264 GOP
perf: merge H.264 SPS and PPS into I-frame perf: refactor MJPEG frame detection perf: update log timestamp to millisecond precision perf: update script `update-nanokvm.py` chore: bump Go to 1.23 chore: bump golang.org/x/net to v0.37.0
This commit is contained in:
@@ -46,32 +46,6 @@ def download_firmware() -> None:
|
|||||||
print("Completed downloading firmware.")
|
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:
|
def update() -> None:
|
||||||
backup_dir = pathlib.Path("/root/old")
|
backup_dir = pathlib.Path("/root/old")
|
||||||
firmware_dir = pathlib.Path("/kvmapp")
|
firmware_dir = pathlib.Path("/kvmapp")
|
||||||
@@ -94,7 +68,7 @@ def change_permissions() -> None:
|
|||||||
os.chmod(file_path, 0o755)
|
os.chmod(file_path, 0o755)
|
||||||
|
|
||||||
print("change permissions done")
|
print("change permissions done")
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
try:
|
try:
|
||||||
@@ -105,7 +79,6 @@ def main() -> None:
|
|||||||
|
|
||||||
mkdir()
|
mkdir()
|
||||||
download_firmware()
|
download_firmware()
|
||||||
# download_lib()
|
|
||||||
update()
|
update()
|
||||||
change_permissions()
|
change_permissions()
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ var (
|
|||||||
kvmVisionOnce sync.Once
|
kvmVisionOnce sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
type KvmVision struct {
|
type KvmVision struct{}
|
||||||
mutex sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetKvmVision() *KvmVision {
|
func GetKvmVision() *KvmVision {
|
||||||
kvmVisionOnce.Do(func() {
|
kvmVisionOnce.Do(func() {
|
||||||
@@ -44,9 +42,6 @@ func GetKvmVision() *KvmVision {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (k *KvmVision) ReadMjpeg(width uint16, height uint16, quality uint16) (data []byte, result int) {
|
func (k *KvmVision) ReadMjpeg(width uint16, height uint16, quality uint16) (data []byte, result int) {
|
||||||
k.mutex.Lock()
|
|
||||||
defer k.mutex.Unlock()
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
kvmData *C.uint8_t
|
kvmData *C.uint8_t
|
||||||
dataSize C.uint32_t
|
dataSize C.uint32_t
|
||||||
@@ -72,10 +67,7 @@ func (k *KvmVision) ReadMjpeg(width uint16, height uint16, quality uint16) (data
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *KvmVision) ReadH264(width uint16, height uint16, bitRate uint16) (data []byte, sps []byte, pps []byte, result int) {
|
func (k *KvmVision) ReadH264(width uint16, height uint16, bitRate uint16) (data []byte, result int) {
|
||||||
k.mutex.Lock()
|
|
||||||
defer k.mutex.Unlock()
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
kvmData *C.uint8_t
|
kvmData *C.uint8_t
|
||||||
dataSize C.uint32_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))
|
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)
|
log.Debugf("read kvm image: %v", result)
|
||||||
return
|
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 {
|
func (k *KvmVision) SetHDMI(enable bool) int {
|
||||||
hdmiEnable := C.uint8_t(0)
|
hdmiEnable := C.uint8_t(0)
|
||||||
if enable {
|
if enable {
|
||||||
@@ -154,10 +108,17 @@ func (k *KvmVision) SetHDMI(enable bool) int {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *KvmVision) Close() {
|
func (k *KvmVision) SetGop(gop uint8) {
|
||||||
k.mutex.Lock()
|
_gop := C.uint8_t(gop)
|
||||||
defer k.mutex.Unlock()
|
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()
|
C.kvmv_deinit()
|
||||||
log.Debugf("stop kvm vision...")
|
log.Debugf("stop kvm vision...")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type Screen struct {
|
|||||||
FPS int
|
FPS int
|
||||||
Quality uint16
|
Quality uint16
|
||||||
BitRate uint16
|
BitRate uint16
|
||||||
|
GOP uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -46,6 +47,7 @@ func GetScreen() *Screen {
|
|||||||
Quality: 80,
|
Quality: 80,
|
||||||
FPS: 30,
|
FPS: 30,
|
||||||
BitRate: 3000,
|
BitRate: 3000,
|
||||||
|
GOP: 30,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -70,6 +72,9 @@ func SetScreen(key string, value int) {
|
|||||||
|
|
||||||
case "fps":
|
case "fps":
|
||||||
screen.FPS = validateFPS(value)
|
screen.FPS = validateFPS(value)
|
||||||
|
|
||||||
|
case "gop":
|
||||||
|
screen.GOP = uint8(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,8 @@
|
|||||||
module NanoKVM-Server
|
module NanoKVM-Server
|
||||||
|
|
||||||
go 1.22.1
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.23.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
@@ -15,7 +17,7 @@ require (
|
|||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
github.com/unrolled/secure v1.15.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
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,9 +73,9 @@ require (
|
|||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/net v0.33.0 // indirect
|
golang.org/x/net v0.37.0 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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/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 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
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 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
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 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
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 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
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=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -50,20 +50,20 @@ void set_venc_auto_recyc(uint8_t _enable);
|
|||||||
-5: Retrieving image, please wait
|
-5: Retrieving image, please wait
|
||||||
-4: Modifying image resolution, please wait
|
-4: Modifying image resolution, please wait
|
||||||
-3: img buffer full
|
-3: img buffer full
|
||||||
-2: VENC Errorl
|
-2: VENC Error
|
||||||
-1: No images were acquired
|
-1: No images were acquired
|
||||||
0: Acquire MJPEG encoded images
|
0: Acquire MJPEG encoded images
|
||||||
1: Acquire H264 encoded images(SPS)
|
1: Acquire H264 encoded images(SPS)[Deprecated]
|
||||||
2: Acquire H264 encoded images(PPS)
|
2: Acquire H264 encoded images(PPS)[Deprecated]
|
||||||
3: Acquire H264 encoded images(I)
|
3: Acquire H264 encoded images(I)
|
||||||
4: Acquire H264 encoded images(P)
|
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_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);
|
int free_kvmv_data(uint8_t ** _pp_kvm_data);
|
||||||
void free_all_kvmv_data();
|
void free_all_kvmv_data();
|
||||||
void set_h264_gop(uint8_t _gop);
|
void set_h264_gop(uint8_t _gop);
|
||||||
|
void set_frame_detact(uint8_t _frame_detact);
|
||||||
void kvmv_deinit();
|
void kvmv_deinit();
|
||||||
uint8_t kvmv_hdmi_control(uint8_t _en);
|
uint8_t kvmv_hdmi_control(uint8_t _en);
|
||||||
|
|
||||||
@@ -71,4 +71,4 @@ uint8_t kvmv_hdmi_control(uint8_t _en);
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif // KVM_VISION_H_
|
#endif // KVM_VISION_H_
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func (f *formatter) Format(entry *logrus.Entry) ([]byte, error) {
|
|||||||
buffer = &bytes.Buffer{}
|
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() {
|
if entry.HasCaller() {
|
||||||
fileName := filepath.Base(entry.Caller.File)
|
fileName := filepath.Base(entry.Caller.File)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package proto
|
package proto
|
||||||
|
|
||||||
type GetFrameDetectRsp struct {
|
type UpdateFrameDetectReq struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateFrameDetectRsp struct {
|
type StopFrameDetectReq struct {
|
||||||
Enabled bool `json:"enabled"`
|
Duration int `validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ func streamRouter(r *gin.Engine) {
|
|||||||
api := r.Group("/api").Use(middleware.CheckToken())
|
api := r.Group("/api").Use(middleware.CheckToken())
|
||||||
|
|
||||||
api.GET("/stream/mjpeg", mjpeg.Connect) // mjpeg stream
|
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
|
||||||
api.POST("/stream/mjpeg/detect", mjpeg.UpdateFrameDetect) // update frame detect state
|
|
||||||
api.POST("/stream/mjpeg/detect/stop", mjpeg.StopFrameDetect) // temporary stop frame detect
|
api.POST("/stream/mjpeg/detect/stop", mjpeg.StopFrameDetect) // temporary stop frame detect
|
||||||
|
|
||||||
api.GET("/stream/h264", h264.Connect) // h264 stream
|
api.GET("/stream/h264", h264.Connect) // h264 stream
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package h264
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
@@ -9,8 +10,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
ws *websocket.Conn
|
ws *websocket.Conn
|
||||||
pc *webrtc.PeerConnection
|
pc *webrtc.PeerConnection
|
||||||
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
@@ -36,9 +38,7 @@ func (c *Client) addTrack() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mutex.Lock()
|
|
||||||
trackMap[c.ws] = videoTrack
|
trackMap[c.ws] = videoTrack
|
||||||
mutex.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// register callback events
|
// register callback events
|
||||||
@@ -61,13 +61,9 @@ func (c *Client) register() {
|
|||||||
// ICE connection state has changed
|
// ICE connection state has changed
|
||||||
c.pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
c.pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
||||||
if state == webrtc.ICEConnectionStateConnected && !isSending {
|
if state == webrtc.ICEConnectionStateConnected && !isSending {
|
||||||
mutex.Lock()
|
// start sending h264 data
|
||||||
if !isSending {
|
go send()
|
||||||
// start sending h264 data
|
isSending = true
|
||||||
go send()
|
|
||||||
isSending = true
|
|
||||||
}
|
|
||||||
mutex.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("ice connection state has changed to %s", state.String())
|
log.Debugf("ice connection state has changed to %s", state.String())
|
||||||
@@ -81,13 +77,11 @@ func (c *Client) readMessage() {
|
|||||||
for {
|
for {
|
||||||
_, raw, err := c.ws.ReadMessage()
|
_, raw, err := c.ws.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mutex.Lock()
|
|
||||||
delete(trackMap, c.ws)
|
delete(trackMap, c.ws)
|
||||||
if len(trackMap) == 0 && isSending {
|
if isSending && len(trackMap) == 0 {
|
||||||
// stop sending when all websocket connections are closed
|
// stop sending when all websocket connections are closed
|
||||||
exitSig <- true
|
isSending = false
|
||||||
}
|
}
|
||||||
mutex.Unlock()
|
|
||||||
|
|
||||||
log.Debugf("failed to read message: %s", err)
|
log.Debugf("failed to read message: %s", err)
|
||||||
return
|
return
|
||||||
@@ -144,9 +138,6 @@ func (c *Client) readMessage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
case "heartbeat":
|
|
||||||
_ = c.sendMessage("heartbeat", "")
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
log.Debugf("unhandled message event: %s", message.Event)
|
log.Debugf("unhandled message event: %s", message.Event)
|
||||||
}
|
}
|
||||||
@@ -155,8 +146,8 @@ func (c *Client) readMessage() {
|
|||||||
|
|
||||||
// send websocket message
|
// send websocket message
|
||||||
func (c *Client) sendMessage(event string, data string) error {
|
func (c *Client) sendMessage(event string, data string) error {
|
||||||
mutex.RLock()
|
c.mutex.Lock()
|
||||||
defer mutex.RUnlock()
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
message := &Message{
|
message := &Message{
|
||||||
Event: event,
|
Event: event,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"NanoKVM-Server/config"
|
"NanoKVM-Server/config"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
@@ -18,9 +19,7 @@ var (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
trackMap = make(map[*websocket.Conn]*webrtc.TrackLocalStaticSample)
|
trackMap = make(map[*websocket.Conn]*webrtc.TrackLocalStaticSample)
|
||||||
mutex = sync.RWMutex{}
|
|
||||||
isSending = false
|
isSending = false
|
||||||
exitSig = make(chan bool, 1)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Connect(c *gin.Context) {
|
func Connect(c *gin.Context) {
|
||||||
@@ -35,6 +34,9 @@ func Connect(c *gin.Context) {
|
|||||||
log.Debugf("h264 websocket disconnected")
|
log.Debugf("h264 websocket disconnected")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
var zeroTime time.Time
|
||||||
|
_ = wsConn.SetReadDeadline(zeroTime)
|
||||||
|
|
||||||
conf := config.GetInstance()
|
conf := config.GetInstance()
|
||||||
|
|
||||||
var iceServers []webrtc.ICEServer
|
var iceServers []webrtc.ICEServer
|
||||||
@@ -67,8 +69,9 @@ func Connect(c *gin.Context) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
client := &Client{
|
client := &Client{
|
||||||
ws: wsConn,
|
ws: wsConn,
|
||||||
pc: peerConn,
|
pc: peerConn,
|
||||||
|
mutex: sync.Mutex{},
|
||||||
}
|
}
|
||||||
|
|
||||||
client.addTrack()
|
client.addTrack()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package h264
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"NanoKVM-Server/common"
|
"NanoKVM-Server/common"
|
||||||
"NanoKVM-Server/service/stream"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/webrtc/v4/pkg/media"
|
"github.com/pion/webrtc/v4/pkg/media"
|
||||||
@@ -18,63 +17,45 @@ func send() {
|
|||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
vision := common.GetKvmVision()
|
vision := common.GetKvmVision()
|
||||||
for {
|
for range ticker.C {
|
||||||
select {
|
if !isSending && len(trackMap) == 0 {
|
||||||
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()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeSample(data []byte, duration time.Duration) {
|
height := screen.Height
|
||||||
sample := media.Sample{
|
width, ok := common.ResolutionMap[height]
|
||||||
Data: data,
|
if !ok {
|
||||||
Duration: duration,
|
width = 0
|
||||||
}
|
height = 0
|
||||||
|
}
|
||||||
|
|
||||||
mutex.RLock()
|
bitRate := screen.BitRate
|
||||||
defer mutex.RUnlock()
|
if _, ok := common.BitRateMap[bitRate]; !ok {
|
||||||
|
bitRate = 3000
|
||||||
|
}
|
||||||
|
|
||||||
for _, track := range trackMap {
|
data, result := vision.ReadH264(width, height, bitRate)
|
||||||
if err := track.WriteSample(sample); err != nil {
|
if result < 0 {
|
||||||
log.Errorf("failed to send h264 data: %s", err)
|
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))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,132 +1,55 @@
|
|||||||
package mjpeg
|
package mjpeg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"NanoKVM-Server/common"
|
||||||
"os"
|
"NanoKVM-Server/proto"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"NanoKVM-Server/proto"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const FrameDetectInterval uint8 = 60
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateFrameDetect(c *gin.Context) {
|
func UpdateFrameDetect(c *gin.Context) {
|
||||||
|
var req proto.UpdateFrameDetectReq
|
||||||
var rsp proto.Response
|
var rsp proto.Response
|
||||||
|
|
||||||
isEnabled, err := isFrameDetectEnabled()
|
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||||
if err != nil {
|
rsp.ErrRsp(c, -1, "invalid parameters")
|
||||||
rsp.ErrRsp(c, -1, "unknown frame status")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if isEnabled {
|
var frame uint8 = 0
|
||||||
_ = os.Remove(frameDetect)
|
if req.Enabled {
|
||||||
_ = os.Remove(frameDetectTmp)
|
frame = FrameDetectInterval
|
||||||
} 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()
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isEnabled, err = isFrameDetectEnabled()
|
common.GetKvmVision().SetFrameDetect(frame)
|
||||||
if err != nil {
|
|
||||||
rsp.ErrRsp(c, -4, "unknown frame status")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rsp.OkRspWithData(c, &UpdateFrameDetectRsp{
|
rsp.OkRsp(c)
|
||||||
Enabled: isEnabled,
|
log.Debugf("update frame detect: %t", req.Enabled)
|
||||||
})
|
|
||||||
log.Debugf("update frame detect success, enabled: %t", isEnabled)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func StopFrameDetect(c *gin.Context) {
|
func StopFrameDetect(c *gin.Context) {
|
||||||
|
var req proto.StopFrameDetectReq
|
||||||
var rsp proto.Response
|
var rsp proto.Response
|
||||||
|
|
||||||
exist, err := isFileExist(frameDetect)
|
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||||
if err != nil {
|
rsp.ErrRsp(c, -1, "invalid parameters")
|
||||||
rsp.ErrRsp(c, -1, "unknown frame status")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exist {
|
duration := 10 * time.Second
|
||||||
rsp.OkRsp(c)
|
if req.Duration > 0 {
|
||||||
return
|
duration = time.Duration(req.Duration) * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.Rename(frameDetect, frameDetectTmp)
|
vision := common.GetKvmVision()
|
||||||
if err != nil {
|
|
||||||
rsp.ErrRsp(c, -2, "stop operation failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
vision.SetFrameDetect(0)
|
||||||
time.Sleep(20 * time.Second)
|
time.Sleep(duration)
|
||||||
_ = os.Rename(frameDetectTmp, frameDetect)
|
vision.SetFrameDetect(FrameDetectInterval)
|
||||||
log.Debug("frame detect started")
|
|
||||||
}()
|
|
||||||
|
|
||||||
rsp.OkRsp(c)
|
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
chanMap = make(map[*gin.Context]chan struct{})
|
chanMap = make(map[*gin.Context]bool)
|
||||||
mutex = sync.RWMutex{}
|
mutex = sync.Mutex{}
|
||||||
exitSig = make(chan bool, 1)
|
exitSig = make(chan bool, 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,13 +23,13 @@ func Connect(c *gin.Context) {
|
|||||||
c.Header("Pragma", "no-cache")
|
c.Header("Pragma", "no-cache")
|
||||||
|
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
chanMap[c] = make(chan struct{}, 1)
|
chanMap[c] = true
|
||||||
if len(chanMap) == 1 {
|
if len(chanMap) == 1 {
|
||||||
go send()
|
go send()
|
||||||
}
|
}
|
||||||
mutex.Unlock()
|
mutex.Unlock()
|
||||||
|
|
||||||
<-chanMap[c]
|
<-c.Request.Context().Done()
|
||||||
|
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
delete(chanMap, c)
|
delete(chanMap, c)
|
||||||
@@ -63,14 +63,13 @@ func send() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data, result := vision.ReadMjpeg(width, height, quality)
|
data, result := vision.ReadMjpeg(width, height, quality)
|
||||||
if result < 0 {
|
if result < 0 || result == 5 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for c, ch := range chanMap {
|
for c := range chanMap {
|
||||||
if err := write(c, data); err != nil {
|
if err := write(c, data); err != nil {
|
||||||
log.Debugf("failed to write mjpeg data: %s", err)
|
log.Debugf("failed to write mjpeg data: %s", err)
|
||||||
close(ch)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Debugf("send mjpeg data: %d", len(data))
|
log.Debugf("send mjpeg data: %d", len(data))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"NanoKVM-Server/common"
|
"NanoKVM-Server/common"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -22,30 +23,34 @@ func (s *Service) SetScreen(c *gin.Context) {
|
|||||||
var req proto.SetScreenReq
|
var req proto.SetScreenReq
|
||||||
var rsp proto.Response
|
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")
|
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file, ok := screenFileMap[req.Type]
|
switch req.Type {
|
||||||
if !ok {
|
case "type":
|
||||||
rsp.ErrRsp(c, -2, "invalid arguments")
|
data := "h264"
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data := fmt.Sprintf("%d", req.Value)
|
|
||||||
if req.Type == "type" {
|
|
||||||
if req.Value == 0 {
|
if req.Value == 0 {
|
||||||
data = "mjpeg"
|
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 {
|
if err != nil {
|
||||||
log.Errorf("write kvm %s failed: %s", file, err)
|
rsp.ErrRsp(c, -2, "update screen failed")
|
||||||
rsp.ErrRsp(c, -3, "update screen failed")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,3 +59,18 @@ func (s *Service) SetScreen(c *gin.Context) {
|
|||||||
log.Debugf("update screen: %+v", req)
|
log.Debugf("update screen: %+v", req)
|
||||||
rsp.OkRsp(c)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { http } from '@/lib/http.ts';
|
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
|
// enable/disable frame detect
|
||||||
export function updateFrameDetect() {
|
export function updateFrameDetect(enabled: boolean) {
|
||||||
return http.post('/api/stream/mjpeg/detect');
|
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)
|
// pause frame detect for a while (prevent a black screen when opening the page for the first time)
|
||||||
export function stopFrameDetect() {
|
export function stopFrameDetect(duration: number) {
|
||||||
return http.post('/api/stream/mjpeg/detect/stop');
|
const data = {
|
||||||
|
duration
|
||||||
|
};
|
||||||
|
return http.post('/api/stream/mjpeg/detect/stop', data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ const VIDEO_MODE_KEY = 'nano-kvm-vide-mode';
|
|||||||
const WEB_RESOLUTION_KEY = 'nano-kvm-web-resolution';
|
const WEB_RESOLUTION_KEY = 'nano-kvm-web-resolution';
|
||||||
const FPS_KEY = 'nano-kvm-fps';
|
const FPS_KEY = 'nano-kvm-fps';
|
||||||
const QUALITY_KEY = 'nano-kvm-quality';
|
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_STYLE_KEY = 'nano-kvm-mouse-style';
|
||||||
const MOUSE_MODE_KEY = 'nano-kvm-mouse-mode';
|
const MOUSE_MODE_KEY = 'nano-kvm-mouse-mode';
|
||||||
const SKIP_UPDATE_KEY = 'nano-kvm-check-update';
|
const SKIP_UPDATE_KEY = 'nano-kvm-check-update';
|
||||||
@@ -92,6 +94,24 @@ export function setQuality(quality: number) {
|
|||||||
localStorage.setItem(QUALITY_KEY, String(quality));
|
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() {
|
export function getMouseStyle() {
|
||||||
return localStorage.getItem(MOUSE_STYLE_KEY);
|
return localStorage.getItem(MOUSE_STYLE_KEY);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,39 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { LoaderCircleIcon, Tally4Icon, Tally5Icon } from 'lucide-react';
|
import { LoaderCircleIcon, Tally4Icon, Tally5Icon } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 = () => {
|
export const FrameDetect = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isEnabled, setIsEnabled] = useState(false);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isEnabled, setIsEnabled] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getFrameDetect().then((rsp) => {
|
const enabled = ls.getFrameDetect();
|
||||||
if (rsp.code === 0) {
|
if (enabled) {
|
||||||
setIsEnabled(rsp.data.enabled);
|
setIsEnabled(true);
|
||||||
}
|
} else {
|
||||||
});
|
api.updateFrameDetect(false);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
updateFrameDetect()
|
const enabled = !isEnabled;
|
||||||
|
|
||||||
|
api
|
||||||
|
.updateFrameDetect(enabled)
|
||||||
.then((rsp) => {
|
.then((rsp) => {
|
||||||
if (rsp.code === 0) {
|
if (rsp.code === 0) {
|
||||||
setIsEnabled(rsp.data.enabled);
|
setIsEnabled(enabled);
|
||||||
|
ls.setFrameDetect(enabled);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -41,18 +49,20 @@ export const FrameDetect = () => {
|
|||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<LoaderCircleIcon className="animate-spin" size={18} />
|
<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" />
|
{isEnabled ? <Tally4Icon color="#22c55e" size={18} /> : <Tally5Icon size={18} />}
|
||||||
<Tally4Icon color="#22c55e" size={18} className="hidden group-hover:block" />
|
|
||||||
</div>
|
<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>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
57
web/src/pages/desktop/menu/screen/gop.tsx
Normal file
57
web/src/pages/desktop/menu/screen/gop.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -11,6 +11,7 @@ import { MenuItem } from '@/components/menu-item.tsx';
|
|||||||
import { BitRateMap, QualityMap } from './constants.ts';
|
import { BitRateMap, QualityMap } from './constants.ts';
|
||||||
import { Fps } from './fps';
|
import { Fps } from './fps';
|
||||||
import { FrameDetect } from './frame-detect';
|
import { FrameDetect } from './frame-detect';
|
||||||
|
import { Gop } from './gop.tsx';
|
||||||
import { Quality } from './quality';
|
import { Quality } from './quality';
|
||||||
import { Reset } from './reset.tsx';
|
import { Reset } from './reset.tsx';
|
||||||
import { Resolution } from './resolution';
|
import { Resolution } from './resolution';
|
||||||
@@ -24,37 +25,47 @@ export const Screen = () => {
|
|||||||
|
|
||||||
const [fps, setFps] = useState(30);
|
const [fps, setFps] = useState(30);
|
||||||
const [quality, setQuality] = useState(2);
|
const [quality, setQuality] = useState(2);
|
||||||
|
const [gop, setGop] = useState(30);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateScreen('type', videoMode === 'mjpeg' ? 0 : 1);
|
updateScreen('type', videoMode === 'mjpeg' ? 0 : 1);
|
||||||
updateScreen('resolution', resolution!.height);
|
updateScreen('resolution', resolution!.height);
|
||||||
updateQuality();
|
updateQuality();
|
||||||
updateFps();
|
updateFps();
|
||||||
|
updateGop();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function updateQuality() {
|
async function updateQuality() {
|
||||||
const cookieQuality = ls.getQuality();
|
const cookieQuality = ls.getQuality();
|
||||||
if (!cookieQuality) return;
|
if (!cookieQuality) return;
|
||||||
|
|
||||||
const key = cookieQuality >= 1 && cookieQuality <= 4 ? cookieQuality : 2;
|
const key = cookieQuality >= 1 && cookieQuality <= 4 ? cookieQuality : 2;
|
||||||
const value = videoMode === 'mjpeg' ? QualityMap.get(key)! : BitRateMap.get(key)!;
|
const value = videoMode === 'mjpeg' ? QualityMap.get(key)! : BitRateMap.get(key)!;
|
||||||
|
|
||||||
updateScreen('quality', value).then((rsp) => {
|
const rsp = await updateScreen('quality', value);
|
||||||
if (rsp.code === 0) {
|
if (rsp.code === 0) {
|
||||||
setQuality(key);
|
setQuality(key);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFps() {
|
async function updateFps() {
|
||||||
const cookieFps = ls.getFps();
|
const cookieFps = ls.getFps();
|
||||||
if (!cookieFps) return;
|
if (!cookieFps) return;
|
||||||
|
|
||||||
updateScreen('fps', cookieFps).then((rsp) => {
|
const rsp = await updateScreen('fps', cookieFps);
|
||||||
if (rsp.code === 0) {
|
if (rsp.code === 0) {
|
||||||
setFps(cookieFps);
|
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 = (
|
const content = (
|
||||||
@@ -63,6 +74,7 @@ export const Screen = () => {
|
|||||||
<Resolution />
|
<Resolution />
|
||||||
<Quality quality={quality} setQuality={setQuality} />
|
<Quality quality={quality} setQuality={setQuality} />
|
||||||
<Fps fps={fps} setFps={setFps} />
|
<Fps fps={fps} setFps={setFps} />
|
||||||
|
{videoMode === 'h264' && <Gop gop={gop} setGop={setGop} />}
|
||||||
{videoMode === 'mjpeg' && <FrameDetect />}
|
{videoMode === 'mjpeg' && <FrameDetect />}
|
||||||
<Reset />
|
<Reset />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ export const Resolution = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span className="flex w-[32px]">{res.width}</span>
|
<span>{res.width}</span>
|
||||||
<span>x</span>
|
<span className="px-1">x</span>
|
||||||
<span className="w-[36px]">{res.height}</span>
|
<span>{res.height}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export const H264 = () => {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let heartbeatTimer: any;
|
|
||||||
const videoElement = document.getElementById('screen') as HTMLVideoElement;
|
const videoElement = document.getElementById('screen') as HTMLVideoElement;
|
||||||
|
|
||||||
const url = `${getBaseUrl('ws')}/api/stream/h264`;
|
const url = `${getBaseUrl('ws')}/api/stream/h264`;
|
||||||
@@ -52,10 +51,6 @@ export const H264 = () => {
|
|||||||
ws.send(JSON.stringify({ event: 'offer', data: JSON.stringify(offer) }));
|
ws.send(JSON.stringify({ event: 'offer', data: JSON.stringify(offer) }));
|
||||||
})
|
})
|
||||||
.catch(console.log);
|
.catch(console.log);
|
||||||
|
|
||||||
heartbeatTimer = setInterval(() => {
|
|
||||||
ws.send(JSON.stringify({ event: 'heartbeat', data: '' }));
|
|
||||||
}, 60 * 1000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
@@ -75,9 +70,6 @@ export const H264 = () => {
|
|||||||
pc.addIceCandidate(data).catch(console.log);
|
pc.addIceCandidate(data).catch(console.log);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'heartbeat':
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('unhandled event: ', msg.event);
|
console.log('unhandled event: ', msg.event);
|
||||||
}
|
}
|
||||||
@@ -91,9 +83,6 @@ export const H264 = () => {
|
|||||||
}, 15 * 1000);
|
}, 15 * 1000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (heartbeatTimer) {
|
|
||||||
clearInterval(heartbeatTimer);
|
|
||||||
}
|
|
||||||
ws.close();
|
ws.close();
|
||||||
pc.close();
|
pc.close();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useAtomValue } from 'jotai';
|
|||||||
|
|
||||||
import MonitorXIcon from '@/assets/images/monitor-x.svg';
|
import MonitorXIcon from '@/assets/images/monitor-x.svg';
|
||||||
import { stopFrameDetect } from '@/api/stream.ts';
|
import { stopFrameDetect } from '@/api/stream.ts';
|
||||||
|
import { getFrameDetect } from '@/lib/localstorage.ts';
|
||||||
import { getBaseUrl } from '@/lib/service.ts';
|
import { getBaseUrl } from '@/lib/service.ts';
|
||||||
import { mouseStyleAtom } from '@/jotai/mouse.ts';
|
import { mouseStyleAtom } from '@/jotai/mouse.ts';
|
||||||
import { resolutionAtom } from '@/jotai/screen.ts';
|
import { resolutionAtom } from '@/jotai/screen.ts';
|
||||||
@@ -15,7 +16,10 @@ export const Mjpeg = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// stop frame detect for a while
|
// stop frame detect for a while
|
||||||
stopFrameDetect();
|
const enabled = getFrameDetect();
|
||||||
|
if (enabled) {
|
||||||
|
stopFrameDetect(10);
|
||||||
|
}
|
||||||
}, [resolution]);
|
}, [resolution]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user