实现音视频通话

更新时间: 2024/03/15 16:57:57

网易云信音视频通话产品的基本功能包括高质量的实时音视频通话。当您成功初始化 SDK 之后,您可以简单体验本产品的基本业务流程。本文档为您展示音视频通话提供的基本业务流程。

前提条件

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

示例代码

通过 MFC 实现基础音视频通话的示例代码
#pragma once
#include "videoDlg.h"
#include "nertc_event_handler.h" 
#include "./utils/windows_util.h"
#include "nertc_engine_ex.h"
#include "nertc_audio_device_manager.h"
#include "nertc_video_device_manager.h"
#include <set>
#include <memory>
#include <string>
#include <unordered_map>

using namespace std;
class CmainWindow : public CDialogEx
{
public:
	CmainWindow(CWnd* pParent = nullptr);
	virtual ~CmainWindow();

#ifdef AFX_DESIGN_TIME
	enum { IDD = IDD_NERTCSAMPLE_DIALOG };
#endif
protected:
	virtual void DoDataExchange(CDataExchange* pDX);

protected:
	HICON m_hIcon;
	virtual BOOL OnInitDialog();
	afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
	afx_msg void OnPaint();
	afx_msg HCURSOR OnQueryDragIcon();

	//处理nertc回调相关
	afx_msg	LRESULT OnUserLeft(WPARAM wParam, LPARAM lParam);
	afx_msg	LRESULT OnUserVideoStart(WPARAM wParam, LPARAM lParam);
	afx_msg	LRESULT OnUserVideoStop(WPARAM wParam, LPARAM lParam);
	afx_msg	LRESULT OnUserSubStreamVideoStart(WPARAM wParam, LPARAM lParam);
	afx_msg	LRESULT OnUserSubStreamVideoStop(WPARAM wParam, LPARAM lParam);
	DECLARE_MESSAGE_MAP()

public:
	//加入房间
	afx_msg void OnJoinChannelBtnClicked();
	//离开房间
	afx_msg void OnLeavechannelBtnClicked();
	//开始取屏
	afx_msg void OnStartScreen();
	//关闭取屏
	afx_msg void StopScreenCapturer();

private:
	//枚举屏幕窗口信息
	size_t RefreshCaptureTarget();

private:
	nertc::IRtcEngineEx *rtc_engine_ = nullptr; //nertc实例
	nertc::IAudioDeviceManager *_adm = nullptr; //音频设备管理
	nertc::IVideoDeviceManager *_vdm = nullptr; //视频设备管理
	NertcEventHandler *event_hander_; //回调事件子类
	std::unordered_map<nertc::uid_t, CvideoDlg * > _remote_video; //远端用户视频窗口管理
	CvideoDlg * _plocal_sub_video = nullptr; //本地视频辅流窗口
	std::unordered_map<nertc::uid_t, CvideoDlg * > _remote_sub_video; //远端视频辅流窗口

	//视屏辅流(屏幕共享)相关
	std::set<HWND> exclude_wnd_list_;
	tools::CaptureTargetInfoList s_capture_windows_;
	tools::CaptureTargetInfo cur_external_capture_info_;
};



//mainWindow.cpp
#include "framework.h"
#include "afxdialogex.h"
#include "wnd_msg_define.h"
#include "nertcApp.h"
#include "mainWindow.h"
#include "nertc_engine_defines.h"
#include "./utils/time_util.h"

using namespace  tools;

#define SDK_CHECK \
if(!rtc_engine_) \
{\
return;\
}\

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

CmainWindow::CmainWindow(CWnd* pParent /*=nullptr*/)
	: CDialogEx(IDD_NERTCSAMPLE_DIALOG, pParent)
{
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

CmainWindow::~CmainWindow()
{
	if (nullptr != rtc_engine_) {
		rtc_engine_->enableLocalVideo(false);
		rtc_engine_->release();
		destroyNERtcEngine((void *&)rtc_engine_);
		_adm = nullptr;
		_vdm = nullptr;
		rtc_engine_ = nullptr;
		delete event_hander_;
		event_hander_ = nullptr;
	}

	if (nullptr != _plocal_sub_video) {
		delete _plocal_sub_video;
		_plocal_sub_video = nullptr;
	}
}

void CmainWindow::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
}


