diff --git a/backend/controller/rdp.go b/backend/controller/rdp.go index b493793..1aeaba5 100644 --- a/backend/controller/rdp.go +++ b/backend/controller/rdp.go @@ -658,7 +658,7 @@ func isMostlyBlackRGBA(img *image.RGBA, threshold float64) bool { func writeRDPFrame(mgr *wshub.WSManager, conn *websocket.Conn, frame rdpengine.ServerFrame) error { mgr.SetWriteDeadline(conn) if frame.Type == "bitmap" && len(frame.RGBA) > 0 { - return conn.WriteMessage(websocket.BinaryMessage, encodeRDPBitmapBinary(frame)) + return writeCompressedRDPBitmap(conn, frame) } return conn.WriteJSON(frame) } diff --git a/backend/controller/rdp_live_frame.go b/backend/controller/rdp_live_frame.go new file mode 100644 index 0000000..3759aba --- /dev/null +++ b/backend/controller/rdp_live_frame.go @@ -0,0 +1,48 @@ +package controller + +import ( + "bytes" + "encoding/base64" + "fmt" + "image" + "image/png" + + rdpengine "ezops/pkg/rdp" + + "github.com/gorilla/websocket" +) + +// encodeRDPBitmapPNG 将实时 RGBA 脏矩形压缩为 PNG,避免原始整帧占满低带宽 WebSocket。 // Logan.Li +func encodeRDPBitmapPNG(frame rdpengine.ServerFrame) (rdpengine.ServerFrame, error) { + if frame.Type != "bitmap" || len(frame.RGBA) == 0 { + return frame, nil + } + if frame.Width <= 0 || frame.Height <= 0 { + return rdpengine.ServerFrame{}, fmt.Errorf("RDP 位图尺寸无效: %dx%d", frame.Width, frame.Height) + } + required := frame.Width * frame.Height * 4 + if len(frame.RGBA) < required { + return rdpengine.ServerFrame{}, fmt.Errorf("RDP 位图数据不足: got=%d want=%d", len(frame.RGBA), required) + } + + img := image.NewRGBA(image.Rect(0, 0, frame.Width, frame.Height)) + copy(img.Pix, frame.RGBA[:required]) + var buf bytes.Buffer + encoder := png.Encoder{CompressionLevel: png.BestSpeed} + if err := encoder.Encode(&buf, img); err != nil { + return rdpengine.ServerFrame{}, fmt.Errorf("压缩 RDP 位图失败: %w", err) + } + + frame.Image = "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()) + frame.RGBA = nil + return frame, nil +} + +// writeCompressedRDPBitmap 发送浏览器已支持的 JSON PNG 帧,兼容现有前端绘制逻辑。 // Logan.Li +func writeCompressedRDPBitmap(conn *websocket.Conn, frame rdpengine.ServerFrame) error { + encoded, err := encodeRDPBitmapPNG(frame) + if err != nil { + return err + } + return conn.WriteJSON(encoded) +} diff --git a/backend/controller/rdp_live_frame_test.go b/backend/controller/rdp_live_frame_test.go new file mode 100644 index 0000000..d6480f2 --- /dev/null +++ b/backend/controller/rdp_live_frame_test.go @@ -0,0 +1,66 @@ +package controller + +import ( + "bytes" + "encoding/base64" + "image/png" + "strings" + "testing" + + rdpengine "ezops/pkg/rdp" +) + +func TestEncodeRDPBitmapPNGCompressesFrame(t *testing.T) { + const width = 900 + const height = 540 + rgba := make([]byte, width*height*4) + for i := 0; i < len(rgba); i += 4 { + rgba[i+3] = 255 + } + + encoded, err := encodeRDPBitmapPNG(rdpengine.ServerFrame{ + Type: "bitmap", + X: 12, + Y: 34, + Width: width, + Height: height, + RGBA: rgba, + }) + if err != nil { + t.Fatalf("压缩 RDP 位图失败: %v", err) + } + if len(encoded.RGBA) != 0 { + t.Fatalf("压缩后仍携带原始 RGBA: %d", len(encoded.RGBA)) + } + const prefix = "data:image/png;base64," + if !strings.HasPrefix(encoded.Image, prefix) { + t.Fatalf("压缩帧格式错误: %q", encoded.Image[:min(len(encoded.Image), len(prefix))]) + } + pngData, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(encoded.Image, prefix)) + if err != nil { + t.Fatalf("解码 PNG 数据失败: %v", err) + } + img, err := png.Decode(bytes.NewReader(pngData)) + if err != nil { + t.Fatalf("解析 PNG 数据失败: %v", err) + } + if img.Bounds().Dx() != width || img.Bounds().Dy() != height { + t.Fatalf("PNG 尺寸错误: got=%dx%d", img.Bounds().Dx(), img.Bounds().Dy()) + } + if len(encoded.Image) >= len(rgba)/4 { + t.Fatalf("PNG 压缩率不足: encoded=%d raw=%d", len(encoded.Image), len(rgba)) + } + t.Logf("RDP 帧压缩: raw=%d encoded=%d", len(rgba), len(encoded.Image)) +} + +func TestEncodeRDPBitmapPNGRejectsShortPayload(t *testing.T) { + _, err := encodeRDPBitmapPNG(rdpengine.ServerFrame{ + Type: "bitmap", + Width: 10, + Height: 10, + RGBA: make([]byte, 8), + }) + if err == nil { + t.Fatal("短位图数据应返回错误") + } +}