1. 项目背景与核心目标
最近在折腾安卓平台的实时音视频通信方案,发现WebRTC这个开源项目确实是个宝藏。不过对于刚接触的新手来说,从零开始搭建测试环境还是有不少坑要踩的。今天我就来分享一个基于Node.js+Vue的WebRTC测试Demo的完整运行过程,这个方案特别适合移动端开发者快速验证核心功能。
这个Demo的价值在于:
- 提供了完整的信令服务器实现(Node.js)
- 包含简洁的前端交互界面(Vue)
- 支持安卓设备与桌面浏览器的互通测试
- 演示了ICE协商、媒体流交换等关键流程
2. 环境准备与项目结构
2.1 基础环境配置
首先需要准备以下环境(以macOS为例,其他系统类似):
bash复制# 安装Node.js(建议16.x以上版本)
brew install node
# 验证安装
node -v
npm -v
# 全局安装vue-cli
npm install -g @vue/cli
2.2 项目目录解析
下载Demo代码后,目录结构如下:
code复制webrtc-demo/
├── server/ # 信令服务器
│ ├── package.json
│ ├── server.js # 核心信令逻辑
├── client/ # 前端界面
│ ├── public/
│ ├── src/
│ │ ├── components/
│ │ ├── App.vue # 主界面
│ │ ├── webrtc.js # WebRTC核心逻辑
├── README.md
注意:实际项目中建议使用yarn替代npm,能更好地处理依赖版本问题
3. 信令服务器实现解析
3.1 Node.js服务器核心代码
信令服务器使用Socket.io实现,关键代码如下:
javascript复制// server/server.js
const express = require('express');
const socketio = require('socket.io');
const app = express();
const server = app.listen(3000);
const io = socketio(server);
io.on('connection', socket => {
// 处理加入房间请求
socket.on('join', roomId => {
const clients = io.sockets.adapter.rooms.get(roomId);
const numClients = clients ? clients.size : 0;
if(numClients >= 2) {
socket.emit('room_full');
return;
}
socket.join(roomId);
socket.emit('joined', roomId);
if(numClients === 1) {
socket.to(roomId).emit('ready');
}
});
// 转发ICE候选
socket.on('ice_candidate', (candidate, roomId) => {
socket.to(roomId).emit('ice_candidate', candidate);
});
// 转发offer/answer
socket.on('offer', (offer, roomId) => {
socket.to(roomId).emit('offer', offer);
});
socket.on('answer', (answer, roomId) => {
socket.to(roomId).emit('answer', answer);
});
});
3.2 信令流程说明
-
房间加入流程:
- 客户端A加入房间 → 服务器记录房间ID
- 客户端B加入同一房间 → 服务器发送'ready'事件
- 超过2人加入时返回'room_full'错误
-
媒体协商流程:
- 客户端A创建offer → 通过信令转发给B
- 客户端B收到offer → 创建answer → 回传给A
- 双方交换ICE候选完成NAT穿透
4. 客户端实现关键点
4.1 Vue组件结构
vue复制<!-- client/src/App.vue -->
<template>
<div>
<video ref="localVideo" autoplay muted></video>
<video ref="remoteVideo" autoplay></video>
<button @click="start">开始通话</button>
<input v-model="roomId" placeholder="输入房间号">
</div>
</template>
<script>
import { initWebRTC } from './webrtc';
export default {
data() {
return { roomId: '' }
},
methods: {
async start() {
await initWebRTC(this.roomId,
this.$refs.localVideo,
this.$refs.remoteVideo);
}
}
}
</script>
4.2 WebRTC核心逻辑
javascript复制// client/src/webrtc.js
export async function initWebRTC(roomId, localVideo, remoteVideo) {
const socket = io('http://localhost:3000');
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
// 获取本地媒体流
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
localVideo.srcObject = stream;
// 添加媒体轨道到连接
stream.getTracks().forEach(track => {
pc.addTrack(track, stream);
});
// ICE候选处理
pc.onicecandidate = ({candidate}) => {
if(candidate) {
socket.emit('ice_candidate', candidate, roomId);
}
};
// 远程流处理
pc.ontrack = ({streams}) => {
remoteVideo.srcObject = streams[0];
};
// 信令交互
socket.on('ready', async () => {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.emit('offer', offer, roomId);
});
socket.on('offer', async offer => {
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('answer', answer, roomId);
});
socket.on('answer', answer => {
pc.setRemoteDescription(answer);
});
socket.on('ice_candidate', candidate => {
pc.addIceCandidate(new RTCIceCandidate(candidate));
});
socket.emit('join', roomId);
}
5. 安卓端适配要点
5.1 安卓WebView配置
在安卓应用中需要特殊配置才能支持WebRTC:
java复制// MainActivity.java
WebView webView = findViewById(R.id.webview);
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setMediaPlaybackRequiresUserGesture(false);
// 关键配置:允许访问摄像头和麦克风
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
settings.setMediaPlaybackRequiresUserGesture(false);
}
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onPermissionRequest(PermissionRequest request) {
request.grant(request.getResources());
}
});
5.2 常见兼容性问题
-
权限问题:
- 确保AndroidManifest.xml中添加了相机和麦克风权限
xml复制<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> -
Https限制:
- 安卓9以上要求安全上下文才能使用媒体设备
- 开发时可通过
android:usesCleartextTraffic="true"临时解决
6. 完整运行流程
6.1 启动服务端
bash复制cd server
npm install
node server.js
6.2 启动客户端
bash复制cd client
npm install
npm run serve
6.3 测试步骤
- 在桌面浏览器打开
http://localhost:8080 - 在安卓WebView加载相同地址
- 两端输入相同房间号并点击"开始通话"
- 等待媒体协商完成(通常3-5秒)
7. 常见问题排查
7.1 连接失败检查清单
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 无法获取媒体流 | 权限未授权 | 检查浏览器/安卓权限设置 |
| ICE失败 | NAT穿透问题 | 添加turn服务器配置 |
| 信令超时 | 网络问题 | 检查服务器地址和端口 |
| 黑屏 | 编解码不匹配 | 统一两端编解码格式 |
7.2 调试技巧
-
chrome://webrtc-internals
- 在Chrome地址栏输入可查看详细WebRTC状态
- 包含ICE候选、统计信息等关键数据
-
adb logcat调试
bash复制
adb logcat | grep -i webview查看安卓端WebView的详细日志
-
信令日志
在server.js中添加:javascript复制socket.onAny((event, ...args) => { console.log(`[${socket.id}] ${event}`, args); });
8. 性能优化建议
-
视频参数调整:
javascript复制const constraints = { video: { width: { ideal: 640 }, height: { ideal: 480 }, frameRate: { ideal: 24 } } }; -
ICE服务器配置:
javascript复制const pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'turn:your.turn.server:3478', username: 'user', credential: 'pass' } ] }); -
带宽估计与适配:
javascript复制pc.onconnectionstatechange = () => { if(pc.connectionState === 'connected') { const sender = pc.getSenders()[0]; const parameters = sender.getParameters(); parameters.encodings[0].maxBitrate = 500000; // 500kbps sender.setParameters(parameters); } };
这个Demo虽然简单,但已经包含了WebRTC最核心的流程。在实际安卓项目中,还需要考虑更多细节,比如前后台处理、断线重连、编解码选择等。建议先把这个基础版本跑通,再逐步添加业务功能