BEGIN_MESSAGE_MAP(CmainWindow, CDialogEx)
	ON_WM_SYSCOMMAND()
	ON_WM_PAINT()
	ON_WM_QUERYDRAGICON()
	ON_BN_CLICKED(IDC_BUTTON2_JOINCHANNEL, &CmainWindow::OnJoinChannelBtnClicked)
	ON_BN_CLICKED(IDC_BUTTON3_LEAVECHANNEL, &CmainWindow::OnLeavechannelBtnClicked)
	ON_BN_CLICKED(IDC_BUTTON1_START, &CmainWindow::OnStartScreen)
	ON_BN_CLICKED(IDC_BUTTON2_CLOSE, &CmainWindow::StopScreenCapturer)

	//sdk回调事件转发
	ON_MESSAGE(WM_ON_USER_LEFT, &CmainWindow::OnUserLeft)
	ON_MESSAGE(WM_ON_USER_VIDEO_START, &CmainWindow::OnUserVideoStart)
	ON_MESSAGE(WM_ON_USER_VIDEO_STOP, &CmainWindow::OnUserVideoStop)
	ON_MESSAGE(WM_ON_USER_SUB_VIDEO_START, &CmainWindow::OnUserSubStreamVideoStart)
	ON_MESSAGE(WM_ON_USER__SUB_VIDEO_STOP, &CmainWindow::OnUserSubStreamVideoStop)

END_MESSAGE_MAP()

BOOL CmainWindow::OnInitDialog()
{
	CDialogEx::OnInitDialog();
	ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
	ASSERT(IDM_ABOUTBOX < 0xF000);

	CMenu* pSysMenu = GetSystemMenu(FALSE);
	if (pSysMenu != nullptr)
	{
		BOOL bNameValid;
		CString strAboutMenu;
		bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
		ASSERT(bNameValid);
		if (!strAboutMenu.IsEmpty())
		{
			pSysMenu->AppendMenu(MF_SEPARATOR);
			pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
		}
	}
	SetIcon(m_hIcon, TRUE);
	SetIcon(m_hIcon, FALSE);

	return TRUE;
}

void CmainWindow::OnSysCommand(UINT nID, LPARAM lParam)
{
	if ((nID & 0xFFF0) == IDM_ABOUTBOX)
	{
		/*CAboutDlg dlgAbout;
		dlgAbout.DoModal();*/
	}
	else
	{
		CDialogEx::OnSysCommand(nID, lParam);
	}
}

void CmainWindow::OnPaint()
{
	if (IsIconic())
	{
		CPaintDC dc(this);
		SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);
		int cxIcon = GetSystemMetrics(SM_CXICON);
		int cyIcon = GetSystemMetrics(SM_CYICON);
		CRect rect;
		GetClientRect(&rect);
		int x = (rect.Width() - cxIcon + 1) / 2;
		int y = (rect.Height() - cyIcon + 1) / 2;
		dc.DrawIcon(x, y, m_hIcon);
	}
	else
	{
		CDialogEx::OnPaint();
	}
}

HCURSOR CmainWindow::OnQueryDragIcon()
{
	return static_cast<HCURSOR>(m_hIcon);
}

LRESULT CmainWindow::OnUserLeft(WPARAM wParam, LPARAM lParam)
{
	//关闭远端视频窗口
	LPNERtcUserInfo lpData = (LPNERtcUserInfo)wParam;
	if (_remote_video.end() != _remote_video.find(lpData->uid)) {
		CvideoDlg * pdlg = _remote_video[lpData->uid];
		pdlg->ShowWindow(SW_HIDE);
		_remote_video.erase(lpData->uid);
		delete pdlg;
		pdlg = nullptr;
	}
	//关闭远端屏幕共享窗口
	if (_remote_sub_video.end() != _remote_sub_video.find(lpData->uid)) {
		CvideoDlg * pdlg = _remote_sub_video.find(lpData->uid)->second;
		pdlg->ShowWindow(SW_HIDE);
		_remote_sub_video.erase(lpData->uid);
		delete pdlg;
		pdlg = nullptr;
	}
	return 0;
}

LRESULT CmainWindow::OnUserVideoStart(WPARAM wParam, LPARAM lParam)
{
	LPNERtcUserInfo lpData = (LPNERtcUserInfo)wParam;
	if (_remote_video.end() == _remote_video.find(lpData->uid)) {
		CvideoDlg * p_remote_video_dlg = new CvideoDlg();
		p_remote_video_dlg->Create(IDD_DIALOG1_VIDEO);
		p_remote_video_dlg->ShowWindow(SW_HIDE);
		_remote_video[lpData->uid] = p_remote_video_dlg;

		HWND hwd = p_remote_video_dlg->GetDlgItem(IDC_REMOTE_VIDEO)->m_hWnd;
		nertc::NERtcVideoCanvas canvas;
		canvas.cb = nullptr;
		canvas.user_data = nullptr;
		canvas.window = hwd;
		//设置远端用户视频画布
		rtc_engine_->setupRemoteVideoCanvas(lpData->uid, &canvas);
	}

	//订阅远端用户视频流
	rtc_engine_->subscribeRemoteVideoStream(lpData->uid, nertc::kNERtcRemoteVideoStreamTypeHigh, true);

	_remote_video[lpData->uid]->ShowWindow(SW_SHOWNORMAL);
	return 0;
}

