WebRTC作为实时音视频通信的主流技术,在屏幕共享、在线教学、远程协作等场景中被广泛使用,而屏幕录制时鼠标轨迹的同步质量直接影响录制内容的可读性。当鼠标移动速度较快或者网络出现波动时,很容易出现鼠标位置和实际录制画面不匹配的情况,需要通过特定的方法实现精确同步。

鼠标轨迹不同步的核心原因
要实现精确同步,首先需要明确不同步问题的来源,主要集中在以下几个方面:
- 时间戳偏差:鼠标事件的采集时间戳和视频帧的采集时间戳没有统一基准,导致两者在时间轴上无法对齐。
- 坐标映射误差:屏幕录制时可能存在画面缩放、裁剪,鼠标采集的原始坐标没有对应转换到录制画面的坐标空间。
- 事件丢失与延迟:网络传输或者主线程阻塞导致部分鼠标事件丢失,或者鼠标事件的到达时间晚于对应视频帧的渲染时间。
核心同步方法实现
1. 统一时间戳基准
首先需要使用相同的时间源为鼠标事件和视频帧打上时间戳,推荐使用performance.now()获取高精度时间,避免使用Date.now()带来的系统时间调整误差。
// 采集鼠标事件时记录高精度时间戳
let lastMouseTime = 0;
document.addEventListener('mousemove', (e) => {
const timestamp = performance.now();
const mouseData = {
x: e.clientX,
y: e.clientY,
type: 'move',
timestamp: timestamp
};
// 将鼠标数据存入待处理队列
mouseEventQueue.push(mouseData);
lastMouseTime = timestamp;
});
// 采集视频帧时同样记录时间戳
async function captureVideoFrame(track) {
const reader = new MediaStreamTrackProcessor({ track });
const generator = new MediaStreamTrackGenerator({ kind: 'video' });
const transform = new TransformStream({
transform: async (frame, controller) => {
const frameTimestamp = performance.now();
// 为视频帧附加时间戳信息
frame.timestamp = frameTimestamp;
// 处理鼠标队列,匹配对应时间戳的鼠标数据
matchMouseWithFrame(frameTimestamp, frame);
controller.enqueue(frame);
}
});
reader.readable.pipeThrough(transform).pipeTo(generator.writable);
return generator;
}
2. 坐标空间映射
如果录制的画面不是原始屏幕的完整分辨率,需要将鼠标坐标映射到录制画面的坐标空间,避免位置偏差。
/**
* 将原始鼠标坐标映射到录制画面坐标
* @param {number} mouseX 原始鼠标X坐标
* @param {number} mouseY 原始鼠标Y坐标
* @param {object} sourceRect 原始屏幕区域 {width, height}
* @param {object} targetRect 录制画面区域 {width, height}
* @returns {object} 映射后的坐标
*/
function mapMouseCoordinate(mouseX, mouseY, sourceRect, targetRect) {
const scaleX = targetRect.width / sourceRect.width;
const scaleY = targetRect.height / sourceRect.height;
return {
x: Math.round(mouseX * scaleX),
y: Math.round(mouseY * scaleY)
};
}
// 使用示例:原始屏幕分辨率1920*1080,录制画面分辨率1280*720
const mappedCoord = mapMouseCoordinate(960, 540, {width:1920, height:1080}, {width:1280, height:720});
console.log(mappedCoord); // 输出 {x: 640, y: 360}
3. 轨迹插值补全
当鼠标事件采集频率低于视频帧率,或者部分事件丢失时,可以通过线性插值补全中间轨迹,让鼠标移动更平滑。
let prevMouseData = null;
let nextMouseData = null;
let mouseQueue = [];
/**
* 为指定时间戳的视频帧匹配对应的鼠标位置,缺失则插值
* @param {number} frameTimestamp 视频帧时间戳
* @returns {object} 对应时间戳的鼠标坐标
*/
function getMousePositionForFrame(frameTimestamp) {
// 清理队列中过期的鼠标数据
while (mouseQueue.length > 0 && mouseQueue[0].timestamp < frameTimestamp - 100) {
mouseQueue.shift();
}
if (mouseQueue.length === 0) {
return prevMouseData ? {x: prevMouseData.x, y: prevMouseData.y} : null;
}
// 找到前后两个鼠标事件
let prev = null;
let next = null;
for (let i = 0; i < mouseQueue.length; i++) {
if (mouseQueue[i].timestamp <= frameTimestamp) {
prev = mouseQueue[i];
} else {
next = mouseQueue[i];
break;
}
}
// 如果没有前一个事件,用第一个事件
if (!prev) {
prev = mouseQueue[0];
}
// 如果没有后一个事件,用前一个事件
if (!next) {
return {x: prev.x, y: prev.y};
}
// 线性插值计算中间位置
const timeDiff = next.timestamp - prev.timestamp;
const ratio = (frameTimestamp - prev.timestamp) / timeDiff;
return {
x: Math.round(prev.x + (next.x - prev.x) * ratio),
y: Math.round(prev.y + (next.y - prev.y) * ratio)
};
}
4. 录制时轨迹渲染
将同步后的鼠标位置绘制到录制画面的Canvas上,最终生成包含鼠标轨迹的视频流。
// 创建离屏Canvas用于绘制鼠标轨迹
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 1280;
canvas.height = 720;
function drawMouseOnFrame(frame, mousePos) {
if (!mousePos) return frame;
// 将视频帧绘制到Canvas
const tempCanvas = new OffscreenCanvas(frame.displayWidth, frame.displayHeight);
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(frame, 0, 0);
// 绘制鼠标光标
tempCtx.fillStyle = 'rgba(255, 0, 0, 0.8)';
tempCtx.beginPath();
tempCtx.arc(mousePos.x, mousePos.y, 8, 0, Math.PI * 2);
tempCtx.fill();
// 将Canvas内容转回视频帧
return new VideoFrame(tempCanvas, { timestamp: frame.timestamp });
}
注意事项
实际落地时还需要注意几个细节:一是鼠标事件的采集尽量放在独立的微任务中,避免主线程阻塞导致事件丢失;二是如果使用的是getDisplayMedia采集的屏幕流,需要注意不同浏览器返回的屏幕分辨率可能有差异,坐标映射的源区域需要动态获取;三是网络传输时可以为鼠标数据设置较高的优先级,减少传输延迟带来的影响。
通过以上方法的组合,就可以在WebRTC屏幕录制场景中实现鼠标轨迹和画面的精确同步,提升录制视频的整体质量。