从零到一:基于声网Agora的医疗视频问诊前端实战指南
关键词:声网Agora、实时视频通话、微信小程序、医疗问诊、前端架构
📋 文章导读
本文基于一个真实的医疗问诊项目,深入剖析如何从零开始 构建一个稳定、高效的实时视频通话系统。无论你是刚接触音视频开发的新手,还是需要快速上手现有项目的前端工程师,这篇文章都将为你提供完整的实现思路和实战代码。
你将学到:
- ✅ 声网Agora在小程序中的完整集成流程
- ✅ 前后端协同开发的8个关键节点
- ✅ 视频布局管理的核心算法
- ✅ 组件化架构的设计思路
- ✅ 生产环境中的最佳实践
🏥 项目背景:医疗视频问诊
一句话看懂项目 :这是一个为医疗 开发的在线医疗服务平台,核心功能是患者与医生的实时视频通话,支持一对一、一对多的远程问诊。
技术栈速览
- 前端框架:微信小程序原生开发
- 音视频SDK:声网Agora
- 状态管理:Redux(自定义实现)
- 即时通讯:网易云信NIM(配合视频使用)
- 网络请求:基于wx.request的自定义封装
为什么要用Agora?
- 合规性:医疗行业对数据安全和隐私保护要求极高
- 稳定性:99.999%的可用性,确保问诊过程不中断
- 低延迟:全球实时网络,平均延迟<200ms
- 高清质量:支持720P/1080P高清视频
- 完整生态:完善的文档和技术支持
🚀 第一章:快速上手(5分钟跑通)
如果你是完全的新手,先从这里开始
1.1 最简视频通话实现
javascript
// 1. 引入Agora SDK
const AgoraMiniappSDK = require("../lib/agora/Agora_Miniapp_SDK_for_WeChat.js");
// 2. 初始化客户端
const client = new AgoraMiniappSDK.Client();
// 3. 初始化SDK
client.init(
"你的APPID", // 从声网控制台获取
() => {
console.log("SDK初始化成功");
// 4. 加入频道
client.join(
null, // Token(生产环境需要)
"问诊房间_123", // 频道名
"患者_456", // 用户ID
() => {
console.log("加入频道成功");
// 5. 开始推流(如果是主播)
client.publish((url) => {
console.log("推流地址:", url);
// 这个url就是你的视频流地址
});
}
);
}
);
1.2 项目中的实际调用
上面的逻辑被封装:
javascript
// 实际项目中的初始化方法
initAgoraChannel: function (uid, channel) {
return new Promise((resolve, reject) => {
let client = new AgoraMiniappSDK.Client();
// 订阅事件(后面会详细讲)
this.subscribeEvents(client);
this.client = client;
client.init(
wx.getStorageSync("APPID"), // 从本地存储读取
() => {
client.join(
null, // Token(这里简化了)
channel, // 频道名
uid, // 用户ID
() => {
client.setRole(this.role); // 设置角色
if (this.isBroadcaster()) {
client.publish(
(url) => resolve(url), // 成功回调
(e) => reject(e) // 失败回调
);
} else {
resolve();
}
}
);
}
);
});
}
🏗️ 第二章:前后端协同开发(8个关键节点)
音视频开发不是前端单打独斗,需要前后端紧密配合
2.1 后端需要提供的API
javascript
// 1. 获取APPID(环境相关)
// 后端需要根据不同的环境返回不同的APPID
GET /api/config/agora/appid
// 2. 获取Token(安全认证)
// Token是加入频道的安全凭证,必须由后端生成
POST /api/agora/token
{
"channel": "问诊房间_123",
"uid": "患者_456",
"role": "broadcaster" // 或 "audience"
}
// 3. 注册视频问诊(业务关联)
// 在项目中,这个接口还关联了业务逻辑
POST /XHealthWebService/XVideoVisit/videoRegistration/registration/visit/{visitXID}
// 4. 注销视频问诊(清理资源)
POST /XHealthWebService/XVideoVisit/videoRegistration/unregistration/visit/{visitXID}/token/{streamToken}
2.2 前端如何调用这些接口
javascript
// 1. 获取APPID并存储
// 位置:visit-detail.js 第44-54行
wx.setStorageSync("APPID", "c17ac42b1d5f494fa26ea8431efad2ef");
// 2. 注册视频问诊(获取streamToken)
// 位置:video-visit.js 第143-157行
request({
url: "XHealthWebService/XVideoVisit/videoRegistration/registration/visit/" +
wx.getStorageSync("visitXID"),
method: "post"
}).then(({ data: { data = {} } }) => {
this.setData({
streamToken: data.register_token, // 后端返回的token
});
});
// 3. 获取用户属性(显示医生信息)
// 位置:video-visit.js 第886-901行
request({
url: `/${wx.getStorageSync("APPID")}/${uid}/${this.channel}`,
method: "get",
header: {
Authorization: "Basic ZThiNjAzMDAyOGU4NDE1MGIxMzg4YTI0MzQ4MWU4MGI6NWM5MDE1YmNlODRkNDJiZjgxYWYzNzM3YzNlOThlOGY="
}
}, "https://api.agora.io/dev/v1/channel/user/property")
.then((res) => {
// res.data.data.account 就是医生的账号信息
});
2.3 数据流向图
┌─────────┐ 注册请求 ┌─────────┐
│ 前端 │ ───────────► │ 后端 │
│ │ │ │
│ App │ │ API │
│ │ │ │
│ SDK │ ◄─────────── │ Token │
└─────────┘ 返回Token └─────────┘
│ │
│ 加入频道 │
│ 带Token │
▼ │
┌─────────┐ │
│ 声网 │ │
│ 服务器 │ ◄──────────────────┘
└─────────┘
│
│ 实时音视频流
▼
┌─────────┐
│ 对方 │
│ 设备 │
└─────────┘
🎨 第三章:视频布局管理(核心算法)
这是项目中最精彩的部分,也是面试常考的点
3.1 为什么需要布局管理?
想象一下:1个人全屏显示,2个人上下平分,3个人"品"字形,4个人田字格...
手动计算位置?累死你!
3.2 项目中的Layout类
javascript
class Layouter {
constructor(containerWidth, containerHeight) {
this.containerWidth = containerWidth;
this.containerHeight = containerHeight;
}
// 核心方法:根据用户数返回布局位置
getSize(totalUser) {
switch (totalUser) {
case 1: // 一个人:全屏
return [{ x: 0, y: 0, width: 整个宽度, height: 整个高度 }];
case 2: // 两个人:上下平分
return [
{ x: 0, y: 0, width: 整个宽度, height: 一半高度 },
{ x: 0, y: 一半高度, width: 整个宽度, height: 一半高度 }
];
case 3: // 三个人:品字形
return [
{ x: 0, y: 0, width: 一半宽度, height: 一半高度 },
{ x: 一半宽度, y: 0, width: 一半宽度, height: 一半高度 },
{ x: 0, y: 一半高度, width: 一半宽度, height: 一半高度 }
];
// ... 支持最多10人
}
}
}
3.3 实际使用场景
javascript
// 1. 初始化布局管理器
initLayouter: function() {
wx.getSystemInfo({
success: (res) => {
this.layouter = new Layouter(res.windowWidth, res.windowHeight);
}
});
}
// 2. 添加视频流时自动计算位置
addMedia: function(type, uid, url, options) {
// 获取当前总用户数
const totalUser = this.data.media.length + 1;
// 获取布局位置
const layouts = this.layouter.getSize(totalUser);
const position = layouts[totalUser - 1]; // 最后一个位置给新用户
// 添加到media数组
this.setData({
media: [...this.data.media, {
type: type, // 0=本地,1=远程
uid: uid,
url: url,
left: position.x,
top: position.y,
width: position.width,
height: position.height,
...options
}],
totalUser: totalUser
});
}
3.4 布局算法的优化点
javascript
// 项目中的实际考虑
getSize(totalUser) {
let videoContainerHeight = this.containerHeight;
let videoContainerWidth = this.containerWidth;
// 根据设备类型调整
if (totalUser > 4 && 设备是手机) {
// 手机上超过4人时采用滚动布局
return this.getScrollLayout(totalUser);
}
// 保持视频的宽高比(医疗问诊需要清晰度)
const aspectRatio = 4/3; // 4:3的比例
// 计算时考虑边距
const margin = 10;
// ... 具体的布局计算
}
🔧 第四章:组件化架构设计
好的架构让代码可维护性提升10倍
4.1 为什么需要组件化?
- 复用性:推流、拉流逻辑可以复用
- 可维护性:每个组件职责单一
- 可测试性:组件可以独立测试
- 团队协作:不同人负责不同组件
4.2 常规项目中的组件结构
pages/video-subpage/components/
├── agora-pusher/ # 推流组件(本地视频)
│ ├── agora-pusher.js # 组件逻辑
│ ├── agora-pusher.wxml # 模板
│ └── agora-pusher.wxss # 样式
├── agora-player/ # 拉流组件(远程视频)
│ ├── agora-player.js
│ ├── agora-player.wxml
│ └── agora-player.wxss
└── time-down/ # 倒计时组件
4.3 推流组件详解
javascript
// agora-pusher.js 核心代码
Component({
properties: {
url: { type: String, value: "" }, // 推流地址
enableCamera: { type: Boolean, value: true }, // 是否启用摄像头
muted: { type: Boolean, value: false }, // 是否静音
width: { type: Number, value: 0 }, // 宽度
height: { type: Number, value: 0 }, // 高度
},
methods: {
// 开始推流
start() {
this.data.pusherContext.start();
},
// 停止推流
stop() {
this.data.pusherContext.stop();
},
// 切换摄像头
switchCamera() {
this.data.pusherContext.switchCamera();
},
// 静音/取消静音
mute() {
this.data.pusherContext.mute();
},
unmute() {
this.data.pusherContext.unmute();
}
}
});
4.4 页面中如何使用组件
xml
<!-- video-visit.wxml -->
<view class="video-container n{{ totalUser }}">
<!-- 本地视频(推流组件) -->
<agora-pusher
wx:if="{{ item.type === 0 && !item.holding }}"
id="rtc-pusher"
url="{{ item.url }}"
x="{{ item.left }}"
y="{{ item.top }}"
width="{{ item.width }}"
height="{{ item.height }}"
agoramuted="{{ muted }}"
enableCamera="{{ enableCamera }}"
bindpushfailed="onPusherFailed"
></agora-pusher>
<!-- 远程视频(拉流组件) -->
<agora-player
wx:if="{{ item.type === 1 && !item.holding }}"
id="rtc-player-{{ item.uid }}"
provideId="{{ item.provideId }}"
x="{{ item.left }}"
y="{{ item.top }}"
width="{{ item.width }}"
height="{{ item.height }}"
uid="{{ item.uid }}"
url="{{ item.url }}"
></agora-player>
</view>
4.5 组件与页面的通信
javascript
// 页面获取组件实例
getPusherComponent: function() {
return this.selectComponent("#rtc-pusher");
},
// 调用组件方法
onSwitchCamera: function() {
const agoraPusher = this.getPusherComponent();
agoraPusher && agoraPusher.switchCamera();
},
toggleMute: function() {
this.setData({ muted: !this.data.muted });
const agoraPusher = this.getPusherComponent();
if (agoraPusher) {
if (this.data.muted) {
agoraPusher.mute();
} else {
agoraPusher.unmute();
}
}
}
🎯 第五章:事件驱动架构
音视频开发本质是事件驱动的
5.1 Agora SDK的核心事件
javascript
// 位置:video-visit.js 第841-940行
subscribeEvents: function(client) {
// 1. 视频旋转事件(横竖屏切换)
client.on("video-rotation", (e) => {
console.log(`视频旋转: ${e.rotation}度,用户: ${e.uid}`);
// 调整视频方向
const player = this.getPlayerComponent(e.uid);
player && player.rotate(e.rotation);
});
// 2. 新流加入事件(有人进入房间)
client.on("stream-added", (e) => {
console.log(`新用户加入: ${e.uid}`);
// 订阅这个用户的视频流
client.subscribe(e.uid, (url, rotation) => {
console.log(`订阅成功,URL: ${url}`);
this.addMedia(1, e.uid, url, { rotation: rotation });
});
});
// 3. 流移除事件(有人离开房间)
client.on("stream-removed", (e) => {
console.log(`用户离开: ${e.uid}`);
this.removeMedia(e.uid);
});
// 4. 错误事件
client.on("error", (err) => {
console.error(`Agora错误: ${err.code}, 原因: ${err.reason}`);
// 触发重连机制
this.reconnect();
});
}
5.2 项目中的完整事件流
┌─────────────────┐
│ 页面onLoad │
└─────────┬───────┘
│ 1. 初始化布局
▼
┌─────────┴───────┐
│ initLayouter() │
└─────────┬───────┘
│ 2. 初始化Agora
▼
┌─────────┴───────┐
│ initAgoraChannel│
└─────────┬───────┘
│ 3. 订阅事件
▼
┌─────────┴───────┐
│ subscribeEvents │
└─────────┬───────┘
│ 4. 加入频道
▼
┌─────────┴───────┐
│ client.join │
└─────────┬───────┘
│ 5. 开始推流
▼
┌─────────┴───────┐
│ client.publish │
└─────────┬───────┘
│ 6. 获取推流URL
▼
┌─────────┴───────┐
│ 回调URL │
└─────────────────┘
5.3 错误处理与重连机制
javascript
// 位置:video-visit.js 第706-740行(重连实现)
reinitAgoraChannel: function(uid, channel) {
return new Promise((resolve, reject) => {
// 1. 创建新的客户端
let client = new AgoraMiniappSDK.Client();
// 2. 重新订阅事件
this.subscribeEvents(client);
// 3. 获取之前的所有用户ID
let uids = this.data.media.map((item) => item.uid);
// 4. 设置角色
client.setRole(this.role);
// 5. 重新初始化
client.init(wx.getStorageSync("APPID"), () => {
// 6. 重新加入(带之前的用户列表)
client.rejoin(null, channel, uid, uids, () => {
// 7. 重新发布
if (this.isBroadcaster()) {
client.publish((url) => resolve(url));
} else {
resolve();
}
});
});
});
}
🛠️ 第六章:生产环境最佳实践
6.1 性能优化
javascript
// 1. 按需加载SDK
// 不要一开始就加载所有SDK,等用户点击"开始问诊"再加载
loadAgoraSDK: function() {
return new Promise((resolve) => {
if (typeof AgoraMiniappSDK !== 'undefined') {
resolve(AgoraMiniappSDK);
} else {
// 动态加载
require("../lib/agora/Agora_Miniapp_SDK_for_WeChat.js");
resolve(AgoraMiniappSDK);
}
});
}
// 2. 内存管理
onUnload: function() {
// 清理资源
if (this.client) {
this.client.leave();
}
clearInterval(this._intervalTime);
clearInterval(this.logTimer);
clearTimeout(this.reconnectTimer);
}
// 3. 网络状态监测
// 位置:video-visit.js 第935-950行
client.on("error", (err) => {
if (err.code === 1005) { // 网络断开
this.startReconnect();
}
});
6.2 日志与监控
javascript
// 1. 设置日志级别
AgoraMiniappSDK.LOG.setLogLevel(-1); // -1=所有日志,0=错误,1=警告,2=信息
// 2. 自定义日志回调
AgoraMiniappSDK.LOG.onLog = (text) => {
// 可以在这里将日志发送到自己的服务器
Utils.log(`[Agora] ${text}`);
// 项目中的实际实现还有日志上传
this.uploadLogs();
};
// 3. 定期上传日志
uploadLogs: function() {
const uploader = new LogUploader({
appId: wx.getStorageSync("APPID"),
channel: this.channel,
uid: this.uid
});
uploader.upload();
}
6.3 用户体验优化
javascript
// 1. 加载状态提示
startVideoCall: function() {
wx.showLoading({ title: "正在连接医生...", mask: true });
this.initAgoraChannel(this.uid, this.channel)
.then((url) => {
wx.hideLoading();
this.startPublish();
})
.catch((err) => {
wx.hideLoading();
wx.showToast({
title: "连接失败,请重试",
icon: "none"
});
});
}
// 2. 网络状态提示
showNetworkStatus: function() {
const networkType = wx.getNetworkType();
if (networkType !== 'wifi') {
wx.showToast({
title: "当前使用移动网络,请注意流量",
icon: "none",
duration: 3000
});
}
}
// 3. 电量优化
wx.setKeepScreenOn({
keepScreenOn: true // 通话期间保持屏幕常亮
});
🎓 第七章:新手上路指南
熟悉环境
- 跑通demo:一个简单视频通话
- 理解数据流:画一下消息从发送到接收的流程图
动手实践
- 修改样式:先改个简单的,比如视频边框颜色
- 添加功能:比如添加"美颜强度"调节
- 调试问题:模拟网络断开,看重连机制
7.2 学习路径建议
javascript
// Day 1: 基础概念
1. 什么是声网Agora?
2. 实时音视频的基本原理
3. 项目中的使用场景
// Day 2: 代码结构
1. 查看视频通话页面的onLoad方法
2. 理解initAgoraChannel的流程
3. 查看组件的基本使用
// Day 3: 事件系统
1. 理解stream-added事件
2. 理解stream-removed事件
3. 理解错误处理和重连
// Day 4: 布局管理
1. 查看layout.js的实现
2. 理解不同人数下的布局算法
3. 尝试修改布局规则
// Day 5: 前后端协同
1. 理解APPID和Token的作用
2. 查看后端接口调用
3. 理解业务关联逻辑
7.3 常见问题排查
javascript
// Q1: 视频黑屏怎么办?
// A: 按以下顺序排查
1. 检查摄像头权限
2. 检查enableCamera是否为true
3. 检查推流URL是否正确
4. 查看控制台日志
// Q2: 听不到声音怎么办?
// A:
1. 检查麦克风权限
2. 检查muted是否为false
3. 检查设备音量
4. 检查网络状况
// Q3: 连接失败怎么办?
// A:
1. 检查APPID是否正确
2. 检查网络连接
3. 检查Token是否过期
4. 查看错误代码
// Q4: 视频卡顿怎么办?
// A:
1. 检查网络带宽
2. 降低视频分辨率
3. 检查设备性能
4. 查看Agora控制台的质量报告
📈 第八章:进阶思考
8.1 架构优化方向
javascript
// 1. 状态管理优化
// 目前使用Redux,可以考虑:
// - 使用Mobx简化状态管理
// - 使用Context API管理局部状态
// - 实现状态持久化
// 2. 组件通信优化
// 目前使用selectComponent,可以考虑:
// - 使用EventBus进行组件通信
// - 使用Redux管理组件状态
// - 使用自定义事件
// 3. 性能监控
// 可以添加:
// - 视频质量监控
// - 网络状态监控
// - 用户行为分析
8.2 业务扩展可能
javascript
// 1. 多人会诊
// 目前支持最多10人,可以扩展到:
// - 支持更多人的视频布局
// - 实现主持人控制
// - 添加屏幕共享
// 2. 录制功能
// 医疗问诊需要留存记录:
// - 云录制服务
// - 本地录制
// - 录制文件管理
// 3. AI辅助
// 结合AI技术:
// - 语音转文字
// - 病情分析
// - 智能导诊
8.3 技术债务清理
javascript
// 1. 代码重构
// - 提取公共工具函数
// - 统一错误处理
// - 优化组件接口
// 2. 文档完善
// - 添加API文档
// - 添加部署文档
// - 添加故障排查手册
// 3. 测试完善
// - 添加单元测试
// - 添加集成测试
// - 添加性能测试
🎉 总结
通过这个医疗问诊项目的实战分析,我们看到了一个完整的、生产级别的实时视频通话系统 是如何构建的。从前后端协同 到组件化架构 ,从事件驱动 到错误处理,每一个环节都体现了工程化的思考。
关键收获:
- 音视频开发不是孤立的:需要前后端紧密配合
- 组件化是复杂应用的基础:好的架构提升可维护性
- 错误处理决定用户体验:完善的错误处理让产品更稳定
- 监控和日志是生产环境的眼睛:没有监控就等于盲人摸象
给新人的最后建议:
不要试图一次性理解所有代码!
按照这个路线图:
- 先跑通最简单的demo
- 理解核心流程
- 逐个模块深入学习
- 动手实践,从简单修改开始
音视频开发虽然复杂,但有章可循 。这个项目已经为你搭建好了完整的框架,你需要的不是从头发明轮子,而是学会使用这些轮子,然后让它们跑得更快、更稳。
📚 参考资料
-
官方文档
-
项目内部文档
项目核心技术解析文档.md声网Agora SDK详细使用指南.md快速上手开发指南.md
-
相关技术
- WebRTC原理
- 实时网络传输
- 视频编码/解码