从零到一:基于声网Agora的医疗视频问诊前端实战指南

从零到一:基于声网Agora的医疗视频问诊前端实战指南

关键词:声网Agora、实时视频通话、微信小程序、医疗问诊、前端架构

📋 文章导读

本文基于一个真实的医疗问诊项目,深入剖析如何从零开始 构建一个稳定、高效的实时视频通话系统。无论你是刚接触音视频开发的新手,还是需要快速上手现有项目的前端工程师,这篇文章都将为你提供完整的实现思路和实战代码

你将学到:

  1. 声网Agora在小程序中的完整集成流程
  2. 前后端协同开发的8个关键节点
  3. 视频布局管理的核心算法
  4. 组件化架构的设计思路
  5. 生产环境中的最佳实践

🏥 项目背景:医疗视频问诊

一句话看懂项目 :这是一个为医疗 开发的在线医疗服务平台,核心功能是患者与医生的实时视频通话,支持一对一、一对多的远程问诊。

技术栈速览

  • 前端框架:微信小程序原生开发
  • 音视频SDK:声网Agora
  • 状态管理:Redux(自定义实现)
  • 即时通讯:网易云信NIM(配合视频使用)
  • 网络请求:基于wx.request的自定义封装

为什么要用Agora?

  1. 合规性:医疗行业对数据安全和隐私保护要求极高
  2. 稳定性:99.999%的可用性,确保问诊过程不中断
  3. 低延迟:全球实时网络,平均延迟<200ms
  4. 高清质量:支持720P/1080P高清视频
  5. 完整生态:完善的文档和技术支持

🚀 第一章:快速上手(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 为什么需要组件化?

  1. 复用性:推流、拉流逻辑可以复用
  2. 可维护性:每个组件职责单一
  3. 可测试性:组件可以独立测试
  4. 团队协作:不同人负责不同组件

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  // 通话期间保持屏幕常亮
});

🎓 第七章:新手上路指南

熟悉环境
  1. 跑通demo:一个简单视频通话
  2. 理解数据流:画一下消息从发送到接收的流程图
动手实践
  1. 修改样式:先改个简单的,比如视频边框颜色
  2. 添加功能:比如添加"美颜强度"调节
  3. 调试问题:模拟网络断开,看重连机制

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. 测试完善
// - 添加单元测试
// - 添加集成测试
// - 添加性能测试

🎉 总结

通过这个医疗问诊项目的实战分析,我们看到了一个完整的、生产级别的实时视频通话系统 是如何构建的。从前后端协同组件化架构 ,从事件驱动错误处理,每一个环节都体现了工程化的思考。

关键收获:

  1. 音视频开发不是孤立的:需要前后端紧密配合
  2. 组件化是复杂应用的基础:好的架构提升可维护性
  3. 错误处理决定用户体验:完善的错误处理让产品更稳定
  4. 监控和日志是生产环境的眼睛:没有监控就等于盲人摸象

给新人的最后建议:

不要试图一次性理解所有代码!

按照这个路线图:

  1. 先跑通最简单的demo
  2. 理解核心流程
  3. 逐个模块深入学习
  4. 动手实践,从简单修改开始

音视频开发虽然复杂,但有章可循 。这个项目已经为你搭建好了完整的框架,你需要的不是从头发明轮子,而是学会使用这些轮子,然后让它们跑得更快、更稳


📚 参考资料

  1. 官方文档

  2. 项目内部文档

    • 项目核心技术解析文档.md
    • 声网Agora SDK详细使用指南.md
    • 快速上手开发指南.md
  3. 相关技术

    • WebRTC原理
    • 实时网络传输
    • 视频编码/解码

相关推荐
GISer_Jing1 小时前
LangChain浏览器Agent开发全攻略
前端·ai·langchain
小李子呢02112 小时前
前端八股---脚手架工具Vue CLI(Webpack) vs Vite
前端·vue.js·webpack
2401_885885042 小时前
群发彩信接口怎么开发?企业级彩信发送说明
前端·python
PILIPALAPENG2 小时前
第2周 Day 5:前端转型AI开发,朋友问我,你到底在折腾啥?
前端·人工智能·python
Mintopia2 小时前
前端卡顿的真相:不是你代码慢,是你阻塞了
前端
kyriewen2 小时前
可选链 `?.`——再也不用写一长串 `&&` 了!
前端·javascript·ecmascript 6
Mintopia2 小时前
别再乱加缓存:一套判断"该不该缓存"的方法
前端
Leisureconfused2 小时前
【记录】Node版本兼容性问题及解决
前端·vue.js·npm·node.js
Highcharts.js2 小时前
React 应用中的图表选择:Highcharts vs Apache ECharts 深度对比
前端·javascript·react.js·echarts·highcharts·可视化图表·企业级图表