LRESULT CmainWindow::OnUserVideoStop(WPARAM wParam, LPARAM lParam)
{
	nertc::uid_t uid = (nertc::uid_t)wParam;
	//取消订阅远端用户视频流
	rtc_engine_->subscribeRemoteVideoStream(uid, nertc::kNERtcRemoteVideoStreamTypeHigh, false);

	if (_remote_video.end() != _remote_video.find(uid)) {
		CvideoDlg * pdlg = _remote_video[uid];
		pdlg->ShowWindow(SW_HIDE);
		_remote_video.erase(uid);
		delete pdlg;
		pdlg = nullptr;
	}
	return 0;
}

LRESULT CmainWindow::OnUserSubStreamVideoStart(WPARAM wParam, LPARAM lParam)
{
	LPNERtcUserInfo lpData = (LPNERtcUserInfo)wParam;
	if (_remote_sub_video.end() == _remote_sub_video.find(lpData->uid)) {
		CvideoDlg * pdlg = new CvideoDlg();
		pdlg->Create(IDD_DIALOG1_VIDEO);
		pdlg->ShowWindow(SW_HIDE);
		_remote_sub_video[lpData->uid] = pdlg;

		HWND hwd = pdlg->GetDlgItem(IDC_REMOTE_VIDEO)->m_hWnd;
		nertc::NERtcVideoCanvas canvas;
		canvas.cb = nullptr;
		canvas.user_data = nullptr;
		canvas.window = hwd;
		//设置远端用户视频辅流画布
		rtc_engine_->setupRemoteSubStreamVideoCanvas(lpData->uid, &canvas);
	}

	//订阅远端用户视频辅流
	int ret = rtc_engine_->subscribeRemoteVideoSubStream(lpData->uid, true);

	_remote_sub_video[lpData->uid]->ShowWindow(SW_SHOWNORMAL);
	return 0;
}

LRESULT CmainWindow::OnUserSubStreamVideoStop(WPARAM wParam, LPARAM lParam)
{
	nertc::uid_t uid = (nertc::uid_t)wParam;
	//取消订阅远端视频辅流
	rtc_engine_->subscribeRemoteVideoSubStream(uid, false);

	if (_remote_sub_video.end() != _remote_sub_video.find(uid)) {
		CvideoDlg * pdlg = _remote_sub_video.find(uid)->second;
		pdlg->ShowWindow(SW_HIDE);
		_remote_sub_video.erase(uid);
		delete pdlg;
		pdlg = nullptr;
	}
	return 0;
}

void CmainWindow::OnJoinChannelBtnClicked()
{
	//初始化sdk
	if (nullptr == rtc_engine_) {
		rtc_engine_ = (nertc::IRtcEngineEx *)createNERtcEngine();
		event_hander_ = new NertcEventHandler(this->m_hWnd);
	}
	std::string appKey = ""; //用户自己的appkey
	nertc::NERtcEngineContext context;
	context.app_key = appKey.c_str();
	context.event_handler = event_hander_;
	context.video_use_exnternal_render = true;
	context.video_prefer_hw_decoder = false;
	context.video_prefer_hw_encoder = false;
	context.log_level = nertc::kNERtcLogLevelInfo;
	context.log_file_max_size_KBytes = 20 * 1024;
	context.log_dir_path = "D:/";
	int ret = rtc_engine_->initialize(context);
	if (ret == 0)
	{
		rtc_engine_->queryInterface(nertc::kNERtcIIDAudioDeviceManager, (void **)&_adm);
		rtc_engine_->queryInterface(nertc::kNERtcIIDVideoDeviceManager, (void **)&_vdm);
	}

	HWND hwd = GetDlgItem(ID_LOCAL_VIDEO)->m_hWnd;
	//设置本地视频画布
	nertc::NERtcVideoCanvas canvas;
	canvas.cb = nullptr;
	canvas.user_data = nullptr;
	canvas.window = hwd;
	ret = rtc_engine_->setupLocalVideoCanvas(&canvas);
	//是否开启本地视频
	ret = rtc_engine_->enableLocalVideo(true);
	//设置用户角色
	rtc_engine_->setClientRole(nertc::kNERtcClientRoleBroadcaster);
	//设置通话场景
	rtc_engine_->setChannelProfile(nertc::kNERtcChannelProfileCommunication);
	//设置视频属性(可房间内调用,需重启摄像头)
	nertc::NERtcVideoConfig config;
	config.max_profile = nertc::kNERtcVideoProfileHD720P;
	config.crop_mode_ = nertc::kNERtcVideoCropModeDefault;
	config.mirror_mode = nertc::kNERtcVideoMirrorModeDisabled;
	rtc_engine_->setVideoConfig(config);
	string token = ""; //用户自己的真实token
	std::string channelName = "159369";
	nertc::uid_t uid = 1133;

	//加入房间
	ret = rtc_engine_->joinChannel(token.c_str(), channelName.c_str(), uid);
}


