实现音视频通话

更新时间: 2025/07/18 10:31:03

网易云信音视频通话产品的基本功能包括高质量的实时音视频通话。当您成功集成并初始化 NERTC SDK 之后,您可以简单体验本产品的基本业务流程。本文介绍如何通过小程序 Demo 实现音视频通话的基本业务流程。

GitHub 项目

网易云信在 GitHub 上提供以下开源的小程序音视频通话示例项目 netease-kit/NEVideoCall-1to1.git。在搭建自己的小程序项目前,您可以下载体验或参考源代码。

Demo 体验

网易云信为您提供微信小程序的音视频通话 Demo。您可以在手机上安装微信 App,通过微信扫描识别以下二维码,快速体验网易云信的微信小程序音视频通话业务场景。

G2-MiniAppDemo02

前提条件

请确认您已完成以下操作:

NERTC SDK 依赖微信小程序原生的媒体组件实现音视频推拉流。因此,建议您在开始前阅读微信官方文档,了解其基本开发要点。

第一步:初始化客户端对象

调用 YunXinMiniappSDK.Client 方法创建一个用于控制通话的客户端对象,然后调用 init() 进行初始化。

请务必在调用 init() 方法后,立即注册所有需要监听的事件。否则,可能会错过 SDK 在初始化阶段抛出的事件。

JavaScriptlet client = YunXinMiniappSDK.Client({
    debug: true,
    appkey: 'YOUR_NETEASE_APPKEY' // 请替换为您的 App Key
});

client.init();

// 立即监听各类事件
this.client.on('clientLeave', (data) => {
  const { uid, isBigNumber } = data;
  // isBigNumber 标志位用于处理超出 JavaScript number 精度的 UID。
  // 若为 true,uid 为 String 类型;否则为 number 类型。
  console.log('[clientLeave 通知] 用户离开: ', uid);
});
this.client.on('clientJoin', (data) => {
  const { uid, isBigNumber } = data;
  console.log('[clientJoin 通知] 用户加入: ', uid);
});
// ... 监听其他事件

第二步:加入房间

调用 client.join 方法加入房间。

client.join 的参数中,您需要提供房间名、用户ID(uid)以及在安全模式下必需的 token

网易云信 NERTC SDK 支持用户角色管理,角色包括主播(broadcaster)和观众(audience),默认以主播角色加入房间。加入之后,可以通过 setRole() 切换用户角色。如果在主播已 publish 的状态下调用该方法将用户角色设置为观众,会导致之前的推流地址失效,需要重新处理推流逻辑。

JavaScriptlet joinParam = {
    channelName: 'YOUR_CHANNEL_NAME', // 请替换为您的房间名
    uid: 100, // 如果 uid 超出 number 精度范围,建议以字符串形式传入,SDK 内部会按 BigNumber 处理。
    token: 'YOUR_TOKEN', // 安全模式下必填。若为调试模式,请勿设置此字段。
    liveEnable: 0, // 是否开启互动直播,请参考 API 文档
    recordType: '0', // 服务器录制相关参数,请参考 API 文档
    recordAudio: 0,
    recordVideo: 0,
    isHostSpeaker: 0,
  }
  client.join(joinParam).then(data => {
     console.log('!!!! 加入房间成功');
  }).catch(e => {
     console.error('!!!! 加入房间失败:', e);
  })

第三步:发布本地流

成功加入房间后,主播可以调用 client.publish 方法将本地音视频流发布到房间中。成功发布后,SDK 会返回该路音视频流的推流 URL,您需要将此 URL 绑定到页面的 live-pusher 组件上。

关于 mediaType 参数

  • 传入空字符串 '' (推荐):表示同时发布音频流和视频流。
  • 传入 'audio':仅发布音频流。
  • 传入 'video':仅发布视频流。

最佳实践

如果用户入房后需要立即开启音视频,建议首次调用 publish 时将 mediaType 设置为 ''。SDK 会为音视频返回同一个推流 URL。后续若需单独控制音视频的开关,可再次调用 publishunpublish 并指定具体的 mediaType

