29、WebRTC测试:单元测试(Jest)、端到端测试(Puppeteer)、媒体质量测试(客观指标)、压力测试
说实话,很多做WebRTC的同学,写完代码能跑通就谢天谢地了。但我在实际项目中吃过不少亏——有一次上线前没做压力测试,结果用户一多,服务器直接崩了。从那以后,我养成了一个习惯:测试不是可选项,而是必须项。
今天咱们就聊聊WebRTC的四大测试维度。我会把我在项目中踩过的坑、总结的经验,一股脑儿倒出来。
一、单元测试:用Jest守住代码底线
单元测试,说白了就是测你的函数对不对。WebRTC里有很多逻辑密集的模块,比如SDP解析、码率计算、ICE状态机。这些地方一旦出错,排查起来特别痛苦。
我个人习惯用Jest,轻量、速度快,而且对TypeScript支持很好。
1.1 测试SDP解析函数
举个例子,我们经常需要从SDP里提取视频的编码参数:
// sdpParser.js
function extractVideoCodec(sdp) {
const match = sdp.match(/a=rtpmap:(\d+) H264/);
return match ? match[1] : null;
}
// sdpParser.test.js
test('should extract H264 payload type from SDP', () => {
const sdp = `a=rtpmap:96 H264/90000`;
expect(extractVideoCodec(sdp)).toBe('96');
});
test('should return null if no H264 found', () => {
const sdp = `a=rtpmap:97 VP8/90000`;
expect(extractVideoCodec(sdp)).toBeNull();
});
嗯,这里要注意:SDP的格式在不同浏览器里可能有细微差别。我曾经遇到过Chrome和Firefox生成的SDP里,rtpmap行的顺序不一样,导致解析失败。所以测试用例里最好覆盖多种浏览器生成的SDP样本。
1.2 测试ICE状态机
ICE状态机是WebRTC里最容易出bug的地方之一。我建议把状态迁移逻辑单独抽出来测试:
// iceStateMachine.js
const STATES = {
NEW: 'new',
CHECKING: 'checking',
CONNECTED: 'connected',
FAILED: 'failed'
};
function transition(currentState, event) {
switch (currentState) {
case STATES.NEW:
if (event === 'start') return STATES.CHECKING;
break;
case STATES.CHECKING:
if (event === 'success') return STATES.CONNECTED;
if (event === 'timeout') return STATES.FAILED;
break;
default:
return currentState;
}
return currentState;
}
test('should transition from NEW to CHECKING on start', () => {
expect(transition('new', 'start')).toBe('checking');
});
test('should stay in CONNECTED on any event', () => {
expect(transition('connected', 'timeout')).toBe('connected');
});
你看,这样写测试,每个状态迁移都是可验证的。我遇到过最坑的一次,是ICE状态从CHECKING直接跳到了FAILED,但日志里没有任何错误信息。后来发现是底层网络库抛了个异常,但没被捕获。单元测试帮我提前发现了这个漏洞。
二、端到端测试:用Puppeteer模拟真实用户
单元测试只能保证函数逻辑正确,但两个浏览器之间能不能真正建立连接、音视频能不能正常传输,这得靠端到端测试。
我推荐用Puppeteer,它可以控制无头Chrome,模拟两个用户加入同一个房间。
2.1 搭建测试环境
首先,你需要一个信令服务器。我一般用Node.js写一个简单的WebSocket服务器:
// signalingServer.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (message) => {
// 广播给其他客户端
wss.clients.forEach(client => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
});
2.2 编写Puppeteer测试脚本
// e2e.test.js
const puppeteer = require('puppeteer');
test('two peers should establish a WebRTC connection', async () => {
const browser = await puppeteer.launch({ headless: true });
const page1 = await browser.newPage();
const page2 = await browser.newPage();
await page1.goto('http://localhost:3000');
await page2.goto('http://localhost:3000');
// 模拟点击"加入房间"按钮
await page1.click('#join-btn');
await page2.click('#join-btn');
// 等待连接建立
await page1.waitForSelector('.connection-state.connected', { timeout: 10000 });
await page2.waitForSelector('.connection-state.connected', { timeout: 10000 });
// 验证双方都收到了对方的媒体流
const remoteVideo1 = await page1.$eval('#remote-video', el => el.srcObject);
const remoteVideo2 = await page2.$eval('#remote-video', el => el.srcObject);
expect(remoteVideo1).not.toBeNull();
expect(remoteVideo2).not.toBeNull();
await browser.close();
});
这里有个坑:Puppeteer默认的无头模式可能不支持WebRTC的某些特性。我建议用headless: 'new'模式,或者直接用有头模式调试。另外,记得在页面里暴露一些状态信息,比如connectionState、iceConnectionState,方便测试脚本读取。
三、媒体质量测试:用客观指标说话
主观感受很重要,但你不能靠"我觉得画面挺清晰"来交付产品。我们需要客观指标:PSNR、SSIM、VMAF、延迟、丢包率。
我个人习惯用getStats() API来采集实时数据,然后计算这些指标。
3.1 采集WebRTC统计信息
// 每隔1秒采集一次统计信息
setInterval(async () => {
const stats = await peerConnection.getStats();
stats.forEach(report => {
if (report.type === 'inbound-rtp' && report.kind === 'video') {
console.log('丢包率:', report.packetsLost / report.packetsReceived);
console.log('抖动:', report.jitter);
console.log('帧率:', report.framesPerSecond);
}
});
}, 1000);
3.2 计算PSNR
PSNR是衡量视频质量的经典指标。虽然WebRTC不直接提供,但你可以通过比较原始帧和接收帧来计算:
function calculatePSNR(original, received) {
let mse = 0;
for (let i = 0; i < original.length; i++) {
mse += (original[i] - received[i]) ** 2;
}
mse /= original.length;
if (mse === 0) return Infinity;
return 10 * Math.log10(255 ** 2 / mse);
}
嗯,这里要注意:PSNR对像素级差异敏感,但人眼其实对某些失真不敏感。所以现在业界更推荐VMAF(Video Multi-Method Assessment Fusion),它结合了多种指标,更接近人眼感受。
3.3 延迟测试
延迟是实时通信的生命线。我一般用两种方法测:
- RTT测量: 通过RTCP的Sender Report和Receiver Report计算往返时间
- 端到端延迟: 在发送端打时间戳,接收端解码后显示时间差
// 发送端
const startTime = Date.now();
const dataChannel = peerConnection.createDataChannel('latency');
dataChannel.send(JSON.stringify({ type: 'ping', time: startTime }));
// 接收端
dataChannel.onmessage = (event) => {
const { type, time } = JSON.parse(event.data);
if (type === 'ping') {
const latency = Date.now() - time;
console.log('端到端延迟:', latency, 'ms');
}
};
四、压力测试:看看系统能扛多少人
压力测试,说白了就是模拟大量用户同时在线,看看你的服务器和客户端能不能扛住。
我常用的工具是k6和artillery,但针对WebRTC,我更推荐自己写脚本,因为WebRTC的连接建立过程比较复杂。
4.1 模拟多用户并发
// stressTest.js
const puppeteer = require('puppeteer');
async function simulateUser(roomId) {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto(`http://localhost:3000/room/${roomId}`);
await page.click('#join-btn');
// 保持连接10秒
await page.waitForTimeout(10000);
await browser.close();
}
// 同时启动50个用户
const promises = [];
for (let i = 0; i < 50; i++) {
promises.push(simulateUser('stress-test-room'));
}
await Promise.all(promises);
4.2 监控服务器资源
压力测试时,一定要同时监控服务器的CPU、内存、网络带宽。我一般用pidstat和iftop:
# 监控CPU和内存
pidstat -p $(pgrep -f signaling-server) 1
# 监控网络流量
iftop -i eth0
4.3 关键指标
| 指标 | 正常范围 | 告警阈值 | 说明 |
|---|---|---|---|
| CPU使用率 | < 70% | > 90% | 超过90%说明需要扩容 |
| 内存使用率 | < 80% | > 95% | 内存泄漏是常见问题 |
| 连接建立成功率 | > 99% | < 95% | 失败率过高说明信令或ICE有问题 |
| 平均延迟 | < 150ms | > 300ms | 延迟飙升通常是带宽瓶颈 |
知识体系总览
下面这张图,把WebRTC测试的四个维度串起来了。你可以看到,每个维度关注的点不同,但最终目标都是保证用户体验。
你看,这四个维度层层递进:单元测试守住代码底线,端到端测试验证功能完整性,媒体质量测试量化用户体验,压力测试确保系统稳定性。缺一个,你的WebRTC应用就可能在某一天突然翻车。
好了,今天就聊到这儿。测试这件事,投入越多,回报越大。别等到线上出问题了再后悔——嗯,我当年就是这么过来的。
公众号:蓝海资料掘金营,微信deep3321