void CmainWindow::OnLeavechannelBtnClicked()
{
	//关闭本地视频
	rtc_engine_->enableLocalVideo(false);
	nertc::NERtcVideoCanvas canvas;
	canvas.cb = nullptr;
	canvas.user_data = nullptr;
	canvas.window = nullptr;
	rtc_engine_->setupLocalVideoCanvas(&canvas);

	//离开房间
	int ret = rtc_engine_->leaveChannel();
}

size_t CmainWindow::RefreshCaptureTarget()
{
	s_capture_windows_.clear();
	int i = 0;
	s_capture_windows_.push_back({ tools::CaptureTargetType_Desktop, 0, L"桌面", RECT{0, 0, 0, 0} });


	EnumDisplayMonitors(nullptr, nullptr,
		[](HMONITOR hmon, HDC, LPRECT pRC, LPARAM lparam) {
		auto& monitors = *reinterpret_cast<tools::CaptureTargetInfoList*>(lparam);
		wchar_t buf[100] = { 0 };
		swprintf_s(buf, L"Monitor:(%d,%d,%d,%d)", pRC->left, pRC->top, pRC->right, pRC->bottom);
		monitors.push_back({ tools::CaptureTargetType_Monitor, (HWND)hmon, buf, *pRC });

		return TRUE;
	},
		reinterpret_cast<LPARAM>(&s_capture_windows_));
	/// for windows
	GetCaptureWindowList(&s_capture_windows_);
	return s_capture_windows_.size();
}

//开始屏幕共享
void CmainWindow::OnStartScreen()
{
	RefreshCaptureTarget();
	tools::CaptureTargetInfo info = s_capture_windows_[0];
	nertc::NERtcScreenCaptureParameters capture_params;
	capture_params.profile = nertc::kNERtcScreenProfileHD720P;
	capture_params.profile = nertc::kNERtcScreenProfileHD720P;
	capture_params.dimensions = { 1280, 720 };
	capture_params.frame_rate = 5;
	capture_params.bitrate = 0;
	capture_params.capture_mouse_cursor = true;
	capture_params.window_focus = true;
	capture_params.excluded_window_list = nullptr;
	capture_params.excluded_window_count = 0;

	if (nullptr == _plocal_sub_video) {
		_plocal_sub_video = new CvideoDlg();
		_plocal_sub_video->Create(IDD_DIALOG1_VIDEO);
		HWND hwd = _plocal_sub_video->GetDlgItem(IDC_REMOTE_VIDEO)->m_hWnd;
		nertc::NERtcVideoCanvas canvas;
		canvas.cb = nullptr;
		canvas.user_data = nullptr;
		canvas.window = hwd;
		//设置本地预览画布
		rtc_engine_->setupLocalSubStreamVideoCanvas(&canvas);
	}
	_plocal_sub_video->ShowWindow(SW_SHOWNORMAL);
	if (info.type == CaptureTargetType_Window) {
		//开始共享
		rtc_engine_->startScreenCaptureByWindowId(info.id, { 0, 0, 0, 0 }, capture_params);
	}
	else {
		HWND* wnd_list = NULL;
		if (!exclude_wnd_list_.empty()) {
			wnd_list = new HWND[exclude_wnd_list_.size()];
			for (auto e : exclude_wnd_list_) {
				*(wnd_list + capture_params.excluded_window_count++) = e;
			}
		}
		capture_params.excluded_window_list = (nertc::source_id_t*)wnd_list;
		nertc::NERtcRectangle rc{ info.rc.left, info.rc.top, info.rc.right - info.rc.left, info.rc.bottom - info.rc.top };
		//开始共享
		rtc_engine_->startScreenCaptureByScreenRect(rc, { 0, 0, 0, 0 }, capture_params);
		if (wnd_list) {
			delete[] wnd_list;
		}
	}


}

