fix: 压缩RDP实时画面传输
- 将原始RGBA位图压缩为PNG后通过WebSocket发送 - 降低低带宽代理链路下的RDP首帧延迟 - 增加位图压缩与异常数据测试
This commit is contained in:
1 parent
699560a270
commit
7a1680884d
3 files changed
+115
-1
No files matched your search
@@ -658,7 +658,7 @@ func isMostlyBlackRGBA(img *image.RGBA, threshold float64) bool {
|
|||||||
func writeRDPFrame(mgr *wshub.WSManager, conn *websocket.Conn, frame rdpengine.ServerFrame) error {
|
func writeRDPFrame(mgr *wshub.WSManager, conn *websocket.Conn, frame rdpengine.ServerFrame) error {
|
||||||
mgr.SetWriteDeadline(conn)
|
mgr.SetWriteDeadline(conn)
|
||||||
if frame.Type == "bitmap" && len(frame.RGBA) > 0 {
|
if frame.Type == "bitmap" && len(frame.RGBA) > 0 {
|
||||||
return conn.WriteMessage(websocket.BinaryMessage, encodeRDPBitmapBinary(frame))
|
return writeCompressedRDPBitmap(conn, frame)
|
||||||
}
|
}
|
||||||
return conn.WriteJSON(frame)
|
return conn.WriteJSON(frame)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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("短位图数据应返回错误")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user