可以使用的版本
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
|
*.log
|
||||||
# Local History for Visual Studio Code
|
# Local History for Visual Studio Code
|
||||||
.history/
|
.history/
|
||||||
*.app
|
*.app
|
||||||
|
|||||||
Vendored
+1
-2
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"python-envs.defaultEnvManager": "ms-python.python:conda",
|
"python-envs.defaultEnvManager": "ms-python.python:conda",
|
||||||
"python-envs.defaultPackageManager": "ms-python.python:conda",
|
"python-envs.defaultPackageManager": "ms-python.python:conda"
|
||||||
"python-envs.pythonProjects": []
|
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
+39
-13
@@ -267,18 +267,39 @@ def mono_to_png(data: bytes, width: int, height: int) -> Optional[bytes]:
|
|||||||
height=height,
|
height=height,
|
||||||
len_=len(data),
|
len_=len(data),
|
||||||
)
|
)
|
||||||
img = Image.new("1", (width, height)) # 创建 1bpp 黑白图像
|
expected_bits = (width * height + 7) // 8 # 1bpp 打包长度
|
||||||
pix = img.load() # 获取像素访问对象
|
expected_bytes = width * height # 每像素 1 字节(0/1或0-255)长度
|
||||||
'''
|
|
||||||
for y in range(height):
|
if len(data) == expected_bits:
|
||||||
for x in range(width):
|
# 1bpp:每字节 8 像素,高位在前
|
||||||
bi = (y * width + x) // 8 # 字节索引:每 8 像素一字节
|
logger.debug("[mono_to_png] 使用 1bpp 打包解码")
|
||||||
bit = 7 - (x % 8) # 位索引:高位在前
|
img = Image.new("1", (width, height))
|
||||||
pix[x, y] = 0 if (bi < len(data) and (data[bi] >> bit) & 1) else 255 # 1→白 0→黑
|
pix = img.load()
|
||||||
'''
|
for y in range(height):
|
||||||
for y in range(height):
|
row_base = y * width
|
||||||
for x in range(width):
|
for x in range(width):
|
||||||
pix[x, y] = data[y * width + x]
|
byte_i = (row_base + x) // 8
|
||||||
|
bit_i = 7 - (x % 8)
|
||||||
|
bit = (data[byte_i] >> bit_i) & 1
|
||||||
|
pix[x, y] = 255 if bit else 0
|
||||||
|
elif len(data) == expected_bytes:
|
||||||
|
# 8bit:每字节直接对应一个像素,非零当作白
|
||||||
|
logger.debug("[mono_to_png] 使用每像素 1 字节解码")
|
||||||
|
img = Image.new("1", (width, height))
|
||||||
|
pix = img.load()
|
||||||
|
for y in range(height):
|
||||||
|
row_base = y * width
|
||||||
|
for x in range(width):
|
||||||
|
pix[x, y] = 255 if data[row_base + x] else 0
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"[mono_to_png] payload 长度不匹配,丢弃该帧: data_len={len_}, expected_bits={bits}, expected_bytes={bytes}",
|
||||||
|
len_=len(data),
|
||||||
|
bits=expected_bits,
|
||||||
|
bytes=expected_bytes,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
buf = io.BytesIO() # 内存缓冲区
|
buf = io.BytesIO() # 内存缓冲区
|
||||||
img.save(buf, format="PNG") # 保存为 PNG 格式
|
img.save(buf, format="PNG") # 保存为 PNG 格式
|
||||||
png_bytes = buf.getvalue()
|
png_bytes = buf.getvalue()
|
||||||
@@ -329,7 +350,7 @@ def _screen_refresh_loop():
|
|||||||
# 当前协议未返回宽高,这里固定为 160x160,可根据实际情况调整
|
# 当前协议未返回宽高,这里固定为 160x160,可根据实际情况调整
|
||||||
w, h = 160, 160
|
w, h = 160, 160
|
||||||
png = mono_to_png(data, w, h) # 转为 PNG
|
png = mono_to_png(data, w, h) # 转为 PNG
|
||||||
if png:
|
if png and isinstance(png, (bytes, bytearray)) and png.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||||
b64 = base64.b64encode(png).decode("ascii") # base64 编码便于 JSON 传输
|
b64 = base64.b64encode(png).decode("ascii") # base64 编码便于 JSON 传输
|
||||||
for sid in list(_screen_clients): # 复制列表避免迭代时修改
|
for sid in list(_screen_clients): # 复制列表避免迭代时修改
|
||||||
try:
|
try:
|
||||||
@@ -344,6 +365,11 @@ def _screen_refresh_loop():
|
|||||||
sid=sid,
|
sid=sid,
|
||||||
err=e,
|
err=e,
|
||||||
)
|
)
|
||||||
|
elif png:
|
||||||
|
logger.warning(
|
||||||
|
"[_screen_refresh_loop] 生成的 PNG 无效或签名错误,丢弃该帧 png_len={len_}",
|
||||||
|
len_=len(png),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# 拉取失败,累计连续失败次数
|
# 拉取失败,累计连续失败次数
|
||||||
consecutive_failures += 1
|
consecutive_failures += 1
|
||||||
|
|||||||
+32
-9
@@ -392,8 +392,12 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 服务端推送屏幕:data.png 为 base64 编码的 PNG,解码后绘制到 canvas
|
// 服务端推送屏幕:data.png 为 base64 编码的 PNG,解码后绘制到 canvas
|
||||||
|
const scale = 2; // 放大倍数(160x160 -> 320x320)
|
||||||
|
let canvasInitialized = false;
|
||||||
|
let latestFrameId = 0; // 用于丢弃旧帧,避免异步解码乱序导致闪烁
|
||||||
socket.on('screen', (data) => {
|
socket.on('screen', (data) => {
|
||||||
if (!data.png) return;
|
if (!data.png) return;
|
||||||
|
const frameId = ++latestFrameId;
|
||||||
// 无 base64 数据则跳过
|
// 无 base64 数据则跳过
|
||||||
const binary = atob(data.png);
|
const binary = atob(data.png);
|
||||||
// base64 解码为二进制字符串
|
// base64 解码为二进制字符串
|
||||||
@@ -406,21 +410,38 @@
|
|||||||
// 创建临时 URL
|
// 创建临时 URL
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
// 图片加载完成后,放大一倍显示(160x160 -> 320x320)
|
// 图片加载完成后:若该帧不是最新帧则直接丢弃
|
||||||
const scale = 2;
|
if (frameId !== latestFrameId) {
|
||||||
canvas.width = img.width * scale;
|
URL.revokeObjectURL(url);
|
||||||
canvas.height = img.height * scale;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首帧初始化 canvas 尺寸(只做一次,避免反复重设导致清空闪烁)
|
||||||
|
const targetW = img.width * scale;
|
||||||
|
const targetH = img.height * scale;
|
||||||
|
if (!canvasInitialized) {
|
||||||
|
canvas.width = targetW;
|
||||||
|
canvas.height = targetH;
|
||||||
|
canvasInitialized = true;
|
||||||
|
canvas.style.display = 'block';
|
||||||
|
placeholder.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
// 关闭平滑,像素清晰放大
|
// 关闭平滑:像素清晰放大
|
||||||
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height);
|
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height);
|
||||||
// 绘制到 canvas(2 倍尺寸)
|
|
||||||
canvas.style.display = 'block';
|
|
||||||
placeholder.style.display = 'none';
|
|
||||||
// 显示 canvas,隐藏占位符
|
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
// 释放临时 URL
|
// 释放临时 URL
|
||||||
};
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
// 发生错误时同样释放临时 URL
|
||||||
|
if (frameId === latestFrameId) {
|
||||||
|
// 不弹窗,避免影响观察;必要时可在这里加日志
|
||||||
|
}
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
img.src = url;
|
img.src = url;
|
||||||
// 触发加载
|
// 触发加载
|
||||||
});
|
});
|
||||||
@@ -465,6 +486,8 @@
|
|||||||
placeholder.style.display = '';
|
placeholder.style.display = '';
|
||||||
placeholder.textContent = '点击「连接」连接装置';
|
placeholder.textContent = '点击「连接」连接装置';
|
||||||
// 隐藏 canvas,显示占位符
|
// 隐藏 canvas,显示占位符
|
||||||
|
canvasInitialized = false;
|
||||||
|
latestFrameId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll('.dpad .btn, .action-btn').forEach(btn => {
|
document.querySelectorAll('.dpad .btn, .action-btn').forEach(btn => {
|
||||||
|
|||||||
Reference in New Issue
Block a user