//停止屏幕共享
void CmainWindow::StopScreenCapturer()
{
	rtc_engine_->stopScreenCapture();
	_plocal_sub_video->ShowWindow(SW_HIDE);
}

实现流程

实现音频通话的 API 时序图如下图所示。

sequenceDiagram
    participant App
    participant NERtcSDK
    participant 云信服务端

    Note over App, 云信服务端: 初始化NERtcEngine

    App->>NERtcSDK: initialize

    Note over App, 云信服务端: 加入房间

    App->>NERtcSDK: joinChannel
    NERtcSDK->>云信服务端: 请求加入房间
    NERtcSDK-->>App: onJoinChannel

    Note over App, 云信服务端: 离开房间

    App->>NERtcSDK: leaveChannel
    NERtcSDK->>云信服务端: 请求离开房间

    Note over App, 云信服务端: 销毁实例

    App->>NERtcSDK: release

实现视频通话的 API 时序图如下图所示。

sequenceDiagram
    participant App
    participant NERtcSDK
    participant 云信服务端

    Note over App, 云信服务端: 初始化NERtcEngine

    App->>NERtcSDK: initialize

    Note over App, 云信服务端: 设置本地视图

    App->>NERtcSDK: setupLocalVideoCanvas
    App->>NERtcSDK: enableLocallVideo

    Note over App, 云信服务端: 加入房间

    App->>NERtcSDK: joinChannel
    NERtcSDK->>云信服务端: 请求加入房间
    NERtcSDK-->>App: onJoinChannel

    Note over App, 云信服务端: 设置远端视图

    Note right of NERtcSDK: 远端用户加入房间
    NERtcSDK-->>App: onUserJoined 远端用户加入房间的回调
    NERtcSDK-->>App: onUserVideoStart 远端用户发布视频流的回调
    App->>NERtcSDK: setupRemoteVideoCanvas 设置远端视频画布
    App->>NERtcSDK: subscribeRemoteVideoStream 订阅远端视频流
    云信服务端-->>App: onFirstVideoFrameDecoded 已接收到远端视频首帧并完成解码的回调

    Note over App, 云信服务端: 离开房间

    App->>NERtcSDK: leaveChannel
    NERtcSDK->>云信服务端: 请求离开房间

    Note over App, 云信服务端: 销毁实例

    App->>NERtcSDK: release

实现音视频通话

步骤一 创建音视频通话的界面

请根据您的业务场景,创建相应的音视频通话界面。若您已实现相应界面,请跳过此步骤。

实现基础的音视频通话,建议您在界面上添加如下控件:

  • 房间 ID
  • 用户昵称
  • 本端视频窗口
  • 远端视频窗口
  • 麦克风按钮
  • 摄像头按钮
  • 结束通话按钮

用户界面-windows.png

步骤二 引入头文件

在您的工程中对应的文件里添加如下代码引入头文件:

cpp  #include "nrtc_engine_ex.h"

步骤三 初始化

在操作 SDK 其他接口前,需要先完成初始化。

  1. 调用 createNERtcEngine 方法创建一个 NERtcEngine 实例。
  2. 调用 initialize 方法完成初始化。
  3. 根据 App 的应用场景,调用setChannelProfile设置房间的场景属性,不同场景的推荐配置请参见音视频参数配置推荐
  4. 通过初始化接口中注册IRtcEngineEventHandlerEx子类,监听事件,您需要重点关注的事件类型请参见常用的回调

示例代码如下:

cpp    // 创建 RTC 引擎对象并返回指针。
    nertc::IRtcEngineEx *rtc_engine_ = (IRtcEngineEx *)createNERtcEngine();
    // 设置已开通音视频功能的云信应用的AppKey。
    rtc_engine_context_.app_key = app_key_.c_str();
    // 设置日志目录的完整路径,采用UTF-8 编码。
    rtc_engine_context_.log_dir_path = log_dir_path_.c_str();
    // 设置日志级别,默认级别为 kNERtcLogLevelInfo。
    rtc_engine_context_.log_level = log_level;
    // 指定 SDK 输出日志文件的大小上限,单位为 KB。如果设置为 0,则默认为 20 M。
    rtc_engine_context_.log_file_max_size_KBytes = log_file_max_size_KBytes;
    // 设置SDK向应用发送回调事件的通知。
    rtc_engine_context_.event_handler = event_hander_;
    // 初始化 NERTC SDK 服务。
    if (kNERtcNoError != rtc_engine_->initialize(rtc_engine_context_))
    {
	    //获取音频设备管理指针
        rtc_engine_->queryInterface(nertc::kNERtcIIDAudioDeviceManager, (void **)&_adm);
	    //获取视频谁管理指针
	    rtc_engine_->queryInterface(nertc::kNERtcIIDVideoDeviceManager, (void **)&_vdm);
	    //设置房间场景
	 	rtc_engine_->setChannelProfile(nertc::kNERtcChannelProfileCommunication);
	    //设置用户角色
	    rtc_engine_->setClientRole(nertc::kNERtcClientRoleBroadcaster);
    }

