实现音视频互动
本文介绍如何集成声网实时互动 SDK,通过少量代码从 0 开始实现一个简单的实时互动 App,适用于互动直播和视频通话场景。
首先,你需要了解以下有关音视频实时互动的基础概念:
- 声网实时互动 SDK:由声网开发的、帮助开发者在 App 中实现实时音视频互动的 SDK。
- 频道:用于传输数据的通道,在同一个频道内的用户可以进行实时互动。
- 主播:可以在频道内发布音视频,同时也可以订阅其他主播发布的音视频。
- 观众:可以在频道内订阅音视频,不具备发布音视频权限。
更多概念详见关键概念。
下图展示在 App 中实现音视频互动的基本工作流程:
- 所有用户调用
joinChannel
方法加入频道,并根据需要设置用户角色:- 互动直播:如果用户需要在频道中发流,则设为主播;如果用户只需要收流,则设为观众。
- 视频通话:将所有的用户角色都为主播。
- 加入频道后,不同角色的用户具备不同的行为:
- 所有用户默认都可以接收频道中的音视频流。
- 主播可以在频道内发布音视频流。
- 观众如果需要发流,可在频道内调用
setClientRole
方法修改用户角色,使其具备发流权限。
前提条件
-
Flutter 2.10.5 或更高版本。
-
Dart 2.14.0 或更高版本。
-
根据你的目标平台,准备对应的开发和运行环境:
目标平台 环境要求 iOS - macOS 10.15 或更高版本
- 最新版本的 Xcode
- 两台 iOS 设备
Android - macOS 10.15 或更高版本
- Windows 10 或更高版本
- 最新版本的 Android Studio
- 两台 Android 设备
macOS - macOS 10.15 或更高版本
- 最新版本的 Xcode
- 两台 macOS 设备
Windows - Windows 10 或更高版本
- 最新版本的 Visual Studio
- 两台 Windows 设备
信息- 更多环境要求细节,详见 Install Flutter。
- 你可以运行
flutter doctor
命令检查开发和运行环境是否达到要求。
-
可以访问互联网的计算机。如果你的网络环境部署了防火墙,参考应对防火墙限制以正常使用声网服务。
-
一个有效的声网账号以及声网项目。请参考开通服务从声网控制台获得以下信息:
- App ID:声网随机生成的字符串,用于识别你的项目。
- 临时 Token:Token 也称为动态密钥,在客户端加入频道时对用户鉴权。临时 Token 的有效期为 24 小时。
创建项目
根据你选用的 IDE 不同,创建 Flutter 项目有多种方式,详见 Create the app。在本文中,以在终端中运行 flutter create
命令创建 Flutter 项目为例。
# 将 <Project Name> 替换为项目名
flutter create <Project Name>
集成 SDK
-
打开
pubspec.yaml
文件,添加以下依赖:- 添加
agora_rtc_engine
依赖项,集成声网 Flutter SDK。关于agora_rtc_engine
的最新版本可以查询 https://pub.dev/packages/agora_rtc_engine - 添加
permission_handler
依赖项,安装权限处理插件。
YAMLenvironment:
sdk: ">=2.12.0 <3.0.0"
# 依赖项
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.3
# 声网 Flutter SDK 依赖项,请使用最新版本的 agora_rtc_engine
agora_rtc_engine: ^6.0.0
# 权限处理插件依赖项
permission_handler: ^8.3.0 - 添加
-
添加依赖并保存文件后,打开终端,在项目路径下执行以下命令获取并安装依赖:
Shellflutter pub get
创建用户界面
根据实时音视频互动的场景需要,为你的项目创建两个视图框,分别用于展示本地视频和远端视频。复制以下代码即可快速创建场景所需的用户界面。
// 构建 UI,显示本地视图和远端视图
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Agora Video Call'),
),
body: Stack(
children: [
Center(
child: _remoteVideo(),
),
Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 100,
height: 150,
child: Center(
child: _localUserJoined
? AgoraVideoView(
controller: VideoViewController(
rtcEngine: _engine,
canvas: const VideoCanvas(uid: 0),
),
)
: const CircularProgressIndicator(),
),
),
),
],
),
),
);
}
实现步骤
本小节介绍如何实现一个实时音视频互动 App。你可以先复制完整的示例代码到你的项目中,快速体验实时音视频互动的基础功能,再按照实现步骤了解其中的核心 API 调用。
下图展示了使用声网 RTC SDK 实现音视频互动的基本流程:
下面列出了一段实现实时音视频互动基本流程的完整代码以供参考。复制以下代码替换 /lib/main.dart
文件的全部内容,即可快速体验实时音视频互动基础功能。
将 <-- Insert App Id -->
、<-- Insert Token -->
和 <-- Insert Channel Name -->
分别替换为你在控制台获取到的 App ID、临时 Token,以及生成临时 Token 时填入的频道名。
import 'dart:async';
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
void main() => runApp(MyApp());
// 填写声网控制台中获取的 App ID
const appId = "<-- Insert App Id -->";
// 填写声网控制台中生成的临时 Token
const token = "<-- Insert Token -->";
// 填写频道名
const channel = "<-- Insert Channel Name -->";
// 应用类
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
_MyAppState createState() => _MyAppState();
}
// 应用状态类
class _MyAppState extends State<MyApp> {
int? _remoteUid; // 用于存储远端用户的 uid
bool _localUserJoined = false; // 表示本地用户是否加入频道,初始值为 false
late RtcEngine _engine; // 用于存储 RtcEngine 实例
void initState() {
super.initState();
initAgora();
}
Future<void> initAgora() async {
// 获取麦克风和摄像头权限
await [Permission.microphone, Permission.camera].request();
// 创建 RtcEngine 对象
_engine = await createAgoraRtcEngine();
// 初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景)
await _engine.initialize(const RtcEngineContext(
appId: appId,
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
));
// 添加回调事件
_engine.registerEventHandler(
RtcEngineEventHandler(
// 成功加入频道回调
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
debugPrint("local user ${connection.localUid} joined");
setState(() {
_localUserJoined = true;
});
},
// 远端用户或主播加入当前频道回调
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
debugPrint("remote user $remoteUid joined");
setState(() {
_remoteUid = remoteUid;
});
},
// 远端用户或主播离开当前频道回调
onUserOffline: (RtcConnection connection, int remoteUid,
UserOfflineReasonType reason) {
debugPrint("remote user $remoteUid left channel");
setState(() {
_remoteUid = null;
});
},
),
);
// 启用视频模块
await _engine.enableVideo();
// 开启本地预览
await _engine.startPreview();
// 加入频道
await _engine.joinChannel(
token: token,
channelId: channel,
options: const ChannelMediaOptions(
// 自动订阅所有视频流
autoSubscribeVideo: true,
// 自动订阅所有音频流
autoSubscribeAudio: true,
// 发布摄像头采集的视频
publishCameraTrack: true,
// 发布麦克风采集的音频
publishMicrophoneTrack: true,
// 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众)
clientRoleType: ClientRoleType.clientRoleBroadcaster),
uid: 0,
);
}
void dispose() {
super.dispose();
_dispose();
}
Future<void> _dispose() async {
await _engine.leaveChannel(); // 离开频道
await _engine.release(); // 释放资源
}
// 构建 UI,显示本地视图和远端视图
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Agora Video Call'),
),
body: Stack(
children: [
Center(
child: _remoteVideo(),
),
Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 100,
height: 150,
child: Center(
child: _localUserJoined
? AgoraVideoView(
controller: VideoViewController(
rtcEngine: _engine,
canvas: const VideoCanvas(uid: 0),
),
)
: const CircularProgressIndicator(),
),
),
),
],
),
),
);
}
// 生成远端视频
Widget _remoteVideo() {
if (_remoteUid != null) {
return AgoraVideoView(
controller: VideoViewController.remote(
rtcEngine: _engine,
canvas: VideoCanvas(uid: _remoteUid),
connection: const RtcConnection(channelId: channel),
),
);
} else {
return const Text(
'Please wait for remote user to join',
textAlign: TextAlign.center,
);
}
}
}
导入 package
导入构建 App 所需的 package。
import 'dart:async';
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
定义 App ID 和 Token
传入从声网控制台获取的 App ID、临时 Token,以及生成临时 Token 时填入的频道名,用于后续初始化引擎和加入频道。
// 填写声网控制台中获取的 App ID
const appId = "<-- Insert App Id -->";
// 填写声网控制台中生成的临时 Token
const token = "<-- Insert Token -->";
// 填写频道名
const channel = "<-- Insert Channel Name -->";
处理权限请求
获取体验实时音视频所需的麦克风和摄像头权限。
await [Permission.microphone, Permission.camera].request();
如果你的目标平台是 iOS 或 macOS,需要在 Info.plist
中添加实时互动所需的权限声明如下:
- 麦克风:
key
为Privacy - Microphone Usage Description
,value
填写使用目的,如:for a video call。 - 摄像头:
key
为Privacy - Camera Usage Description
,value
填写使用目的,如:for a video call。
初始化引擎
调用 createAgoraRtcEngine
方法创建一个 RtcEngine
对象,然后调用 initialize
初始化引擎并设置频道场景为 channelProfileLiveBroadcasting
(直播场景)。
在初始化 SDK 前,需确保终端用户已经充分了解并同意相关的隐私政策。
// 创建 RtcEngine 对象
_engine = await createAgoraRtcEngine();
// 初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景)
await _engine.initialize(const RtcEngineContext(
appId: appId,
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
));
启用视频模块
按照以下步骤启用视频模块:
- 调用
enableVideo
方法,启用视频模块。 - 调用
startPreview
方法,开启本地视频预览。
// 启用视频模块
await _engine.enableVideo();
// 开启本地预览
await _engine.startPreview();
加入频道并发布音视频流
调用 joinChannel
加入频道。在 ChannelMediaOptions
中进行如下配置:
- 设置用户角色为
clientRoleBroadcaster
(主播)或clientRoleAudience
(观众)。 - 将
publishMicrophoneTrack
和publishCameraTrack
设置为true
,发布麦克风采集的音频和摄像头采集的视频。 - 将
autoSubscribeAudio
和autoSubscribeVideo
设置为true
,自动订阅所有音视频流。
await _engine.joinChannel(
// 使用临时 Token 和频道名加入频道
token: token,
channelId: channel,
options: const ChannelMediaOptions(
// 自动订阅所有视频流
autoSubscribeVideo: true,
// 自动订阅所有音频流
autoSubscribeAudio: true,
// 发布摄像头采集的视频
publishCameraTrack: true,
// 发布麦克风采集的音频
publishMicrophoneTrack: true,
// 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众)
clientRoleType: ClientRoleType.clientRoleBroadcaster),
// uid 为 0 表示引擎内部随机生成用户名
uid: 0,
);
实现常用回调
根据使用场景,定义必要的回调。以下示例代码展示如何调用 registerEventHandler
添加并实现 onJoinChannelSuccess
、 onUserJoined
和 onUserOffline
回调。
// 添加回调事件
_engine.registerEventHandler(
RtcEngineEventHandler(
// 成功加入频道回调
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
debugPrint("local user ${connection.localUid} joined");
setState(() {
_localUserJoined = true;
});
},
// 远端用户或主播加入当前频道回调
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
debugPrint("remote user $remoteUid joined");
setState(() {
_remoteUid = remoteUid;
});
},
// 远端用户或主播离开当前频道回调
onUserOffline: (RtcConnection connection, int remoteUid,
UserOfflineReasonType reason) {
debugPrint("remote user $remoteUid left channel");
setState(() {
_remoteUid = null;
});
},
),
);
设置远端视图
当远端用户加入频道时,触发 onUserJoined
回调。通过该回调获取远端用户的 uid
后,创建一个 AgoraVideoView
控件,用于显示对应远端用户的视频流。
// 生成远端视频
Widget _remoteVideo() {
if (_remoteUid != null) {
return AgoraVideoView(
controller: VideoViewController.remote(
rtcEngine: _engine,
canvas: VideoCanvas(uid: _remoteUid),
connection: const RtcConnection(channelId: channel),
),
);
} else {
return const Text(
'Please wait for remote user to join',
textAlign: TextAlign.center,
);
}
}
开始音视频互动
在 initState()
方法中初始化声网音视频功能并开始实时音视频互动。
void initState() {
super.initState();
initAgora();
}
结束音视频互动
按照以下步骤结束音视频互动:
- 调用
leaveChannel
离开当前频道,释放所有会话相关的资源。 - 调用
release
销毁引擎,并释放声网 SDK 中使用的所有资源。警告调用
release
后,你将无法再使用 SDK 的所有方法和回调。如需再次使用实时音视频互动功能,你必须重新创建一个新的引擎。详见初始化引擎。
Future<void> _dispose() async {
await _engine.leaveChannel(); // 离开频道
await _engine.release(); // 释放资源
}
调试 App
按照以下步骤测试直播 App:
-
将目标设备连接到电脑。
-
打开终端,在项目路径下执行以下命令在目标设备上运行示例项目:
Shellflutter run
-
启动 App,授予麦克风和摄像头权限,如果你将用户角色设置为主播,便会在本地视图中看到自己。
-
使用第二台设备,重复以上步骤,在该设备上安装 App、打开 App 加入频道,观察测试结果:
- 如果两台设备均作为主播加入频道,则可以看到对方并且听到对方的声音。
- 如果两台设备分别作为主播和观众加入,则主播可以在本地视频窗口看到自己;观众可以在远端视频窗口看到主播、并听到主播的声音。
以 Android 设备为例,编译成功后的 App 如下图所示:
后续步骤
- 本文的示例使用了临时 Token 加入频道。在测试或生产环境中,为确保通信安全,声网推荐使用 Token 服务器来生成 Token,详见使用 Token 鉴权。
- 如果你想要实现极速直播场景,可以在互动直播的基础上,通过修改观众端的延时级别为低延时 (
audienceLatencyLevelLowLatency
)实现。详见实现极速直播。
参考信息
示例项目
声网提供了开源的实时音视频互动示例项目供你参考,你可以前往下载或查看其中的源代码。
常见问题
- 直播场景下,如何监听远端观众角色用户加入/离开频道的事件?
- 如何处理视频黑屏问题?
- 为什么我无法打开摄像头?
- 如何处理频道相关常见问题?
- 如何设置日志文件?
- 为什么部分 Android 版本应用锁屏或切后台后采集音视频无效?
- 编译 Xcode 项目时遇到无法打开 framework 的弹窗警告怎么办?
- 为什么 SDK 中使用的是动态库而不是静态库?