修复若干bug和ui
This commit is contained in:
@@ -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
64
src/utils/h264Renderer.js
Normal 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.264,Annex-B(SPS/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 { }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
42
src/workers/decoder.worker.js
Normal file
42
src/workers/decoder.worker.js
Normal 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--;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user