微信小程序实现视频面诊:从 0 到 1 的完整技术方案

微信小程序中怎么实现视频面诊功能?本文介绍了视频面诊的技术模块、实时音视频方案选型以及基于 ZEGO SDK 的关键实现步骤。

视频面诊

前言:为什么要做视频面诊

2020 年以来,国家卫健委陆续出台政策,明确互联网医院可开展部分常见病、慢性病的复诊服务,视频问诊从”灰色地带”走向合规主流。2022 年《互联网诊疗监管细则》进一步规范了平台资质和诊疗行为,给了行业一个清晰的合规框架。

哪些科室最适合视频面诊?

科室适合原因
皮肤科皮损可视化,医生通过高清视频即可初步判断
中医望诊面色、舌苔、神态是核心诊断依据
慢病复诊高血压、糖尿病患者定期随访,无需每次到院
心理咨询面部表情和肢体语言是重要的诊断信息
儿科初诊减少交叉感染风险,家长在家即可完成初步问诊

为什么选微信小程序而不是 APP?

  • 免安装,患者打开微信即用,转化率远高于 APP。
  • 微信生态内天然具备社交传播能力。
  • 开发和维护成本低于双端原生 APP。
  • 微信支付、实名认证等基础能力开箱即用。

核心能力拆解:视频面诊需要哪些技术模块

很多开发者一开始把视频面诊理解成”接个视频通话”,上线后才发现坑远不止于此。完整的视频面诊系统一般需要以下模块:

模块核心功能技术复杂度
实时音视频通话医患双向视频,低延迟、抗弱网★★★
候诊排队系统叫号、等待状态推送、超时处理★★
身份核验医生资质认证 + 患者实名制★★
电子病历联动问诊前填写主诉、诊后记录处方★★
录制与存档合规要求的诊疗录像保存(境内服务器)★★★
消息通知叫号提醒、问诊结束通知

本文聚焦最核心的实时音视频通话模块,其余模块可在此基础上逐步叠加。

技术选型:实时音视频方案怎么选

微信小程序的实时音视频方案主要有三条路:

方案 A:微信原生音视频通话组件

  • 原生组件,需申请特定类目资质。
  • 限制多(仅支持特定场景)、审核严,但接入后体验最原生。
  • 适合:已有微信生态深度合作的大型医疗机构。

方案 B:接入即构 ZEGO 实时音视频 SDK

  • 跨平台能力强,同一套业务逻辑可复用到 iOS/Android/Web等平台。
  • 微信小程序端提供专用 SDK,封装了 <zego-pusher> / <zego-player> 组件,屏蔽了原生组件的复杂性。
  • 全球平均端到端延迟 200ms,支持 80% 丢包环境下的流畅通话。
  • 提供远程医疗场景的完整解决方案参考。

方案 C:腾讯云实时音视频(TRTC)

  • 与微信小程序原生集成最顺畅,底层共用腾讯基础设施。
  • 使用 <live-pusher> + <live-player> 原生组件。
  • 文档完善,腾讯云全家桶用户迁移成本低。

三方案对比

维度TRTC微信原生ZEGO
延迟低(<300ms)低(<200ms)
费用按分钟计费免费(限制多)按分钟计费
接入难度中等高(类目限制严)
跨平台复用一般不支持
合规录制支持一般
弱网优化一般

本文以 ZEGO 作为实现方案,以下为核心实现步骤。

关键实现步骤(基于即构 ZEGO SDK)

前提条件

注册即构账号,创建项目

  1. 前往ZEGO 控制台注册账号;
  2. 创建项目,申请有效的 AppID 和 AppSign;
  3. 开通实时音视频(RTC)服务

小程序类目申请(必须先做)

微信小程序使用音视频推拉流组件,需满足:

条件说明
小程序类目医疗 > 互联网医院 / 在线问诊
所需资质互联网医院牌照 或 卫健委备案证明
组件权限在微信公众平台后台手动开启”实时播放音视频流”和”实时录制音视频流”

⚠️ 未开通类目直接调用会静默失败,是最常见的踩坑点。开发阶段可用测试号绕过,但上线前必须完成类目审核。

配置微信小程序后台域名白名单

在”小程序后台 > 开发管理 > 开发设置 > 服务器域名”中,将 ZEGO 的 Server 地址和 LogUrl 填入 request 合法域名socket 合法域名。具体地址在 ZEGO 控制台的项目信息页获取。

集成 ZEGO 小程序实时音视频 SDK

ZEGO 提供专用的微信小程序 SDK,推荐通过 npm 安装:

npm install zego-express-engine-miniprogram

安装后在微信开发者工具菜单栏选择”工具 > 构建 npm”,并勾选”使用 npm 模块”。

在页面 JS 文件顶部引入:

import { ZegoExpressEngine } from "zego-express-engine-miniprogram";

同时,将官方示例代码中的 components/zego-pushercomponents/zego-player 两个组件文件夹复制到项目的 components 目录下(示例源码地址)。

后端生成 Token(安全鉴权)

Token 必须由服务端生成,不能在前端硬编码 AppSign。每次用户进入问诊房间前,前端向后端请求一个有时效的 Token。