JavaScriptlet mediaType = ''; // 推荐,表示同时发布音视频
client.publish(mediaType).then(url => {
    console.log('推流成功, 获取推流地址: ', url);
    // 在页面的 data 中设置此 url,并绑定到 wxml 的 live-pusher 组件
    // this.setData({ pusherUrl: url });
}).catch(e => {
    console.log('推流失败,原因: ', e.message);
})

第四步:订阅远端流

当房间内有其他用户发布媒体流时,SDK 会触发 stream-added 事件。您需要监听此事件,并在回调中调用 client.subscribe 来订阅该媒体流。订阅成功后,SDK 会返回拉流 URL,您需要为该用户动态创建一个 live-player 组件并绑定此 URL。

核心逻辑

  • 如果远端用户同时开启了音频和视频,SDK 会触发两次 stream-added 事件,mediaType 分别为 'audio''video'。您需要对这两个事件都调用 subscribe 方法,否则服务器不会转发完整的音视频流。
  • 对于同一个用户的音视频主流('audio''video'),SDK 返回的拉流 URL 是相同的。因此,您只需要使用一个 live-player 组件来播放该用户的音视频。
  • 对于辅流(如屏幕共享 'screenShare' 或音频辅流 'slaveAudio'),SDK 会返回独立的 URL,需要为其创建单独的 live-player 组件。
JavaScriptclient.on('stream-added', ({uid, mediaType, isBigNumber}) => {
    console.log(`用户 ${uid} 发布了 ${mediaType} 流`);
    
    client.subscribe(uid, mediaType).then(data => {
        console.log(`订阅用户 ${uid}${mediaType} 成功,拉流地址: `, data.url);
        // 业务层需要将拉流 url 设置到对应的 live-player 组件中。
        // 您可以维护一个用户列表,将 url 与 uid 关联起来,并动态渲染 live-player。
    }).catch(e => {
        console.log(`订阅用户 ${uid} 失败,原因: `, e);
    })
});

当远端用户停止发布流(stream-removed)或离开房间(clientLeave)时,您需要相应地销毁或清理对应的 live-player 组件。

JavaScriptclient.on("stream-removed", {uid, mediaType, isBigNumber} => {
  console.log(`[stream-removed 通知] ${uid} 停止发布自己的 ${mediaType}`)
  // 业务层需要判断:
  // 1. 如果 mediaType 是 'audio' 或 'video',通常不需要立即移除 live-player,因为对方可能只关闭了其中一个,另一个仍在传输。
  // 2. 如果 mediaType 是 'slaveAudio' 或 'screenShare',因为它们是独立流,可以安全地移除对应的 live-player 组件。
});

 this.client.on('clientLeave', (data) => {
  const { uid, isBigNumber } = data
  //如果对端 uid 超出了 number 精度范围,isBigNumber 值为 true,uid 为 String 类型,否则 isBigNumber 为 false,uid 为 number 类型

  console.log('[clientLeave 通知] 有人离开了: ', uid)
  //业务层需要自己去判断处理,如果这个 uid 的远端用户只发布了 audio 或者 video 需要清除对应 url 的 live-player 组件即可,但是如果该用户也发布过'slaveAudio'或者'screenShare',记得也需要清除这两个拉流 url 对应的 live-payer 组件,总之对方离开房间后,需要清除这个 uid 相关的所有 live-player 组件
})

第五步:事件通知

SDK 内部处理了网络重连等逻辑,并通过事件向您同步状态。合理利用这些事件可以提升应用的用户体验。

成员状态事件

JavaScript// 有人加入房间
client.on('clientJoin', ({uid, isBigNumber}) => {
  console.log(`音视频通知:用户 ${uid} 加入`);
});

// 有人离开房间
client.on('clientLeave', ({uid, isBigNumber}) => {
  console.log(`音视频通知:用户 ${uid} 离开`);
  // 在此清理该用户对应的 UI 资源
});

