如果你的目标平台为 iOS,你的开发环境需要满足以下需求:
如果你的目标平台为 Android,你的开发环境需要满足以下需求:
flutter doctor
命令检查开发环境和运行环境是否满足要求。一个有效的 Agora 开发者账号。
按照以下步骤,在控制台创建一个 Agora 项目。
在项目管理页面,点击创建按钮。
在弹出的对话框内输入项目名称,选择鉴权机制为 APP ID + Token。
点击提交,新建的项目就会显示在项目管理页中。
Agora 会给每个项目自动分配一个 App ID 作为项目唯一标识。
在 Agora 控制台的项目管理页面,找到你的项目,点击 App ID 右侧的眼睛图标就可以直接复制项目的 App ID。
为提高项目的安全性,Agora 使用 Token(动态密钥)对即将加入频道的用户进行鉴权。
为了方便测试,Agora 控制台提供生成临时 Token 的功能,具体步骤如下:
在控制台的项目管理页面,点击已创建项目的 图标,打开 Token 页面。
输入一个频道名,例如 test,然后点击生成临时Token。临时 Token 的有效期为 24 小时。
本文使用 Visual Studio Code 创建 Flutter 项目。你需要在 Visual Studio Code 中安装 Flutter plugin。关于详细设置可以参考 Set up an editor。
在 pubspec.yaml
文件中添加以下依赖项:
agora_rtc_engine
依赖项,集成 Agora Flutter SDK。关于 agora_rtc_engine
的最新版本可以查询 https://pub.dev/packages/agora_rtc_engine。permission_handler
依赖项,安装权限处理插件。environment:
sdk: ">=2.7.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
// Agora Flutter SDK 依赖项,请使用最新版本的 agora_rtc_engine
agora_rtc_engine: ^3.1.2
// 权限处理插件依赖项
permission_handler: ^3.0.0
打开 main.dart
,删除
void main() => runApp(MyApp());
语句下方的全部代码。并按照步骤增加以下代码:
定义 App ID 和 Token。详见校验用户权限。
/// 定义 App ID 和 Token
const APP_ID = '<Your App ID>';
const Token = '<Your Token>';
定义 MyApp 应用类:
/// MyApp 类扩展 StatelessWidget 类
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: IndexPage(),
);
}
}
import 'dart:async';
import 'package:agora_rtc_engine/rtc_engine.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
IndexPage
和页面状态类 IndexState
:/// IndexPage 类扩展 StatefulWidget 类
class IndexPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => IndexState();
}
/// IndexState 类扩展 State 类,获取 IndexPage 的状态
class IndexState extends State<IndexPage> {
final _channelController = TextEditingController();
bool _validateError = false;
ClientRole _role = ClientRole.Broadcaster;
@override
void dispose() {
// dispose input controller
_channelController.dispose();
super.dispose();
}
/// 设置登录页面的 UI 布局
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Agora Flutter QuickStart'),
),
body: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20),
height: 400,
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: TextField(
controller: _channelController,
decoration: InputDecoration(
errorText:
_validateError ? 'Channel name is mandatory' : null,
border: UnderlineInputBorder(
borderSide: BorderSide(width: 1),
),
hintText: 'Channel name',
),
))
],
),
Column(
children: [
ListTile(
title: Text(ClientRole.Broadcaster.toString()),
leading: Radio(
value: ClientRole.Broadcaster,
groupValue: _role,
onChanged: (ClientRole value) {
setState(() {
_role = value;
});
},
),
),
ListTile(
title: Text(ClientRole.Audience.toString()),
leading: Radio(
value: ClientRole.Audience,
groupValue: _role,
onChanged: (ClientRole value) {
setState(() {
_role = value;
});
},
),
)
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Row(
children: <Widget>[
Expanded(
child: RaisedButton(
onPressed: onJoin,
child: Text('Join'),
color: Colors.blueAccent,
textColor: Colors.white,
),
)
],
),
)
],
),
),
),
);
}
/// 设置加入频道按钮逻辑
Future<void> onJoin() async {
setState(() {
_channelController.text.isEmpty
? _validateError = true
: _validateError = false;
});
if (_channelController.text.isNotEmpty) {
// 等待麦克风的权限批准后再进入直播页面
await _handleMic();
// 进入直播页面,使用登录页面的频道名和角色登录频道
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CallPage(
channelName: _channelController.text,
role: _role,
),
),
);
}
}
// 设置权限管理逻辑
Future<void> _handleMic() async {
await PermissionHandler().requestPermissions(
[PermissionGroup.microphone],
);
}
}
import 'package:agora_rtc_engine/rtc_local_view.dart' as RtcLocalView;
import 'package:agora_rtc_engine/rtc_remote_view.dart' as RtcRemoteView;
CallPage
:/// 定义直播页面 CallPage 类
class CallPage extends StatefulWidget {
final String channelName;
final ClientRole role;
const CallPage({Key key, this.channelName, this.role}) : super(key: key);
@override
_CallPageState createState() => _CallPageState();
}
CallPageState
:/// 定义直播页面的状态类:
class _CallPageState extends State<CallPage> {
final _users = <int>[];
final _infoStrings = <String>[];
bool muted = false;
RtcEngine _engine;
@override
void dispose() {
_users.clear();
_engine.leaveChannel();
_engine.destroy();
super.dispose();
}
@override
void initState() {
super.initState();
initialize();
}
Future<void> initialize() async {
if (APP_ID.isEmpty) {
setState(() {
_infoStrings.add(
'APP_ID missing, please provide your APP_ID in settings.dart',
);
_infoStrings.add('Agora Engine is not starting');
});
return;
}
await _initAgoraRtcEngine();
_addAgoraEventHandlers();
await _engine.enableWebSdkInteroperability(true);
await _engine.joinChannel(Token, widget.channelName, null, 0);
}
/// 创建 Agora RTC SDK 客户端实例,设置频道属性和用户角色
Future<void> _initAgoraRtcEngine() async {
_engine = await RtcEngine.create(APP_ID);
await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
await _engine.setClientRole(widget.role);
}
/// 定义事件处理方法
void _addAgoraEventHandlers() {
_engine.setEventHandler(RtcEngineEventHandler(error: (code) {
setState(() {
final info = 'onError: $code';
_infoStrings.add(info);
});
}, joinChannelSuccess: (channel, uid, elapsed) {
setState(() {
final info = 'onJoinChannel: $channel, uid: $uid';
_infoStrings.add(info);
});
}, leaveChannel: (stats) {
setState(() {
_infoStrings.add('onLeaveChannel');
_users.clear();
});
}, userJoined: (uid, elapsed) {
setState(() {
final info = 'userJoined: $uid';
_infoStrings.add(info);
_users.add(uid);
});
}, userOffline: (uid, elapsed) {
setState(() {
final info = 'userOffline: $uid';
_infoStrings.add(info);
_users.remove(uid);
});
}));
}
/// 工具栏布局
Widget _toolbar() {
if (widget.role == ClientRole.Audience) return Container();
return Container(
alignment: Alignment.bottomCenter,
padding: const EdgeInsets.symmetric(vertical: 48),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RawMaterialButton(
onPressed: _onToggleMute,
child: Icon(
muted ? Icons.mic_off : Icons.mic,
color: muted ? Colors.white : Colors.blueAccent,
size: 20.0,
),
shape: CircleBorder(),
elevation: 2.0,
fillColor: muted ? Colors.blueAccent : Colors.white,
padding: const EdgeInsets.all(12.0),
),
RawMaterialButton(
onPressed: () => _onCallEnd(context),
child: Icon(
Icons.call_end,
color: Colors.white,
size: 35.0,
),
shape: CircleBorder(),
elevation: 2.0,
fillColor: Colors.redAccent,
padding: const EdgeInsets.all(15.0),
),
RawMaterialButton(
onPressed: _onSwitchCamera,
child: Icon(
Icons.switch_camera,
color: Colors.blueAccent,
size: 20.0,
),
shape: CircleBorder(),
elevation: 2.0,
fillColor: Colors.white,
padding: const EdgeInsets.all(12.0),
)
],
),
);
}
// 信息栏,显示日志信息
Widget _panel() {
return Container(
padding: const EdgeInsets.symmetric(vertical: 48),
alignment: Alignment.bottomCenter,
child: FractionallySizedBox(
heightFactor: 0.5,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 48),
child: ListView.builder(
reverse: true,
itemCount: _infoStrings.length,
itemBuilder: (BuildContext context, int index) {
if (_infoStrings.isEmpty) {
return null;
}
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 3,
horizontal: 10,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 2,
horizontal: 5,
),
decoration: BoxDecoration(
color: Colors.yellowAccent,
borderRadius: BorderRadius.circular(5),
),
child: Text(
_infoStrings[index],
style: TextStyle(color: Colors.blueGrey),
),
),
)
],
),
);
},
),
),
),
);
}
/// 结束直播
void _onCallEnd(BuildContext context) {
Navigator.pop(context);
}
/// 静音
void _onToggleMute() {
setState(() {
muted = !muted;
});
_engine.muteLocalAudioStream(muted);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Agora Flutter QuickStart'),
),
backgroundColor: Colors.black,
body: Center(
child: Stack(
children: <Widget>[
_panel(),
_toolbar(),
],
),
),
);
}
}
在项目根目录运行以下命令安装依赖项。
flutter packages get
运行项目。
flutter run
如果运行环境为 Android,对于中国大陆用户,运行 flutter run
时可能会卡在 Running Gradle task 'assembleDebug'...
或出现以下错误:
Running Gradle task 'assembleDebug'...
Exception in thread "main" java.net.ConnectException: Connection timed out: connect
解决方案如下:
build.gradle
文件中,对于 google
和 jcenter
使用国内镜像源。下面的示例代码使用了阿里镜像源。buildscript {
ext.kotlin_version = '1.3.50'
repositories {
// google()
// jcenter()
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/public' }
}
...
allprojects {
repositories {
// google()
// jcenter()
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/public' }
}
}
gradle-wrapper.properties
文件中,将 distributionUrl
设为本地路径。以 gradle 5.6.4 为例,你可以将 gradle-5.6.4-all.zip
文件复制到 gradle/wrapper
目录,然后 distributionUrl
设置为:distributionUrl=gradle-5.6.4-all.zip