Node.js 示例(后端):

const crypto = require('crypto');

function generateToken04(appId, userId, serverSecret, expireTime) {
  const expire = Math.floor(Date.now() / 1000) + expireTime;
  const nonce = Math.floor(Math.random() * 2147483647);
  const ctime = Math.floor(Date.now() / 1000);

  const plaintext = `${appId}${userId}${nonce}${ctime}${expire}`;
  const hmac = crypto.createHmac('sha256', serverSecret);
  hmac.update(plaintext);
  const hash = hmac.digest('hex');

  const tokenInfo = {
    ver: 4,
    hash,
    expire,
    nonce,
    ctime,
  };

  const tokenStr = Buffer.from(JSON.stringify(tokenInfo)).toString('base64');
  return `04${tokenStr}`;
}

// 接口:前端调用获取 Token
app.get('/api/token', (req, res) => {
  const { userId } = req.query;
  const token = generateToken04(
    YOUR_APP_ID,
    userId,
    YOUR_SERVER_SECRET, // 控制台获取,绝不能暴露给前端
    3600  // 1小时有效
  );
  res.json({ token });
});

注意:控制台提供临时 Token 生成工具,方便开发调试,但生产环境必须走自己的服务端。

初始化 ZEGO 引擎

在问诊页面的 JSON 文件中引入组件:

{
  "usingComponents": {
    "zego-pusher": "../../components/zego-pusher/zego-pusher",
    "zego-player": "../../components/zego-player/zego-player"
  }
}

在页面 JS 的 onLoad 中初始化引擎:

import { ZegoExpressEngine } from "zego-express-engine-miniprogram";