// 自己被踢出房间
client.on('kicked', (data) => {
  console.log('音视频通知:您已被踢出房间');
  // 在此处理退出逻辑,如返回上一页
});

// 房间被解散
client.on('liveRoomClose', (data) => {
  console.log('音视频通知:房间已解散');
});

连接状态事件

JavaScript// 与服务器的 WebSocket 连接已建立
client.on('open', (data) => {
  console.log('音视频通知:信令通道连接成功');
});

// WebSocket 连接已断开
// WebSocket 连接已断开
client.on('disconnect', (data) => {
  console.log('音视频通知:信令通道已关闭。可能原因:主动离开房间,或网络断开且重连失败。');
});

// 准备重连
client.on('willreconnect', (data) => {
  console.log('音视频通知:网络断开,正在尝试重连...');
});

// 重连成功
client.on('reconnected', () => {
  console.log('音视频通知: 信令重连成功');
  // 关键操作:重连成功后,需要重新发布本地流以恢复推流。
  client.publish('').then(url => {
      console.log('重连后重新推流成功, 获取新推流地址: ', url);
      // 业务层将新的推流 url 更新到 live-pusher 组件中
      // this.setData({ pusherUrl: url });
  }).catch(e => {
      console.log('重连后推流失败,原因: ', e.message);
  });
});

// SDK 信令发送超时
client.on('sendCommandOverTime', (data) => {
  console.log('音视频通知:SDK 信令发送超时');
});

流地址更新事件

JavaScript// 推拉流地址因服务调度等原因发生变化
client.on('syncDone', (data) => {
  const {uid, url, isBigNumber} = data;
  console.log('[syncDone 通知] 推流地址已更新, data: ', data);
  // 如果是自己的 uid,则更新 live-pusher 的推流 url
  // 如果是远端的 uid,则更新对应 live-player 的拉流 url
});

第六步:监听质量数据

小程序 SDK 的音视频推拉流依赖于微信平台的 live-pusherlive-player 组件。为了监控通话质量和排查问题,您需要监听这两个组件的状态事件,并将数据上报给 SDK。

强烈建议您完整实现本章节的功能。将小程序组件的质量数据通过 client.dataReporter 接口上报给 NERTC SDK,是快速定位和解决音视频问题的关键。

live-pusher 组件监听事件

请参考 微信小程序QQ 小程序 live-pusher 文档查看详细信息。重要事件为:

  • bindstatechange:状态变化事件

  • bindnetstatus:网络状态通知

  • binderror:渲染错误事件

    XML<live-pusher
        wx:if="{{rtmpUrl!==''}}"
        wx:key="livePusher"
        style="display:inline-block;width:180px;height:150px;"
        aspect="16:9"
        mode="RTC"
        enable-agc="true"
        enable-ans="true"
        orientation="vertical"
        url="{{rtmpUrl}}" //推流地址,client.publish()接口可以获取
        enable-mic="true"
        enable-camera="true"
        beauty="0"
        waiting-image="../../images/cover.png"
        max-bitrate="500"
        min-bitrate="200"
        bindstatechange="pusherStateChangeHandler"
        bindnetstatus="pusherNetstatusHandler"
        binderror="pusherErrorHandler"
        debug="true"
        autopush="true"
    >
    

在对应的 JS 文件中,实现这些回调函数,并调用 client.dataReporter 上报数据。

JavaScriptfunction pusherStateChangeHandler(data) {
    let { code } = data.detail
    client.dataReporter('push', 'bindstatechange', {
      code,
      reason: '' //状态码的说明,小程序 live-pusher 组件文档(https://developers.weixin.qq.com/miniprogram/dev/component/live-pusher.html#%E7%8A%B6%E6%80%81%E7%A0%81%EF%BC%88code%EF%BC%89)有详细说明
    })
}