步骤四 设置本地视图

初始化成功后,可以设置本地视图,来预览本地图像。您可以根据业务需要实现加入房间之前预览或加入房间后预览。

在加入房间前,默认预览分辨率为 640*480,您可以通过 setVideoConfig 接口的 widthheight 参数调整采集分辨率。

  • 实现加入房间前预览。

    1. 调用 setupLocalVideoCanvasstartVideoPreview(type) 方法,在加入房间前设置本地视图,预览本地图像。

    示例代码如下:

    cpp  NERtcVideoCanvas canvas;
      canvas.cb = nullptr;
      canvas.user_data = nullptr;
      canvas.window = window;
      //设置视频缩放模式。
      canvas.scaling_mode = mode;
      //设置本地视频画布
      rtc_engine_->setupLocalVideoCanvas(&canvas);
      //以开启本地视频主流预览为例
      rtc_engine_->startVideoPreview(kNERTCVideoStreamMain);
    
    1. 若要结束预览,或者准备加入房间时,调用 stopVideoPreview(type) 方法停止预览。

    stopVideoPreview(type)type 参数请与 startVideoPreview(type) 的保持一致,即同为主流或辅流的开启和停止预览。

  • 实现加入房间后预览。

    调用 enableLocalVideo 方法进行视频的采集发送与预览。成功加入房间后,即可预览本地图像。

    示例代码如下:

    cpp  NERtcVideoCanvas canvas;
      canvas.cb = nullptr;
      canvas.user_data = nullptr;
      canvas.window = window;
      //设置视频缩放模式。
      canvas.scaling_mode = mode;
      //设置本地视频画布
      rtc_engine_->setupLocalVideoCanvas(&canvas);
      bool enabled = true;
      //以开启本地视频主流采集并发送为例
      rtc_engine_->enableLocalVideo(nertc::kNERTCVideoStreamMain, enabled);
    

步骤四 加入房间

加入房间前,请确保已完成初始化相关事项。若您的业务中涉及呼叫邀请等机制,建议通过信令实现,总体实现流程请参见一对一会话操作流程,具体呼叫邀请机制的实现请参见邀请机制

调用 joinChannel 方法加入房间。

示例代码如下:

cpp  rtc_engine_->joinChannel(token, channel_name, uid);

重要参数说明

参数 说明
token 安全认证签名(NERTC Token)。
  • 调试模式下:可设置为 null。产品默认为安全模式,您可以在网易云信控制台将鉴权模式修改为调试模式,具体请参见Token 鉴权
    调试模式的安全性不高,请在产品正式上线前修改为安全模式。
  • 产品正式上线后:请设置为已获取的NERTC Token。安全模式下必须设置为获取到的 Token 。若未传入正确的 Token 将无法进入房间。

    推荐使用安全模式

channel_name 房间名称,长度为 1 ~ 64 字节。目前支持以下 89 个字符:a-z, A-Z, 0-9, space, !#$%&()+-:;≤.,>? @[]^_{|}~"。
设置相同房间名称的用户会进入同一个通话房间。
您也可以在加入通道前,通过创建房间接口创建房间。加入房间时,若传入的 {channelName} 未事先创建,则云信服务器内部将为其自动创建一个名为 {channelName} 的通话房间。
uid 用户的唯一标识 id,房间内每个用户的 uid 必须是唯一的。此 uid 为用户在您应用中的 ID,请在您的业务服务器上自行管理并维护。

SDK 发起加入房间请求后,服务器会进行响应,您可以通过初始化时设置的 rtc_engine_context_.event_handleronJoinChannel 回调监听加入房间的结果,同时该回调会抛出当前通话房间的 channelId 与加入房间总耗时(毫秒);其中 channelId 即音视频通话的 ID,建议您在业务层保存该数据,以便于后续问题排查。

