实现音视频互动
本文介绍如何集成声网实时互动 SDK,通过少量代码从 0 开始实现一个简单的实时互动 App,适用于互动直播和视频通话场景。
首先,你需要了解以下有关音视频实时互动的基础概念:
- 声网实时互动 SDK:由声网开发的、帮助开发者在 App 中实现实时音视频互动的 SDK。
- 频道:用于传输数据的通道,在同一个频道内的用户可以进行实时互动。
- 主播:可以在频道内发布音视频,同时也可以订阅其他主播发布的音视频。
- 观众:可以在频道内订阅音视频,不具备发布音视频权限。
更多概念详见关键概念。
下图展示在 App 中实现音视频互动的基本工作流程:
- 所有用户调用
joinChannel
方法加入频道,并根据需要设置用户角色:- 互动直播:如果用户需要在频道中发流,则设为主播;如果用户只需要收流,则设为观众。
- 视频通话:将所有的用户角色都为主播。
- 加入频道后,不同角色的用户具备不同的行为:
- 所有用户默认都可以接收频道中的音视频流。
- 主播可以在频道内发布音视频流。
- 观众如果需要发流,可在频道内调用
setClientRole
方法修改用户角色,使其具备发流权限。
前提条件
在实现功能以前,请按照以下要求准备开发环境:
- Windows 7 或以上版本的设备。
- Microsoft Visual Studio 2017 或以上版本。
- C++ 11 或以上版本。
- 如果使用 C# 开发,还需要 .NET 桌面开发组件。
- 可以访问互联网的计算机。如果你的网络环境部署了防火墙,参考应对防火墙限制以正常使用声网服务。
- 一个有效的声网账号以及声网项目。请参考开通服务从声网控制台获得 和临时 。
临时 Token 的有效期是 24 小时。Token 过期会导致加入频道失败。
- C++
- C#
创建项目
以 Windows 11 为例,参考以下操作或官方文档在 Visual Studio 2022 中创建一个项目来实现实时音视频互动功能,如果你已有自己的项目,可跳过这一步骤。
-
在 Visual Studio 中,选择文件 > 新建 > 项目 来创建一个新项目。在弹出的窗口中,选择 MFC 应用作为项目模板,点击下一步将项目名称设为 AgoraQuickStart 并设置项目储存位置,然后点击创建。
-
在弹出的 MFC 应用程序窗口中,将应用程序类型设为基于对话框,将使用 MFC 设为在共享 DLL 中使用 MFC。进入生成的类,将生成的类设为 Dlg,将基类设为 CDialog,最后点击完成。
集成 SDK
- 从下载获取最新的 Windows SDK,解压并打开。
- 打开已下载的 SDK 文件,并将其中的
sdk
文件夹复制到你的项目路径下。保证sdk
文件夹和你的sln
文件处于同一目录。
配置项目属性
在解决方案资源管理器窗口中,右击项目名称并点击属性进行以下配置:
-
进入 C/C++ > 常规 > 附加包含目录菜单,输入
$(SolutionDir)sdk\high_level_api\include
。 -
进入链接器 > 常规 > 附加库目录菜单,输入
$(SolutionDir)sdk\x86_64
。信息如果你是 x86 的 Windows 操作系统,请输入
$(SolutionDir)sdk\x86
。 -
进入链接器 > 输入 > 附加依赖项菜单,输入
$(SolutionDir)sdk\x86_64\agora_rtc_sdk.dll.lib
。信息如果你是 x86 的 Windows 操作系统,请输入
$(SolutionDir)sdk\x86\agora_rtc_sdk.dll.lib
。 -
进入高级菜单,在高级属性中,把将内容复制到 OutDir 和将 C++ 运行时复制到输出目录设为是。
-
进入生成事件 > 生成后事件 >命令行 菜单,输入
copy $(SolutionDir)sdk\x86_64\*.dll $(SolutionDir)$(Platform)\$(Configuration)
。 -
完成上述配置后点击应用。
创建用户界面
根据实时音视频互动的场景需要,你需要在你的应用中添加如下 UI:
- 展示本地视频的视图框
- 展示远端视频的视图框
- 输入频道名称的输入框
- 加入和离开频道按钮
你可以参考下列步骤来创建用户界面。
- 在右侧菜单栏将项目切换为资源视图,然后打开
.Dialog
文件,此时你的界面如下图所示: - 添加展示远端视频的视图框。在视图 > 工具箱中,选择添加 Picture Control 控件,在属性 > 杂项中,将该控件的 ID 设为 IDC_STATIC_REMOTE,如下图所示:
- 添加展示本地视频的视图框。在视图 > 工具箱中,选择添加 Picture Control 控件,在属性 > 杂项中,将该控件的 ID 设为 IDC_STATIC_LOCAL。
- 添加输入频道名称的输入框。在视图 > 工具箱中,选择添加 Static Text 控件,在属性中将描述文字改为频道名。然后再添加一个 Edit Control 控件作为输入框,在属性 > 杂项中,将该控件的 ID 设为 IDC_EDIT_CHANNEL。
- 添加加入和离开频道按钮。在视图 > 工具箱中,选择添加两个 Button 控件,在属性 > 杂项中,将控件的 ID 分设为 ID_BTN_JOIN 和 ID_BTN_LEAVE,将描述文字分别设为加入、离开。 此时的用户界面如下图所示:
实现步骤
本小节介绍如何实现一个实时音视频互动应用。你可以先复制完整的示例代码到你的项目中,快速体验实时音视频互动的基础功能,再按照实现步骤了解核心 API 调用。
下图展示了使用声网 RTC SDK 实现音视频互动的基本流程。
以下展示实现实时互动基本流程的完整示例代码:
复制完整示例代码到你的项目之后,你需要将代码中的 APP_ID
和 token
替换成你在声网控制台获取的 App ID 和临时 Token。
#pragma once
#include <string>
// 引入相关头文件
#include <IAgoraRtcEngine.h>
using namespace agora;
using namespace agora::rtc;
using namespace agora::media;
using namespace agora::media::base;
// 定义消息 ID
#define WM_MSGID(code) (WM_USER+0x200+code)
#define EID_JOIN_CHANNEL_SUCCESS 0x00000002
#define EID_USER_JOINED 0x00000004
#define EID_USER_OFFLINE 0x00000004
// 定义 CAgoraQuickStartRtcEngineEventHandler 类,用于处理用户加入、离开频道等回调事件
class CAgoraQuickStartRtcEngineEventHandler : public IRtcEngineEventHandler {
public:
// 设置消息接收窗口的句柄
void SetMsgReceiver(HWND hWnd) {
m_hMsgHanlder = hWnd;
}
// 注册 onJoinChannelSuccess 回调,本地用户成功加入频道时,会触发该回调
virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) {
if (m_hMsgHanlder) {
::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOIN_CHANNEL_SUCCESS), uid, 0);
}
}
// 注册 onUserJoined 回调,远端主播成功加入频道时,会触发该回调
virtual void onUserJoined(uid_t uid, int elapsed) {
if (m_hMsgHanlder) {
::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), uid, 0);
}
}
// 注册 onUserOffline 回调,远端主播离开频道或掉线时,会触发该回调
virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) {
if (m_hMsgHanlder) {
::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), uid, 0);
}
}
private:
HWND m_hMsgHanlder;
};
// CAgoraQuickStartDlg 对话框
class CAgoraQuickStartDlg : public CDialog {
public:
CAgoraQuickStartDlg(CWnd* pParent = nullptr);
virtual ~CAgoraQuickStartDlg();
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_AGORAQUICKSTART_DIALOG };
#endif
// 处理加入/离开按钮点击事件
afx_msg void OnBnClickedBtnJoin();
afx_msg void OnBnClickedBtnLeave();
// 处理用户加入频道、用户离开等回调事件
afx_msg LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam);
protected:
HICON m_hIcon;
CStatic m_staRemote;
CStatic m_staLocal;
CEdit m_edtChannelName;
// DDX/DDV 支持
virtual void DoDataExchange(CDataExchange* pDX);
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
DECLARE_MESSAGE_MAP()
std::string cs2utf8(CString str);
private:
IRtcEngine* m_rtcEngine = nullptr;
CAgoraQuickStartRtcEngineEventHandler m_eventHandler;
bool m_initialize = false;
bool m_remoteRender = false;
};
// AgoraQuickStartDlg.cpp: 实现文件
//
#include "pch.h"
#include "framework.h"
#include "AgoraQuickStart.h"
#include "AgoraQuickStartDlg.h"
#include "afxdialogex.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// 用于应用程序“关于”菜单项的 CAboutDlg 对话框
class CAboutDlg : public CDialogEx {
public:
CAboutDlg();
// 对话框数据
#ifdef AFX_DESIGN_TIME
enum {
IDD = IDD_ABOUTBOX
}
;
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX);
// DDX/DDV 支持
// 实现
protected:
DECLARE_MESSAGE_MAP()
}
;
CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX) {
}
void CAboutDlg::DoDataExchange(CDataExchange* pDX) {
CDialogEx::DoDataExchange(pDX);
}
BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx)
END_MESSAGE_MAP()
// CAgoraQuickStartDlg 对话框,用于处理主要的用户交互和回调事件
CAgoraQuickStartDlg::CAgoraQuickStartDlg(CWnd* pParent
/*=nullptr*/
)
: CDialog(IDD_AGORAQUICKSTART_DIALOG, pParent) {
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
CAgoraQuickStartDlg::~CAgoraQuickStartDlg() {
CDialog::~CDialog();
// 在删除 CAgoraQuickStartDlg 对象时,释放引擎和相关资源
if(m_rtcEngine) {
m_rtcEngine->release(true);
m_rtcEngine = NULL;
}
}
void CAgoraQuickStartDlg::DoDataExchange(CDataExchange* pDX) {
CDialog::DoDataExchange(pDX);
// 将控件和变量关联起来,以便于读写控件的数据
DDX_Control(pDX, IDC_EDIT_CHANNEL, m_edtChannelName);
DDX_Control(pDX, IDC_STATIC_REMOTE, m_staRemote);
DDX_Control(pDX, IDC_STATIC_LOCAL, m_staLocal);
}
BEGIN_MESSAGE_MAP(CAgoraQuickStartDlg, CDialog)
// 声明消息映射,用于处理 Windows 消息和用户加入、离开频道等事件
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(ID_BTN_JOIN, &CAgoraQuickStartDlg::OnBnClickedBtnJoin)
ON_BN_CLICKED(ID_BTN_LEAVE, &CAgoraQuickStartDlg::OnBnClickedBtnLeave)
ON_MESSAGE(WM_MSGID(EID_JOIN_CHANNEL_SUCCESS), CAgoraQuickStartDlg::OnEIDJoinChannelSuccess)
ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraQuickStartDlg::OnEIDUserJoined)
ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraQuickStartDlg::OnEIDUserOffline)
END_MESSAGE_MAP()
// CAgoraQuickStartDlg 消息处理程序
// 填入你项目的 App ID,在声网控制台获取
#define "<APP_ID>"
// 填入你在声网控制台获取的临时 Token
#define "<token>"
BOOL CAgoraQuickStartDlg::OnInitDialog() {
CDialog::OnInitDialog();
// 将“关于...”菜单项添加到系统菜单中
// IDM_ABOUTBOX 必须在系统命令范围内
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);
// 设置小图
m_eventHandler.SetMsgReceiver(m_hWnd);
// 创建 rtcEngine 对象
m_rtcEngine = createAgoraRtcEngine();
RtcEngineContext context;
context.appId = APP_ID;
context.eventHandler = &m_eventHandler;
//初始化
int ret = m_rtcEngine->initialize(context);
if (ret == 0) {
m_initialize = true;
} else {
m_initialize = false;
}
// 启用视频模块
m_rtcEngine->enableVideo();
return TRUE;
// 除非将焦点设置到控件,否则返回 TRUE
}
void CAgoraQuickStartDlg::OnSysCommand(UINT nID, LPARAM lParam) {
if ((nID & 0xFFF0) == IDM_ABOUTBOX) {
CAboutDlg dlgAbout;
dlgAbout.DoModal();
} else {
CDialog::OnSysCommand(nID, lParam);
}
}
// 如果向对话框添加最小化按钮,则需要下面的代码
// 来绘制该图标。对于使用文档/视图模型的 MFC 应用程序,这将由框架自动完成
void CAgoraQuickStartDlg::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 {
CDialog::OnPaint();
}
}
//当用户拖动最小化窗口时系统调用此函数取得光标
HCURSOR CAgoraQuickStartDlg::OnQueryDragIcon() {
return static_cast<HCURSOR>(m_hIcon);
}
std::string CAgoraQuickStartDlg::cs2utf8(CString str) {
char szBuf[2 * MAX_PATH] = {
0
}
;
WideCharToMultiByte(CP_UTF8, 0, str.GetBuffer(0), str.GetLength(), szBuf, 2 *MAX_PATH, NULL, NULL);
return szBuf;
}
void CAgoraQuickStartDlg::OnBnClickedBtnJoin() {
// 加入频道
// 获取频道名
CString strChannelName;
m_edtChannelName.GetWindowText(strChannelName);
if (strChannelName.IsEmpty()) {
AfxMessageBox(_T("Fill channel name first"));
return;
}
ChannelMediaOptions option;
// 设置频道场景为直播
options.channelProfile = CHANNEL_PROFILE_LIVE_BROADCASTING;
// 设置用户角色为主播
options.clientRoleType = CLIENT_ROLE_BROADCASTER;
// 发布麦克风采集的音频流
options.publishMicrophoneTrack = true;
// 发布摄像头采集的视频流
options.publishCameraTrack = true;
// 自动订阅频道内的音频流
options.autoSubscribeAudio = true;
// 自动订阅频道内的视频流
options.autoSubscribeVideo = true;
// 填入你在控制台获取的临时 Token 加入频道
int ret = m_rtcEngine->joinChannel(token, cs2utf8(strChannelName).c_str(), 0,option);
// 渲染本地视图
VideoCanvas canvas;
canvas.renderMode = RENDER_MODE_TYPE::RENDER_MODE_HIDDEN;
canvas.uid = 0;
canvas.view = m_staLocal.GetSafeHwnd();
m_rtcEngine->setupLocalVideo(canvas);
// 开启本地视频预览
m_rtcEngine->startPreview();
}
void CAgoraQuickStartDlg::OnBnClickedBtnLeave() {
// 停止本地视频预览
m_rtcEngine->stopPreview();
// 离开频道
m_rtcEngine->leaveChannel();
// 清除本地视图
VideoCanvas canvas;
canvas.uid = 0;
m_rtcEngine->setupLocalVideo(canvas);
m_remoteRender = false;
}
LRESULT CAgoraQuickStartDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) {
// 加入频道成功回调
uid_t localUid = wParam;
return 0;
}
LRESULT CAgoraQuickStartDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) {
// 远端用户加入回调
uid_t remoteUid = wParam;
if (m_remoteRender) {
return 0;
}
// 渲染远端视图
VideoCanvas canvas;
canvas.renderMode = RENDER_MODE_TYPE::RENDER_MODE_HIDDEN;
canvas.uid = remoteUid;
canvas.view = m_staRemote.GetSafeHwnd();
m_rtcEngine->setupRemoteVideo(canvas);
m_remoteRender = true;
return 0;
}
LRESULT CAgoraQuickStartDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) {
// 远端用户离开回调
uid_t remoteUid = wParam;
if (!m_remoteRender) {
return 0;
}
// 清除远端视图
VideoCanvas canvas;
canvas.uid = remoteUid;
m_rtcEngine->setupRemoteVideo(canvas);
m_remoteRender = false;
return 0;
}
参考下列步骤来在你的应用中实现实时互动:
初始化引擎
在调用其他声网 API 前,你需要调用 createAgoraRtcEngine
创建一个IRtcEngine
对象,然后调用 initialize
并传入 App ID,初始化 IRtcEngine
。
在初始化 SDK 前,需确保终端用户已经充分了解并同意相关的隐私政策。
// 创建 IRtcEngine 对象
m_rtcEngine = createAgoraRtcEngine();
// 创建 IRtcEngine 上下文对象
RtcEngineContext context;
// 输入你的 App ID。你可以在声网控制台获取你的项目的 App ID
context.appId = APP_ID;
// 添加注册回调和事件
context.eventHandler = &m_eventHandler;
// 初始化
int ret = m_rtcEngine->initialize(context);
if (ret == 0) {
m_initialize = true;
} else {
m_initialize = false;
}
启用视频模块
- 调用
enableVideo
方法,启用视频模块。 - 调用
setupLocalVideo
初始化本地视图,同时设置本地的视频显示属性。 - 调用
startPreview
方法,开启本地视频预览。
// 启用视频模块
m_rtcEngine->enableVideo();
...
// 设置本地视频显示属性
VideoCanvas canvas;
// 设置视频尺寸等比缩放
canvas.renderMode = RENDER_MODE_TYPE::RENDER_MODE_HIDDEN;
// 用户 ID
canvas.uid = 0;
// 视频显示窗口
canvas.view = m_staLocal.GetSafeHwnd();
m_rtcEngine->setupLocalVideo(canvas);
// 开启本地视频预览
m_rtcEngine->startPreview();
加入频道并发布音视频流
调用 joinChannel
[2/2] 方法、填入你在控制台获取的临时 Token,以及获取 Token 时填入的频道名加入频道,并设置用户角色。
void CAgoraQuickStartDlg::OnBnClickedBtnJoin() {
CString strChannelName;
// 获取频道名
m_edtChannelName.GetWindowText(strChannelName);
if (strChannelName.IsEmpty()) {
AfxMessageBox(_T("Fill channel name first"));
return;
}
ChannelMediaOptions option;
// 设置频道场景为直播场景
options.channelProfile = CHANNEL_PROFILE_LIVE_BROADCASTING;
// 设置用户角色为主播;如果要将用户角色设置为观众,保持默认值即可
options.clientRoleType = CLIENT_ROLE_BROADCASTER;
// 发布麦克风采集的音频流
options.publishMicrophoneTrack = true;
// 发布摄像头采集的视频流
options.publishCameraTrack = true;
// 自动订阅所有音频流
options.autoSubscribeAudio = true;
// 自动订阅所有视频流
options.autoSubscribeVideo = true;
// 填入你在控制台获取的临时 Token 加入频道
m_rtcEngine->joinChannel(token, cs2utf8(strChannelName).c_str(), 0, option);
}
实现常用回调
你可以根据需求,在初始化时实现其他功能,如注册用户加入、离开频道等回调。
class CAgoraQuickStartRtcEngineEventHandler : public IRtcEngineEventHandler {
public:
// 注册 onJoinChannelSuccess 回调
// 本地用户成功加入频道时,会触发该回调
virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) {
if (m_hMsgHanlder) {
::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOIN_CHANNEL_SUCCESS), uid, 0);
}
}
// 注册 onUserJoined 回调
// 远端主播成功加入频道时,会触发该回调
virtual void onUserJoined(uid_t uid, int elapsed) {
if (m_hMsgHanlder) {
::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), uid, 0);
}
}
// 注册 onUserOffline 回调
// 远端主播离开频道或掉线时,会触发该回调
virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) {
if (m_hMsgHanlder) {
::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), uid, 0);
}
}
private:
HWND m_hMsgHanlder;
};
设置远端视图
当远端用户加入频道时,从远端用户加入频道的回调中获取到的远端用户的 uid,然后调用 setupRemoteVideo
设置并渲染远端视图。
LRESULT CAgoraQuickStartDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) {
// 远端用户加入回调
uid_t remoteUid = wParam;
if (m_remoteRender) {
return;
}
// 设置远端视频显示属性
VideoCanvas canvas;
// 设置视频尺寸等比缩放
canvas.renderMode = RENDER_MODE_TYPE::RENDER_MODE_HIDDEN;
// 远端用户 ID
canvas.uid = remoteUid;
canvas.view = m_staRemote.GetSafeHwnd();
m_rtcEngine->setupRemoteVideo(canvas);
m_remoteRender = true;
}
离开频道
根据场景需要,如关闭应用或应用切换至后台时,调用 leaveChannel
离开当前频道。
void CAgoraQuickStartDlg::OnBnClickedBtnLeave() {
// 停止本地视频预览
m_rtcEngine->stopPreview();
// 离开频道
m_rtcEngine->leaveChannel();
// 清除本地视图
VideoCanvas canvas;
canvas.uid = 0;
m_rtcEngine->setupLocalVideo(canvas);
m_remoteRender = false;
}
如果你不再需要互动,调用 release
方法释放引擎资源。
CDialog::~CDialog() {
// 在对象被销毁时释放资源
if(m_rtcEngine){
m_rtcEngine->release(true);
m_rtcEngine = NULL;
}
}
调试应用
按照以下步骤来测试你的应用:
-
在 Visual Studio 中,选择本地 Windows 调试器开始编译应用。
-
在输入框中输入你要加入的频道名,并点击加入按钮加入频道,你将在本地视图中看到自己。
-
邀请一位朋友通过另一台设备来使用相同的 App ID、频道名、Token 加入频道。你的朋友加入频道后,你们可以听见、看见对方。
创建项目
参考以下操作,在 Visual Studio 中创建一个 Windows 项目:
- 在 Visual Studio 上创建一个 Windows Forms 应用项目,详见 Create a Windows Forms app in Visual Studio with C#。
- 打开 Configuration Manager 窗口,在 Active solution platform 下拉菜单中选择 New,在弹出的窗口中选择 x64 或 x86 作为目标平台。
集成 SDK
使用 Visual Studio 中的 NuGet 包管理器,将 SDK 集成到项目中。详见快速入门:在 Visual Studio 中安装和使用包。
你可以在 agora_rtc_sdk 中获取最新发布的 SDK 的版本信息。
创建用户界面
为直观地体验视频通话,你需要根据应用场景创建用户界面 (UI)。若你的项目中已有用户界面,可略过此步骤。
如果你想实现音视频互动,推荐在 UI 上添加以下控件:
- 本地视图窗口
- 远端视图窗口
- 加入频道按钮
- 离开频道按钮
参考以下步骤创建 UI。
-
创建 Join 和 Leave 按钮
- 在你的项目中,打开 Solution Explore 窗口,双击 Form1.cs,打开 Toolbox 窗口,选择 Button 控件,依次添加两个按钮,并将两个按钮拖放至合适位置。
- 将鼠标移至其中一个按钮上,点击鼠标右键,选中 Properties,在打开的 Properties 窗口中修改 Text 属性为 Join,修改 Name 属性为 btnJoin。
- 重复上一个步骤来修改另一个按钮的属性:修改 Text 为 Leave;修改 Name 为 btnLeave。
-
创建本地及远端视图窗口
- 打开 Toolbox 窗口,选中 PictureBox 控件,依次为本地视图和远端视图在合适的位置添加控件。
- 将鼠标移至本地视图窗口控件上,点击鼠标右键,选中 Properties,在打开的 Properties 窗口中修改 Name 属性为 videoboxLocal;修改 BorderStyle 为 FixedSingle。
- 重复上一个步骤修改远端视图窗口控件的属性:修改 Name 为 videoboxRemote;修改 BorderStyle 为 FixedSingle。
-
创建频道名输入框
- 打开 Toolbox 窗口,选中 TextBox 控件,然后将其拖拽至合适位置。
- 将鼠标移至添加好的输入框控件上,点击鼠标右键,选中 Properties ,修改其属性 Name 为 txChannelName。
-
保存上述步骤的更改。
-
分别双击 Join 和 Leave 按钮,IDE 会自动关联点击事件处理函数。
此时你的 UI 界面如下图所示:
实现步骤
本小节介绍如何实现一个实时音视频互动 App。你可以先复制完整的示例代码到你的项目中,快速体验实时音视频互动的基础功能,再按照实现步骤了解核心 API 调用。
下图展示了使用声网 RTC SDK 实现音视频互动的基本流程。
下面列出了一段实现实时互动基本流程的完整代码以供参考。
在 APP_ID
和 Token
字段中传入你在控制台获取到的 App ID 和临时 Token。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Agora.Rtc;
namespace WindowsFormsApp2
{
public partial class Form1 : Form
{
private readonly string APP_ID = "<APP_ID>";
private readonly String APP_TOKEN = "<Token>";
private IRtcEngine engine_ = null;
private RtcEventHandler handler_ = null;
public Form1()
{
InitializeComponent();
// 创建引擎
engine_ = RtcEngine.CreateAgoraRtcEngine();
// 配置引擎实例
RtcEngineContext ctx = new RtcEngineContext()
{
appId = APP_ID,
areaCode = AREA_CODE.AREA_CODE_GLOB,
logConfig = { filePath = "rtc.log" }
};
// 初始化引擎
var ret = engine_.Initialize(ctx);
if (ret != 0)
{
Console.WriteLine("=====>Initialize failed {0}", ret);
return;
}
// 注册事件句柄
handler_ = new RtcEventHandler();
handler_.EventOnUserJoined += OnUserJoined;
engine_.InitEventHandler(handler_);
// 启用视频模块
ret = engine_.EnableVideo();
if (ret != 0)
{
Console.WriteLine("=====>EnableVideo failed {0}", ret);
}
// 开启本地视频采集
ret = engine_.EnableLocalVideo(true);
if (ret != 0)
{
Console.WriteLine("=====>EnableLocalVideo failed {0}", ret);
}
// 开始本地视频预览
ret = engine_.StartPreview(VIDEO_SOURCE_TYPE.VIDEO_SOURCE_CAMERA_PRIMARY);
if (ret != 0)
{
Console.WriteLine("=====>StartPreview failed {0}", ret);
}
// 设置本地视图
VideoCanvas canvas = new VideoCanvas();
canvas.view = (long)videoboxLocal.Handle;
canvas.renderMode = RENDER_MODE_TYPE.RENDER_MODE_FIT;
ret = engine_.SetupLocalVideo(canvas);
if (ret != 0)
{
Console.WriteLine("=====>SetupLocalVideo failed {0}", ret);
}
}
private void btnJoin_Click(object sender, EventArgs e)
{
if (null != engine_)
{
ChannelMediaOptions options = new ChannelMediaOptions();
// 设置频道场景为直播
options.channelProfile.SetValue(CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING);
// 设置用户角色为主播
options.clientRoleType.SetValue(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER);
// 发布麦克风采集的音频流
options.publishMicrophoneTrack = true;
// 发布摄像头采集的视频流
options.publishCameraTrack = true;
// 自动订阅所有音频流
options.autoSubscribeAudio = true;
// 自动订阅所有视频流
options.autoSubscribeVideo = true;
var ret = engine_.JoinChannel(APP_TOKEN, txChannelName.Text, 0, options);
Console.WriteLine("=====>JoinChannel result {0}", ret);
}
}
private void btnLeave_Click(object sender, EventArgs e)
{
if (null != engine_)
{
var ret = engine_.StopPreview();
var ret = engine_.LeaveChannel();
Console.WriteLine("=====>LeaveChannel result {0}", ret);
}
}
private void OnUserJoined(RtcConnection connection, uint remoteUid, int elapsed)
{
VideoCanvas canvas = new VideoCanvas();
canvas.view = (long)videoboxRemote.Handle;
canvas.renderMode = RENDER_MODE_TYPE.RENDER_MODE_FIT;
canvas.uid = remoteUid;
int ret = engine_.SetupRemoteVideo(canvas);
Console.WriteLine("----->SetupRemoteVideo, ret={0}", ret);
}
}
internal class RtcEventHandler : IRtcEngineEventHandler
{
public delegate void OnUserJoinedHandler(
RtcConnection connection,
uint remoteUid,
int elapsed
);
// 声明 OnUserJoined 回调
public event OnUserJoinedHandler EventOnUserJoined;
public RtcEventHandler() { }
public override void OnError(int error, string msg)
{
Console.WriteLine("=====>OnError {0} {1}", error, msg);
}
public override void OnJoinChannelSuccess(RtcConnection connection, int elapsed)
{
Console.WriteLine("----->OnJoinChannelSuccess channel={0} uid={1}", connection.channelId,connection.localUid);
}
public override void OnLeaveChannel(RtcConnection connection, RtcStats stats)
{
Console.WriteLine("----->OnLeaveChannel duration={0}", stats.duration);
}
public override void OnUserJoined(RtcConnection connection, uint remoteUid, int elapsed)
{
Console.WriteLine("----->OnUserJoined uid={0}", remoteUid);
if (EventOnUserJoined != null)
EventOnUserJoined.Invoke(connection, remoteUid, elapsed);
}
public override void OnUserOffline(
RtcConnection connection,
uint remoteUid,
USER_OFFLINE_REASON_TYPE reason
)
{
Console.WriteLine("----->OnUserOffline, channel={0}, remoteUid={1}, reason={2}", connection.channelId, remoteUid, reason);
}
public override void OnRemoteVideoStateChanged(
RtcConnection connection,
uint remoteUid,
REMOTE_VIDEO_STATE state,
REMOTE_VIDEO_STATE_REASON reason,
int elapsed
)
{
Console.WriteLine("----->OnRemoteVideoStateChanged, channel={0}, remoteUid={1}, state={2}, reason={3}", connection.channelId, remoteUid, state, reason);
}
}
}
导入声网相关的类
导入声网 RTC SDK 相关的类。
using Agora.Rtc;
初始化引擎
调用 CreateAgoraRtcEngine
方法创建一个 RtcEngine
对象,然后调用 initialize
初始化 RtcEngine
。
// 创建引擎
engine_ = RtcEngine.CreateAgoraRtcEngine();
// 配置引擎实例
RtcEngineContext ctx = new RtcEngineContext()
{
appId = APP_ID,
areaCode = AREA_CODE.AREA_CODE_GLOB,
logConfig = { filePath = "rtc.log" }
};
// 初始化引擎
var ret = engine_.Initialize(ctx);
if (ret != 0)
{
Console.WriteLine("=====>Initialize failed {0}", ret);
return;
}
启用视频模块
- 调用
EnableVideo
方法,启用视频模块。 - 调用
SetupLocalVideo
初始化本地视图,同时设置本地的视频显示属性。 - 调用
StartPreview
方法,开启本地视频预览。
// 启用视频模块
ret = engine_.EnableVideo();
if (ret != 0)
{
Console.WriteLine("=====>EnableVideo failed {0}", ret);
}
// 设置本地视图
VideoCanvas canvas = new VideoCanvas();
canvas.view = (long)videoboxLocal.Handle;
canvas.renderMode = RENDER_MODE_TYPE.RENDER_MODE_FIT;
ret = engine_.SetupLocalVideo(canvas);
if (ret != 0)
{
Console.WriteLine("=====>SetupLocalVideo failed {0}", ret);
}
// 开启本地视频预览
ret = engine_.StartPreview(VIDEO_SOURCE_TYPE.VIDEO_SOURCE_CAMERA_PRIMARY);
if (ret != 0)
{
Console.WriteLine("=====>StartPreview failed {0}", ret);
}
加入频道并发布音视频流
调用 JoinChannel
[2/2] 加入频道,填入你在控制台获取的临时 Token,以及获取 Token 时填入的频道名加入频道,并设置用户角色。
ChannelMediaOptions options = new ChannelMediaOptions();
// 设置频道场景为直播
options.channelProfile.SetValue(CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING);
// 设置用户角色为主播
options.clientRoleType.SetValue(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER);
// 发布麦克风采集的音频流
options.publishMicrophoneTrack = true;
// 发布摄像头采集的视频流
options.publishCameraTrack = true;
// 自动订阅所有音频流
options.autoSubscribeAudio = true;
// 自动订阅所有视频流
options.autoSubscribeVideo = true;
var ret = engine_.JoinChannel(APP_TOKEN, txChannelName.Text, 0, options);
设置远端用户视图
调用 SetupRemoteVideo
方法初始化远端用户视图,同时设置远端用户的视图在本地显示属性。你可以通过 OnUserJoined
回调获取远端用户的 uid
。
private void OnUserJoined(RtcConnection connection, uint remoteUid, int elapsed)
{
VideoCanvas canvas = new VideoCanvas();
canvas.view = (long)videoboxRemote.Handle;
canvas.renderMode = RENDER_MODE_TYPE.RENDER_MODE_FIT;
canvas.uid = remoteUid
int ret = engine_.SetupRemoteVideo(canvas);
Console.WriteLine("----->SetupRemoteVideo, ret={0}", ret);
}
实现常用回调
根据使用场景,定义必要的回调。以下示例代码展示如何实现 OnJoinChannelSuccess
、 OnUserJoined
和 OnUserOffline
回调。
public override void OnJoinChannelSuccess(RtcConnection connection, int elapsed)
{
Console.WriteLine("----->OnJoinChannelSuccess channel={0} uid={1}", connection.channelId,connection.localUid);
}
public override void OnUserJoined(RtcConnection connection, uint remoteUid, int elapsed)
{
Console.WriteLine("----->OnUserJoined uid={0}", remoteUid);
if (EventOnUserJoined != null)
EventOnUserJoined.Invoke(connection, remoteUid, elapsed);
}
public override void OnUserOffline(
RtcConnection connection,
uint remoteUid,
USER_OFFLINE_REASON_TYPE reason
)
开始音视频互动
在 btnJoin_Click
函数中调用 JoinChannel
[2/2] 方法,当用户点击 Join 按钮时开始音视频互动。
private void btnJoin_Click(object sender, EventArgs e)
{
if (null != engine_)
{
...
var ret = engine_.JoinChannel(APP_TOKEN, txChannelName.Text, 0, options);
Console.WriteLine("=====>JoinChannel result {0}", ret);
}
}
结束音视频互动
-
在 btnLeave_Click 函数中调用
LeaveChannel
方法,当用户点击 Leave 按钮时结束音视频互动。C#private void btnLeave_Click(object sender, EventArgs e)
{
if (null != engine_)
{
var ret = engine_.StopPreview();
var ret = engine_.LeaveChannel();
Console.WriteLine("=====>LeaveChannel result {0}", ret);
}
} -
调用
Dispose
销毁引擎,并释放声网 SDK 中使用的所有资源。C#engine_.Dispose()
警告调用
Dispose
后,你将无法再使用 SDK 的所有方法和回调。如需再次使用实时音视频互动功能,你必须重新创建一个新的引擎。详见初始化引擎。
测试你的项目
按照以下步骤来测试你的视频通话项目:
- 将你从声网控制台获取的 App ID 和临时 Token 分别填入到
Form1.cs
文件的APP_ID
和APP_TOKEN
中。 - 在 Visual Studio 中点击 Start 按钮运行你的项目,你将在本地视图中看到自己。
- 在输入框中输入你在声网控制台生成临时 Token 时填写的频道名,并点击 Join 按钮加入频道。
- 邀请一位朋友通过另一台设备来使用相同的 App ID、频道名、Token 加入频道。你的朋友加入频道后,你们可以听见、看见对方。
后续步骤
- 在测试或生产环境中,为确保通信安全,声网推荐使用 Token 服务器来生成 Token,详见使用 Token 鉴权。
- 如果你想要实现极速直播场景,可以在互动直播的基础上,通过修改观众端的延时级别为低延时 (
AUDIENCE_LATENCY_LEVEL_LOW_LATENCY
)实现。详见实现极速直播。
参考信息
示例项目
声网提供了开源的音视频互动示例项目供你参考,你可以前往下载或查看其中的源代码。