function pusherNetstatusHandler(data) {
    let { info } = data.detail
    client.dataReporter('push', 'bindnetstatus', info)
}

function pusherErrorHandler(data) {
    let { errMsg, errCode } = data.detail
    client.dataReporter('push', 'binderror', {
      errCode,
      errMsg
    })
}

live-player 组件监听事件

请参考 微信小程序QQ 小程序 live-player 组件文档查看详细信息。重要事件为:

  • bindstatechange:播放状态变化事件

  • bindnetstatus:网络状态通知

    XML<live-player
       bindtap='videoClickHandler'
       style="display:inline-block;width:180px;height:150px;"
       data-user="{{uid}}" //可以设置成 client.subscribe(uid)中的 uid
       wx:if="{{item.url}}" //要记得及时清除不存在的拉流地址
       src="{{item.url}}" //client.subscribe(uid)订阅成功的拉流地址
       mode="RTC"
       debug="true"
       min-cache="0.2"
       max-cache="0.8"
       auto-pause-if-navigate= "true"
       auto-pause-if-open-native= "true"
       bindstatechange="pullerStateChangeHandler"
       bindnetstatus="pullerNetstatusHandler"
       autoplay="true"
       >
    

您可以调用 SDK 的数据上报接口,实时传递给 SDK 这些数据,可以帮助 SDK 进行问题排查、质量优化。也可以自己监听这些信息,进行进一步的业务处理。

示例代码

JavaScriptfunction pullerStateChangeHandler(data) {
    let { code } = data.detail
    client.dataReporter('pull', 'bindstatechange', {
      code,
      reason: '' //状态码的说明,小程序 live-player 组件文档(https://developers.weixin.qq.com/miniprogram/dev/component/live-player.html#%E7%BD%91%E7%BB%9C%E7%8A%B6%E6%80%81%E6%95%B0%E6%8D%AE)有详细说明
    })
}

function pullerNetstatusHandler(data) {
    let { info } = data.detail
    client.dataReporter('pull', 'bindnetstatus', info)
}

第七步:离开房间

当通话结束时,调用 client.leave() 方法离开房间并释放资源。该方法会断开与服务器的连接,并停止所有推拉流活动。

JavaScriptclient.leave().then(() => {
  console.log('成功离开房间');
  // 在此执行清理工作,例如销毁 client 实例,返回上一页等
}).catch(err => {
  console.error('离开房间失败:', err);
});

运行项目

为保证最佳效果,请务必在真机上运行项目。完成开发后,单击微信开发者工具界面的 真机调试。扫描生成的二维码,即可在手机端运行和调试项目。

完整示例

您可以直接参考如下示例代码,将其整合到您的小程序 Page 中,以快速实现核心功能。

JavaScriptniappSDK from '../../sdk/NIM_Web_Netcall_weixin_G2.js'

let client = YunXinMiniappSDK.Client({
    debug: true,
    appkey: ''
});

client.init();

//监听事件通知
initEvent()

let joinParam = {
    channelName: '',
    uid: 100,
    token: ''
}
client.join(joinParam).then(data => {
    console.log('!!!! 房间房间成功:')
    client.publish('').then(url => {
        console.log('推流成功, 获取推流地址: ', url);
        //mediaType 不同分别为'audio'、'video',目前 SDK 的逻辑是'audio'和'video'使用相同的推流 url,如果开发分别使用 mediaType 为'audio'或者'video'作为参数,两次调用 publish()接口去推流,得到的 url 是相同的,因此建议用户如果加入房间后就需要同时开启音频和视频时,将 mediaType 设置为'',中途需要单独控制音频和视频的开关时,在设置 mediaType 为具体的参数
        //业务层将推流 url 设置的 live-pusher 组件中

    }).catch(e => {
        console.log('推流失败,原因: ', e);
    })
}).catch(e => {
    console.error('加入房间失败,原因:', e)
})

