修复若干bug和ui

This commit is contained in:
2025-08-11 22:00:45 +08:00
parent aa59af1d66
commit abd52c712d
4 changed files with 295 additions and 53 deletions

View File

@@ -55,11 +55,11 @@ export function update(data) {
} }
//获取话术 //获取话术
export function prologue(data) { export function prologue() {
return getAxios({ url: 'api/common/prologue', data }) return getAxios({ url: 'api/common/prologue' })
} }
//获取评论 //获取评论
export function comment(data) { export function comment() {
return getAxios({ url: 'api/common/comment', data }) return getAxios({ url: 'api/common/comment' })
} }

64
src/utils/h264Renderer.js Normal file
View File

@@ -0,0 +1,64 @@
// utils/h264Renderer.js
export function createH264WebCodecsRenderer(videoEl) {
// 1) 生成一个可写的视频轨,绑定到 <video>.srcObject
const trackGen = new MediaStreamTrackGenerator({ kind: 'video' });
const writer = trackGen.writable.getWriter();
const stream = new MediaStream([trackGen]);
videoEl.srcObject = stream;
// 简单的 Annex-B 关键帧判断(为了给 EncodedVideoChunk 标 type
function isKeyframeAnnexB(buf) {
const u8 = new Uint8Array(buf);
for (let i = 0; i + 4 < u8.length; i++) {
// 00 00 01
if (u8[i] === 0 && u8[i + 1] === 0 && u8[i + 2] === 1) {
const nalType = u8[i + 3] & 0x1f;
if (nalType === 5) return true; // IDR
}
// 00 00 00 01
if (u8[i] === 0 && u8[i + 1] === 0 && u8[i + 2] === 0 && u8[i + 3] === 1) {
const nalType = u8[i + 4] & 0x1f;
if (nalType === 5) return true;
}
}
return false;
}
// 2) 建立解码器:输出即写入 trackGen再立刻释放帧
const decoder = new VideoDecoder({
output: async (frame) => {
await writer.write(frame);
frame.close();
},
error: (e) => console.error('VideoDecoder error:', e)
});
// H.264Annex-BSPS/PPS 从码流里来)
decoder.configure({
codec: 'avc1.64001f', // High@3.1(占位即可)
optimizeForLatency: true
});
// 用一个单调时间戳微秒。30fps ≈ 33333us
let ts = 0;
const stepUs = 33333;
// 3) 返回一个“喂数据”的函数 & 清理函数
return {
push(buffer /* ArrayBuffer */) {
const key = isKeyframeAnnexB(buffer);
const chunk = new EncodedVideoChunk({
type: key ? 'key' : 'delta',
timestamp: ts,
data: new Uint8Array(buffer) // 视图包裹,不拷贝
});
ts += stepUs;
decoder.decode(chunk);
},
close() {
try { writer.close(); } catch { }
try { decoder.close(); } catch { }
try { videoEl.srcObject = null; } catch { }
}
};
}

View File