步骤五 设置远端视图

音视频通话过程中,除了要显示本地的视频画面,通常也要显示参与互动的其他连麦者/主播的远端视频画面。

  1. 监听远端用户进出频道。

    IRtcEngineEventHandler 通过以下回调获取相关信息:

    • onUserJoined:监听远端用户加入通话房间的事件,并抛出对方的 uid。当本端加入房间后,也会通过此回调抛出通话房间内已有的其他用户。

    • onUserVideoStart:监听远端用户发布视频流的事件,回调中携带对方的 uid 与发布的视频分辨率。

  2. 设置远端视频画布。

    在监听到远端用户加入房间或发布视频流后,本端可以调用 setupRemoteVideoCanvas 方法设置远端用户在本地显示时的缩放模式和镜像模式。

    • 视频缩放模式(NERtcVideoScalingMode):包括适应区域、填充、适应视频。
    • 视频镜像模式:默认为关闭视频镜像模式,您也可以根据需要开启镜像模式。

    示例代码

    cpp// 示例
    NERtcVideoCanvas canvas;
    canvas.cb = nullptr;
    canvas.user_data = nullptr;
    canvas.window = window;
    // 设置视频缩放模式。
    canvas.scaling_mode = mode;
    rtc_engine_->setupRemoteVideoCanvas(uid, &canvas);
    
  3. 监听远端视频流发布。

    当房间中的其他用户发布视频流时,本端会触发 onUserVideoStart 回调。

  4. 订阅远端视频流。

    在设置完远端视频画布后,且监听到远端用户发布视频流时,本端可以调用 subscribeRemoteVideoStream 方法对其发起视频流的订阅,来将对方的视频流渲染到视频画布上。

    示例代码如下:

    cpp//以订阅指定远端用户的视频主流为例
    void NRTCEngine::subscribeRemoteUserVideoStream(nertc::uid_t uid) {
        int ret_temp = rtc_engine_->subscribeRemoteVideoStream(uid, nertc::kNERtcRemoteVideoStreamTypeHigh, true);
        if (ret_temp) {
            qDebug("[ERROR] can not subscribe remote video stream! ERROR CODE: %d", ret_temp);
        }
    } 
    
  5. 监听远端用户离开房间或停止发布视频。

步骤六 音频流

本地音频的采集发布和远端音频订阅播放默认启动,正常情况下无需开发者主动干预。

步骤七 退出通话房间

调用 leaveChannel 方法退出通话房间。

示例代码如下:

cpp  rtc_engine_->leaveChannel();

真正退出房间后,SDK 会走入初始化时设置的 rtc_engine_context_.event_handler 回调事件通知中的 onLeaveChannel

步骤八 销毁实例

当确定短期内不再使用音视频通话实例时,可以释放对应的对象资源。

示例代码如下:

cpp  // 同步销毁 IRtcEngine 对象
  rtc_engine_->release(true);
  // 销毁 RTC 引擎对象
  destroyNERtcEngine((void*&)rtc_engine_);
  rtc_engine_ = nullptr;
  // 音频设备管理指针
  _adm = nullptr;
  // 视频设备管理指针
  _vdm = nullptr;

建议您在调用 releasedestroyNERtcEngine 方法彻底销毁 NERtc 实例后再卸载库,否则可能会导致异常崩溃。

常用的回调

//nertc_event_handler.h
#pragma once
#include "nertc_engine_event_handler_ex.h"
#include "nertc_engine_media_stats_observer.h"

//用户继承nertc::IRtcEngineEventHandlerEx,根据实际业务场景重写相关回调即可
class NertcEventHandler: public nertc::IRtcEngineEventHandlerEx,
	                  public nertc::IRtcMediaStatsObserver
{
public:
	NertcEventHandler();
	~NertcEventHandler();

public:
	virtual void onError(int error_code, const char* msg) override;
	virtual void onWarning(int warn_code, const char* msg) override;
	virtual void onJoinChannel(nertc::channel_id_t cid, nertc::uid_t uid, nertc::NERtcErrorCode result, uint64_t elapsed) override;
	virtual void onConnectionStateChange(nertc::NERtcConnectionStateType state, nertc::NERtcReasonConnectionChangedType reason) override;
	virtual void onLeaveChannel(nertc::NERtcErrorCode result) override;
	virtual void onUserJoined(nertc::uid_t uid, const char * user_name) override;
	virtual void onUserLeft(nertc::uid_t uid, nertc::NERtcSessionLeaveReason reason) override;
	virtual void onUserAudioStart(nertc::uid_t uid) override;
	virtual void onUserAudioStop(nertc::uid_t uid) override;
	virtual void onUserVideoStart(nertc::uid_t uid, nertc::NERtcVideoProfileType max_profile) override;
	virtual void onUserVideoStop(nertc::uid_t uid) override;
	virtual void onUserSubStreamVideoStart(nertc::uid_t uid, nertc::NERtcVideoProfileType max_profile) override;
	virtual void onUserSubStreamVideoStop(nertc::uid_t uid) override;
	virtual void onUserAudioMute(nertc::uid_t uid, bool mute) override;
	virtual void onUserVideoMute(nertc::uid_t uid, bool mute) override;

    virtual void onNetworkQuality(const nertc::NERtcNetworkQualityInfo *infos, unsigned int user_count) override;

};

