别再死记硬背API了!用Agora RTC SDK手把手教你从零搭建一个1v1视频通话Demo(Web版)
从零构建1v1视频通话:Agora RTC实战指南
每次看到API文档里密密麻麻的方法列表就头疼?作为前端开发者,我们更习惯通过实际项目来理解技术。今天我们就用Agora RTC SDK,从零开始搭建一个最简单的1v1视频通话应用。这个过程中,你会自然掌握那些看似复杂的API到底在什么场景下使用,以及它们如何协同工作。
1. 环境准备与项目初始化
在开始编码前,我们需要准备好开发环境。现代Web开发已经离不开npm和现代前端框架,但为了专注于Agora的核心功能,我们先从最基础的HTML+JS项目开始。
首先创建一个空目录,初始化package.json:
mkdir agora-video-call cd agora-video-call npm init -y然后安装Agora RTC SDK:
npm install agora-rtc-sdk提示:Agora RTC SDK目前有v3和v4两个主要版本,本文基于更现代的v4版本。如果你看到不同的API文档,请注意版本差异。
创建项目基础结构:
/agora-video-call |- /public |- index.html |- style.css |- /src |- app.js |- package.json在index.html中引入SDK和我们的应用代码:
<!DOCTYPE html> <html> <head> <title>Agora 1v1视频通话</title> <link rel="stylesheet" href="style.css"> </head> <body> <div id="app"> <div id="local-video" class="video-container"></div> <div id="remote-video" class="video-container"></div> <button id="join-btn">加入频道</button> <button id="leave-btn" disabled>离开频道</button> </div> <script src="https://unpkg.com/agora-rtc-sdk-ng@latest"></script> <script src="app.js"></script> </body> </html>2. 初始化Agora客户端
现在我们来编写核心的app.js。Agora RTC的核心是Client对象,它管理着整个音视频通话的生命周期。
// 初始化Agora客户端 const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" }); // 定义全局变量存储音视频轨道 let localTracks = { audioTrack: null, videoTrack: null }; let remoteUsers = {}; // 频道配置 const config = { appId: "你的AppID", // 从Agora控制台获取 channel: "test-channel", // 频道名称 token: null // 测试时可暂时留空,生产环境必须使用Token };注意:在生产环境中,Token应该从你的服务器动态获取,而不是硬编码在客户端代码中。这里为了演示简化流程。
Agora客户端需要处理几个重要事件:
// 监听远端用户发布流 client.on("user-published", async (user, mediaType) => { await client.subscribe(user, mediaType); if (mediaType === "video") { const remoteVideoTrack = user.videoTrack; document.getElementById("remote-video").append(remoteVideoTrack.getMediaStream()); } if (mediaType === "audio") { const remoteAudioTrack = user.audioTrack; remoteAudioTrack.play(); } }); // 监听远端用户取消发布 client.on("user-unpublished", (user) => { // 清理远端视频 const remoteVideoContainer = document.getElementById("remote-video"); remoteVideoContainer.innerHTML = ""; }); // 监听远端用户离开 client.on("user-left", (user) => { // 清理远端用户数据 delete remoteUsers[user.uid]; const remoteVideoContainer = document.getElementById("remote-video"); remoteVideoContainer.innerHTML = ""; });3. 实现本地音视频采集与发布
视频通话的核心功能是采集本地音视频并发布到频道中。Agora提供了简洁的API来完成这些操作。
首先实现加入频道的功能:
document.getElementById("join-btn").addEventListener("click", async () => { try { // 加入频道 const uid = await client.join(config.appId, config.channel, config.token); // 创建本地音视频轨道 localTracks.audioTrack = await AgoraRTC.createMicrophoneAudioTrack(); localTracks.videoTrack = await AgoraRTC.createCameraVideoTrack(); // 播放本地视频 document.getElementById("local-video").append(localTracks.videoTrack.getMediaStream()); // 发布本地轨道 await client.publish(Object.values(localTracks)); // 更新UI状态 document.getElementById("join-btn").disabled = true; document.getElementById("leave-btn").disabled = false; console.log("加入频道成功,UID:", uid); } catch (error) { console.error("加入频道失败:", error); } });离开频道的实现:
document.getElementById("leave-btn").addEventListener("click", async () => { try { // 停止并关闭所有本地轨道 for (const trackName in localTracks) { if (localTracks[trackName]) { localTracks[trackName].stop(); localTracks[trackName].close(); localTracks[trackName] = null; } } // 离开频道 await client.leave(); // 清理本地视频显示 document.getElementById("local-video").innerHTML = ""; // 更新UI状态 document.getElementById("join-btn").disabled = false; document.getElementById("leave-btn").disabled = true; console.log("离开频道成功"); } catch (error) { console.error("离开频道失败:", error); } });4. 添加基础UI与错误处理
一个完整的应用需要良好的用户界面和健壮的错误处理机制。让我们增强一下基础功能。
首先在style.css中添加基本样式:
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } .video-container { width: 100%; height: 300px; background: #eee; margin-bottom: 20px; position: relative; } button { padding: 10px 15px; margin-right: 10px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } button:disabled { background: #cccccc; cursor: not-allowed; } #local-video video, #remote-video video { width: 100%; height: 100%; object-fit: cover; }然后增强错误处理逻辑:
// 检查浏览器兼容性 if (!AgoraRTC.checkSystemRequirements()) { alert("您的浏览器不支持WebRTC,请使用Chrome、Firefox或Safari的最新版本"); } // 处理音视频自动播放限制 AgoraRTC.onAudioAutoplayFailed = () => { alert("点击任意位置以启用音频播放"); document.body.addEventListener("click", () => { if (localTracks.audioTrack) { localTracks.audioTrack.play(); } }, { once: true }); }; // 设备变更监听 AgoraRTC.onMicrophoneChanged = async (changedDevice) => { if (localTracks.audioTrack && localTracks.audioTrack.getDeviceId() === changedDevice.deviceId) { await localTracks.audioTrack.setDevice(changedDevice.deviceId); } }; AgoraRTC.onCameraChanged = async (changedDevice) => { if (localTracks.videoTrack && localTracks.videoTrack.getDeviceId() === changedDevice.deviceId) { await localTracks.videoTrack.setDevice(changedDevice.deviceId); } };5. 进阶功能扩展
基础功能完成后,我们可以考虑添加一些进阶功能来提升用户体验。
设备选择器
允许用户切换不同的摄像头和麦克风:
async function setupDeviceSelector() { const devices = await AgoraRTC.getDevices(); const audioInputs = devices.filter(d => d.kind === "audioinput"); const videoInputs = devices.filter(d => d.kind === "videoinput"); // 创建设备选择UI const deviceSelector = document.createElement("div"); deviceSelector.className = "device-selector"; if (audioInputs.length > 1) { const select = document.createElement("select"); audioInputs.forEach(device => { const option = document.createElement("option"); option.value = device.deviceId; option.text = device.label || `麦克风 ${select.length + 1}`; select.appendChild(option); }); select.addEventListener("change", async (e) => { if (localTracks.audioTrack) { await localTracks.audioTrack.setDevice(e.target.value); } }); deviceSelector.appendChild(select); } if (videoInputs.length > 1) { const select = document.createElement("select"); videoInputs.forEach(device => { const option = document.createElement("option"); option.value = device.deviceId; option.text = device.label || `摄像头 ${select.length + 1}`; select.appendChild(option); }); select.addEventListener("change", async (e) => { if (localTracks.videoTrack) { await localTracks.videoTrack.setDevice(e.target.value); } }); deviceSelector.appendChild(select); } document.getElementById("app").appendChild(deviceSelector); } // 在页面加载完成后调用 window.addEventListener("load", setupDeviceSelector);通话状态显示
添加通话计时器和状态显示:
let callTimer = null; let callStartTime = null; function startCallTimer() { callStartTime = new Date(); callTimer = setInterval(() => { const now = new Date(); const duration = Math.floor((now - callStartTime) / 1000); const minutes = Math.floor(duration / 60).toString().padStart(2, "0"); const seconds = (duration % 60).toString().padStart(2, "0"); document.getElementById("call-timer").textContent = `${minutes}:${seconds}`; }, 1000); } function stopCallTimer() { clearInterval(callTimer); document.getElementById("call-timer").textContent = "00:00"; }记得在HTML中添加计时器显示元素:
<div id="call-status"> <span id="call-timer">00:00</span> </div>6. 部署与测试
完成开发后,我们需要测试和部署应用。可以使用任何静态文件服务器来运行这个应用:
npx serve public测试时需要注意以下几点:
- 在浏览器中打开两个标签页,分别作为两个用户加入同一频道
- 测试不同的网络环境(WiFi、4G等)
- 尝试切换不同的音视频设备
- 测试离开和重新加入频道的场景
提示:在生产环境中,你应该实现用户认证和Token生成机制。Agora提供了服务端SDK来帮助你生成临时Token。
7. 常见问题排查
开发过程中可能会遇到各种问题,这里列出一些常见问题及解决方法:
没有视频/音频:
- 检查浏览器是否获得了麦克风和摄像头权限
- 确认没有其他应用独占音频设备
- 检查控制台是否有错误信息
加入频道失败:
- 确认AppID正确
- 检查网络连接
- 如果是正式环境,确认Token有效
回声或啸叫:
- 使用耳机而非扬声器
- 降低麦克风灵敏度
- 启用Agora的音频处理功能
// 启用高级音频处理 localTracks.audioTrack = await AgoraRTC.createMicrophoneAudioTrack({ AEC: true, // 回声消除 ANS: true, // 噪声抑制 AGC: true // 自动增益控制 });- 视频卡顿或模糊:
- 检查网络状况
- 调整视频编码参数
- 考虑使用更适合网络状况的编解码器
// 调整视频编码参数 localTracks.videoTrack = await AgoraRTC.createCameraVideoTrack({ encoderConfig: "720p_1", // 720p分辨率 bitrateMin: 1000, // 最小码率 bitrateMax: 3000 // 最大码率 });8. 性能优化建议
随着功能的完善,我们需要考虑性能优化:
- 按需发布流:不是所有场景都需要同时发布音视频
// 只发布视频 await client.publish(localTracks.videoTrack); // 只发布音频 await client.publish(localTracks.audioTrack);- 自适应码率:根据网络状况动态调整
// 监听网络质量 client.on("network-quality", (stats) => { // 根据下行/上行网络质量调整码率 if (localTracks.videoTrack) { if (stats.uplinkNetworkQuality > 3) { // 网络质量差 localTracks.videoTrack.setEncoderConfiguration("480p_1"); } else { localTracks.videoTrack.setEncoderConfiguration("720p_1"); } } });- 带宽估计与流回退:在弱网环境下自动降级
// 启用流回退 await client.setStreamFallbackOption(remoteUser.uid, 2); // 2表示在网络差时只接收小流或音频 // 或者手动控制订阅 await client.subscribe(remoteUser, "audio"); // 只订阅音频- 日志收集:帮助排查问题
// 启用详细日志 AgoraRTC.setLogLevel(4); // DEBUG级别 AgoraRTC.enableLogUpload();- 内存管理:及时释放资源
// 离开频道时释放资源 window.addEventListener("beforeunload", async () => { if (client.connectionState === "CONNECTED") { await client.leave(); } for (const trackName in localTracks) { if (localTracks[trackName]) { localTracks[trackName].close(); } } });9. 安全最佳实践
音视频应用需要特别注意安全问题:
- 使用Token认证:永远不要将App Certificate硬编码在客户端
- 频道权限控制:合理设置用户角色和权限
- 端到端加密:对敏感通信启用加密
// 启用加密 await client.join(config.appId, config.channel, config.token, null, { encryption: { mode: "aes-128-gcm", // 加密模式 key: "your-encryption-key" // 加密密钥 } });- 合理设置Token过期时间:临时Token应该有较短的TTL
- 用户身份验证:将Agora UID与你的业务系统用户关联
10. 从Demo到生产环境
将这个Demo转化为生产级应用还需要考虑:
- 用户系统集成:将Agora UID与你的用户系统关联
- 房间管理:实现创建/加入/离开房间的逻辑
- UI/UX优化:添加更多用户友好的功能
- 通话控制(静音、关闭视频)
- 屏幕共享
- 通话录制
- 美颜滤镜
// 屏幕共享示例 async function startScreenShare() { const screenTrack = await AgoraRTC.createScreenVideoTrack(); await client.unpublish(localTracks.videoTrack); await client.publish(screenTrack); localTracks.videoTrack = screenTrack; }- 错误监控:实现前端错误收集和分析
- 数据分析:跟踪通话质量和用户行为
// 获取通话统计数据 setInterval(async () => { const stats = await client.getRTCStats(); console.log("通话统计:", stats); }, 10000);- 多平台支持:考虑移动端和桌面端的适配
在实际项目中,我们会发现Agora RTC SDK的API设计其实非常直观,关键是要理解几个核心概念:Client、Track、Publish/Subscribe。通过这个简单的1v1视频通话Demo,你应该已经掌握了这些概念的实际应用。
