agora_fpa_service 1.0.0 copy "agora_fpa_service: ^1.0.0" to clipboard
agora_fpa_service: ^1.0.0 copied to clipboard

Flutter plugin to integrate Agora FPA service into your app with just a few lines of code.

example/lib/main.dart

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';

/// Get your own App ID at https://dashboard.agora.io/
String appId = const String.fromEnvironment(
  'TEST_APP_ID',
  defaultValue: '<YOUR_APP_ID>',
);

/// Please refer to https://docs.agora.io/en/Agora%20Platform/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
  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
  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
  void dispose() {
    _controller.dispose();
    FpaProxyService.instance.stop();
    super.dispose();
  }

  void _resetFpa(bool enable) {
    if (enable) {
      (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
          (client) {
        // config the http client
        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 complated, 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 complated, time: ${stopwatch.elapsedMilliseconds}ms',
        ),
        appendTimestampToTag: false,
      );
    } catch (e) {
      _logSink.sink(
        key,
        _prefixFPATag(
          '(times: $currentTime)Upload error: ${e.toString()}',
        ),
        appendTimestampToTag: false,
      );
    }
  }

  @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
  void onAccelerationSuccess(FpaProxyConnectionInfo info) {
    _logSink.sink('onAccelerationSuccess', 'info: ${info.toJson()}');
  }

  @override
  void onConnected(FpaProxyConnectionInfo info) {
    _logSink.sink('onConnected', 'info: ${info.toJson()}');
  }

  @override
  void onConnectionFailed(
      FpaProxyConnectionInfo info, FpaProxyServiceReasonCode reason) {
    _logSink.sink(
        'onConnectionFailed', 'info: ${info.toJson()}, reason: $reason');
  }

  @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
  _OutputLogWidgetState createState() => _OutputLogWidgetState();
}

class _OutputLogWidgetState extends State<OutputLogWidget> {
  late final LogSink _logSink;

  @override
  void initState() {
    super.initState();

    _logSink = widget.logSink;
    _logSink.addListener(() {
      setState(() {});
    });
  }

  @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
  _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
  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
  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'),
        ),
      ],
    );
  }
}
0
likes
120
pub points
0%
popularity

Publisher

unverified uploader

Flutter plugin to integrate Agora FPA service into your app with just a few lines of code.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (LICENSE)

Dependencies

ffi, flutter, json_annotation

More

Packages that depend on agora_fpa_service