Page({
  data: {
    pusher: {},       // 推流属性,由 SDK 管理
    playerList: [],   // 拉流列表,由 SDK 管理
    zegoPlayerList: [],
  },

  async onLoad(options) {
    const { roomId, userId } = options;

    // 1. 从后端获取 Token
    const res = await new Promise(resolve =>
      wx.request({ url: `https://your-server.com/api/token?userId=${userId}`, success: resolve })
    );
    const token = res.data.token;

    // 2. 创建引擎实例(appID 为数字,server 为控制台获取的 Server 地址)
    const zg = new ZegoExpressEngine(YOUR_APP_ID, YOUR_SERVER);

    // 3. 绑定页面上下文(必须在登录房间前调用)
    zg.initContext({
      wxContext: this,
      pushAtr: 'pusher',      // 与 data 中的字段名一致
      playAtr: 'playerList',  // 与 data 中的字段名一致
    });

    this.zg = zg;
    this.roomId = roomId;
    this.userId = userId;
    this.token = token;

    // 4. 注册事件监听(必须在 loginRoom 前设置)
    this.registerEvents();
  },

加入房间 + 推拉流

  registerEvents() {
    const zg = this.zg;

    // 房间连接状态
    zg.on('roomStateUpdate', (roomID, state, errorCode) => {
      if (state === 'CONNECTED') {
        // 登录成功后开始推流
        this.startPublish();
      }
      if (state === 'DISCONNECTED') {
        this.handleDisconnect(errorCode);
      }
    });

    // 远端流变化(医生进入/离开)
    zg.on('roomStreamUpdate', (roomID, updateType, streamList) => {
      if (updateType === 'ADD') {
        streamList.forEach(stream => this.startPlay(stream.streamID));
      }
      if (updateType === 'DELETE') {
        streamList.forEach(stream => this.stopPlay(stream.streamID));
      }
    });
  },

  async joinRoom() {
    const { roomId, userId, token } = this;

    const result = await this.zg.loginRoom(roomId, token, {
      userID: userId,
      userName: `用户_${userId}`,
    }, {
      userUpdate: true,
    });

    if (!result) {
      wx.showToast({ title: '进入房间失败,请重试', icon: 'none' });
    }
  },

  async startPublish() {
    const streamID = `${this.userId}_${this.roomId}`;
    const zegoPusher = this.selectComponent('#zegoPusher');
    await zegoPusher.startPush(this.zg, streamID);
    this.localStreamID = streamID;
  },

  async startPlay(streamID) {
    // 更新播放列表,SDK 会自动处理组件渲染
    const zegoPlayer = this.selectComponent(`#zegoPlayer_${streamID}`);
    if (zegoPlayer) {
      await zegoPlayer.startPlay(this.zg, streamID);
    }
    // 将新流加入列表以触发 wx:for 渲染
    const zegoPlayerList = [...this.data.zegoPlayerList, {
      id: streamID,
      componentID: `zegoPlayer_${streamID}`,
      playerId: streamID,
    }];
    this.setData({ zegoPlayerList });
  },

WXML 页面布局示例

微信小程序实现视频面诊:从 0 到 1 的完整技术方案

麦克风 / 摄像头控制

  // 静音切换
  toggleMic() {
    const { micOn } = this.data;
    this.zg.muteMicrophone(!micOn);
    this.setData({ micOn: !micOn });
  },

  // 摄像头开关(仅关闭视频画面,音频保持)
  toggleCamera() {
    const { cameraOn } = this.data;
    this.zg.mutePublishStreamVideo(!cameraOn);
    this.setData({ cameraOn: !cameraOn });
  },

  // 切换前后摄像头(中医望诊场景常用)
  switchCamera() {
    this.zg.useFrontCamera(!this.data.isFrontCamera);
    this.setData({ isFrontCamera: !this.data.isFrontCamera });
  },

弱网降级策略

医疗场景对连接稳定性要求高,需要主动处理弱网情况:

  // 监听推流网络质量(需在 live-pusher 的 bindnetstatus 中透传)
  onPushNetStateChange(e) {
    this.zg.updatePlayerNetStatus(this.localStreamID, e);
  },

  // 监听拉流网络质量
  onPlayNetStateChange(e) {
    this.zg.updatePlayerNetStatus(e.currentTarget.id, e);
  },

  // 推流质量回调
  setupQualityMonitor() {
    this.zg.on('publishQualityUpdate', (streamID, stats) => {
      // videoBitrate: 视频码率(Kbps), videoFPS: 帧率
      const quality = this.calcQualityLevel(stats.videoBitrate, stats.videoFPS);
      this.setData({ networkQuality: quality });

      if (quality >= 4) {
        wx.showToast({ title: '网络较差,画质已自动降低', icon: 'none' });
      }
      if (quality === 5) {
        wx.showModal({
          title: '网络信号极差',
          content: '是否切换为纯语音问诊?',
          success: (res) => {
            if (res.confirm) {
              this.zg.mutePublishStreamVideo(true);
              this.setData({ cameraOn: false });
            }
          }
        });
      }
    });
  },

结束问诊,释放资源

  async endConsultation() {
    // 停止推流
    const zegoPusher = this.selectComponent('#zegoPusher');
    if (zegoPusher) zegoPusher.stopPush();

    // 退出房间(SDK 会自动停止所有拉流)
    await this.zg.logoutRoom(this.roomId);

    // 销毁引擎(重要!防止内存泄漏)
    this.zg.destroyEngine();
    this.zg = null;

    // 跳转到问诊结束页
    wx.redirectTo({ url: '/pages/consultation-end/index' });
  },

  onUnload() {
    // 页面卸载时确保资源释放
    if (this.zg) {
      this.zg.logoutRoom(this.roomId);
      this.zg.destroyEngine();
    }
  },

完整流程图

患者进入问诊页
      │
      ▼
前端请求后端 → 生成 RoomID + Token
      │
      ▼
new ZegoExpressEngine(appID, server)
      │
      ▼
initContext(绑定页面上下文)
      │
      ▼
注册事件监听(roomStateUpdate / roomStreamUpdate)
      │
      ▼
loginRoom(加入房间)
      │
      ├─── roomStateUpdate: CONNECTED
      │         └─── zegoPusher.startPush() 开始推本地摄像头
      │
      ├─── roomStreamUpdate: ADD(医生进入)
      │         └─── zegoPlayer.startPlay() 拉取医生视频
      │
      ▼
双方视频接通(是)
      │
      ├─── publishQualityUpdate → 弱网降级(降画质 → 关视频 → 纯语音)
      ├─── 支持静音 / 关摄像头 / 切换摄像头
      │
      ▼
问诊结束 → stopPush → logoutRoom → destroyEngine
      │
      ▼
后端记录问诊记录 + 触发云端录制存档(如需)

合规与审核要点

这部分内容需要注意,这是项目上线被卡的高发区。

互联网医院资质

开展视频问诊必须持有以下资质之一:

  • 互联网医院牌照:由省级卫健委审批,依托实体医院设立。
  • 卫健委备案证明:部分省份允许第三方平台备案后开展服务。

没有资质直接上线视频问诊,属于违规行为,微信审核也会拒绝。

小程序类目审核

申请”医疗 > 互联网医院”或”医疗 > 在线问诊”类目时,需提交:

  • 互联网医院许可证或备案证明
  • 医疗机构执业许可证
  • 法人身份证明

审核周期通常 3~7 个工作日,建议提前准备。

视频录像的存储合规

根据《互联网诊疗监管细则》,诊疗过程的音视频记录需要:

  • 境内服务器存储:不能使用境外云存储。
  • 保存期限:不少于 3 年。
  • 访问控制:仅授权人员可查看。

ZEGO 提供云端录制服务,支持将录像直接存储到阿里云 OSS、腾讯云 COS 等境内存储,可满足合规要求。

隐私协议中的音视频采集说明

用户协议和隐私政策中必须明确告知:

  • 采集摄像头和麦克风数据的目的。
  • 数据的存储方式和期限。
  • 用户的撤回授权方式。

微信审核会重点检查这一项,缺失会直接导致审核被拒。

处方和病历的电子签名

如果涉及电子处方,需要接入符合《电子签名法》要求的 CA 认证服务,医生的电子签名需具备法律效力。

参考资料

原创文章,作者:ZEGO即构科技,如若转载,请注明出处:https://market-blogs.zego.im/reports-technique/3410/

(0)
上一篇 1天前
下一篇 4月 8, 2025 7:59 上午

相关推荐

发表回复

登录后才能评论