From 35c6422ab562cf112dab0ca91dd94e4fd4e5bd09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=A1=E5=A4=8D=E4=B9=A0?= <2353956224@qq.com> Date: Thu, 14 Aug 2025 19:52:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=86=E5=8C=85+=E5=88=B7=E6=96=B0=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/useCanvasPointer.js | 78 +++++++ src/composables/useTaskQueue.js | 30 +++ src/composables/useVideoStream.js | 106 +++++++++ src/views/VideoStream.vue | 335 ++++++++++------------------ tk-ai-adb.zip | Bin 0 -> 6124723 bytes 5 files changed, 330 insertions(+), 219 deletions(-) create mode 100644 src/composables/useCanvasPointer.js create mode 100644 src/composables/useTaskQueue.js create mode 100644 src/composables/useVideoStream.js create mode 100644 tk-ai-adb.zip diff --git a/src/composables/useCanvasPointer.js b/src/composables/useCanvasPointer.js new file mode 100644 index 0000000..0a00a26 --- /dev/null +++ b/src/composables/useCanvasPointer.js @@ -0,0 +1,78 @@ +// src/composables/useCanvasPointer.js +import { ref } from "vue"; + +/** + * @param {{ phone, toBuffer, getWs: (index:number)=>WebSocket|null }} deps + * 依赖项对象,包含手机信息、缓冲转换和WebSocket获取函数 + */ +export function useCanvasPointer(deps) { + const { phone, toBuffer, getWs } = deps; + + const canvasRef = ref({}); // { [udid]: HTMLCanvasElement } - 存储设备ID到Canvas元素的映射 + const frameMeta = ref({}); // { [udid]: { w,h, rotation? } } - 存储设备ID到帧元数据的映射 + + /** + * 初始化画布 + * @param {string} udid - 设备唯一标识符 + */ + function initCanvas(udid) { + const canvas = canvasRef.value[udid]; + if (!canvas) return; + const dpr = window.devicePixelRatio || 1; // 获取设备像素比 + // 设置画布样式尺寸 + canvas.style.width = `${phone.value.width * 1.4}px`; + canvas.style.height = `${phone.value.height * 1.4}px`; + // 设置画布实际像素尺寸 + canvas.width = phone.value.width * 1.4 * dpr; + canvas.height = phone.value.height * 1.4 * dpr; + const ctx = canvas.getContext("2d"); + ctx.scale(dpr, dpr); + // 可选:参考网格(已设为透明) + ctx.strokeStyle = "#ffffff00"; + for (let x = 0; x <= phone.value.width; x += 100) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, phone.value.height); + ctx.stroke(); + } + } + + function getCanvasCoordinate(event, udid) { + const canvas = canvasRef.value[udid]; + const rect = canvas.getBoundingClientRect(); + const rx = (event.clientX - rect.left) / rect.width; + const ry = (event.clientY - rect.top) / rect.height; + + const meta = frameMeta.value[udid] || { w: 320, h: 720, rotation: 0 }; + let x = rx * meta.w; + let y = ry * meta.h; + + switch (meta.rotation ?? 0) { + case 90: [x, y] = [meta.w - y, x]; break; + case 180: [x, y] = [meta.w - x, meta.h - y]; break; + case 270: [x, y] = [y, meta.h - x]; break; + } + x = Math.max(0, Math.min(meta.w - 1, x)); + y = Math.max(0, Math.min(meta.h - 1, y)); + return { x: Math.round(x), y: Math.round(y), w: meta.w, h: meta.h }; + } + + // 统一发包:point 用帧坐标,screenSize 用帧宽高 + function sendPointer(udid, index, action /* 0 down,1 up,2 move */, x, y) { + const meta = frameMeta.value[udid] || { w: 320, h: 720, rotation: 0 }; + const payload = { + type: 2, + action, + pointerId: 0, + position: { point: { x, y }, screenSize: { width: meta.w, height: meta.h } }, + pressure: action === 1 ? 0 : 1, + buttons: action === 1 ? 0 : 1, + }; + const ws = getWs(index); + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(toBuffer(payload)); + } + } + + return { canvasRef, frameMeta, initCanvas, getCanvasCoordinate, sendPointer }; +} diff --git a/src/composables/useTaskQueue.js b/src/composables/useTaskQueue.js new file mode 100644 index 0000000..01ea40b --- /dev/null +++ b/src/composables/useTaskQueue.js @@ -0,0 +1,30 @@ +// src/composables/useTaskQueue.js +// 创建任务队列 + +const _queues = new Map(); // index -> task[] + +export function createTaskQueue(index) { + if (!_queues.has(index)) _queues.set(index, []); + return { + enqueue(task) { + const q = _queues.get(index); + q.push(task); + if (q.length === 1) task(); // 只有第一个任务时立即执行 + }, + next() { + const q = _queues.get(index) || []; + q.shift(); + if (q.length > 0) q[0](); // 执行下一个 + }, + clear() { + _queues.set(index, []); + }, + getNum() { + return _queues.get(index) || []; + } + }; +} + +export function clearAllQueues() { + _queues.clear(); +} diff --git a/src/composables/useVideoStream.js b/src/composables/useVideoStream.js new file mode 100644 index 0000000..d5f2305 --- /dev/null +++ b/src/composables/useVideoStream.js @@ -0,0 +1,106 @@ +// src/composables/useVideoStream.js + +/** + * 将 h264-converter 的 MSE SourceBuffer 做“回放缓冲裁剪”, + * 防止 buffered 越积越多导致内存上涨。 + * + * @param {Array} instanceList - 你的 instanceList(每个 index 有 converter) + * @param {object} videoElementRef - 你的 videoElement ref 对象(udid->video) + * @param {import('vue').Ref} deviceInformationRef - 设备列表(取 udid) + * @param {number} index + * @param {number} backBufferSec - 保留最近多少秒 + * @param {number} intervalMs - 多久裁剪一次 + */ +export function attachTrimmerForIndex( + instanceList, + videoElementRef, + deviceInformationRef, + index, + backBufferSec = 10, + intervalMs = 2000 +) { + const conv = instanceList[index]?.converter; + if (!conv) return; + + const ensureAttach = () => { + const ms = conv.mediaSource; + if (!ms) return false; + if (ms.readyState !== "open") return false; + if (!conv.sourceBuffer) return false; + return true; + }; + + if (conv._trimTimer) { + clearInterval(conv._trimTimer); + conv._trimTimer = null; + } + if (conv._mseListenersInstalled !== true && conv.mediaSource) { + conv._mseListenersInstalled = true; + conv.mediaSource.addEventListener("sourceopen", () => { + attachTrimmerForIndex( + instanceList, + videoElementRef, + deviceInformationRef, + index, + backBufferSec, + intervalMs + ); + }); + conv.mediaSource.addEventListener("sourceclose", () => { + if (conv._trimTimer) { + clearInterval(conv._trimTimer); + conv._trimTimer = null; + } + }); + conv.mediaSource.addEventListener("error", () => { + if (conv._trimTimer) { + clearInterval(conv._trimTimer); + conv._trimTimer = null; + } + }); + } + + if (!ensureAttach()) { + const waitId = setInterval(() => { + if (ensureAttach()) { + clearInterval(waitId); + attachTrimmerForIndex( + instanceList, + videoElementRef, + deviceInformationRef, + index, + backBufferSec, + intervalMs + ); + } + }, 300); + return; + } + + conv._trimTimer = setInterval(() => { + const currentConv = instanceList[index]?.converter; + const ms = currentConv?.mediaSource; + const sb = currentConv?.sourceBuffer; + const udid = deviceInformationRef.value[index]?.udid; + const video = udid ? videoElementRef.value[udid] : null; + + if (!currentConv || !ms || ms.readyState !== "open" || !sb || !video) return; + if (sb.updating || video.seeking || video.readyState < 2) return; + + const cur = video.currentTime || 0; + const trimTo = Math.max(0, cur - backBufferSec); + + try { + for (let i = 0; i < sb.buffered.length; i++) { + const start = sb.buffered.start(i); + const end = sb.buffered.end(i); + if (end < trimTo - 0.25) { + try { sb.remove(0, end); } catch { } + break; + } + } + } catch (e) { + // 忽略一次性错误(例如 SourceBuffer 被移除) + } + }, intervalMs); +} diff --git a/src/views/VideoStream.vue b/src/views/VideoStream.vue index 41a0fa4..b54cdd3 100644 --- a/src/views/VideoStream.vue +++ b/src/views/VideoStream.vue @@ -64,7 +64,7 @@