function initEvent() {
    client.on('stream-added', ({uid, mediaType, isBigNumber}) => {
        console.log(`[stream-added 通知] ${uid} 发布了自己的 ${mediaType}, isBigNumber: ${isBigNumber}`)
        client.subscribe(uid, mediaType).then(data => {
            console.log('订阅别人成功,获取到拉流地址: ', data.url)
            //业务层将拉流 url 设置到对应 live-player 组件中

            //注意: 同一个 uid,两次订阅(分别订阅 audio、video)返回的 url 是相同的的,可以重复更新相同 live-player 组件的 url,没有什么影响
            //注意: 同一个 uid,如果对方同时发布了 'audio': 音频、'video': 视频、'slaveAudio': 音频辅流、'screenShare',这 4 中类型的媒体数据,理论上需要 4 次订阅,其中订阅 audio、video 返回的 url 是相同的的,可以重复更新相同 live-player 组件的 url,使用同一个 live-player 组件,slaveAudio 需要单独一个 live-player 组件,screenShare 也需要单独一个 live-player 组件,此时有 3 个 live-player 组件去拉取对端所有类型的媒体

        }).catch(e => {
            console.log('订阅别人失败,原因: ', e)
        })
    })
    //通知应用程序更新后的推流地址和拉流地址。
    client.on('syncDone', (data) => {
      const {uid, url, isBigNumber} = data
      console.log('[syncDone 通知] 推流地址发生了变化, data: ', data)

      //此时需要更新 live-pusher 组件中的 url
    })

    //通知应用程序有人离开房间。
    client.on('clientLeave', (data) => {
      const { uid, isBigNumber } = data
      //如果对端 uid 超出了 number 精度范围,isBigNumber 值为 true,uid 为 String 类型,否则 isBigNumber 为 false,uid 为 number 类型

      console.log('[clientLeave 通知] 有人离开了: ', uid)
      //业务层需要自己去判断处理,如果这个 uid 的远端用户只发布了 audio 或者 video 需要清除对应 url 的 live-player 组件即可,但是如果该用户也发布过'slaveAudio'或者'screenShare',记得也需要清除这两个拉流 url 对应的 live-payer 组件,总之对方离开房间后,需要清除这个 uid 相关的所有 live-player 组件
    })

    //通知应用程序有人加入房间。
    client.on('clientJoin', (data) => {
      const {uid, isBigNumber} = data
      //如果对端 uid 超出了 number 精度范围,isBigNumber 值为 true,uid 为 String 类型,否则 isBigNumber 为 false,uid 为 number 类型

      console.log('音视频通知:有人加入')
    })

    //通知应用程序自己被踢出。
    client.on('kicked', (data) => {
      console.log('音视频通知:被踢')
    })

    //通知应用程序 socket 建立成功。
    client.on('open', (data) => {
      console.log('音视频通知:和服务器 socket 建立成功')
    })

    //通知应用程序音视频 socket 关闭,触发时机:主动离开房间了,或者 SDK 中途断网重连没有成功,SDK 自动离开了房间。
    client.on('disconnect', (data) => {
      console.log('音视频通知:和服务器 socket 关闭了')
    })

    //通知应用程序准备重连。
    client.on('willreconnect', (data) => {
      console.log('音视频通知:准备重新建立和服务器之间的联系')
    })

    //通知应用程序准备重连。
    client.on('reconnected', (data) => {
        console.log('音视频通知:已经重新建立和服务器之间的联系')
        //此时需要重新执行 publish()
        let mediaType = ''//也可以设置为'audio'或者'video'。如果是空字符串'',表示同时发布 audio 音频流和 video 视频流
        client.publish().then(url => {
            console.log('推流成功, 获取推流地址: ', url);
            //业务层将推流 url 更新到 live-pusher 组件中
        }).catch(e => {
            console.log('推流失败,原因: ', e.message);
        })
    })

    //通知应用程序 SDK 信令发送超时。
    client.on('sendCommandOverTime', (data) => {
      console.log('音视频通知:SDK 信令发送超时')
    })

    //通知应用程序房间被解散。
    client.on('liveRoomClose', (data) => {
      console.log('音视频通知:房间解散了')
    })
}