//nertc_event_handler.cpp
#include "nertc_event_impl.h"

NertcEventHandler::NertcEventHandler()
{
}

NertcEventHandler::~NertcEventHandler()
{
}

void NertcEventHandler::onError(int error_code, const char* msg)
{
	//错误信息回调
}

void NertcEventHandler::onWarning(int warn_code, const char* msg)
{
	//警告信息回调
}

void NertcEventHandler::onJoinChannel(nertc::channel_id_t cid, nertc::uid_t uid, nertc::NERtcErrorCode code, uint64_t elapsed)
{
	//本端用户成功加入房间通知回调,建议在收到此回调后再进行推拉流或订阅的操作
}

void NertcEventHandler::onConnectionStateChange(nertc::NERtcConnectionStateType state, nertc::NERtcReasonConnectionChangedType reason)
{
	//房间连接状态改变通知回调
}

void NertcEventHandler::onLeaveChannel(nertc::NERtcErrorCode result)
{
	//本端用户成功退出房间通知回调
}

void NertcEventHandler::onUserJoined(nertc::uid_t uid, const char * user_name)
{
	//远端用户加入房间通知回调,建议在收到此回调后再进行设置远端视图等的操作
}

void NertcEventHandler::onUserLeft(nertc::uid_t uid, nertc::NERtcSessionLeaveReason reason)
{
	//本端用户成功退出房间通知回调
}

void NertcEventHandler::onUserAudioStart(nertc::uid_t uid)
{
	//远端用户推音频流通知回调,建议在收到此回调后再进行订阅音频流的操作
}

void NertcEventHandler::onUserAudioStop(nertc::uid_t uid)
{
	//远端用户停止推音频流通知回调,建议在收到此回调后再进行取消订阅音频流的操作
}

void NertcEventHandler::onUserVideoStart(nertc::uid_t uid, nertc::NERtcVideoProfileType max_profile)
{
	//远端用户推视频流通知回调,建议在收到此回调后再进行订阅视频流的操作
}

void NertcEventHandler::onUserVideoStop(nertc::uid_t uid)
{
	//远端用户停止推视频流通知回调,建议在收到此回调后再进行取消订阅视频流的操作
}

void NertcEventHandler::onUserSubStreamVideoStart(nertc::uid_t uid, nertc::NERtcVideoProfileType max_profile)
{
	//远端用户开启屏幕共享回调,建议在收到此回调后再进行设置视频辅流画布设置及订阅操作
}

void NertcEventHandler::onUserSubStreamVideoStop(nertc::uid_t uid)
{
	//远端用户停止屏幕共享回调,建议在收到此回调后再取消订阅视频辅流及清空画布

}

void NertcEventHandler::onUserAudioMute(nertc::uid_t uid, bool mute)
{
	// 远端用户是否静音回调,感知并刷新界面展示远端用户麦克风状态
}

void NertcEventHandler::onUserVideoMute(nertc::uid_t uid, bool mute)
{
	//远端用户暂停/恢复发送视频流回调,感知并刷新界面,展示远端用户摄像头状态
}

void NertcEventHandler::onNetworkQuality(const nertc::NERtcNetworkQualityInfo *infos, unsigned int user_count)
{
	//用户网络质量回调,感知并提示用户当前网络状态
}
此文档是否对你有帮助?
有帮助
去反馈
  • 前提条件
  • 示例代码
  • 实现流程
  • 实现音视频通话
  • 步骤一 创建音视频通话的界面
  • 步骤二 引入头文件
  • 步骤三 初始化
  • 步骤四 设置本地视图
  • 步骤四 加入房间
  • 步骤五 设置远端视图
  • 步骤六 音频流
  • 步骤七 退出通话房间
  • 步骤八 销毁实例
  • 常用的回调