Flutter音视频服务插件agora_fpa_service的使用
Flutter音视频服务插件agora_fpa_service的使用
在本文中,我们将介绍如何在Flutter应用中集成Agora FPA服务。通过使用agora_fpa_service
插件,您可以轻松地将Agora的FPA功能集成到您的应用程序中。
开始使用
要开始使用agora_fpa_service
插件,请参考以下步骤:
- 安装依赖:确保在项目的
pubspec.yaml
文件中添加了agora_fpa_service
插件。
dependencies:
agora_fpa_service: ^版本号
- 获取App ID和Token:前往Agora控制台获取您的App ID和Token。
示例代码
以下是一个完整的示例代码,展示了如何使用agora_fpa_service
插件。
import 'dart:async';
import 'dart:io';
import 'package:agora_fpa_service/agora_fpa_service.dart';
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
// 替换为您的App ID
String appId = const String.fromEnvironment(
'TEST_APP_ID',
defaultValue: '<YOUR_APP_ID>',
);
// 替换为您的Token
String token = const String.fromEnvironment(
'TEST_TOEKN',
defaultValue: '<YOUR_TOEKN>',
);
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
[@override](/user/override)
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> implements FpaProxyServiceObserver {
static const String uploadHttpUrl = "http://148.153.93.30:30103/upload";
static const String downloadHttpUrl = "http://148.153.93.30:30103/10MB.txt";
static const String uploadHttpsUrl =
"https://frank-web-demo.rtns.sd-rtn.com:30113/upload";
static const String downloadHttpsUrl =
"https://frank-web-demo.rtns.sd-rtn.com:30113/1MB.txt";
late final Dio _dio;
late final LogSink _logSink;
bool _uploadHttpsEnabled = false;
bool _uploadHttpEnabled = false;
bool _enableFpa = true;
int _downloadUploadTimes = 1;
bool _isEditDownloadUploadTimes = false;
late String _logFilePath;
bool _isInit = false;
late TextEditingController _controller;
[@override](/user/override)
void initState() {
super.initState();
_controller = TextEditingController(text: _downloadUploadTimes.toString());
_init();
}
void _init() async {
final status = await Permission.storage.request();
if (!status.isGranted) return;
final externalStorage = await getApplicationDocumentsDirectory();
_logFilePath =
path.join(externalStorage.absolute.path, 'agora', 'fp_log_sdk.log');
FpaProxyServiceConfig fpaConfig = FpaProxyServiceConfig(
appId: appId,
token: token,
logFileSizeKb: 1024,
logLevel: FpaProxyServiceLogLevel.error,
logFilePath: _logFilePath,
);
try {
FpaProxyService.instance.start(fpaConfig);
} on FpaProxyServiceException catch (e) {
_logSink.sink('start', 'with exception: ${e.toString()}');
return;
}
FpaHttpProxyChainConfig chainConfig = FpaHttpProxyChainConfig(
chainArray: [
FpaChainInfo(
chainId: 259,
address: 'www.qq.com',
port: 80,
enableFallback: true,
),
FpaChainInfo(
chainId: 254,
address: 'frank-web-demo.rtns.sd-rtn.com',
port: 30113,
enableFallback: true,
),
FpaChainInfo(
chainId: 204,
address: '148.153.93.30',
port: 30103,
enableFallback: true,
),
],
fallbackWhenNoChainAvailable: true,
);
FpaProxyService.instance.setOrUpdateHttpProxyChainConfig(chainConfig);
FpaProxyService.instance.setObserver(this);
_dio = Dio();
_resetFpa(true);
_logSink = LogSink();
setState(() {
_isInit = true;
});
}
[@override](/user/override)
void dispose() {
_controller.dispose();
FpaProxyService.instance.stop();
super.dispose();
}
void _resetFpa(bool enable) {
if (enable) {
(_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) {
// 配置HTTP客户端
client.findProxy = (uri) {
return 'PROXY ${FpaProxyService.kLocalHost}:${FpaProxyService.instance.getHttpProxyPort()}';
};
return client;
};
} else {
(_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) {
client.findProxy = null;
return client;
};
}
}
String _prefixFPATag(String input) {
return '${_enableFpa ? '[FPA] ' : ''} $input';
}
void _download(String url, String fileName, int times) async {
final status = await Permission.storage.request();
if (!status.isGranted) return;
for (int ct = 0; ct < times; ct++) {
_downloadInner(url, fileName, ct + 1);
}
}
Future<void> _downloadInner(
String url, String fileName, int currentTime) async {
await Future.delayed(const Duration(seconds: 1));
final externalStorage = await getApplicationDocumentsDirectory();
Stopwatch stopwatch = Stopwatch();
stopwatch.start();
final key = fileName;
try {
_logSink.sink(
key,
_prefixFPATag(
'(times: $currentTime)Downloading...',
),
appendTimestampToTag: false,
);
await _dio.download(
url,
path.join(externalStorage.absolute.path, fileName),
onReceiveProgress: (int count, int total) {},
);
stopwatch.stop();
setState(() {
if (url.startsWith('https')) {
_uploadHttpsEnabled = true;
} else {
_uploadHttpEnabled = true;
}
});
_logSink.sink(
key,
_prefixFPATag(
'(times: $currentTime)Download completed, time: ${stopwatch.elapsedMilliseconds}ms',
),
appendTimestampToTag: false,
);
} catch (e) {
_logSink.sink(
key,
_prefixFPATag(
'(times: $currentTime)Download error: ${e.toString()}',
),
appendTimestampToTag: false,
);
}
}
Future<void> _upload(String url, String fileName, int times) async {
for (int ct = 0; ct < times; ct++) {
_uploadInner(url, fileName, ct + 1);
}
}
Future<void> _uploadInner(
String url, String fileName, int currentTime) async {
await Future.delayed(const Duration(seconds: 1));
final externalStorage = await getApplicationDocumentsDirectory();
FormData formData = FormData.fromMap({
"file": await MultipartFile.fromFile(
path.join(externalStorage.absolute.path, fileName),
filename: fileName),
});
Stopwatch stopwatch = Stopwatch();
stopwatch.start();
final key = fileName;
try {
_logSink.sink(
key,
_prefixFPATag(
'(times: $currentTime)Uploading...',
),
appendTimestampToTag: false,
);
await _dio.post(url,
data: formData, onSendProgress: (int count, int total) {});
stopwatch.stop();
_logSink.sink(
key,
_prefixFPATag(
'(times: $currentTime)Upload completed, time: ${stopwatch.elapsedMilliseconds}ms',
),
appendTimestampToTag: false,
);
} catch (e) {
_logSink.sink(
key,
_prefixFPATag(
'(times: $currentTime)Upload error: ${e.toString()}',
),
appendTimestampToTag: false,
);
}
}
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Fpa Service example app'),
),
body: !_isInit
? Container()
: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
title: Text(
'Enable FPA${_enableFpa ? ' (fpa port: ${FpaProxyService.instance.getHttpProxyPort()})' : ''}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
value: _enableFpa,
onChanged: (changed) {
_enableFpa = changed;
_resetFpa(_enableFpa);
setState(() {});
}),
if (_enableFpa)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('log file path: $_logFilePath'),
Text(
'sdk version: ${FpaProxyService.getSDKVersion()}'),
Text('build info: ${FpaProxyService.getBuildInfo()}'),
],
),
Row(
children: [
const Text('Download/Upload times: '),
if (_isEditDownloadUploadTimes)
Expanded(
child: TextField(
controller: _controller,
),
),
if (!_isEditDownloadUploadTimes)
Text('$_downloadUploadTimes'),
ElevatedButton(
onPressed: () {
if (_isEditDownloadUploadTimes) {
_downloadUploadTimes =
int.parse(_controller.value.text);
}
_isEditDownloadUploadTimes =
!_isEditDownloadUploadTimes;
// _outputMap.clear();
setState(() {});
},
child:
Text(_isEditDownloadUploadTimes ? 'OK' : 'Edit'),
),
],
),
const Text(downloadHttpsUrl),
ElevatedButton(
onPressed: () {
_download(downloadHttpsUrl, 'download1M.pptx',
_downloadUploadTimes);
},
child: Text(_prefixFPATag('Download Https')),
),
const Text(uploadHttpsUrl),
ElevatedButton(
onPressed: _uploadHttpsEnabled
? () {
_upload(uploadHttpsUrl, 'download1M.pptx',
_downloadUploadTimes);
}
: null,
child: Text(_prefixFPATag('Upload Https')),
),
const Text(downloadHttpUrl),
ElevatedButton(
onPressed: () {
_download(downloadHttpUrl, 'download10M.pptx',
_downloadUploadTimes);
},
child: Text(_prefixFPATag('Download Http')),
),
const Text(uploadHttpUrl),
ElevatedButton(
onPressed: _uploadHttpEnabled
? () {
_upload(uploadHttpUrl, 'download10M.pptx',
_downloadUploadTimes);
}
: null,
child: Text(_prefixFPATag('Upload Http'))),
TransparentProxyWidget(
logSink: _logSink,
downloadUploadTimes: _downloadUploadTimes,
),
OutputLogWidget(
logSink: _logSink,
),
],
),
),
),
);
}
[@override](/user/override)
void onAccelerationSuccess(FpaProxyConnectionInfo info) {
_logSink.sink('onAccelerationSuccess', 'info: ${info.toJson()}');
}
[@override](/user/override)
void onConnected(FpaProxyConnectionInfo info) {
_logSink.sink('onConnected', 'info: ${info.toJson()}');
}
[@override](/user/override)
void onConnectionFailed(
FpaProxyConnectionInfo info, FpaProxyServiceReasonCode reason) {
_logSink.sink(
'onConnectionFailed', 'info: ${info.toJson()}, reason: $reason');
}
[@override](/user/override)
void onDisconnectedAndFallback(
FpaProxyConnectionInfo info, FpaProxyServiceReasonCode reason) {
_logSink.sink(
'onDisconnectedAndFallback', 'info: ${info.toJson()}, reason: $reason');
}
}
class LogSink extends ChangeNotifier {
final StringBuffer stringBuffer = StringBuffer();
void sink(String tag, String log, {bool appendTimestampToTag = true}) {
final now = DateTime.now();
stringBuffer.writeln('${now.hour}:${now.minute}:${now.second} [$tag] $log');
stringBuffer.writeln();
notifyListeners();
}
String getOutput() {
return stringBuffer.toString();
}
void clear() {
stringBuffer.clear();
notifyListeners();
}
}
class OutputLogWidget extends StatefulWidget {
const OutputLogWidget({Key? key, required this.logSink}) : super(key: key);
final LogSink logSink;
[@override](/user/override)
_OutputLogWidgetState createState() => _OutputLogWidgetState();
}
class _OutputLogWidgetState extends State<OutputLogWidget> {
late final LogSink _logSink;
[@override](/user/override)
void initState() {
super.initState();
_logSink = widget.logSink;
_logSink.addListener(() {
setState(() {});
});
}
[@override](/user/override)
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'Log',
style: TextStyle(fontWeight: FontWeight.bold),
),
TextButton(
onPressed: () {
_logSink.clear();
},
child: const Text('Clear'))
],
),
Text(_logSink.getOutput()),
],
);
}
}
class TransparentProxyWidget extends StatefulWidget {
const TransparentProxyWidget({
Key? key,
required this.logSink,
required this.downloadUploadTimes,
}) : super(key: key);
final LogSink logSink;
final int downloadUploadTimes;
[@override](/user/override)
_TransparentProxyWidgetState createState() => _TransparentProxyWidgetState();
}
class _TransparentProxyWidgetState extends State<TransparentProxyWidget> {
late final LogSink _logSink;
final List<List<Object>> _chainInfos = [
[
FpaChainInfo(
address: 'BAD',
port: -1,
chainId: -1,
enableFallback: true,
),
'Error data',
],
[
FpaChainInfo(
address: '164.52.28.236',
port: 30102,
chainId: 203,
enableFallback: true,
),
'nomal domain, normal chain, can fallback',
],
[
FpaChainInfo(
address: '164.52.28.236',
port: 30102,
chainId: 10086,
enableFallback: false,
),
'normal domain, un-normal chain, can not fallback'
],
[
FpaChainInfo(
address: '164.52.28.236',
port: 30102,
chainId: 10011,
enableFallback: true,
),
'normal domain, un-normal chain, can fallback'
],
];
int _selectedChainInfoIndex = 0;
int _port = 0;
String _formatChainInfo(List<Object> infos) {
FpaChainInfo chainInfo = infos[0] as FpaChainInfo;
String des = infos[1] as String;
return '${chainInfo.address}:${chainInfo.port}@${chainInfo.chainId} ${chainInfo.enableFallback}\n $des';
}
[@override](/user/override)
void initState() {
super.initState();
_logSink = widget.logSink;
}
void _refreshPort() {
if (_selectedChainInfoIndex == 0) return;
FpaProxyServiceDiagnosisInfo info =
FpaProxyService.instance.getDiagnosisInfo();
_logSink.sink(
'TransparentProxy',
'FpaProxyServiceDiagnosisInfo installId: ${info.installId}, instanceId: ${info.instanceId}',
);
final chainInfo = _chainInfos[_selectedChainInfoIndex][0] as FpaChainInfo;
_port = FpaProxyService.instance.getTransparentProxyPort(chainInfo);
if (_port <= 0) {
_logSink.sink(
'TransparentProxy',
'can not get transparent port in ${chainInfo.toString()}',
);
}
}
void _connectAll() {
for (int i = 0; i < widget.downloadUploadTimes; i++) {
_connect(_port, i);
}
}
void _connect(int port, int currentTime) async {
late final Socket socket;
try {
socket = await Socket.connect(FpaProxyService.kLocalHost, port);
} catch (e) {
_logSink.sink(
'TransparentProxy',
'(times: $currentTime) Error in connect socket: ${e.toString()}',
);
}
socket.listen(
(data) {
if (data != null) {
String readData = String.fromCharCodes(data).trim();
_logSink.sink(
'TransparentProxy',
'(times: $currentTime) Read data (port:$_port):\n$readData\nsize:${readData.length}',
);
}
},
onError: (error, StackTrace trace) {
_logSink.sink(
'TransparentProxy',
'(times: $currentTime) Error in socket request(port:$_port): ${error.toString()}',
);
},
onDone: () {
socket.destroy();
},
cancelOnError: false,
);
socket.write('GET /1KB.txt? HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n');
await socket.flush();
}
[@override](/user/override)
Widget build(BuildContext context) {
List<DropdownMenuItem<int>> items = [];
for (int i = 0; i < _chainInfos.length; i++) {
final info = _chainInfos[i];
items.add(DropdownMenuItem<int>(
child: Text(
_formatChainInfo(info),
style: const TextStyle(fontSize: 10),
),
value: i,
));
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Transparent proxy (port: $_port)',
style: const TextStyle(fontWeight: FontWeight.bold),
),
DropdownButton<int>(
hint: const Text('Select chain info'),
value: _selectedChainInfoIndex,
items: items,
onChanged: (value) {
setState(() {
_selectedChainInfoIndex = value as int;
_refreshPort();
});
},
),
ElevatedButton(
onPressed: _port <= 0
? null
: () {
_connectAll();
},
child: const Text('Start transparent proxy'),
),
],
);
}
}
更多关于Flutter音视频服务插件agora_fpa_service的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter音视频服务插件agora_fpa_service的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
agora_fpa_service
是 Agora 提供的一个 Flutter 插件,用于在 Flutter 应用中集成 Agora 的音视频服务。这个插件主要用于实现低延迟的音视频通信,特别适用于需要实时互动的场景,如直播、视频会议、在线教育等。
以下是如何在 Flutter 项目中使用 agora_fpa_service
插件的基本步骤:
1. 添加依赖
首先,你需要在 pubspec.yaml
文件中添加 agora_fpa_service
插件的依赖:
dependencies:
flutter:
sdk: flutter
agora_fpa_service: ^1.0.0 # 请使用最新版本
然后运行 flutter pub get
来获取依赖。
2. 初始化 Agora 引擎
在你的 Flutter 项目中,首先需要初始化 Agora 引擎。通常,你可以在 main.dart
或某个单独的初始化文件中进行初始化。
import 'package:agora_fpa_service/agora_fpa_service.dart';
void initializeAgora() async {
await AgoraFpaService.initialize(appId: 'YOUR_AGORA_APP_ID');
}
确保将 YOUR_AGORA_APP_ID
替换为你从 Agora 控制台获取的实际 App ID。
3. 加入频道
初始化完成后,你可以使用 joinChannel
方法加入一个音视频频道。
void joinChannel() async {
await AgoraFpaService.joinChannel(
token: 'YOUR_TOKEN', // 如果需要 token 验证
channelId: 'YOUR_CHANNEL_ID',
uid: 0, // 用户 ID,如果为 0,Agora 会自动分配一个 UID
);
}
4. 处理音视频流
你可以通过 AgoraFpaService
提供的方法来处理音视频流。例如,显示本地和远程视频流:
import 'package:flutter/material.dart';
import 'package:agora_fpa_service/agora_fpa_service.dart';
class VideoCallScreen extends StatefulWidget {
@override
_VideoCallScreenState createState() => _VideoCallScreenState();
}
class _VideoCallScreenState extends State<VideoCallScreen> {
@override
void initState() {
super.initState();
AgoraFpaService.onUserJoined = (int uid, int elapsed) {
// 当有用户加入时
print('User joined: $uid');
};
AgoraFpaService.onUserOffline = (int uid, UserOfflineReason reason) {
// 当用户离开时
print('User offline: $uid');
};
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(
child: AgoraVideoView(
controller: VideoViewController(
rtcEngine: AgoraFpaService.engine,
canvas: VideoCanvas(uid: 0), // 本地视频
),
),
),
Expanded(
child: AgoraVideoView(
controller: VideoViewController(
rtcEngine: AgoraFpaService.engine,
canvas: VideoCanvas(uid: 1), // 远程视频
),
),
),
],
),
);
}
}
5. 离开频道
当用户结束通话时,可以调用 leaveChannel
方法离开频道。
void leaveChannel() async {
await AgoraFpaService.leaveChannel();
}
6. 释放资源
在应用退出或不再需要 Agora 服务时,记得释放资源。
void disposeAgora() async {
await AgoraFpaService.release();
}
7. 处理权限
确保在 AndroidManifest.xml
和 Info.plist
中添加必要的权限,以访问摄像头和麦克风。
8. 处理异常
在实际使用中,建议添加异常处理逻辑,以应对网络问题、权限问题等。
try {
await AgoraFpaService.joinChannel(
token: 'YOUR_TOKEN',
channelId: 'YOUR_CHANNEL_ID',
uid: 0,
);
} catch (e) {
print('Failed to join channel: $e');
}
9. 调试与日志
你可以启用 Agora 的日志功能来帮助调试。
AgoraFpaService.setLogLevel(LogLevel.info);