常见问题

小程序用户关闭再开启麦克风,其他端无法感知状态变化

问题现象

房间内均为小程序用户。用户 A 关闭麦克风(调用 unpublish('audio')),然后再次开启麦克风(调用 publish('audio')),其他用户 B 和 C 无法收到 stream-added 事件,因此无法听到 A 的声音。

问题原因

这是由小程序底层媒体组件的机制导致的。在 unpublish 后紧接着 publish,网关可能不会向远端用户下发新的 stream-added 通知。

解决方案

live-pusher 组件使用推流的 URL 作为 key 值控制是否渲染,关闭麦克风的时候,调用 SDK 的接口停止发布音频流。具体实现思路如下:

JavaScriptthis.client.unpublish('audio').then(url => {

    //步骤一:将推流的 URL 设置为 null,这样 live-pusher 组件就会停止渲染。

   //步骤二:网易云信只关闭了麦克风,而不关闭摄像头,所以需要立即将 URL 设置回去,以重新渲染 live-pusher 组件。同时,需要记得将 live-pusher 组件中的 enable-mic 设置为 false,这样重新渲染的 live-pusher 组件就不会推送音频流。

})
XML<live-pusher
  wx:if="{{pusher.url}}"
  class="pusher"
  url="{{pusher.ur1}}"
  mode="RTC"
  enable-camera="{{pusher.enableCamera}}"
  enable-mic="{{pusher.enableMic}}"
  min-bitrate="{{pusher.minBitrate}}"
  max-bitrate="{{pusher.maxBitrate}}"
  orientation="{{pusher.videoOrientation}}"
  aspect="{{pusher.videoAspect}}"
  device-position="{{pusher.frontCamera}}"
  background-mute="{{pusher.enableBackgroundMute}}"
  audio-quality="{{pusher.audioQuality}}"
  waiting-image="{{pusher.waitingImage}}"
  bindstatechange="_pusherStateChangeHandler"
  bindnetstatus="_pusherNetStatusHandler"
  binderror="_pusherErrorHandler"
  autopush
></live-pusher>
<!-- 本地 uid 显示 -->

为什么调试 Demo 时显示无法使用摄像头和麦克风?

在调试时,若小程序界面提示 “无法使用摄像头和麦克风”,如下图所示,请按以下步骤排查:

example
  • 方法一:使用真机调试 微信开发者工具的模拟器无法调用真实的摄像头和麦克风。请点击工具栏中的 真机调试,扫描二维码在手机上运行。

  • 方法二:检查小程序权限 请确保您已授权小程序使用摄像头和麦克风。

    • 首次授权:删除手机上已安装的小程序,重新扫码进入,在弹出的授权请求中选择“允许”。
    • 手动设置:在小程序右上角点击 “...” -> 设置,检查并开启“摄像头”和“麦克风”的权限开关。
  • 方法三:检查小程序后台配置 登录微信公众平台,进入小程序的 开发 -> 开发管理 -> 接口设置 页面,确保已开启 “实时播放音视频流”“实时录制音视频流” 的权限。

此文档是否对你有帮助?
有帮助
去反馈
  • GitHub 项目
  • Demo 体验
  • 前提条件
  • 第一步:初始化客户端对象
  • 第二步:加入房间
  • 第三步:发布本地流
  • 第四步:订阅远端流
  • 第五步:事件通知
  • 成员状态事件
  • 连接状态事件
  • 流地址更新事件
  • 第六步:监听质量数据
  • live-pusher 组件监听事件
  • live-player 组件监听事件
  • 第七步:离开房间
  • 运行项目
  • 完整示例
  • 常见问题
  • 小程序用户关闭再开启麦克风,其他端无法感知状态变化
  • 为什么调试 Demo 时显示无法使用摄像头和麦克风?