@@ -3,9 +3,8 @@
<el-scrollbar class="left"> <!-- 左边栏 --> <el-scrollbar class="left"> <!-- 左边栏 -->
<div class="center-line"> <!-- 左边栏按钮 --> <div class="center-line"> <!-- 左边栏按钮 -->
<div v-for="(btn, index) in buttons" :key="index" style="width: 100%;"> <div v-for="(btn, index) in buttons" :key="index" style="width: 100%;">
<div v-if="btn.show?.()" class="left-button" :style="{ <div v-if="btn.show?.()" class="left-button" :style="btn.style ? btn.style() : {}" @click="btn.onClick"
backgroundColor: btn.label == '关闭监测消息' ? 'red' : '', @mouseenter="hoverIndex = index" @mouseleave="hoverIndex = null">
}" @click="btn.onClick" @mouseenter="hoverIndex = index" @mouseleave="hoverIndex = null">
<img :src="hoverIndex === index ? btn.img.hover : btn.img.normal" alt=""> <img :src="hoverIndex === index ? btn.img.hover : btn.img.normal" alt="">
{{ btn.label }} {{ btn.label }}
</div> </div>
@@ -60,7 +59,7 @@
<ChatDialog :visible="openShowChat" :messages="chatList" /> <ChatDialog :visible="openShowChat" :messages="chatList" />
</div> </div>
<MultiLineInputDialog v-model:visible="showDialog" :initialText='""' :title="dialogTitle" :index="selectedDevice" <MultiLineInputDialog v-model:visible="showDialog" :initialText='""' :title="dialogTitle" :index="selectedDevice"
@confirm="onDialogConfirm" /> @confirm="onDialogConfirm" @cancel="stop" />
</div> </div>
</template> </template>
@@ -124,7 +123,7 @@ let isStop = ref(false);
//sse弹窗是否存在 //sse弹窗是否存在
let isMsgPop = ref(false); let isMsgPop = ref(false);
//播放器列表 //播放器列表
let instanceList = ref([{}, {}, {}, {}, {}, {}, {}, {}]); let instanceList = [{}, {}, {}, {}, {}, {}, {}, {}];
//是否是在关注主播 //是否是在关注主播
let runType = ref(['', '', '', '', '', '', '', '']); let runType = ref(['', '', '', '', '', '', '', '']);
//屏幕尺寸系数 //屏幕尺寸系数
@@ -156,6 +155,7 @@ let istranslate = ref(false); //是否是翻译本页
let phoneXYinfo = ref(getphoneXYinfo() == null ? [{}, {}, {}, {}, {}, {}, {}, {}] : getphoneXYinfo()); let phoneXYinfo = ref(getphoneXYinfo() == null ? [{}, {}, {}, {}, {}, {}, {}, {}] : getphoneXYinfo());
// 当前悬浮的按钮索引 // 当前悬浮的按钮索引
const hoverIndex = ref(null) const hoverIndex = ref(null)
const isMonitorOn = ref(false) // false 表示关闭true 表示开启
// 你可以用这种方式声明按钮们 // 你可以用这种方式声明按钮们
const buttons = [ const buttons = [
@@ -188,25 +188,40 @@ const buttons = [
}, },
{ {
label: '打开直播', label: '打开直播',
onClick: () => brushLive(), onClick: () => {
runType.value[0] = 'brushLive'
brushLive()
},
show: () => true, show: () => true,
img: { img: {
normal: new URL('@/assets/video/leftBtn4.png', import.meta.url).href, normal: new URL('@/assets/video/leftBtn4.png', import.meta.url).href,
hover: new URL('@/assets/video/leftBtn4-4.png', import.meta.url).href hover: new URL('@/assets/video/leftBtn4-4.png', import.meta.url).href
} },
style: () => ({
backgroundColor: runType.value[0] == 'brushLive' ? 'red' : ''
})
}, },
{ {
label: '一键养号', label: '一键养号',
onClick: () => parentNum(), onClick: () => {
if (runType.value[0] == 'like') return;
runType.value[0] = 'like'
parentNum()
},
show: () => true, show: () => true,
img: { img: {
normal: new URL('@/assets/video/leftBtn5.png', import.meta.url).href, normal: new URL('@/assets/video/leftBtn5.png', import.meta.url).href,
hover: new URL('@/assets/video/leftBtn5-5.png', import.meta.url).href hover: new URL('@/assets/video/leftBtn5-5.png', import.meta.url).href
} },
style: () => ({
backgroundColor: runType.value[0] == 'like' ? 'red' : ''
})
}, },
{ {
label: '一键关注并打招呼', label: '一键关注并打招呼',
onClick: () => { onClick: () => {
if (runType.value[0] == 'follow') return;
runType.value[0] = 'follow'
showDialog.value = true showDialog.value = true
dialogTitle.value = '主播ID' dialogTitle.value = '主播ID'
selectedDevice.value = 999 selectedDevice.value = 999
@@ -215,26 +230,29 @@ const buttons = [
img: { img: {
normal: new URL('@/assets/video/leftBtn6.png', import.meta.url).href, normal: new URL('@/assets/video/leftBtn6.png', import.meta.url).href,
hover: new URL('@/assets/video/leftBtn6-6.png', import.meta.url).href hover: new URL('@/assets/video/leftBtn6-6.png', import.meta.url).href
} },
style: () => ({
backgroundColor: runType.value[0] == 'follow' ? 'red' : ''
})
}, },
{ {
label: '开启监测消息', label: '监测消息',
onClick: () => openMonitor(), onClick: () => {
show: () => !isShowMes.value, // 只有在未开启时显示 isMonitorOn.value = !isMonitorOn.value
img: { if (isMonitorOn.value) {
normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href, openMonitor()
hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href } else {
cloesMonitor()
} }
}, },
{ show: () => true,
label: '关闭监测消息',
onClick: () => cloesMonitor(),
show: () => isShowMes.value, // 只有在已开启时显示
img: { img: {
normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href, normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href,
hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href
}, },
style: { backgroundColor: 'red' } style: () => ({
backgroundColor: isMonitorOn.value ? 'red' : ''
})
}, },
{ {
label: '全部停止', label: '全部停止',
@@ -256,6 +274,31 @@ const buttons = [
} }
] ]
const feedState = Array(8).fill(null).map(() => ({
processing: false,
pending: null, // ArrayBuffer 等最新一段
}));
function pushFrame(index, buf) {
const st = feedState[index];
if (st.processing) {
// 覆盖旧的等待帧,保留最新
st.pending = buf;
return;
}
st.processing = true;
try {
//推送帧到video
instanceList[index].converter.appendRawData(new Uint8Array(buf));
} finally {
st.processing = false;
if (st.pending) {
const next = st.pending;
st.pending = null;
// 用微任务衔接,避免递归栈增长
queueMicrotask(() => pushFrame(index, next));
}
}
}
const wsCache = new Map(); const wsCache = new Map();
//`````````````````````````````````````````````````````````````````````````````````` //``````````````````````````````````````````````````````````````````````````````````
@@ -264,30 +307,29 @@ const initVideoStream = (udid, index) => {
//`````````````````````````````````````````````````````````````````````````````````` //``````````````````````````````````````````````````````````````````````````````````
// 1. 检查缓存中是否已有实例 // 1. 检查缓存中是否已有实例
if (wsCache.has(udid)) { if (wsCache.has(udid)) {
const cachedWs = wsCache.get(udid); const cached = wsCache.get(udid);
if (cachedWs.readyState === WebSocket.OPEN) { if (cached?.ws?.readyState === WebSocket.OPEN) {
return cachedWs; return cached.ws;
} }
// 如果连接已关闭,清除缓存并重新创建 // 如果连接已关闭,清除缓存并重新创建
wsCache.delete(udid); wsCache.delete(udid);
} }
// 2. 创建专用实例容器 // 2. 创建专用实例容器
instanceList.value[index] = { instanceList[index] = {
wsVideo: null, // wsVideo: null,
converter: null, converter: null,
timer: null timer: null
}; };
//`````````````````````````````````````````````````````````````````````````````````` //``````````````````````````````````````````````````````````````````````````````````
if (!videoElement.value) return; if (!videoElement.value) return;
// 1. 创建 h264-converter 实例 // 1. 创建 h264-converter 实例
instanceList.value[index].converter = new VideoConverter(videoElement.value[udid], 60, 1); instanceList[index].converter = new VideoConverter(videoElement.value[udid], 60, 1);
// instanceList.value[index].converter.play();
// 2. 连接 WebSocket // 2. 连接 WebSocket
wslist[index] = new WebSocket( wslist[index] = new WebSocket(
`ws://127.0.0.1:8000/?action=proxy-adb&remote=tcp%3A8886&udid=${udid}` `ws://127.0.0.1:8000/?action=proxy-adb&remote=tcp%3A8886&udid=${udid}`
); );
wslist[index].binaryType = "arraybuffer"; wslist[index].binaryType = "arraybuffer";
attachTrimmerForIndex(index, 10, 2000); // 挂上修剪器
wslist[index].onopen = () => { wslist[index].onopen = () => {
console.log("手机显示ws已开启"); console.log("手机显示ws已开启");
wsActions = createWsActions(wslist); wsActions = createWsActions(wslist);
@@ -295,12 +337,12 @@ const initVideoStream = (udid, index) => {
setTimeout(() => { setTimeout(() => {
wslist[index].send(openStr); wslist[index].send(openStr);
}, 300); }, 300);
wsCache.set(udid, instanceList.value[index]); wsCache.set(udid, { ws: wslist[index], index });
}; };
const magicSize = stringToUtf8ByteArray('scrcpy_message'); const magicSize = stringToUtf8ByteArray('scrcpy_message');
// 3. 处理接收到的二进制数据 // 3. 处理接收到的二进制数据
wslist[index].onmessage = (event) => { wslist[index].onmessage = (event) => {
const data = new Uint8Array(event.data);
//判断返回的如果是字符串为自定义返回 //判断返回的如果是字符串为自定义返回
if (typeof event.data == 'string') { if (typeof event.data == 'string') {
if (isStop.value) { if (isStop.value) {
@@ -359,7 +401,13 @@ const initVideoStream = (udid, index) => {
console.log('最新消息', mesBox) console.log('最新消息', mesBox)
console.log("翻译", istranslate.value) console.log("翻译", istranslate.value)
if (istranslate.value == false) { if (istranslate.value == false) {
if (mesBox.position == 'right') return if (mesBox.position == 'right') {
Back('', index)
setTimeout(() => {
Back('', index)
}, 1000)
return
}
openShowChat.value = true openShowChat.value = true
console.log("执行ai") console.log("执行ai")
@@ -619,21 +667,28 @@ const initVideoStream = (udid, index) => {
} }
// createTaskQueue(index).next(); // 继续队列中下一个任务 // createTaskQueue(index).next(); // 继续队列中下一个任务
} }
} } else {
//返回粘贴板内容 const buf = event.data; // ArrayBuffer
if (startsWithHeader(magicSize, data)) { const view = new Uint8Array(buf); // 只创建视图,不复制
// 粘贴板消息头判断FIX: 用 view别用未定义的 data
if (startsWithHeader(magicSize, view)) {
if (!isSend.value) { if (!isSend.value) {
const buffer = trimLongArray(data, magicSize); const payload = trimLongArray(view, magicSize);
const paste = bufferToString(buffer); const paste = bufferToString(payload);
console.log('获取粘贴板内容', paste) console.log('获取粘贴板内容', paste);
} }
} return; // 这类消息不走视频通道
//视频流处理 }
if (instanceList.value[index].converter) {
if (isshow.value) { // 视频流
instanceList.value[index].converter.appendRawData(data); if (instanceList[index].converter && isshow.value) {
pushFrame(index, buf); // 用下方新的 pushFrame
} }
} }
}; };
// 4. 错误处理 // 4. 错误处理
wslist[index].onerror = (error) => { wslist[index].onerror = (error) => {
@@ -642,7 +697,7 @@ const initVideoStream = (udid, index) => {
//`````````````````````````````````````````````````````````````````````````````````` //``````````````````````````````````````````````````````````````````````````````````
wslist[index].onclose = (event) => { wslist[index].onclose = (event) => {
wsCache.delete(udid)// 自动清理缓存 wsCache.delete(udid)// 自动清理缓存
clearInterval(instanceList.value[index].timer); // 清理定时器// 移除缓存 clearInterval(instanceList[index].timer); // 清理定时器// 移除缓存
}; };
//`````````````````````````````````````````````````````````````````````````````````` //``````````````````````````````````````````````````````````````````````````````````
}; };
@@ -1402,7 +1457,7 @@ function parentNum() {
function brushLive() { function brushLive() {
isStop.value = false; isStop.value = false;
deviceInformation.value.forEach((device, index) => { deviceInformation.value.forEach((device, index) => {
// runType.value[index] = 'brushLive' runType.value[index] = 'brushLive'
wsActions.toLive(device.udid, index) wsActions.toLive(device.udid, index)
}) })
} }
@@ -1497,10 +1552,91 @@ function getTransformStyle(index) {
? 'translateY(-30%)' ? 'translateY(-30%)'
: 'none'; : 'none';
} }
function attachTrimmerForIndex(index, backBufferSec = 10, intervalMs = 2000) {
const conv = instanceList[index]?.converter;
if (!conv) return;
// 如果还没创建好 MSE/SourceBuffer则等 sourceopen 再挂
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', () => {
// MSE 重新 open 时,重新挂修剪器
attachTrimmerForIndex(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;
}
});
}
// 可能还没 ready轮询等待
if (!ensureAttach()) {
const waitId = setInterval(() => {
if (ensureAttach()) {
clearInterval(waitId);
attachTrimmerForIndex(index, backBufferSec, intervalMs);
}
}, 300);
return;
}
conv._trimTimer = setInterval(() => {
// 每次都重新取,避免拿到被移除的旧 sb 引用
const currentConv = instanceList[index]?.converter;
const ms = currentConv?.mediaSource;
const sb = currentConv?.sourceBuffer;
const video = videoElement.value[deviceInformation.value[index]?.udid];
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 {
// buffered 可能是多段,仅删除完全早于 trimTo 的最前一段
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 has been removed ...” 等异常时,停止本轮,等下次 tick 重取 sb
// console.warn('[trimmer]', e);
}
}, intervalMs);
}
function manualGc() { function manualGc() {
window.electronAPI.manualGc() window.electronAPI.manualGc()
} }
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@@ -0,0 +1,42 @@
// src/workers/decoder.worker.js
let inflight = 0;
const MAX_INFLIGHT = 4;
// 可选:识别 'scrcpy_message' 头
const MAGIC = new TextEncoder().encode('scrcpy_message');
function startsWithHeader(buffer) {
if (buffer.byteLength < MAGIC.length) return false;
const head = new Uint8Array(buffer, 0, MAGIC.length);
for (let i = 0; i < MAGIC.length; i++) if (head[i] !== MAGIC[i]) return false;
return true;
}
function bufferTailToString(buffer, offset) {
const view = new Uint8Array(buffer, offset);
return new TextDecoder('utf-8').decode(view);
}
self.onmessage = (e) => {
const { type, buffer, meta } = e.data || {};
if (type !== 'video' || !buffer) return;
// 控制消息永不丢
if (startsWithHeader(buffer)) {
const text = bufferTailToString(buffer, MAGIC.length);
self.postMessage({ type: 'clipboard', text, meta });
return;
}
// 真正的丢帧:在途已满就扔掉
if (inflight >= MAX_INFLIGHT) {
return; // 丢掉这帧
}
inflight++;
try {
const chunk = new Uint8Array(buffer);
// console.log("进入视频帧")
self.postMessage({ type: 'video', chunk, meta }, [chunk.buffer]);
} finally {
inflight--;
}
};