liuyuqi-dellpc 11 months ago
parent
commit
f1a8aaf505
68 changed files with 12289 additions and 2056 deletions
  1. 2 2
      android/build.gradle
  2. 1 0
      android/gradle.properties
  3. BIN
      assets/agora-logo.png
  4. BIN
      assets/audio_mixing/Agora.io-Interactions.mp3
  5. BIN
      assets/dang.mp3
  6. BIN
      assets/ding.mp3
  7. BIN
      assets/gif.gif
  8. BIN
      assets/jpg.jpg
  9. 195 0
      lib/components/basic_video_configuration_widget.dart
  10. 57 0
      lib/components/dump_video_action.dart
  11. 115 0
      lib/components/example_actions_widget.dart
  12. 188 0
      lib/components/log_sink.dart
  13. 129 0
      lib/components/remote_video_views_widget.dart
  14. 62 0
      lib/components/rgba_image.dart
  15. 28 5
      lib/config/agora.config.dart
  16. 244 0
      lib/examples/advanced/audio_mixing/audio_mixing.dart
  17. 281 0
      lib/examples/advanced/audio_spectrum/audio_spectrum.dart
  18. 243 0
      lib/examples/advanced/channel_media_relay/channel_media_relay.dart
  19. 286 0
      lib/examples/advanced/custom_capture_audio/custom_capture_audio.dart
  20. 130 0
      lib/examples/advanced/custom_capture_audio/custom_capture_audio_api.generated.dart
  21. 209 0
      lib/examples/advanced/device_manager/device_manager.dart
  22. 380 0
      lib/examples/advanced/enable_spatial_audio/enable_spatial_audio.dart
  23. 184 0
      lib/examples/advanced/enable_virtualbackground/enable_virtualbackground.dart
  24. 89 0
      lib/examples/advanced/index.dart
  25. 375 0
      lib/examples/advanced/join_multiple_channel/join_multiple_channel.dart
  26. 395 0
      lib/examples/advanced/media_player/media_player.dart
  27. 243 0
      lib/examples/advanced/media_recorder/media_recorder.dart
  28. 573 0
      lib/examples/advanced/music_player/music_player.dart
  29. 425 0
      lib/examples/advanced/precall_test/precall_test.dart
  30. 304 0
      lib/examples/advanced/process_audio_raw_data/process_audio_raw_data.dart
  31. 262 0
      lib/examples/advanced/process_video_raw_data/process_video_raw_data.dart
  32. 180 0
      lib/examples/advanced/push_audio_frame/push_audio_frame.dart
  33. 222 0
      lib/examples/advanced/push_encoded_video_frame/push_encoded_video_frame.dart
  34. 197 0
      lib/examples/advanced/push_video_frame/push_video_frame.dart
  35. 265 0
      lib/examples/advanced/rtmp_streaming/rtmp_streaming.dart
  36. 580 0
      lib/examples/advanced/screen_sharing/screen_sharing.dart
  37. 252 0
      lib/examples/advanced/send_metadata/send_metadata.dart
  38. 256 0
      lib/examples/advanced/send_multi_camera_stream/send_multi_camera_stream.dart
  39. 277 0
      lib/examples/advanced/send_multi_video_stream/send_multi_video_stream.dart
  40. 367 0
      lib/examples/advanced/set_beauty_effect/set_beauty_effect.dart
  41. 219 0
      lib/examples/advanced/set_content_inspect/set_content_inspect.dart
  42. 286 0
      lib/examples/advanced/set_encryption/set_encryption.dart
  43. 199 0
      lib/examples/advanced/set_video_encoder_configuration/set_video_encoder_configuration.dart
  44. 345 0
      lib/examples/advanced/spatial_audio_with_media_player/spatial_audio_with_media_player.dart
  45. 324 0
      lib/examples/advanced/start_direct_cdn_streaming/start_direct_cdn_streaming.dart
  46. 656 0
      lib/examples/advanced/start_local_video_transcoder/start_local_video_transcoder.dart
  47. 224 0
      lib/examples/advanced/start_rhythm_player/start_rhythm_player.dart
  48. 219 0
      lib/examples/advanced/stream_message/stream_message.dart
  49. 333 0
      lib/examples/advanced/take_snapshot/take_snapshot.dart
  50. 202 0
      lib/examples/advanced/voice_changer/voice_changer.config.dart
  51. 378 0
      lib/examples/advanced/voice_changer/voice_changer.dart
  52. 12 0
      lib/examples/basic/index.dart
  53. 321 0
      lib/examples/basic/join_channel_audio/join_channel_audio.dart
  54. 291 0
      lib/examples/basic/join_channel_video/join_channel_video.dart
  55. 142 0
      lib/examples/basic/string_uid/string_uid.dart
  56. 110 39
      lib/main.dart
  57. 18 11
      lib/model/user.dart
  58. 0 192
      lib/pages/advanced/create_stream_data.dart
  59. 0 21
      lib/pages/advanced/index.dart
  60. 0 236
      lib/pages/advanced/live_streaming.dart
  61. 0 193
      lib/pages/advanced/media_channel_relay.dart
  62. 0 278
      lib/pages/advanced/multi_channel.dart
  63. 0 481
      lib/pages/advanced/voice_change.dart
  64. 0 11
      lib/pages/basic/index.dart
  65. 0 266
      lib/pages/basic/join_channel_audio.dart
  66. 0 181
      lib/pages/basic/join_channel_video.dart
  67. 0 138
      lib/pages/basic/string_uid.dart
  68. 14 2
      pubspec.yaml

+ 2 - 2
android/build.gradle

@@ -2,7 +2,7 @@ buildscript {
     ext.kotlin_version = '1.6.0'
     repositories {
         google()
-        jcenter()
+        mavenCentral()
     }
 
     dependencies {
@@ -14,7 +14,7 @@ buildscript {
 allprojects {
     repositories {
         google()
-        jcenter()
+        mavenCentral()
         maven { url 'https://www.jitpack.io' }
     }
 }

+ 1 - 0
android/gradle.properties

@@ -2,3 +2,4 @@ org.gradle.jvmargs=-Xmx1536M
 android.enableR8=true
 android.useAndroidX=true
 android.enableJetifier=true
+android.injected.testOnly=false

BIN
assets/agora-logo.png


BIN
assets/audio_mixing/Agora.io-Interactions.mp3


BIN
assets/dang.mp3


BIN
assets/ding.mp3


BIN
assets/gif.gif


BIN
assets/jpg.jpg


+ 195 - 0
lib/components/basic_video_configuration_widget.dart

@@ -0,0 +1,195 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:flutter/material.dart';
+
+class BasicVideoConfigurationWidget extends StatefulWidget {
+  const BasicVideoConfigurationWidget({
+    Key? key,
+    required this.rtcEngine,
+    required this.setConfigButtonText,
+    required this.title,
+    this.width = 960,
+    this.height = 540,
+    this.frameRate = 15,
+    this.bitrate = 0,
+    this.onConfigChanged,
+  }) : super(key: key);
+
+  final RtcEngine rtcEngine;
+
+  final String title;
+  final int width;
+  final int height;
+  final int frameRate;
+  final int bitrate;
+
+  final Widget setConfigButtonText;
+  final Function(int width, int height, int frameRate, int bitrate)?
+      onConfigChanged;
+
+  @override
+  State<BasicVideoConfigurationWidget> createState() =>
+      _BasicVideoConfigurationWidgetState();
+}
+
+class _BasicVideoConfigurationWidgetState
+    extends State<BasicVideoConfigurationWidget> {
+  late TextEditingController _heightController;
+  late TextEditingController _widthController;
+  late TextEditingController _frameRateController;
+  late TextEditingController _bitrateController;
+
+  @override
+  void initState() {
+    super.initState();
+
+    _widthController = TextEditingController(text: widget.width.toString());
+    _heightController = TextEditingController(text: widget.height.toString());
+    _frameRateController =
+        TextEditingController(text: widget.frameRate.toString());
+    _bitrateController = TextEditingController(text: widget.bitrate.toString());
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    _widthController.dispose();
+    _heightController.dispose();
+    _frameRateController.dispose();
+    _bitrateController.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      padding: const EdgeInsets.all(8),
+      decoration: BoxDecoration(
+        color: Colors.blueGrey[50],
+        borderRadius: const BorderRadius.all(Radius.circular(4.0)),
+      ),
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        mainAxisAlignment: MainAxisAlignment.start,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(
+            widget.title,
+            style: const TextStyle(fontSize: 10),
+          ),
+          const SizedBox(
+            height: 10,
+          ),
+          Row(
+            mainAxisAlignment: MainAxisAlignment.start,
+            mainAxisSize: MainAxisSize.max,
+            children: [
+              Expanded(
+                child: Column(
+                  mainAxisAlignment: MainAxisAlignment.start,
+                  mainAxisSize: MainAxisSize.min,
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    const Text('width: '),
+                    TextField(
+                      controller: _widthController,
+                      decoration: const InputDecoration(
+                        hintText: 'width',
+                        border: OutlineInputBorder(gapPadding: 0.0),
+                        isDense: true,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+              const SizedBox(
+                width: 10,
+              ),
+              Expanded(
+                child: Column(
+                  mainAxisAlignment: MainAxisAlignment.start,
+                  mainAxisSize: MainAxisSize.min,
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    const Text('heigth: '),
+                    TextField(
+                      controller: _heightController,
+                      decoration: const InputDecoration(
+                        hintText: 'height',
+                        border: OutlineInputBorder(),
+                        isDense: true,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ],
+          ),
+          const SizedBox(
+            height: 10,
+          ),
+          Row(
+            mainAxisAlignment: MainAxisAlignment.start,
+            mainAxisSize: MainAxisSize.max,
+            children: [
+              Expanded(
+                child: Column(
+                  mainAxisAlignment: MainAxisAlignment.start,
+                  mainAxisSize: MainAxisSize.min,
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    const Text('frame rate: '),
+                    TextField(
+                      controller: _frameRateController,
+                      decoration: const InputDecoration(
+                        hintText: 'frame rate',
+                        border: OutlineInputBorder(),
+                        isDense: true,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+              const SizedBox(
+                width: 10,
+              ),
+              Expanded(
+                child: Column(
+                  mainAxisAlignment: MainAxisAlignment.start,
+                  mainAxisSize: MainAxisSize.min,
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    const Text('bitrate: '),
+                    TextField(
+                      controller: _bitrateController,
+                      decoration: const InputDecoration(
+                        hintText: 'bitrate',
+                        border: OutlineInputBorder(),
+                        isDense: true,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ],
+          ),
+          const SizedBox(
+            width: 10,
+          ),
+          ElevatedButton(
+            child: widget.setConfigButtonText,
+            onPressed: () {
+              widget.onConfigChanged?.call(
+                  int.parse(_widthController.text),
+                  int.parse(_heightController.text),
+                  int.parse(_frameRateController.text),
+                  int.parse(_bitrateController.text));
+            },
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 57 - 0
lib/components/dump_video_action.dart

@@ -0,0 +1,57 @@
+import 'dart:io';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine/agora_rtc_engine_debug.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:path_provider/path_provider.dart';
+
+import 'log_sink.dart';
+
+class DumpVideoAction extends StatefulWidget {
+  const DumpVideoAction({Key? key, required this.rtcEngine}) : super(key: key);
+
+  final RtcEngine rtcEngine;
+
+  @override
+  State<DumpVideoAction> createState() => _DumpVideoActionState();
+}
+
+class _DumpVideoActionState extends State<DumpVideoAction> {
+  bool _startDumpVideo = false;
+
+  @override
+  Widget build(BuildContext context) {
+    if (!(defaultTargetPlatform == TargetPlatform.windows ||
+        defaultTargetPlatform == TargetPlatform.macOS)) {
+      return Container();
+    }
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      mainAxisAlignment: MainAxisAlignment.start,
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        ElevatedButton(
+          onPressed: () async {
+            _startDumpVideo = !_startDumpVideo;
+
+            Directory appDocDir = await getApplicationDocumentsDirectory();
+
+            if (_startDumpVideo) {
+              widget.rtcEngine.startDumpVideo(
+                VideoSourceType.videoSourceCamera.value(),
+                appDocDir.absolute.path,
+              );
+              logSink.log('Video data has dump to ${appDocDir.absolute.path}');
+            } else {
+              widget.rtcEngine.stopDumpVideo();
+            }
+
+            setState(() {});
+          },
+          child: Text('${_startDumpVideo ? 'Stop' : 'Start'} dump video'),
+        ),
+      ],
+    );
+  }
+}

+ 115 - 0
lib/components/example_actions_widget.dart

@@ -0,0 +1,115 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+typedef ExampleActionsBuilder = Widget Function(
+    BuildContext context, bool isLayoutHorizontal);
+
+class ExampleActionsWidget extends StatelessWidget {
+  const ExampleActionsWidget({
+    Key? key,
+    required this.displayContentBuilder,
+    this.actionsBuilder,
+  }) : super(key: key);
+
+  final ExampleActionsBuilder displayContentBuilder;
+
+  final ExampleActionsBuilder? actionsBuilder;
+
+  @override
+  Widget build(BuildContext context) {
+    final mediaData = MediaQuery.of(context);
+    final bool isLayoutHorizontal = mediaData.size.aspectRatio >= 1.5 ||
+        (kIsWeb ||
+            !(defaultTargetPlatform == TargetPlatform.android ||
+                defaultTargetPlatform == TargetPlatform.iOS));
+
+    if (actionsBuilder == null) {
+      return displayContentBuilder(context, isLayoutHorizontal);
+    }
+
+    const actionsTitle = Text(
+      'Actions',
+      style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24),
+    );
+
+    if (isLayoutHorizontal) {
+      return Row(
+        mainAxisAlignment: MainAxisAlignment.start,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisSize: MainAxisSize.max,
+        children: [
+          Expanded(
+            flex: 1,
+            child: SingleChildScrollView(
+              child: Container(
+                padding: const EdgeInsets.fromLTRB(16, 24, 16, 24),
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  mainAxisAlignment: MainAxisAlignment.start,
+                  mainAxisSize: MainAxisSize.max,
+                  children: [
+                    actionsTitle,
+                    actionsBuilder!(context, isLayoutHorizontal),
+                  ],
+                ),
+              ),
+            ),
+          ),
+          Container(
+            color: Colors.grey.shade100,
+            width: 20,
+          ),
+          Expanded(
+            flex: 2,
+            child: displayContentBuilder(context, isLayoutHorizontal),
+          ),
+        ],
+      );
+    }
+
+    return Stack(
+      children: [
+        SizedBox.expand(
+          child: Container(
+            padding: const EdgeInsets.only(bottom: 150),
+            child: displayContentBuilder(context, isLayoutHorizontal),
+          ),
+        ),
+        DraggableScrollableSheet(
+          initialChildSize: 0.25,
+          snap: true,
+          maxChildSize: 0.7,
+          builder: (BuildContext context, ScrollController scrollController) {
+            return Container(
+              decoration: const BoxDecoration(
+                  color: Color.fromARGB(255, 253, 253, 253),
+                  borderRadius: BorderRadius.only(
+                    topLeft: Radius.circular(24.0),
+                    topRight: Radius.circular(24.0),
+                  ),
+                  boxShadow: [
+                    BoxShadow(
+                      blurRadius: 20.0,
+                      color: Colors.grey,
+                    ),
+                  ]),
+              padding: const EdgeInsets.fromLTRB(16, 24, 16, 0),
+              child: SingleChildScrollView(
+                controller: scrollController,
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  mainAxisAlignment: MainAxisAlignment.start,
+                  mainAxisSize: MainAxisSize.max,
+                  children: [
+                    actionsTitle,
+                    actionsBuilder!(context, isLayoutHorizontal),
+                  ],
+                ),
+              ),
+            );
+          },
+        )
+      ],
+    );
+  }
+}

+ 188 - 0
lib/components/log_sink.dart

@@ -0,0 +1,188 @@
+import 'package:flutter/material.dart';
+
+/// [AppBar] action that shows a [Overlay] with log.
+class LogActionWidget extends StatefulWidget {
+  /// Construct the [LogActionWidget]
+  const LogActionWidget({Key? key}) : super(key: key);
+
+  @override
+  _LogActionWidgetState createState() => _LogActionWidgetState();
+}
+
+class _LogActionWidgetState extends State<LogActionWidget> {
+  bool _isOverlayShowed = false;
+
+  OverlayEntry? _overlayEntry;
+
+  @override
+  void dispose() {
+    _removeOverlay();
+
+    super.dispose();
+  }
+
+  void _removeOverlay() {
+    if (_overlayEntry != null) {
+      _overlayEntry!.remove();
+      _overlayEntry = null;
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return TextButton(
+        onPressed: () {
+          if (_isOverlayShowed) {
+            _removeOverlay();
+          } else {
+            _overlayEntry = OverlayEntry(builder: (c) {
+              return Positioned(
+                left: 0,
+                top: 0,
+                height: MediaQuery.of(context).size.height * 0.5,
+                width: MediaQuery.of(context).size.width,
+                child: Container(
+                  color: Colors.black87,
+                  child: SafeArea(
+                    bottom: false,
+                    child: Material(
+                      color: Colors.transparent,
+                      child: Column(
+                        crossAxisAlignment: CrossAxisAlignment.start,
+                        mainAxisSize: MainAxisSize.max,
+                        children: [
+                          Row(
+                            mainAxisSize: MainAxisSize.max,
+                            children: [
+                              TextButton(
+                                onPressed: () {
+                                  logSink.clear();
+                                },
+                                child: const Text(
+                                  'Clear log',
+                                  style: TextStyle(color: Colors.white),
+                                ),
+                              ),
+                              Expanded(
+                                child: Align(
+                                  alignment: Alignment.topRight,
+                                  child: IconButton(
+                                      color: Colors.transparent,
+                                      onPressed: () {
+                                        _removeOverlay();
+                                        _isOverlayShowed = !_isOverlayShowed;
+                                      },
+                                      icon: const Icon(
+                                        Icons.close,
+                                        color: Colors.white,
+                                      )),
+                                ),
+                              ),
+                            ],
+                          ),
+                          const Expanded(
+                            child: SingleChildScrollView(
+                              child: LogWidget(),
+                            ),
+                          )
+                        ],
+                      ),
+                    ),
+                  ),
+                ),
+              );
+            });
+            Overlay.of(context)?.insert(_overlayEntry!);
+          }
+          _isOverlayShowed = !_isOverlayShowed;
+          // setState(() {
+
+          // });
+        },
+        child: const Text(
+          'Log',
+          style: TextStyle(color: Colors.white),
+        ));
+  }
+}
+
+/// LogWidget
+class LogWidget extends StatefulWidget {
+  /// Construct the [LogWidget]
+  const LogWidget({
+    Key? key,
+    this.logSink,
+    this.textStyle = const TextStyle(fontSize: 15, color: Colors.white),
+  }) : super(key: key);
+
+  /// This [LogSink] is used to add log.
+  final LogSink? logSink;
+
+  /// The text style of the log.
+  final TextStyle textStyle;
+
+  @override
+  _LogWidgetState createState() => _LogWidgetState();
+}
+
+class _LogWidgetState extends State<LogWidget> {
+  VoidCallback? _listener;
+  late final LogSink _logSink;
+
+  @override
+  void initState() {
+    super.initState();
+
+    _logSink = widget.logSink ?? _defaultLogSink;
+
+    _listener ??= () {
+      setState(() {});
+    };
+
+    _logSink.addListener(_listener!);
+  }
+
+  @override
+  void dispose() {
+    if (_listener != null) {
+      _logSink.removeListener(_listener!);
+      _listener = null;
+    }
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Text(
+      _logSink.output(),
+      style: widget.textStyle,
+    );
+  }
+}
+
+/// Class that add and update the log in [LogActionWidget]
+class LogSink extends ChangeNotifier {
+  final StringBuffer _stringBuffer = StringBuffer();
+
+  /// Add log to [LogActionWidget]
+  void log(String log) {
+    _stringBuffer.writeln(log);
+    notifyListeners();
+  }
+
+  /// Get all logs
+  String output() {
+    return _stringBuffer.toString();
+  }
+
+  /// Clear logs
+  void clear() {
+    _stringBuffer.clear();
+    notifyListeners();
+  }
+}
+
+final LogSink _defaultLogSink = LogSink();
+
+/// The global [LogSink]
+LogSink get logSink => _defaultLogSink;

+ 129 - 0
lib/components/remote_video_views_widget.dart

@@ -0,0 +1,129 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'log_sink.dart';
+import 'package:flutter/material.dart';
+
+mixin KeepRemoteVideoViewsMixin<T extends StatefulWidget> on State<T> {
+  final GlobalKey _keepRemoteVideoViewsKey = GlobalKey();
+  GlobalKey get keepRemoteVideoViewsKey => _keepRemoteVideoViewsKey;
+}
+
+class RemoteVideoViewsWidget extends StatefulWidget {
+  const RemoteVideoViewsWidget(
+      {Key? key,
+      required this.rtcEngine,
+      required this.channelId,
+      this.connectionUid})
+      : super(key: key);
+
+  final RtcEngine rtcEngine;
+
+  final String channelId;
+
+  final int? connectionUid;
+
+  @override
+  State<RemoteVideoViewsWidget> createState() => _RemoteVideoViewsWidgetState();
+}
+
+class _RemoteVideoViewsWidgetState extends State<RemoteVideoViewsWidget> {
+  late final RtcEngineEventHandler _eventHandler;
+  final Set<RtcConnection> _localRtcConnection = {};
+  final Map<int, RtcConnection> _remoteUidsMap = {};
+
+  @override
+  void initState() {
+    super.initState();
+
+    _init();
+  }
+
+  void _init() {
+    _eventHandler = RtcEngineEventHandler(
+      onJoinChannelSuccess: (connection, elapsed) {
+        _localRtcConnection.add(connection);
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        _localRtcConnection
+            .removeWhere((e) => e.localUid == connection.localUid);
+        _remoteUidsMap
+            .removeWhere((key, value) => value.localUid == connection.localUid);
+      },
+      onUserJoined: (connection, remoteUid, elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()}, remoteUid: $remoteUid, elapsed: $elapsed');
+        setState(() {
+          if (!_localRtcConnection.any((e) => e.localUid == remoteUid)) {
+            _remoteUidsMap.putIfAbsent(remoteUid, () => connection);
+          }
+        });
+      },
+      onUserOffline: (RtcConnection connection, int remoteUid,
+          UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}, remoteUid: $remoteUid');
+        setState(() {
+          _remoteUidsMap.remove(remoteUid);
+        });
+      },
+    );
+    widget.rtcEngine.registerEventHandler(_eventHandler);
+  }
+
+  @override
+  void dispose() {
+    widget.rtcEngine.unregisterEventHandler(_eventHandler);
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final widgets = <Widget>[];
+    debugPrint('_remoteUidsMap: $_remoteUidsMap');
+    _remoteUidsMap.forEach((key, value) {
+      widgets.add(
+        SizedBox(
+          width: 150,
+          height: 150,
+          child: Stack(
+            children: [
+              AgoraVideoView(
+                controller: VideoViewController.remote(
+                  rtcEngine: widget.rtcEngine,
+                  canvas: VideoCanvas(uid: key),
+                  connection: RtcConnection(
+                      channelId: widget.channelId,
+                      localUid: widget.connectionUid),
+                ),
+              ),
+              Column(
+                mainAxisAlignment: MainAxisAlignment.start,
+                crossAxisAlignment: CrossAxisAlignment.start,
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  Text(
+                    widget.connectionUid != null
+                        ? 'localuid: ${widget.connectionUid}'
+                        : '',
+                    style: const TextStyle(color: Colors.white, fontSize: 10),
+                  ),
+                  Text(
+                    'remoteuid: $key',
+                    style: const TextStyle(color: Colors.white, fontSize: 10),
+                  ),
+                ],
+              )
+            ],
+          ),
+        ),
+      );
+    });
+    return SingleChildScrollView(
+      scrollDirection: Axis.horizontal,
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.start,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: widgets,
+      ),
+    );
+  }
+}

+ 62 - 0
lib/components/rgba_image.dart

@@ -0,0 +1,62 @@
+import 'dart:typed_data';
+import 'dart:ui' as ui;
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/painting.dart';
+
+class RgbaImage extends ImageProvider<RgbaImage> {
+  final Uint8List bytes;
+  final int width;
+  final int height;
+  final double scale;
+
+  const RgbaImage(
+    this.bytes, {
+    required this.width,
+    required this.height,
+    this.scale = 1.0,
+  });
+
+  Future<ui.Codec> _loadAsync(
+    RgbaImage key,
+    DecoderCallback decode,
+  ) async {
+    assert(key == this);
+
+    final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
+    final descriptor = ui.ImageDescriptor.raw(
+      buffer,
+      width: width,
+      height: height,
+      pixelFormat: ui.PixelFormat.rgba8888,
+    );
+    return await descriptor.instantiateCodec();
+  }
+
+  @override
+  Future<RgbaImage> obtainKey(ImageConfiguration configuration) {
+    return SynchronousFuture<RgbaImage>(this);
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (other.runtimeType != runtimeType) return false;
+    return other is RgbaImage && other.bytes == bytes && other.scale == scale;
+  }
+
+  @override
+  int get hashCode => Object.hash(bytes.hashCode, scale);
+
+  @override
+  String toString() =>
+      '${objectRuntimeType(this, 'RgbaImage')}(${describeIdentity(bytes)}, scale: $scale)';
+
+  @override
+  ImageStreamCompleter load(RgbaImage key, DecoderCallback decode) {
+    return MultiFrameImageStreamCompleter(
+      codec: _loadAsync(key, decode),
+      scale: key.scale,
+      debugLabel: 'RgbaImage(${describeIdentity(key.bytes)})',
+    );
+  }
+}

+ 28 - 5
lib/config/agora.config.dart

@@ -1,14 +1,37 @@
 /// Get your own App ID at https://dashboard.agora.io/
-const appId = "1320816c633c41f7b9f734a206e96663";
+String get appId {
+  // Allow pass an `appId` as an environment variable with name `TEST_APP_ID` by using --dart-define
+  return const String.fromEnvironment('TEST_APP_ID',
+      defaultValue: '<TEST_APP_ID>');
+}
 
 /// Please refer to https://docs.agora.io/en/Agora%20Platform/token
-const token = "904a185e81c4475f8366214f62f0c3b0";
+String get token {
+  // Allow pass a `token` as an environment variable with name `TEST_TOKEN` by using --dart-define
+  return const String.fromEnvironment('TEST_TOKEN',
+      defaultValue: '<TEST_TOKEN>');
+}
 
 /// Your channel ID
-const channelId = "channel-chat";
+String get channelId {
+  // Allow pass a `channelId` as an environment variable with name `TEST_CHANNEL_ID` by using --dart-define
+  return const String.fromEnvironment(
+    'TEST_CHANNEL_ID',
+    defaultValue: '<TEST_CHANNEL_ID>',
+  );
+}
 
 /// Your int user ID
-const uid = 10001;
+const int uid = 0;
+
+/// Your user ID for the screen sharing
+const int screenSharingUid = 10;
 
 /// Your string user ID
-const stringUid = "user-10001";
+const String stringUid = '0';
+
+String get musicCenterAppId {
+  // Allow pass a `token` as an environment variable with name `TEST_TOKEN` by using --dart-define
+  return const String.fromEnvironment('MUSIC_CENTER_APPID',
+      defaultValue: '<MUSIC_CENTER_APPID>');
+}

+ 244 - 0
lib/examples/advanced/audio_mixing/audio_mixing.dart

@@ -0,0 +1,244 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:path/path.dart' as path;
+
+/// AudioMixing Example
+class AudioMixing extends StatefulWidget {
+  /// Construct the [AudioMixing]
+  const AudioMixing({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _AudioMixingState();
+}
+
+class _AudioMixingState extends State<AudioMixing> {
+  late final RtcEngine _engine;
+  String channelId = config.channelId;
+  bool isJoined = false,
+      openMicrophone = true,
+      enableSpeakerphone = true,
+      playEffect = false;
+  late final TextEditingController _controller;
+
+  bool _isStartedAudioMixing = false;
+  bool _loopback = false;
+  double _cycle = 1.0;
+  double _startPos = 1000;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: channelId);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+      onAudioMixingFinished: () {
+        logSink.log('[onAudioMixingFinished]');
+      },
+      onAudioMixingStateChanged:
+          (AudioMixingStateType state, AudioMixingReasonType errorCode) {
+        logSink.log(
+            '[onAudioMixingStateChanged] state:${state.toString()}, errorCode: ${errorCode.toString()}}');
+      },
+      onRemoteAudioStateChanged: (RtcConnection connection, int remoteUid,
+          RemoteAudioState state, RemoteAudioStateReason reason, int elapsed) {
+        logSink.log(
+            '[onRemoteAudioStateChanged] uid: ${connection.toJson()}, state: $state, reason: $reason, elapsed: $elapsed');
+      },
+    ));
+
+    await _engine.enableAudio();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+  }
+
+  void _dispose() async {
+    await _engine.stopAudioMixing();
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _joinChannel() async {
+    _engine.joinChannel(
+        token: '',
+        channelId: _controller.text,
+        uid: 0,
+        options: const ChannelMediaOptions());
+  }
+
+  Future<void> _leaveChannel() async {
+    _stopAudioMixing();
+    _isStartedAudioMixing = false;
+    _loopback = false;
+    _cycle = 1.0;
+    _startPos = 1000;
+    await _engine.leaveChannel();
+  }
+
+  Future<void> _startAudioMixing() async {
+    ByteData data =
+        await rootBundle.load("assets/audio_mixing/Agora.io-Interactions.mp3");
+    List<int> bytes =
+        data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
+
+    Directory appDocDir = await getApplicationDocumentsDirectory();
+    String p = path.join(appDocDir.path, 'Agora.io-Interactions.mp3');
+    final file = File(p);
+    if (!(await file.exists())) {
+      await file.create();
+      await file.writeAsBytes(bytes);
+    }
+
+    await _engine.startAudioMixing(
+      filePath: p,
+      loopback: _loopback,
+      cycle: _cycle.toInt(),
+      startPos: _startPos.toInt(),
+    );
+    setState(() {
+      _isStartedAudioMixing = true;
+    });
+  }
+
+  Future<void> _stopAudioMixing() async {
+    await _engine.stopAudioMixing();
+    setState(() {
+      _isStartedAudioMixing = false;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: [
+        TextField(
+          controller: _controller,
+          decoration: const InputDecoration(hintText: 'Channel ID'),
+          onChanged: (text) {
+            setState(() {
+              channelId = text;
+            });
+          },
+        ),
+        Row(
+          children: [
+            Expanded(
+              flex: 1,
+              child: ElevatedButton(
+                onPressed: isJoined ? _leaveChannel : _joinChannel,
+                child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+              ),
+            )
+          ],
+        ),
+        if (isJoined)
+          Column(
+            mainAxisSize: MainAxisSize.min,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Row(mainAxisSize: MainAxisSize.min, children: [
+                const Text('loopback: '),
+                Switch(
+                  value: _loopback,
+                  onChanged: _isStartedAudioMixing
+                      ? null
+                      : (changed) {
+                          setState(() {
+                            _loopback = changed;
+                          });
+                        },
+                  activeTrackColor: Colors.grey[350],
+                  activeColor: Colors.white,
+                )
+              ]),
+              Row(
+                mainAxisAlignment: MainAxisAlignment.end,
+                children: [
+                  const Text('cycle:'),
+                  Slider(
+                    value: _cycle,
+                    min: 0.0,
+                    max: 10.0,
+                    divisions: 10,
+                    label: 'cycle value ${_cycle.toInt()}',
+                    onChanged: _isStartedAudioMixing
+                        ? null
+                        : (double value) {
+                            setState(() {
+                              _cycle = value;
+                            });
+                          },
+                  )
+                ],
+              ),
+              Row(
+                mainAxisAlignment: MainAxisAlignment.end,
+                children: [
+                  const Text('startPos:'),
+                  Slider(
+                    value: _startPos,
+                    min: 1000,
+                    max: 5000,
+                    divisions: 100,
+                    label: 'startPos value ${_startPos / 1000.0}s',
+                    onChanged: (double value) async {
+                      setState(() {
+                        _startPos = value;
+                      });
+
+                      if (_isStartedAudioMixing) {
+                        await _engine.setAudioMixingPosition(_startPos.toInt());
+                      }
+                    },
+                  )
+                ],
+              ),
+              ElevatedButton(
+                onPressed: !_isStartedAudioMixing
+                    ? _startAudioMixing
+                    : _stopAudioMixing,
+                child: Text(
+                    '${_isStartedAudioMixing ? 'Stop' : 'Start'} Audio Mixing'),
+              ),
+            ],
+          ),
+      ],
+    );
+  }
+}

+ 281 - 0
lib/examples/advanced/audio_spectrum/audio_spectrum.dart

@@ -0,0 +1,281 @@
+import 'dart:math';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+
+/// AudioSpectrum Example
+class AudioSpectrum extends StatefulWidget {
+  /// Construct the [AudioSpectrum]
+  const AudioSpectrum({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<AudioSpectrum> {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+
+  bool isJoined = false, switchCamera = true, switchRender = true;
+  Set<int> remoteUid = {};
+  late TextEditingController _controller;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: config.channelId);
+
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+    ));
+    await _engine.setLogFilter(LogFilterType.logFilterError);
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUid.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUid.removeWhere((element) => element == rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+          remoteUid.clear();
+        });
+      },
+    ));
+
+    await _engine.enableVideo();
+
+    await _engine.setVideoEncoderConfiguration(
+      const VideoEncoderConfiguration(
+        dimensions: VideoDimensions(width: 640, height: 360),
+        frameRate: 15,
+        bitrate: 800,
+      ),
+    );
+
+    await _engine.startPreview();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _joinChannel() async {
+    await _engine.joinChannel(
+      token: config.token,
+      channelId: _controller.text,
+      uid: config.uid,
+      options: const ChannelMediaOptions(
+        channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+        clientRoleType: ClientRoleType.clientRoleBroadcaster,
+      ),
+    );
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return CurveLine(rtcEngine: _engine);
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+          ],
+        );
+      },
+    );
+    // if (!_isInit) return Container();
+  }
+}
+
+class CurveLine extends StatefulWidget {
+  const CurveLine({Key? key, required this.rtcEngine}) : super(key: key);
+
+  final RtcEngine rtcEngine;
+
+  @override
+  State<CurveLine> createState() => _CurveLineState();
+}
+
+class _CurveLineState extends State<CurveLine> {
+  List<double> audioSpectrumData = [];
+
+  @override
+  void initState() {
+    super.initState();
+
+    _init();
+  }
+
+  Future<void> _init() async {
+    await widget.rtcEngine.enableAudioSpectrumMonitor();
+    widget.rtcEngine.registerAudioSpectrumObserver(AudioSpectrumObserver(
+      onLocalAudioSpectrum: (AudioSpectrumData data) {
+        debugPrint('[onLocalAudioSpectrum] data: ${data.toJson()}');
+
+        setState(() {
+          audioSpectrumData = data.audioSpectrumData ?? [];
+        });
+      },
+      onRemoteAudioSpectrum:
+          (List<UserAudioSpectrumInfo> spectrums, int spectrumNumber) {
+        debugPrint(
+            '[onRemoteAudioSpectrum] spectrums: $spectrums, spectrumNumber: $spectrumNumber');
+      },
+    ));
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return CustomPaint(
+      painter: CurveLinePainter(audioSpectrumData),
+      size: const Size.fromHeight(300.0),
+    );
+  }
+}
+
+class CurveLinePainter extends CustomPainter {
+  CurveLinePainter(this.audioSpectrumData) {
+    if (audioSpectrumData.isEmpty) {
+      _max = _min = -1.0;
+    } else {
+      var tempMax = double.minPositive;
+      var tempMin = double.maxFinite;
+      _max = audioSpectrumData.fold(tempMax, (pre, e) {
+        return max(pre, e);
+      });
+      _min = audioSpectrumData.fold(tempMin, (pre, e) {
+        return min(pre, e);
+      });
+    }
+  }
+
+  final List<double> audioSpectrumData;
+
+  late final double _max;
+  late final double _min;
+
+  double getItemSpacing(double width) => width / audioSpectrumData.length;
+
+  double _getXByIndex(double width, int index) {
+    var itemSpacing = getItemSpacing(width);
+    return itemSpacing * index + itemSpacing / 2.0;
+  }
+
+  double _getYByValue(double height, value) {
+    if (_max == -1.0 || _min == -1.0) {
+      return 0.0;
+    } else if (_max == _min) {
+      return (height - 40.0) / 2.0;
+    } else {
+      var drawingHeight = height;
+      var availableDrawingHeight = drawingHeight * 0.4;
+      return drawingHeight -
+          (drawingHeight * 0.3) -
+          (availableDrawingHeight / (_max - _min)) * (value - _min);
+    }
+  }
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    if (audioSpectrumData.isEmpty) return;
+    final linePaint = Paint()
+      ..isAntiAlias = true
+      ..color = const Color.fromARGB(255, 255, 0, 144)
+      ..strokeWidth = 2.0
+      ..style = PaintingStyle.stroke;
+
+    double preX;
+    double preY;
+    double curX;
+    double curY;
+    var firstPoint = audioSpectrumData[0];
+    curX = _getXByIndex(size.width, 0);
+    curY = _getYByValue(size.height, firstPoint);
+    final curvePath = Path();
+    curvePath.moveTo(curX, curY);
+    for (int i = 0; i < audioSpectrumData.length; i++) {
+      final p = audioSpectrumData[i];
+      preX = curX;
+      preY = curY;
+      curX = _getXByIndex(size.width, i);
+      curY = _getYByValue(size.height, p);
+      double cpx = preX + (curX - preX) / 2.0;
+      curvePath.cubicTo(cpx, preY, cpx, curY, curX, curY);
+    }
+
+    canvas.drawPath(curvePath, linePaint);
+  }
+
+  @override
+  bool shouldRepaint(covariant CustomPainter oldDelegate) {
+    return oldDelegate is CurveLinePainter &&
+        oldDelegate.audioSpectrumData != audioSpectrumData;
+  }
+}

+ 243 - 0
lib/examples/advanced/channel_media_relay/channel_media_relay.dart

@@ -0,0 +1,243 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:agora_rtc_engine_example/components/remote_video_views_widget.dart';
+import 'package:flutter/material.dart';
+
+/// ChannelMediaRelay Example
+class ChannelMediaRelay extends StatefulWidget {
+  /// Construct the [ChannelMediaRelay]
+  const ChannelMediaRelay({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<ChannelMediaRelay> with KeepRemoteVideoViewsMixin {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+  bool isJoined = false;
+  int? remoteUid;
+  bool isRelaying = false;
+  late final TextEditingController _channelMediaRelayController;
+  late final TextEditingController _channelController;
+
+  @override
+  void initState() {
+    super.initState();
+    _channelMediaRelayController = TextEditingController();
+    _channelController = TextEditingController(text: config.channelId);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUid = rUid;
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUid = null;
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+      onChannelMediaRelayStateChanged:
+          (ChannelMediaRelayState state, ChannelMediaRelayError code) {
+        logSink.log(
+            '[onChannelMediaRelayStateChanged] state: $state, code: $code');
+        switch (state) {
+          case ChannelMediaRelayState.relayStateIdle:
+            setState(() {
+              isRelaying = false;
+            });
+            break;
+          case ChannelMediaRelayState.relayStateRunning:
+            setState(() {
+              isRelaying = true;
+            });
+            break;
+          case ChannelMediaRelayState.relayStateFailure:
+            setState(() {
+              isRelaying = false;
+            });
+            break;
+          default:
+            setState(() {
+              isRelaying = false;
+            });
+            break;
+        }
+      },
+    ));
+
+    // enable video module and set up video encoding configs
+    await _engine.enableVideo();
+    await _engine.startPreview();
+
+    // make this room live broadcasting room
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  void _joinChannel() async {
+    // start joining channel
+    // 1. Users can only see each other after they join the
+    // same channel successfully using the same app id.
+    // 2. If app certificate is turned on at dashboard, token is needed
+    // when joining channel. The channel name and uid used to calculate
+    // the token has to match the ones used for channel join
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: _channelController.text,
+        uid: 0,
+        options: const ChannelMediaOptions());
+
+    setState(() {
+      isJoined = true;
+    });
+  }
+
+  void _leaveChannel() async {
+    await _onPressRelayOrStop();
+
+    await _engine.leaveChannel();
+
+    setState(() {
+      isJoined = false;
+    });
+  }
+
+  Future<void> _onPressRelayOrStop() async {
+    if (isRelaying) {
+      await _engine.stopChannelMediaRelay();
+      setState(() {
+        isRelaying = !isRelaying;
+      });
+      return;
+    }
+    if (_channelMediaRelayController.text.isEmpty) {
+      return;
+    }
+
+    await _engine.startChannelMediaRelay(ChannelMediaRelayConfiguration(
+      srcInfo: ChannelMediaInfo(
+        channelName: _channelController.text,
+        token: config.token,
+        uid: 0,
+      ),
+      destInfos: [
+        ChannelMediaInfo(
+            channelName: _channelMediaRelayController.text, token: '', uid: 0)
+      ],
+      destCount: 1,
+    ));
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return Stack(
+          children: [
+            AgoraVideoView(
+              controller: VideoViewController(
+                rtcEngine: _engine,
+                canvas: const VideoCanvas(uid: 0),
+              ),
+            ),
+            Align(
+              alignment: Alignment.topLeft,
+              child: RemoteVideoViewsWidget(
+                key: keepRemoteVideoViewsKey,
+                rtcEngine: _engine,
+                channelId: _channelController.text,
+              ),
+            )
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          children: [
+            TextField(
+              controller: _channelController,
+              readOnly: isJoined,
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            if (isJoined)
+              Row(
+                mainAxisSize: MainAxisSize.max,
+                children: [
+                  Expanded(
+                      child: TextField(
+                          controller: _channelMediaRelayController,
+                          decoration: const InputDecoration(
+                            hintText: 'Enter target relay channel name',
+                          ))),
+                  ElevatedButton(
+                    onPressed: _onPressRelayOrStop,
+                    child: Text(!isRelaying ? 'Relay' : 'Stop'),
+                  ),
+                ],
+              )
+          ],
+        );
+      },
+    );
+  }
+}

+ 286 - 0
lib/examples/advanced/custom_capture_audio/custom_capture_audio.dart

@@ -0,0 +1,286 @@
+// import 'package:agora_rtc_engine/rtc_engine.dart';
+// import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+// import 'package:agora_rtc_engine_example/components/log_sink.dart';
+// import 'package:flutter/material.dart';
+// import 'package:permission_handler/permission_handler.dart';
+
+// import 'custom_capture_audio_api.generated.dart';
+
+// const int _defaultSampleRate = 16000;
+// const int _defaultChannelCount = 1;
+
+// /// The push position of the external audio frame. see [Android API reference](https://docs.agora.io/en/Voice/API%20Reference/java/enumio_1_1agora_1_1rtc_1_1_constants_1_1_audio_external_source_pos.html).
+// enum AudioExternalSourcePos {
+//   /// 0: The position before local playback. If you need to play the external audio
+//   /// frame on the local client, set this position.
+//   externalPlayoutSource,
+
+//   /// 1: The position after audio capture and before audio pre-processing. If you
+//   /// need the audio module of the SDK to process the external audio frame, set
+//   /// this position.
+//   externalRecordSourcePreProcess,
+
+//   /// 2: The position after audio pre-processing and before audio encoding. If
+//   /// you do not need the audio module of the SDK to process the external audio
+//   /// frame, set this position.
+//   externalRecordSourcePostProcess
+// }
+
+// /// Extension for AudioExternalSourcePos
+// extension AudioExternalSourcePosExt on AudioExternalSourcePos {
+//   /// Get int value acroding the AudioExternalSourcePos value
+//   int get value {
+//     switch (this) {
+//       case AudioExternalSourcePos.externalPlayoutSource:
+//         return 0;
+//       case AudioExternalSourcePos.externalRecordSourcePreProcess:
+//         return 1;
+//       case AudioExternalSourcePos.externalRecordSourcePostProcess:
+//         return 2;
+//     }
+//   }
+// }
+
+// /// CustomCaptureAudio Example
+// class CustomCaptureAudio extends StatefulWidget {
+//   /// Construct the [CustomCaptureAudio]
+//   const CustomCaptureAudio({Key? key}) : super(key: key);
+
+//   @override
+//   _CustomCaptureAudioState createState() => _CustomCaptureAudioState();
+// }
+
+// class _CustomCaptureAudioState extends State<CustomCaptureAudio> {
+//   final CustomCaptureAudioApi _api = CustomCaptureAudioApi();
+//   bool _isJoined = false;
+//   bool _isMute = false;
+//   double _playoutSliderValue = 100.0;
+//   double _preProcessSliderValue = 100.0;
+//   double _postProcessSliderValue = 100.0;
+//   AudioExternalSourcePos _audioExternalSourcePos =
+//       AudioExternalSourcePos.externalPlayoutSource;
+//   late final RtcEngine _engine;
+//   late final TextEditingController _channelIdController;
+
+//   @override
+//   void initState() {
+//     super.initState();
+//     _channelIdController = TextEditingController(text: config.channelId);
+//     _initEngine();
+//   }
+
+//   @override
+//   void dispose() {
+//     super.dispose();
+//     _destroyEngine();
+//   }
+
+//   void _initEngine() async {
+//     _engine = await RtcEngine.createWithContext(RtcEngineContext(config.appId));
+
+//     _engine.setEventHandler(RtcEngineEventHandler(
+//         joinChannelSuccess: (String channel, int uid, int elapsed) async {
+//       logSink.log('joinChannelSuccess $channel $uid $elapsed');
+//       await _api.startAudioRecord(_defaultSampleRate, _defaultChannelCount);
+//       setState(() {
+//         _isJoined = true;
+//       });
+//     }, leaveChannel: (RtcStats stats) {
+//       logSink.log('leaveChannel ${stats.toJson()}');
+//       setState(() {
+//         _isJoined = false;
+//       });
+//     }));
+//   }
+
+//   void _joinChannel() async {
+//     if (Theme.of(context).platform == TargetPlatform.android) {
+//       await Permission.microphone.request();
+//     }
+
+//     // make this room live broadcasting room
+//     await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
+//     await _engine.setClientRole(ClientRole.Broadcaster);
+
+//     await _engine.setDefaultAudioRouteToSpeakerphone(true);
+
+//     // Sets the external audio source.
+//     // PS: Ensure that you call this method before the joinChannel method
+//     await _api.setExternalAudioSource(
+//         true, _defaultSampleRate, _defaultChannelCount);
+
+//     // Set audio route to speaker
+//     await _engine.setDefaultAudioRouteToSpeakerphone(true);
+
+//     final option = ChannelMediaOptions();
+//     option.autoSubscribeAudio = true;
+//     option.autoSubscribeVideo = false;
+
+//     // start joining channel
+//     // 1. Users can only see each other after they join the
+//     // same channel successfully using the same app id.
+//     // 2. If app certificate is turned on at dashboard, token is needed
+//     // when joining channel. The channel name and uid used to calculate
+//     // the token has to match the ones used for channel join
+//     await _engine.joinChannel(
+//         config.token, _channelIdController.text, null, 0, option);
+//   }
+
+//   Future<void> _leaveChannel() async {
+//     if (_isMute) {
+//       await _muteLocalAudioStream();
+//     }
+//     await _api.stopAudioRecord();
+//     await _engine.leaveChannel();
+//   }
+
+//   void _destroyEngine() async {
+//     await _engine.leaveChannel();
+//     await _engine.destroy();
+//   }
+
+//   void _sourcePosChanged() {
+//     var volume = 0;
+//     switch (_audioExternalSourcePos) {
+//       case AudioExternalSourcePos.externalPlayoutSource:
+//         volume = _playoutSliderValue.toInt();
+//         break;
+//       case AudioExternalSourcePos.externalRecordSourcePreProcess:
+//         volume = _preProcessSliderValue.toInt();
+//         break;
+//       case AudioExternalSourcePos.externalRecordSourcePostProcess:
+//         volume = _postProcessSliderValue.toInt();
+//         break;
+//     }
+//     _api.setExternalAudioSourceVolume(
+//       _audioExternalSourcePos.value,
+//       volume,
+//     );
+//   }
+
+//   @override
+//   Widget build(BuildContext context) {
+//     return Column(
+//       mainAxisAlignment: MainAxisAlignment.start,
+//       crossAxisAlignment: CrossAxisAlignment.start,
+//       children: [
+//         TextField(
+//           controller: _channelIdController,
+//           decoration: const InputDecoration(hintText: 'Channel ID'),
+//         ),
+//         Row(
+//           children: [
+//             Expanded(
+//               flex: 1,
+//               child: ElevatedButton(
+//                 onPressed: _isJoined ? _leaveChannel : _joinChannel,
+//                 child: Text('${_isJoined ? 'Leave' : 'Join'} channel'),
+//               ),
+//             )
+//           ],
+//         ),
+//         if (_isJoined) ...[
+//           ListTile(
+//             title: Row(
+//               children: [
+//                 const Text('PlayOut'),
+//                 Expanded(
+//                   child: Slider(
+//                     value: _playoutSliderValue,
+//                     min: 0,
+//                     max: 100,
+//                     label: _playoutSliderValue.round().toString(),
+//                     onChanged: (double value) {
+//                       _playoutSliderValue = value;
+//                       _sourcePosChanged();
+//                       setState(() {});
+//                     },
+//                   ),
+//                 )
+//               ],
+//             ),
+//             leading: Radio<AudioExternalSourcePos>(
+//               value: AudioExternalSourcePos.externalPlayoutSource,
+//               groupValue: _audioExternalSourcePos,
+//               onChanged: (AudioExternalSourcePos? value) {
+//                 _audioExternalSourcePos = value!;
+//                 _sourcePosChanged();
+//                 setState(() {});
+//               },
+//             ),
+//           ),
+//           ListTile(
+//             title: Row(
+//               children: [
+//                 const Text('PreProcess'),
+//                 Expanded(
+//                   child: Slider(
+//                     value: _preProcessSliderValue,
+//                     min: 0,
+//                     max: 100,
+//                     divisions: 100,
+//                     label: _preProcessSliderValue.round().toString(),
+//                     onChanged: (double value) {
+//                       _preProcessSliderValue = value;
+//                       _sourcePosChanged();
+//                       setState(() {});
+//                     },
+//                   ),
+//                 )
+//               ],
+//             ),
+//             leading: Radio<AudioExternalSourcePos>(
+//               value: AudioExternalSourcePos.externalRecordSourcePreProcess,
+//               groupValue: _audioExternalSourcePos,
+//               onChanged: (AudioExternalSourcePos? value) {
+//                 _audioExternalSourcePos = value!;
+//                 _sourcePosChanged();
+//                 setState(() {});
+//               },
+//             ),
+//           ),
+//           ListTile(
+//             title: Row(
+//               children: [
+//                 const Text('PostProcess'),
+//                 Expanded(
+//                   child: Slider(
+//                     value: _postProcessSliderValue,
+//                     min: 0,
+//                     max: 100,
+//                     label: _postProcessSliderValue.round().toString(),
+//                     onChanged: (double value) {
+//                       _postProcessSliderValue = value;
+
+//                       _sourcePosChanged();
+//                       setState(() {});
+//                     },
+//                   ),
+//                 )
+//               ],
+//             ),
+//             leading: Radio<AudioExternalSourcePos>(
+//               value: AudioExternalSourcePos.externalRecordSourcePostProcess,
+//               groupValue: _audioExternalSourcePos,
+//               onChanged: (AudioExternalSourcePos? value) {
+//                 _audioExternalSourcePos = value!;
+//                 _sourcePosChanged();
+//                 setState(() {});
+//               },
+//             ),
+//           ),
+//           ElevatedButton(
+//             onPressed: !_isJoined ? null : _muteLocalAudioStream,
+//             child: Text('${_isMute ? 'Open' : 'Mute'} microphone'),
+//           ),
+//         ],
+//       ],
+//     );
+//   }
+
+//   Future<void> _muteLocalAudioStream() async {
+//     _isMute = !_isMute;
+//     await _engine.muteLocalAudioStream(_isMute);
+//     setState(() {});
+//   }
+// }

+ 130 - 0
lib/examples/advanced/custom_capture_audio/custom_capture_audio_api.generated.dart

@@ -0,0 +1,130 @@
+// Autogenerated from Pigeon (v1.0.8), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
+// @dart = 2.12
+import 'dart:async';
+import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
+
+import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer;
+import 'package:flutter/services.dart';
+
+class _CustomCaptureAudioApiCodec extends StandardMessageCodec {
+  const _CustomCaptureAudioApiCodec();
+}
+
+class CustomCaptureAudioApi {
+  /// Constructor for [CustomCaptureAudioApi].  The [binaryMessenger] named argument is
+  /// available for dependency injection.  If it is left null, the default
+  /// BinaryMessenger will be used which routes to the host platform.
+  CustomCaptureAudioApi({BinaryMessenger? binaryMessenger})
+      : _binaryMessenger = binaryMessenger;
+
+  final BinaryMessenger? _binaryMessenger;
+
+  static const MessageCodec<Object?> codec = _CustomCaptureAudioApiCodec();
+
+  Future<void> setExternalAudioSource(
+      bool arg_enabled, int arg_sampleRate, int arg_channels) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.CustomCaptureAudioApi.setExternalAudioSource',
+        codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(<Object>[arg_enabled, arg_sampleRate, arg_channels])
+            as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+        details: null,
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return;
+    }
+  }
+
+  Future<void> setExternalAudioSourceVolume(
+      int arg_sourcePos, int arg_volume) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.CustomCaptureAudioApi.setExternalAudioSourceVolume',
+        codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap = await channel
+        .send(<Object>[arg_sourcePos, arg_volume]) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+        details: null,
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return;
+    }
+  }
+
+  Future<void> startAudioRecord(int arg_sampleRate, int arg_channels) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.CustomCaptureAudioApi.startAudioRecord', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap = await channel
+        .send(<Object>[arg_sampleRate, arg_channels]) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+        details: null,
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return;
+    }
+  }
+
+  Future<void> stopAudioRecord() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.CustomCaptureAudioApi.stopAudioRecord', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(null) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+        details: null,
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return;
+    }
+  }
+}

+ 209 - 0
lib/examples/advanced/device_manager/device_manager.dart

@@ -0,0 +1,209 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+
+/// DeviceManager Example
+class DeviceManager extends StatefulWidget {
+  /// Construct the [DeviceManager]
+  const DeviceManager({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<DeviceManager> {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+  String channelId = config.channelId;
+  bool isJoined = false;
+  List<int> remoteUid = [];
+  List<VideoDeviceInfo> devices = [];
+  late final VideoDeviceManager _videoDeviceManager;
+  late TextEditingController _controller;
+  late String _selectedDeviceId;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: channelId);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    _controller.dispose();
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {});
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {});
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+    ));
+
+    await _engine.enableVideo();
+    await _engine.startPreview();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+    await _enumerateVideoDevices();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  void _joinChannel() async {
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: channelId,
+        uid: config.uid,
+        options: const ChannelMediaOptions());
+  }
+
+  _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  Future<void> _enumerateVideoDevices() async {
+    _videoDeviceManager = _engine.getVideoDeviceManager();
+    _selectedDeviceId = await _videoDeviceManager.getDevice();
+    final devices = await _videoDeviceManager.enumerateVideoDevices();
+    setState(() {
+      this.devices = devices;
+    });
+  }
+
+  Widget _devicesDropDown() {
+    if (devices.isEmpty) return Container();
+    final dropDownMenus = <DropdownMenuItem<String>>[];
+    for (var v in devices) {
+      dropDownMenus.add(DropdownMenuItem(
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: [
+            Text(
+              'deviceName: ${v.deviceName!}',
+              style: const TextStyle(fontSize: 10),
+            ),
+            Text(
+              'deviceId: ${v.deviceId!}',
+              style: const TextStyle(fontSize: 10),
+            ),
+          ],
+        ),
+        value: v.deviceId,
+      ));
+    }
+    return DropdownButton<String>(
+      items: dropDownMenus,
+      value: _selectedDeviceId,
+      onChanged: (v) {
+        setState(() {
+          _selectedDeviceId = v!;
+        });
+      },
+    );
+  }
+
+  Future<void> _setVideoDevice(String deviceId) async {
+    _videoDeviceManager.setDevice(deviceId);
+    logSink.log('setVideoDevice deviceId: $deviceId');
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return AgoraVideoView(
+          controller: VideoViewController(
+            rtcEngine: _engine,
+            canvas: const VideoCanvas(uid: 0),
+          ),
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+              onChanged: (text) {
+                setState(() {
+                  channelId = text;
+                });
+              },
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            _devicesDropDown(),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: () {
+                _setVideoDevice(_selectedDeviceId);
+              },
+              child: const Text('Set video device'),
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 380 - 0
lib/examples/advanced/enable_spatial_audio/enable_spatial_audio.dart

@@ -0,0 +1,380 @@
+// ignore_for_file: unnecessary_brace_in_string_interps
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:permission_handler/permission_handler.dart';
+
+/// EnableSpatialAudio Example
+class EnableSpatialAudio extends StatefulWidget {
+  /// Construct the [EnableSpatialAudio]
+  const EnableSpatialAudio({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<EnableSpatialAudio> {
+  late final RtcEngine _engine;
+  bool isJoined = false;
+  late TextEditingController _controller0;
+  bool _isEnableSpatialAudio = false;
+  double _speakerAzimuth = 0.0;
+  double _speakerElevation = 0.0;
+  double _speakerDistance = 0.0;
+
+  /// speaker orientation [0-180]: 0 degree is the same with listener orientation
+  int _speakerOrientation = 0;
+  bool _enableBlur = false;
+  bool _enableAirAbsorb = false;
+
+  final Set<int> _remoteUids = {};
+  int _selectedRemoteUid = 0;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller0 = TextEditingController(text: config.channelId);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  void _dispose() async {
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  void _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          _remoteUids.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          _remoteUids.remove(rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+    ));
+
+    await _engine.enableAudio();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+  }
+
+  void _joinChannel() async {
+    if (defaultTargetPlatform == TargetPlatform.android) {
+      await Permission.microphone.request();
+    }
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: _controller0.text,
+        uid: 1000,
+        options: const ChannelMediaOptions());
+  }
+
+  _leaveChannel() async {
+    setState(() {
+      _isEnableSpatialAudio = false;
+    });
+    await _engine.enableSpatialAudio(_isEnableSpatialAudio);
+
+    await _engine.leaveChannel();
+  }
+
+  Widget _buildSpatialAudioOptions() {
+    if (_selectedRemoteUid == 0 && _remoteUids.isNotEmpty) {
+      _selectedRemoteUid = _remoteUids.first;
+    }
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.start,
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Row(
+          children: [
+            const Text('Remote Uids: '),
+            DropdownButton<int>(
+                items: _remoteUids.map((uid) {
+                  return DropdownMenuItem(
+                    value: uid,
+                    child: Text('$uid'),
+                  );
+                }).toList(),
+                value: _selectedRemoteUid,
+                onChanged: (v) {
+                  setState(() {
+                    _selectedRemoteUid = v!;
+                  });
+                }),
+          ],
+        ),
+        const Text('The options can be set after remote user joined'),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: [
+            const Text('Speaker Azimuth:'),
+            Slider(
+              value: _speakerAzimuth,
+              min: 0,
+              max: 500,
+              divisions: 5,
+              label: 'Speaker Azimuth',
+              onChanged: _remoteUids.isNotEmpty
+                  ? (double value) {
+                      setState(() {
+                        _speakerAzimuth = value;
+                      });
+                      _engine.setRemoteUserSpatialAudioParams(
+                        uid: _selectedRemoteUid,
+                        params: SpatialAudioParams(
+                          speakerAzimuth: _speakerAzimuth,
+                          speakerElevation: _speakerElevation,
+                          speakerDistance: _speakerDistance,
+                          speakerOrientation: _speakerOrientation,
+                          enableBlur: _enableBlur,
+                          enableAirAbsorb: _enableAirAbsorb,
+                        ),
+                      );
+                    }
+                  : null,
+            )
+          ],
+        ),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: [
+            const Text('Speaker Elevation:'),
+            Slider(
+              value: _speakerElevation,
+              min: 0,
+              max: 500,
+              divisions: 5,
+              label: 'Speaker Elevation',
+              onChanged: _remoteUids.isNotEmpty
+                  ? (double value) {
+                      setState(() {
+                        _speakerElevation = value;
+                      });
+                      _engine.setRemoteUserSpatialAudioParams(
+                        uid: _selectedRemoteUid,
+                        params: SpatialAudioParams(
+                          speakerAzimuth: _speakerAzimuth,
+                          speakerElevation: _speakerElevation,
+                          speakerDistance: _speakerDistance,
+                          speakerOrientation: _speakerOrientation,
+                          enableBlur: _enableBlur,
+                          enableAirAbsorb: _enableAirAbsorb,
+                        ),
+                      );
+                    }
+                  : null,
+            )
+          ],
+        ),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: [
+            const Text('Speaker Distance:'),
+            Slider(
+              value: _speakerDistance,
+              min: 0,
+              max: 500,
+              divisions: 5,
+              label: 'Speaker Distance',
+              onChanged: _remoteUids.isNotEmpty
+                  ? (double value) {
+                      setState(() {
+                        _speakerDistance = value;
+                      });
+                      _engine.setRemoteUserSpatialAudioParams(
+                        uid: _selectedRemoteUid,
+                        params: SpatialAudioParams(
+                          speakerAzimuth: _speakerAzimuth,
+                          speakerElevation: _speakerElevation,
+                          speakerDistance: _speakerDistance,
+                          speakerOrientation: _speakerOrientation,
+                          enableBlur: _enableBlur,
+                          enableAirAbsorb: _enableAirAbsorb,
+                        ),
+                      );
+                    }
+                  : null,
+            )
+          ],
+        ),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: [
+            const Text('Speaker Orientation:'),
+            Slider(
+              value: _speakerOrientation.toDouble(),
+              min: 0,
+              max: 180,
+              divisions: 18,
+              label: 'Speaker Orientation',
+              onChanged: _remoteUids.isNotEmpty
+                  ? (double value) {
+                      setState(() {
+                        _speakerOrientation = value.toInt();
+                      });
+                      _engine.setRemoteUserSpatialAudioParams(
+                        uid: _selectedRemoteUid,
+                        params: SpatialAudioParams(
+                          speakerAzimuth: _speakerAzimuth,
+                          speakerElevation: _speakerElevation,
+                          speakerDistance: _speakerDistance,
+                          speakerOrientation: _speakerOrientation,
+                          enableBlur: _enableBlur,
+                          enableAirAbsorb: _enableAirAbsorb,
+                        ),
+                      );
+                    }
+                  : null,
+            )
+          ],
+        ),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            const Text('Enable Blur:'),
+            Switch(
+              value: _enableBlur,
+              onChanged: _remoteUids.isNotEmpty
+                  ? (value) {
+                      setState(() {
+                        _enableBlur = value;
+                      });
+                      _engine.setRemoteUserSpatialAudioParams(
+                        uid: _selectedRemoteUid,
+                        params: SpatialAudioParams(
+                          speakerAzimuth: _speakerAzimuth,
+                          speakerElevation: _speakerElevation,
+                          speakerDistance: _speakerDistance,
+                          speakerOrientation: _speakerOrientation,
+                          enableBlur: _enableBlur,
+                          enableAirAbsorb: _enableAirAbsorb,
+                        ),
+                      );
+                    }
+                  : null,
+              activeTrackColor: Colors.grey[350],
+              activeColor: Colors.white,
+            )
+          ],
+        ),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            const Text('Enable Air Absorb:'),
+            Switch(
+              value: _enableAirAbsorb,
+              onChanged: _remoteUids.isNotEmpty
+                  ? (value) {
+                      setState(() {
+                        _enableAirAbsorb = value;
+                      });
+                      _engine.setRemoteUserSpatialAudioParams(
+                        uid: _selectedRemoteUid,
+                        params: SpatialAudioParams(
+                          speakerAzimuth: _speakerAzimuth,
+                          speakerElevation: _speakerElevation,
+                          speakerDistance: _speakerDistance,
+                          speakerOrientation: _speakerOrientation,
+                          enableBlur: _enableBlur,
+                          enableAirAbsorb: _enableAirAbsorb,
+                        ),
+                      );
+                    }
+                  : null,
+              activeTrackColor: Colors.grey[350],
+              activeColor: Colors.white,
+            )
+          ],
+        ),
+      ],
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: [
+        Column(
+          children: [
+            TextField(
+              controller: _controller0,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined
+                        ? () {
+                            setState(() {
+                              _isEnableSpatialAudio = !_isEnableSpatialAudio;
+                            });
+                            _engine.enableSpatialAudio(_isEnableSpatialAudio);
+                          }
+                        : null,
+                    child: Text(
+                        '${_isEnableSpatialAudio ? 'Disable' : 'Enable'}SpatialAudio'),
+                  ),
+                )
+              ],
+            ),
+            if (_isEnableSpatialAudio) _buildSpatialAudioOptions(),
+          ],
+        ),
+      ],
+    );
+  }
+}

+ 184 - 0
lib/examples/advanced/enable_virtualbackground/enable_virtualbackground.dart

@@ -0,0 +1,184 @@
+import 'dart:io';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:agora_rtc_engine_example/components/remote_video_views_widget.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:path/path.dart' as path;
+import 'package:path_provider/path_provider.dart';
+
+/// EnableVirtualBackground Example
+class EnableVirtualBackground extends StatefulWidget {
+  /// @nodoc
+  const EnableVirtualBackground({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<EnableVirtualBackground> with KeepRemoteVideoViewsMixin {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+
+  bool isJoined = false, switchCamera = true, switchRender = true;
+  late TextEditingController _controller;
+  bool _isEnabledVirtualBackgroundImage = false;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: config.channelId);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+    ));
+
+    await _engine.enableVideo();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    await _engine.startPreview();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _enableVirtualBackground() async {
+    ByteData data = await rootBundle.load("assets/agora-logo.png");
+    List<int> bytes =
+        data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
+
+    Directory appDocDir = await getApplicationDocumentsDirectory();
+    String p = path.join(appDocDir.path, 'agora-logo.png');
+    final file = File(p);
+    if (!(await file.exists())) {
+      await file.create();
+      await file.writeAsBytes(bytes);
+    }
+
+    await _engine.enableVirtualBackground(
+        enabled: !_isEnabledVirtualBackgroundImage,
+        backgroundSource: VirtualBackgroundSource(
+            backgroundSourceType: BackgroundSourceType.backgroundImg,
+            source: p),
+        segproperty:
+            const SegmentationProperty(modelType: SegModelType.segModelAi));
+    setState(() {
+      _isEnabledVirtualBackgroundImage = !_isEnabledVirtualBackgroundImage;
+    });
+  }
+
+  void _joinChannel() async {
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: _controller.text,
+        uid: config.uid,
+        options: const ChannelMediaOptions());
+  }
+
+  _leaveChannel() async {
+    if (_isEnabledVirtualBackgroundImage) {
+      await _enableVirtualBackground();
+    }
+    await _engine.leaveChannel();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return Stack(
+          children: [
+            AgoraVideoView(
+                controller: VideoViewController(
+              rtcEngine: _engine,
+              canvas: const VideoCanvas(uid: 0),
+            )),
+            Align(
+              alignment: Alignment.topLeft,
+              child: RemoteVideoViewsWidget(
+                key: keepRemoteVideoViewsKey,
+                rtcEngine: _engine,
+                channelId: _controller.text,
+              ),
+            ),
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            Column(
+              mainAxisAlignment: MainAxisAlignment.start,
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                const Text('Virtual background image: agora-logo'),
+                SizedBox(
+                  height: 100,
+                  width: 100,
+                  child: Image.asset('assets/agora-logo.png'),
+                ),
+                ElevatedButton(
+                  onPressed: isJoined ? _enableVirtualBackground : null,
+                  child: Text(
+                      '${_isEnabledVirtualBackgroundImage ? 'disable' : 'enable'} virtual background image'),
+                ),
+              ],
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 89 - 0
lib/examples/advanced/index.dart

@@ -0,0 +1,89 @@
+import 'dart:io';
+
+import 'package:agora_rtc_engine_example/examples/advanced/music_player/music_player.dart';
+import 'package:agora_rtc_engine_example/examples/advanced/push_encoded_video_frame/push_encoded_video_frame.dart';
+import 'package:agora_rtc_engine_example/examples/advanced/push_video_frame/push_video_frame.dart';
+import 'package:agora_rtc_engine_example/examples/advanced/rtmp_streaming/rtmp_streaming.dart';
+import 'package:agora_rtc_engine_example/examples/advanced/screen_sharing/screen_sharing.dart';
+import 'package:agora_rtc_engine_example/examples/advanced/send_multi_camera_stream/send_multi_camera_stream.dart';
+import 'package:agora_rtc_engine_example/examples/advanced/send_multi_video_stream/send_multi_video_stream.dart';
+import 'package:agora_rtc_engine_example/examples/advanced/set_beauty_effect/set_beauty_effect.dart';
+import 'package:agora_rtc_engine_example/examples/advanced/set_encryption/set_encryption.dart';
+import 'package:agora_rtc_engine_example/examples/advanced/set_video_encoder_configuration/set_video_encoder_configuration.dart';
+import 'package:agora_rtc_engine_example/examples/advanced/spatial_audio_with_media_player/spatial_audio_with_media_player.dart';
+import 'package:agora_rtc_engine_example/examples/advanced/start_direct_cdn_streaming/start_direct_cdn_streaming.dart';
+import 'package:agora_rtc_engine_example/examples/advanced/start_local_video_transcoder/start_local_video_transcoder.dart';
+import 'package:agora_rtc_engine_example/examples/advanced/stream_message/stream_message.dart';
+import 'package:agora_rtc_engine_example/examples/advanced/take_snapshot/take_snapshot.dart';
+import 'package:flutter/foundation.dart';
+
+import 'audio_mixing/audio_mixing.dart';
+import 'audio_spectrum/audio_spectrum.dart';
+import 'channel_media_relay/channel_media_relay.dart';
+import 'device_manager/device_manager.dart';
+import 'enable_virtualbackground/enable_virtualbackground.dart';
+import 'join_multiple_channel/join_multiple_channel.dart';
+import 'media_player/media_player.dart';
+import 'media_recorder/media_recorder.dart';
+import 'precall_test/precall_test.dart';
+import 'process_audio_raw_data/process_audio_raw_data.dart';
+import 'process_video_raw_data/process_video_raw_data.dart';
+import 'send_metadata/send_metadata.dart';
+import 'set_content_inspect/set_content_inspect.dart';
+import 'start_rhythm_player/start_rhythm_player.dart';
+import 'voice_changer/voice_changer.dart';
+
+/// Data source for advanced examples
+final advanced = [
+  {'name': 'Advanced'},
+  {'name': 'AudioMixing', 'widget': const AudioMixing()},
+  {'name': 'ChannelMediaRelay', 'widget': const ChannelMediaRelay()},
+  if (kIsWeb || !(Platform.isAndroid || Platform.isIOS))
+    {'name': 'DeviceManager', 'widget': const DeviceManager()},
+  {'name': 'JoinMultipleChannel', 'widget': const JoinMultipleChannel()},
+  {'name': 'RtmpStreaming', 'widget': const RtmpStreaming()},
+  if (!kIsWeb) {'name': 'ScreenSharing', 'widget': const ScreenSharing()},
+  {'name': 'SetEncryption', 'widget': SetEncryption()},
+  {
+    'name': 'SetVideoEncoderConfiguration',
+    'widget': const SetVideoEncoderConfiguration()
+  },
+  {'name': 'StreamMessage', 'widget': const StreamMessage()},
+  {'name': 'VoiceChanger', 'widget': const VoiceChanger()},
+  {
+    'name': 'EnableVirtualBackground',
+    'widget': const EnableVirtualBackground()
+  },
+  {'name': 'MediaPlayer', 'widget': const MediaPlayer()},
+  {'name': 'SendMultiVideoStream', 'widget': const SendMultiVideoStream()},
+  {'name': 'TakeSnapshot', 'widget': const TakeSnapshot()},
+  {
+    'name': 'StartDirectCDNStreaming',
+    'widget': const StartDirectCDNStreaming()
+  },
+  {'name': 'SendMetadata', 'widget': const SendMetadata()},
+  {'name': 'SetBeautyEffect', 'widget': const SetBeautyEffect()},
+  {'name': 'SetContentInspect', 'widget': const SetContentInspect()},
+  if (kIsWeb || !(Platform.isAndroid || Platform.isIOS))
+    {'name': 'SendMultiCameraStream', 'widget': const SendMultiCameraStream()},
+  {'name': 'StartRhythmPlayer', 'widget': const StartRhythmPlayer()},
+  {
+    'name': 'StartLocalVideoTranscoder',
+    'widget': const StartLocalVideoTranscoder()
+  },
+  {'name': 'ProcessVideoRawData', 'widget': const ProcessVideoRawData()},
+  {'name': 'ProcessAudioRawData', 'widget': const ProcessAudioRawData()},
+  {'name': 'AudioSpectrum', 'widget': const AudioSpectrum()},
+  {'name': 'MediaRecorder', 'widget': const MediaRecorderExample()},
+  {'name': 'PushVideoFrame', 'widget': const PushVideoFrame()},
+  // {'name': 'PushAudioFrame', 'widget': const PushAudioFrame()},
+  {'name': 'PushEncodedVideoFrame', 'widget': const PushEncodedVideoFrame()},
+  {
+    'name': 'SpatialAudioWithMediaPlayer',
+    'widget': const SpatialAudioWithMediaPlayer()
+  },
+  if (kIsWeb || !(Platform.isAndroid || Platform.isIOS))
+    {'name': 'PreCallTest', 'widget': const PreCallTest()},
+  if (Platform.isAndroid || Platform.isIOS)
+    {'name': 'MusicPlayer', 'widget': const MusicPlayerExample()},
+];

+ 375 - 0
lib/examples/advanced/join_multiple_channel/join_multiple_channel.dart

@@ -0,0 +1,375 @@
+import 'dart:io';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine/agora_rtc_engine_debug.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:path_provider/path_provider.dart';
+
+const _channelId0 = 'channel0';
+const _channelId1 = 'channel1';
+
+/// JoinMultipleChannel Example
+class JoinMultipleChannel extends StatefulWidget {
+  /// Construct the [JoinMultipleChannel]
+  const JoinMultipleChannel({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<JoinMultipleChannel> {
+  late final RtcEngineEx _engine;
+  bool _isReadyPreview = false;
+  late RtcConnection _channel0, _channel1;
+  String? renderChannelId;
+  bool isJoined0 = false, isJoined1 = false;
+  List<int> remoteUid0 = [], remoteUid1 = [];
+  late final TextEditingController _channel0UidController;
+  late final TextEditingController _channel1UidController;
+  bool _startDumpVideo = false;
+
+  @override
+  void initState() {
+    super.initState();
+    _channel0UidController = TextEditingController(text: '1000');
+    _channel1UidController = TextEditingController(text: '1001');
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _engine.release();
+  }
+
+  _initEngine() async {
+    _engine = createAgoraRtcEngineEx();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        if (connection.channelId == _channelId0) {
+          setState(() {
+            isJoined0 = true;
+          });
+        } else if (connection.channelId == _channelId1) {
+          setState(() {
+            isJoined1 = true;
+          });
+        }
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        if (connection.channelId == _channelId0) {
+          setState(() {
+            remoteUid0.add(rUid);
+          });
+        } else if (connection.channelId == _channelId1) {
+          setState(() {
+            remoteUid1.add(rUid);
+          });
+        }
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        if (connection.channelId == _channelId0) {
+          setState(() {
+            remoteUid0.remove(rUid);
+          });
+        } else if (connection.channelId == _channelId1) {
+          setState(() {
+            remoteUid1.remove(rUid);
+          });
+        }
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        if (connection.channelId == _channelId0) {
+          setState(() {
+            isJoined0 = false;
+            remoteUid0.clear();
+          });
+        } else if (connection.channelId == _channelId1) {
+          setState(() {
+            isJoined1 = false;
+            remoteUid1.clear();
+          });
+        }
+      },
+    ));
+
+    await _engine.enableVideo();
+    await _engine.startPreview();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  void _joinChannel0() async {
+    final uid = int.tryParse(_channel0UidController.text);
+    if (uid == null) return;
+    _channel0 = RtcConnection(channelId: _channelId0, localUid: uid);
+    await _engine.joinChannelEx(
+        token: '',
+        connection: _channel0,
+        options: const ChannelMediaOptions(
+            clientRoleType: ClientRoleType.clientRoleBroadcaster));
+  }
+
+  void _joinChannel1() async {
+    final uid = int.tryParse(_channel1UidController.text);
+    if (uid == null) return;
+    _channel1 = RtcConnection(channelId: _channelId1, localUid: uid);
+    await _engine.joinChannelEx(
+        token: '',
+        connection: _channel1,
+        options: const ChannelMediaOptions(
+            clientRoleType: ClientRoleType.clientRoleBroadcaster));
+  }
+
+  _publishChannel0() async {
+    if (isJoined1) {
+      await _engine.updateChannelMediaOptionsEx(
+          options: const ChannelMediaOptions(
+              publishMicrophoneTrack: false, publishCameraTrack: false),
+          connection: _channel1);
+    }
+
+    if (isJoined0) {
+      await _engine.updateChannelMediaOptionsEx(
+          options: const ChannelMediaOptions(
+              publishMicrophoneTrack: true, publishCameraTrack: true),
+          connection: _channel0);
+    }
+  }
+
+  _publishChannel1() async {
+    if (isJoined0) {
+      await _engine.updateChannelMediaOptionsEx(
+          options: const ChannelMediaOptions(
+              publishMicrophoneTrack: false, publishCameraTrack: false),
+          connection: _channel0);
+    }
+
+    if (isJoined1) {
+      await _engine.updateChannelMediaOptionsEx(
+          options: const ChannelMediaOptions(
+              publishMicrophoneTrack: true, publishCameraTrack: true),
+          connection: _channel1);
+    }
+  }
+
+  _leaveChannel0() async {
+    if (isJoined0) {
+      await _engine.leaveChannelEx(connection: _channel0);
+      await _engine.startPreview();
+    }
+  }
+
+  _leaveChannel1() async {
+    if (isJoined1) {
+      await _engine.leaveChannelEx(connection: _channel1);
+      await _engine.startPreview();
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        late RtcConnection connection;
+        List<int> remoteUid = [];
+        if (renderChannelId == _channelId0) {
+          remoteUid = remoteUid0;
+          connection = _channel0;
+        } else if (renderChannelId == _channelId1) {
+          remoteUid = remoteUid1;
+          connection = _channel1;
+        }
+
+        return Stack(
+          children: [
+            AgoraVideoView(
+              controller: VideoViewController(
+                rtcEngine: _engine,
+                canvas: const VideoCanvas(uid: 0),
+              ),
+            ),
+            if (remoteUid.isNotEmpty)
+              Align(
+                alignment: Alignment.topLeft,
+                child: Wrap(
+                  children: remoteUid
+                      .map(
+                        (e) => SizedBox(
+                            width: 120,
+                            height: 120,
+                            child: AgoraVideoView(
+                              controller: VideoViewController.remote(
+                                rtcEngine: _engine,
+                                canvas: VideoCanvas(uid: e),
+                                connection: connection,
+                              ),
+                            )),
+                      )
+                      .toList(),
+                ),
+              )
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisAlignment: MainAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            const SizedBox(
+              height: 20,
+            ),
+            TextField(
+              controller: _channel0UidController,
+              decoration: const InputDecoration(
+                hintText: 'Enter channel0 uid',
+              ),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: () {
+                      if (isJoined0) {
+                        _leaveChannel0();
+                      } else {
+                        _joinChannel0();
+                      }
+                    },
+                    child: Text('${isJoined0 ? 'Leave' : 'Join'} $_channelId0'),
+                  ),
+                )
+              ],
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            TextField(
+              controller: _channel1UidController,
+              decoration: const InputDecoration(
+                hintText: 'Enter channel1 uid',
+              ),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: () {
+                      if (isJoined1) {
+                        _leaveChannel1();
+                      } else {
+                        _joinChannel1();
+                      }
+                    },
+                    child: Text('${isJoined1 ? 'Leave' : 'Join'} $_channelId1'),
+                  ),
+                )
+              ],
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: isJoined0 ? _publishChannel0 : null,
+              child: const Text('Publish $_channelId0'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: !isJoined0
+                  ? null
+                  : () {
+                      setState(() {
+                        renderChannelId = _channelId0;
+                      });
+                    },
+              child: const Text('Render $_channelId0'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: isJoined1 ? _publishChannel1 : null,
+              child: const Text('Publish $_channelId1'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: !isJoined1
+                  ? null
+                  : () {
+                      setState(() {
+                        renderChannelId = _channelId1;
+                      });
+                    },
+              child: const Text('Render $_channelId1'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            if (defaultTargetPlatform == TargetPlatform.windows)
+              ElevatedButton(
+                onPressed: () async {
+                  _startDumpVideo = !_startDumpVideo;
+
+                  Directory appDocDir =
+                      await getApplicationDocumentsDirectory();
+
+                  if (_startDumpVideo) {
+                    _engine.startDumpVideo(
+                      VideoSourceType.videoSourceCamera.value(),
+                      appDocDir.absolute.path,
+                    );
+                    logSink.log(
+                        'Video data has dump to ${appDocDir.absolute.path}');
+                  } else {
+                    _engine.stopDumpVideo();
+                  }
+
+                  setState(() {});
+                },
+                child: Text('${_startDumpVideo ? 'Stop' : 'Start'} dump video'),
+              ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 395 - 0
lib/examples/advanced/media_player/media_player.dart

@@ -0,0 +1,395 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+
+/// MediaPlayer Example
+class MediaPlayer extends StatefulWidget {
+  const MediaPlayer({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<MediaPlayer> {
+  late final RtcEngineEx _engine;
+  bool _isReadyPreview = false;
+
+  late MediaPlayerController _mediaPlayerController;
+
+  late final TextEditingController _textEditingController;
+
+  late TextEditingController _channelIdController;
+  late TextEditingController _loopCountController;
+  late TextEditingController _streamInfoIndexController;
+  bool _isMuted = false;
+
+  bool _isUrlOpened = false;
+  bool _isPlaying = false;
+  bool _isPause = true;
+
+  int _seekPos = 0;
+  int _duration = 0;
+  int _playoutVolume = 100;
+  int _streamCount = 0;
+
+  bool isJoined = false;
+
+  @override
+  void initState() {
+    super.initState();
+    _channelIdController = TextEditingController(text: config.channelId);
+    _textEditingController = TextEditingController(
+        text:
+            'https://agoracdn.s3.us-west-1.amazonaws.com/videos/Agora.io-Interactions.mp4');
+    _loopCountController = TextEditingController(text: '1');
+    _streamInfoIndexController = TextEditingController(text: '1');
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _channelIdController.dispose();
+    _textEditingController.dispose();
+    _loopCountController.dispose();
+
+    _dispose();
+  }
+
+  void _dispose() async {
+    await _mediaPlayerController.dispose();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngineEx();
+    _mediaPlayerController = MediaPlayerController(
+        rtcEngine: _engine, canvas: const VideoCanvas(uid: 0));
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+    ));
+
+    _mediaPlayerController = MediaPlayerController(
+        rtcEngine: _engine, canvas: const VideoCanvas(uid: 0));
+    await _mediaPlayerController.initialize();
+    _mediaPlayerController.registerPlayerSourceObserver(
+      MediaPlayerSourceObserver(
+        onCompleted: () {
+          logSink.log('[onCompleted]');
+        },
+        onPlayerSourceStateChanged:
+            (MediaPlayerState state, MediaPlayerError ec) async {
+          logSink.log('[onPlayerSourceStateChanged] state: $state ec: $ec');
+          if (state == MediaPlayerState.playerStateOpenCompleted) {
+            _streamCount = await _mediaPlayerController.getStreamCount();
+            _duration = await _mediaPlayerController.getDuration();
+            _isUrlOpened = true;
+            _isPlaying = false;
+            // _isStop = false;
+            _isPause = false;
+          } else if (state == MediaPlayerState.playerStateStopped) {
+            _isUrlOpened = false;
+            _isPlaying = false;
+            // _isStop = true;
+            _isPause = false;
+            _seekPos = 0;
+            _isMuted = false;
+          } else if (state == MediaPlayerState.playerStatePlaying) {
+            _isPlaying = true;
+            _isPause = false;
+          } else if (state == MediaPlayerState.playerStatePaused) {
+            _isPause = true;
+          }
+
+          setState(() {});
+        },
+        onPositionChanged: (int position) {
+          logSink.log('[onPositionChanged] position: $position');
+
+          setState(() {
+            _seekPos = position;
+          });
+        },
+        onPlayerEvent:
+            (MediaPlayerEvent eventCode, int elapsedTime, String message) {
+          logSink.log(
+              '[onPlayerEvent] eventCode: $eventCode, elapsedTime: $elapsedTime, message: $message');
+        },
+      ),
+    );
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  void _leaveChannel() async {
+    if (_isUrlOpened) {
+      await _mediaPlayerController.stop();
+    }
+    await _engine.leaveChannel();
+  }
+
+  void _joinChannel() async {
+    await _engine.joinChannelEx(
+      token: '',
+      connection: RtcConnection(
+        channelId: _channelIdController.text,
+        localUid: 456,
+      ),
+      options: ChannelMediaOptions(
+        clientRoleType: ClientRoleType.clientRoleBroadcaster,
+        publishMediaPlayerAudioTrack: true,
+        publishMediaPlayerVideoTrack: true,
+        publishMediaPlayerId: _mediaPlayerController.getMediaPlayerId(),
+      ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        if (_isUrlOpened) {
+          return AgoraVideoView(
+            controller: _mediaPlayerController,
+          );
+        }
+
+        return const Center(
+          child: Text('MediaPlayer'),
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            Row(children: [
+              Expanded(
+                child: TextField(
+                  controller: _textEditingController,
+                  decoration: const InputDecoration(
+                    hintText: 'Media URL',
+                  ),
+                ),
+              ),
+              ElevatedButton(
+                onPressed: !_isUrlOpened
+                    ? () async {
+                        await _mediaPlayerController.open(
+                            url: _textEditingController.text, startPos: 0);
+                      }
+                    : null,
+                child: const Text('Open'),
+              ),
+              const SizedBox(
+                height: 20,
+              ),
+            ]),
+            Row(
+              children: [
+                Expanded(
+                  child: TextField(
+                    controller: _channelIdController,
+                    decoration: const InputDecoration(hintText: 'Channel ID'),
+                  ),
+                ),
+                ElevatedButton(
+                  onPressed: _isPlaying
+                      ? (isJoined ? _leaveChannel : _joinChannel)
+                      : null,
+                  child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                ),
+              ],
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            if (_isUrlOpened)
+              Column(
+                mainAxisAlignment: MainAxisAlignment.start,
+                crossAxisAlignment: CrossAxisAlignment.start,
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  Row(
+                    children: [
+                      Expanded(
+                        child: Slider(
+                            value: _seekPos.toDouble(),
+                            min: 0,
+                            max: _duration.toDouble(),
+                            divisions: 100,
+                            label: '${(_seekPos / 1000.round())} s',
+                            onChanged: (double value) {
+                              _seekPos = value.toInt();
+                              _mediaPlayerController.seek(_seekPos);
+                              setState(() {});
+                            }),
+                      ),
+                      Text(
+                        '${(_seekPos / 1000).round()}/${(_duration / 1000).round()}s',
+                        style: const TextStyle(fontSize: 10),
+                      ),
+                    ],
+                  ),
+                  const SizedBox(
+                    height: 20,
+                  ),
+                  ElevatedButton(
+                    onPressed: () {
+                      if (!_isPlaying) {
+                        _mediaPlayerController.play();
+                      } else {
+                        _mediaPlayerController.stop();
+                      }
+                    },
+                    child: Text(!_isPlaying ? 'Play' : 'Stop'),
+                  ),
+                  const SizedBox(
+                    height: 20,
+                  ),
+                  ElevatedButton(
+                    onPressed: _isPlaying
+                        ? () {
+                            if (!_isPause) {
+                              _mediaPlayerController.pause();
+                            } else {
+                              _mediaPlayerController.resume();
+                            }
+                          }
+                        : null,
+                    child: Text(!_isPause ? 'Pause' : 'Resume'),
+                  ),
+                  const SizedBox(
+                    height: 20,
+                  ),
+                  Row(
+                    children: [
+                      Expanded(
+                        child: TextField(
+                          controller: _loopCountController,
+                          decoration:
+                              const InputDecoration(hintText: 'Loop count'),
+                          keyboardType: TextInputType.number,
+                        ),
+                      ),
+                      ElevatedButton(
+                        onPressed: () async {
+                          final loopCount =
+                              int.tryParse(_loopCountController.text) ?? -1;
+                          if (loopCount != -1) {
+                            await _mediaPlayerController
+                                .setLoopCount(loopCount);
+                          }
+                        },
+                        child: const Text('Update loop count'),
+                      ),
+                    ],
+                  ),
+                  const SizedBox(
+                    height: 20,
+                  ),
+                  Row(children: [
+                    const Text('mute: '),
+                    Switch(
+                      value: _isMuted,
+                      onChanged: (changed) async {
+                        _isMuted = changed;
+                        await _mediaPlayerController.mute(_isMuted);
+                        setState(() {});
+                      },
+                    )
+                  ]),
+                  const SizedBox(
+                    height: 20,
+                  ),
+                  Row(
+                    children: [
+                      const Text(
+                        'Playout volume',
+                        style: TextStyle(fontSize: 10),
+                      ),
+                      Expanded(
+                        child: Slider(
+                            value: _playoutVolume.toDouble(),
+                            min: 0,
+                            max: 100,
+                            divisions: 100,
+                            label: '${_playoutVolume.round()}',
+                            onChanged: (double value) async {
+                              _playoutVolume = value.toInt();
+                              _mediaPlayerController
+                                  .adjustPlayoutVolume(_playoutVolume);
+
+                              setState(() {});
+                            }),
+                      ),
+                    ],
+                  ),
+                  const SizedBox(
+                    height: 20,
+                  ),
+                  Text('Total stream count: $_streamCount'),
+                  Row(
+                    children: [
+                      Expanded(
+                        child: TextField(
+                          controller: _streamInfoIndexController,
+                          decoration:
+                              const InputDecoration(hintText: 'Loop count'),
+                          keyboardType: TextInputType.number,
+                        ),
+                      ),
+                      ElevatedButton(
+                        onPressed: _streamCount < 1
+                            ? null
+                            : () async {
+                                final streamIndex = int.tryParse(
+                                        _streamInfoIndexController.text) ??
+                                    -1;
+                                if (streamIndex >= 0 &&
+                                    streamIndex < _streamCount) {
+                                  final info = await _mediaPlayerController
+                                      .getStreamInfo(streamIndex);
+                                  logSink.log(
+                                      '[getStreamInfo] index: $streamIndex, info: ${info.toJson()}');
+                                }
+                              },
+                        child: const Text('Get stream info'),
+                      ),
+                    ],
+                  ),
+                ],
+              ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 243 - 0
lib/examples/advanced/media_recorder/media_recorder.dart

@@ -0,0 +1,243 @@
+import 'dart:io';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/foundation.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';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+
+/// MediaRecorder Example
+class MediaRecorderExample extends StatefulWidget {
+  /// @nodoc
+  const MediaRecorderExample({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<MediaRecorderExample> {
+  late final RtcEngine _engine;
+
+  bool isJoined = false, switchCamera = true, switchRender = true;
+  List<int> remoteUid = [];
+  late TextEditingController _controller;
+  bool _isStartedMediaRecording = false;
+  String _recordingFileStoragePath = '';
+  bool _isReadyPreview = false;
+  MediaRecorder? _mediaRecorder;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: config.channelId);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    if (_mediaRecorder != null) {
+      await _engine.destroyMediaRecorder(_mediaRecorder!);
+    }
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUid.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUid.removeWhere((element) => element == rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+          remoteUid.clear();
+        });
+      },
+    ));
+
+    await _engine.enableVideo();
+
+    await _engine.startPreview();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _joinChannel() async {
+    if (defaultTargetPlatform == TargetPlatform.android) {
+      await [Permission.microphone, Permission.camera].request();
+    }
+
+    await _engine.joinChannel(
+      token: config.token,
+      channelId: _controller.text,
+      uid: config.uid,
+      options: const ChannelMediaOptions(
+        channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+        clientRoleType: ClientRoleType.clientRoleBroadcaster,
+      ),
+    );
+  }
+
+  Future<void> _startMediaRecording() async {
+    _mediaRecorder ??= await _engine.createMediaRecorder(
+        RecorderStreamInfo(channelId: _controller.text, uid: 0));
+
+    await _mediaRecorder?.setMediaRecorderObserver(MediaRecorderObserver(
+      onRecorderStateChanged: (String channelId, int uid, RecorderState state,
+          RecorderErrorCode error) {
+        logSink.log(
+            'onRecorderStateChanged channelId: $channelId, uid: $uid state: $state, error: $error');
+      },
+      onRecorderInfoUpdated: (String channelId, int uid, RecorderInfo info) {
+        logSink.log(
+            'onRecorderInfoUpdated channelId: $channelId, uid: $uid, info: ${info.toJson()}');
+      },
+    ));
+
+    Directory appDocDir = Platform.isAndroid
+        ? (await getExternalStorageDirectory())!
+        : await getApplicationDocumentsDirectory();
+    String p = path.join(appDocDir.path, 'example.mp4');
+    await _mediaRecorder
+        ?.startRecording(MediaRecorderConfiguration(storagePath: p));
+    setState(() {
+      _recordingFileStoragePath = 'Recording file storage path: $p';
+      _isStartedMediaRecording = true;
+    });
+  }
+
+  Future<void> _stopMediaRecording() async {
+    await _mediaRecorder?.stopRecording();
+    setState(() {
+      _recordingFileStoragePath = '';
+      _isStartedMediaRecording = false;
+    });
+  }
+
+  Future<void> _leaveChannel() async {
+    if (_isStartedMediaRecording) {
+      await _stopMediaRecording();
+    }
+
+    await _engine.leaveChannel();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return Stack(
+          children: [
+            AgoraVideoView(
+              controller: VideoViewController(
+                rtcEngine: _engine,
+                canvas: const VideoCanvas(uid: 0),
+              ),
+            ),
+            Align(
+              alignment: Alignment.topLeft,
+              child: SingleChildScrollView(
+                scrollDirection: Axis.horizontal,
+                child: Row(
+                  children: List.of(remoteUid.map(
+                    (e) => SizedBox(
+                      width: 120,
+                      height: 120,
+                      child: AgoraVideoView(
+                        controller: VideoViewController.remote(
+                          rtcEngine: _engine,
+                          canvas: VideoCanvas(uid: e),
+                          connection:
+                              RtcConnection(channelId: _controller.text),
+                        ),
+                      ),
+                    ),
+                  )),
+                ),
+              ),
+            )
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            Column(
+              mainAxisAlignment: MainAxisAlignment.start,
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                ElevatedButton(
+                  onPressed: isJoined
+                      ? _isStartedMediaRecording
+                          ? _stopMediaRecording
+                          : _startMediaRecording
+                      : null,
+                  child: Text(
+                      '${_isStartedMediaRecording ? 'Stop' : 'Start'} media recording'),
+                ),
+                Text(_recordingFileStoragePath),
+              ],
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 573 - 0
lib/examples/advanced/music_player/music_player.dart

@@ -0,0 +1,573 @@
+import 'dart:async';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+
+class MusicPlayerExample extends StatefulWidget {
+  const MusicPlayerExample({Key? key}) : super(key: key);
+
+  @override
+  State<MusicPlayerExample> createState() => _MusicPlayerExampleState();
+}
+
+class _MusicPlayerExampleState extends State<MusicPlayerExample> {
+  bool isJoined = false;
+
+  late final RtcEngine _engine;
+  late TextEditingController _controller;
+  late TextEditingController _rtmTokenController;
+  late final TextEditingController _searchMusicController;
+  late final MusicContentCenter _musicContentCenter;
+  late final MusicPlayer _musicPlayer;
+  late final MediaPlayerSourceObserver _mediaPlayerSourceObserver;
+  Completer<void>? _preloadCompleted;
+  Completer<String>? _getLyricCompleted;
+  bool _initRtmToken = false;
+  bool _isPlaying = false;
+
+  List<MusicChartInfo> _musicChartInfos = [];
+  MusicCollection? _musicCollection;
+  String _currentRequestId = '';
+  late Music _selectedMusic;
+  MusicCollection? _searchedMusicCollection;
+  String _searchMusicRequestId = '';
+  String _musicCollectionRequestId = '';
+  String _getLyricRequestId = '';
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: config.channelId);
+    _rtmTokenController = TextEditingController();
+    _searchMusicController = TextEditingController();
+
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    _musicContentCenter.unregisterEventHandler();
+    if (_isPlaying) {
+      await _musicPlayer.stop();
+      await _engine.destroyMediaPlayer(_musicPlayer);
+      await _musicContentCenter.release();
+    }
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+        appId: config.appId,
+        channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+        audioScenario: AudioScenarioType.audioScenarioGameStreaming));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+    ));
+
+    _musicContentCenter = _engine.getMusicContentCenter();
+
+    await _engine.enableVideo();
+  }
+
+  Future<void> _joinChannel() async {
+    await _engine.joinChannel(
+      token: config.token,
+      channelId: _controller.text,
+      uid: config.uid,
+      options: ChannelMediaOptions(
+        clientRoleType: ClientRoleType.clientRoleBroadcaster,
+        autoSubscribeAudio: true,
+        autoSubscribeVideo: true,
+        publishMicrophoneTrack: true,
+        publishCameraTrack: false,
+        publishMediaPlayerAudioTrack: true,
+        publishMediaPlayerVideoTrack: true,
+        enableAudioRecordingOrPlayout: true,
+        publishMediaPlayerId: _musicPlayer.getMediaPlayerId(),
+      ),
+    );
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  Widget _getChartInfosWidget() {
+    if (_musicChartInfos.isEmpty) return Container();
+
+    final listChildren = _musicChartInfos.map(((e) {
+      return GestureDetector(
+        behavior: HitTestBehavior.opaque,
+        onTap: () async {
+          _musicCollectionRequestId =
+              await _musicContentCenter.getMusicCollectionByMusicChartId(
+                  musicChartId: e.id!, page: 1, pageSize: 10);
+        },
+        child: Container(
+          height: 100,
+          alignment: Alignment.center,
+          margin: const EdgeInsets.all(4),
+          padding: const EdgeInsets.all(8),
+          decoration: BoxDecoration(
+              color: Colors.blue[100]!,
+              borderRadius: const BorderRadius.all(Radius.circular(4))),
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              Text('chartName: ${e.chartName!}'),
+              Text('id: ${e.id.toString()}')
+            ],
+          ),
+        ),
+      );
+    })).toList();
+
+    return SizedBox(
+      height: 100,
+      child: ListView(
+        shrinkWrap: true,
+        scrollDirection: Axis.horizontal,
+        children: listChildren,
+      ),
+    );
+  }
+
+  Widget _getMusicCollectionWidget() {
+    final collection = <Music>[];
+    if ((_searchedMusicCollection?.getCount() ?? 0) > 0) {
+      for (int i = 0; i < _searchedMusicCollection!.getCount(); i++) {
+        collection.add(_searchedMusicCollection!.getMusic(i));
+      }
+    } else if ((_musicCollection?.getCount() ?? 0) > 0) {
+      for (int i = 0; i < _musicCollection!.getCount(); i++) {
+        collection.add(_musicCollection!.getMusic(i));
+      }
+    }
+
+    if (collection.isEmpty) {
+      return Container();
+    }
+
+    final listChildren = collection.map(((e) {
+      return Container(
+        height: 100,
+        alignment: Alignment.center,
+        margin: const EdgeInsets.all(4),
+        padding: const EdgeInsets.all(8),
+        decoration: BoxDecoration(
+            color: Colors.pink[100],
+            borderRadius: const BorderRadius.all(Radius.circular(4))),
+        child: GestureDetector(
+          behavior: HitTestBehavior.opaque,
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              Text('songCode: ${e.songCode!}'),
+              Text('name: ${e.name}'),
+              Text('singer: ${e.singer}')
+            ],
+          ),
+          onTap: () async {
+            _selectedMusic = e;
+
+            bool isPreloaded =
+                await _musicContentCenter.isPreloaded(_selectedMusic.songCode!);
+            if (!isPreloaded) {
+              _preloadCompleted = Completer();
+              _getLyricCompleted = Completer();
+              await _musicContentCenter.preload(
+                  songCode: _selectedMusic.songCode!);
+              _getLyricRequestId = await _musicContentCenter.getLyric(
+                  songCode: _selectedMusic.songCode!);
+            } else {
+              _preloadCompleted = null;
+              _getLyricCompleted = null;
+            }
+
+            await showDialog(
+                context: context,
+                barrierDismissible: false,
+                builder: (context) {
+                  return Dialog(
+                    child: SizedBox(
+                      width: 200,
+                      // height: 300,
+                      child: Stack(
+                        children: [
+                          MusicPlayerItem(
+                            music: e,
+                            onPlayChanged: (v) async {
+                              if (v) {
+                                await _musicPlayer.openWithSongCode(
+                                    songCode: e.songCode!);
+                              } else {
+                                await _musicPlayer.stop();
+                              }
+                            },
+                            playing: _isPlaying,
+                            preloadCompleted: _preloadCompleted,
+                            getLyricCompleted: _getLyricCompleted,
+                          ),
+                          Align(
+                            alignment: Alignment.topRight,
+                            child: IconButton(
+                                onPressed: () async {
+                                  await _musicPlayer.stop();
+                                  _isPlaying = false;
+                                  _preloadCompleted = null;
+                                  _getLyricCompleted = null;
+                                  Navigator.pop(context);
+                                  setState(() {});
+                                },
+                                icon: const Icon(Icons.close_rounded)),
+                          ),
+                        ],
+                      ),
+                    ),
+                  );
+                });
+
+            // _preloadCompleted = null;
+          },
+        ),
+      );
+    })).toList();
+
+    return SizedBox(
+      height: 100,
+      child: ListView(
+        shrinkWrap: true,
+        scrollDirection: Axis.horizontal,
+        children: listChildren,
+      ),
+    );
+  }
+
+  Future<void> _initMusicCenter() async {
+    await _musicContentCenter.initialize(MusicContentCenterConfiguration(
+      appId: config.musicCenterAppId,
+      token: _rtmTokenController.text,
+      mccUid: 123,
+    ));
+
+    _musicContentCenter.registerEventHandler(MusicContentCenterEventHandler(
+      onMusicChartsResult: (String requestId, List<MusicChartInfo> result,
+          MusicContentCenterStatusCode errorCode) {
+        logSink.log(
+            '[onMusicChartsResult], requestId: $requestId, errorCode: $errorCode, result: ${result.toString()}');
+        if (errorCode ==
+            MusicContentCenterStatusCode.kMusicContentCenterStatusOk) {
+          if (_currentRequestId == requestId) {
+            setState(() {
+              _musicChartInfos = result;
+            });
+          }
+        }
+      },
+      onMusicCollectionResult: (String requestId, MusicCollection result,
+          MusicContentCenterStatusCode errorCode) {
+        logSink.log(
+            '[onMusicCollectionResult], requestId: $requestId, errorCode: $errorCode, result: ${result.toString()}');
+
+        if (_musicCollectionRequestId == requestId) {
+          setState(() {
+            _musicCollection = result;
+          });
+        } else if (_searchMusicRequestId == requestId) {
+          setState(() {
+            _searchedMusicCollection = result;
+          });
+        }
+      },
+      onPreLoadEvent: (int songCode, int percent, String lyricUrl,
+          PreloadStatusCode status, MusicContentCenterStatusCode errorCode) {
+        logSink.log(
+            '[onPreLoadEvent], songCode: $songCode, percent: $percent status: $status, errorCode: $errorCode, lyricUrl: $lyricUrl');
+        if (_selectedMusic.songCode == songCode &&
+            status == PreloadStatusCode.kPreloadStatusCompleted) {
+          _preloadCompleted?.complete();
+          _preloadCompleted = null;
+        }
+      },
+      onLyricResult: (String requestId, String lyricUrl,
+          MusicContentCenterStatusCode errorCode) {
+        if (_getLyricRequestId == requestId) {
+          _getLyricCompleted?.complete(lyricUrl);
+          _getLyricCompleted = null;
+        }
+      },
+    ));
+
+    _musicPlayer = (await _musicContentCenter.createMusicPlayer())!;
+
+    _mediaPlayerSourceObserver = MediaPlayerSourceObserver(
+      onPlayerSourceStateChanged:
+          (MediaPlayerState state, MediaPlayerError ec) async {
+        logSink.log('[onPlayerSourceStateChanged] state: $state ec: $ec');
+        if (state == MediaPlayerState.playerStateOpenCompleted) {
+          _isPlaying = !_isPlaying;
+
+          await _musicPlayer.play();
+
+          setState(() {});
+        }
+      },
+    );
+
+    _musicPlayer.registerPlayerSourceObserver(_mediaPlayerSourceObserver);
+
+    setState(() {
+      _initRtmToken = true;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        return SingleChildScrollView(
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            mainAxisAlignment: MainAxisAlignment.start,
+            children: [
+              const SizedBox(
+                height: 16,
+              ),
+              if (_musicChartInfos.isNotEmpty)
+                const Text(
+                  'Chart Infos',
+                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
+                ),
+              _getChartInfosWidget(),
+              const SizedBox(height: 16),
+              if (_musicCollection != null || _searchedMusicCollection != null)
+                const Text(
+                  'Music Collection',
+                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
+                ),
+              _getMusicCollectionWidget(),
+            ],
+          ),
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextField(
+              controller: _rtmTokenController,
+              decoration: const InputDecoration(hintText: 'Rtm token'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: !_initRtmToken
+                        ? () async {
+                            _initMusicCenter();
+                          }
+                        : null,
+                    child: const Text('Init Rtm Token'),
+                  ),
+                )
+              ],
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: _initRtmToken
+                  ? () async {
+                      _searchedMusicCollection = null;
+                      _currentRequestId =
+                          await _musicContentCenter.getMusicCharts();
+                    }
+                  : null,
+              child: const Text('GetMusicCharts'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            TextField(
+              controller: _searchMusicController,
+              decoration:
+                  const InputDecoration(hintText: 'Search music Keyword'),
+            ),
+            ElevatedButton(
+              onPressed: _initRtmToken
+                  ? () async {
+                      _musicChartInfos.clear();
+                      _musicCollection = null;
+                      _searchMusicRequestId =
+                          await _musicContentCenter.searchMusic(
+                              keyWord: _searchMusicController.text,
+                              page: 1,
+                              pageSize: 10);
+                    }
+                  : null,
+              child: const Text('SearchMusic'),
+            ),
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: _initRtmToken
+                        ? (isJoined ? _leaveChannel : _joinChannel)
+                        : null,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+          ],
+        );
+      },
+    );
+  }
+}
+
+class MusicPlayerItem extends StatefulWidget {
+  const MusicPlayerItem({
+    Key? key,
+    required this.music,
+    required this.onPlayChanged,
+    required this.playing,
+    required this.preloadCompleted,
+    required this.getLyricCompleted,
+  }) : super(key: key);
+
+  final Music music;
+
+  final ValueChanged<bool> onPlayChanged;
+
+  final bool playing;
+
+  final Completer<void>? preloadCompleted;
+
+  final Completer<String>? getLyricCompleted;
+
+  @override
+  State<MusicPlayerItem> createState() => _MusicPlayerItemState();
+}
+
+class _MusicPlayerItemState extends State<MusicPlayerItem> {
+  bool _isPlaying = false;
+
+  bool _isLoading = false;
+
+  String _lyricUrl = '';
+
+  @override
+  void initState() {
+    super.initState();
+
+    _isPlaying = widget.playing;
+    _preload();
+  }
+
+  Future<void> _preload() async {
+    if (widget.preloadCompleted == null) {
+      return;
+    }
+
+    setState(() {
+      _isLoading = true;
+    });
+    await widget.preloadCompleted?.future;
+
+    _lyricUrl = await widget.getLyricCompleted?.future ?? '';
+
+    setState(() {
+      _isLoading = false;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    Widget child;
+    if (_isLoading) {
+      child = const CircularProgressIndicator();
+    } else {
+      child = IconButton(
+          onPressed: () async {
+            _isPlaying = !_isPlaying;
+
+            widget.onPlayChanged(_isPlaying);
+
+            setState(() {});
+          },
+          icon: Icon(_isPlaying
+              ? Icons.stop_circle_rounded
+              : Icons.play_arrow_rounded));
+    }
+    return Stack(
+      children: [
+        Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            const SizedBox(
+              height: 20,
+            ),
+            Text('name: ${widget.music.name}'),
+            if ((widget.music.poster ?? '').isNotEmpty)
+              Image.network(
+                widget.music.poster ?? '',
+                fit: BoxFit.cover,
+              ),
+            Text('lyricUrl: $_lyricUrl'),
+            Center(
+              child: child,
+            ),
+          ],
+        )
+      ],
+    );
+  }
+}

+ 425 - 0
lib/examples/advanced/precall_test/precall_test.dart

@@ -0,0 +1,425 @@
+import 'dart:io';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:path/path.dart' as path;
+import 'package:path_provider/path_provider.dart';
+
+/// PreCallTest Example
+class PreCallTest extends StatefulWidget {
+  /// Construct the [PreCallTest]
+  const PreCallTest({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<PreCallTest> {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+  String channelId = config.channelId;
+  bool isJoined = false;
+  List<int> remoteUid = [];
+  // List<VideoDeviceInfo> _vidoDevices = [];
+  List<AudioDeviceInfo> _audioRecordingDevices = [];
+  List<AudioDeviceInfo> _audioPlaybackDevices = [];
+  // late final VideoDeviceManager _videoDeviceManager;
+  late final AudioDeviceManager _audioDeviceManager;
+  late TextEditingController _controller;
+  late String _selectedRecordingDeviceId;
+  bool _isSetRecordingDeviceEnabled = false;
+  late String _selectedPlaybackDeviceId;
+  bool _isSetPlaybackDeviceEnabled = false;
+  bool _isStartEchoTest = false;
+  bool _isStartRecordingDeviceTest = false;
+  bool _isStartPlaybackDeviceTest = false;
+  bool _isStartAudioDeviceLoopbackTest = false;
+  bool _isStartLastmileProbeTest = false;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: channelId);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    _controller.dispose();
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {});
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {});
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+      onAudioVolumeIndication: (RtcConnection connection,
+          List<AudioVolumeInfo> speakers, int speakerNumber, int totalVolume) {
+        logSink.log(
+            '[onAudioVolumeIndication] speakers: ${speakers.toString()}, speakerNumber: $speakerNumber, totalVolume: $totalVolume');
+      },
+      onLastmileProbeResult: (LastmileProbeResult result) {
+        logSink.log('[onLastmileProbeResult] result: ${result.toJson()}');
+      },
+      onLastmileQuality: (QualityType quality) {
+        logSink.log('[onLastmileQuality] quality: $quality');
+      },
+    ));
+
+    await _engine.enableVideo();
+    await _engine.startPreview();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+    await _enumerateVideoDevices();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  void _joinChannel() async {
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: channelId,
+        uid: config.uid,
+        options: const ChannelMediaOptions());
+  }
+
+  _leaveChannel() async {
+    if (_isStartEchoTest) {
+      await _engine.stopEchoTest();
+      _isStartEchoTest = false;
+    }
+    if (_isStartRecordingDeviceTest) {
+      await _audioDeviceManager.stopRecordingDeviceTest();
+      _isStartRecordingDeviceTest = false;
+    }
+    if (_isStartPlaybackDeviceTest) {
+      await _audioDeviceManager.stopPlaybackDeviceTest();
+      _isStartPlaybackDeviceTest = false;
+    }
+    if (_isStartAudioDeviceLoopbackTest) {
+      await _audioDeviceManager.stopAudioDeviceLoopbackTest();
+      _isStartAudioDeviceLoopbackTest = false;
+    }
+    if (_isStartLastmileProbeTest) {
+      await _engine.stopLastmileProbeTest();
+      _isStartLastmileProbeTest = false;
+    }
+    await _engine.leaveChannel();
+  }
+
+  Future<void> _enumerateVideoDevices() async {
+    // _videoDeviceManager = _engine.getVideoDeviceManager();
+    _audioDeviceManager = _engine.getAudioDeviceManager();
+    _selectedRecordingDeviceId = await _audioDeviceManager.getRecordingDevice();
+    debugPrint('_selectedRecordingDeviceId: $_selectedRecordingDeviceId');
+    _selectedPlaybackDeviceId = await _audioDeviceManager.getPlaybackDevice();
+    debugPrint('_selectedPlaybackDeviceId: $_selectedPlaybackDeviceId');
+    // _selectedDeviceId = await _videoDeviceManager.getDevice();
+    // final devices = await _videoDeviceManager.enumerateVideoDevices();
+    _audioRecordingDevices =
+        await _audioDeviceManager.enumerateRecordingDevices();
+    debugPrint('_audioRecordingDevices: $_audioRecordingDevices');
+    _audioPlaybackDevices =
+        await _audioDeviceManager.enumeratePlaybackDevices();
+    debugPrint('_audioPlaybackDevices: $_audioPlaybackDevices');
+    setState(() {
+      // _vidoDevices = devices;
+    });
+  }
+
+  Widget _devicesDropDown(List<AudioDeviceInfo> devices, String selectedId,
+      ValueChanged<String> onChanged) {
+    if (devices.isEmpty) return Container();
+    final dropDownMenus = <DropdownMenuItem<String>>[];
+    for (var v in devices) {
+      dropDownMenus.add(DropdownMenuItem(
+        child: Text(
+          v.deviceName!,
+          style: const TextStyle(fontSize: 10),
+        ),
+        value: v.deviceId,
+      ));
+    }
+    return DropdownButton<String>(
+      items: dropDownMenus,
+      value: selectedId,
+      onChanged: (v) {
+        onChanged(v!);
+      },
+    );
+  }
+
+  Future<void> _setVideoDevice(String deviceId) async {
+    await _audioDeviceManager.setRecordingDevice(deviceId);
+
+    setState(() {
+      _isSetRecordingDeviceEnabled = false;
+    });
+    logSink.log('setRecordingDevice deviceId: $deviceId');
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return AgoraVideoView(
+          controller: VideoViewController(
+            rtcEngine: _engine,
+            canvas: const VideoCanvas(uid: 0),
+          ),
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+              onChanged: (text) {
+                setState(() {
+                  channelId = text;
+                });
+              },
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: isJoined
+                  ? null
+                  : () async {
+                      _isStartEchoTest = !_isStartEchoTest;
+
+                      if (_isStartEchoTest) {
+                        await _engine.startEchoTest(
+                            const EchoTestConfiguration(
+                            intervalInSeconds: 10, channelId: 'test'));
+                      } else {
+                        await _engine.stopEchoTest();
+                      }
+
+                      setState(() {});
+                    },
+              child: Text('${_isStartEchoTest ? 'Stop' : 'Start'} echo test'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            _devicesDropDown(_audioRecordingDevices, _selectedRecordingDeviceId,
+                (v) {
+              setState(() {
+                _isSetRecordingDeviceEnabled = _selectedRecordingDeviceId != v;
+                _selectedRecordingDeviceId = v;
+              });
+            }),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: _isSetRecordingDeviceEnabled
+                  ? () {
+                      _setVideoDevice(_selectedRecordingDeviceId);
+                    }
+                  : null,
+              child: const Text('Set video device'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: isJoined
+                  ? null
+                  : () async {
+                      _isStartRecordingDeviceTest =
+                          !_isStartRecordingDeviceTest;
+
+                      if (_isStartRecordingDeviceTest) {
+                        await _audioDeviceManager
+                            .startRecordingDeviceTest(1000);
+
+                        // _videoDeviceManager.startDeviceTest(1000);
+                      } else {
+                        await _audioDeviceManager.stopRecordingDeviceTest();
+                      }
+
+                      setState(() {});
+                    },
+              child: Text(
+                  '${_isStartRecordingDeviceTest ? 'Stop' : 'Start'} recording device test'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            _devicesDropDown(_audioPlaybackDevices, _selectedPlaybackDeviceId,
+                (v) {
+              setState(() {
+                _isSetPlaybackDeviceEnabled = _selectedPlaybackDeviceId != v;
+                _selectedPlaybackDeviceId = v;
+              });
+            }),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: _isSetPlaybackDeviceEnabled
+                  ? () async {
+                      await _audioDeviceManager
+                          .setPlaybackDevice(_selectedPlaybackDeviceId);
+
+                      // _videoDeviceManager.setDevice(deviceId);
+                      setState(() {
+                        _isSetPlaybackDeviceEnabled = false;
+                      });
+                      logSink.log(
+                          'setPlaybackDevice deviceId: $_selectedPlaybackDeviceId');
+                    }
+                  : null,
+              child: const Text('Set playback device'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: isJoined
+                  ? null
+                  : () async {
+                      _isStartPlaybackDeviceTest = !_isStartPlaybackDeviceTest;
+
+                      if (_isStartPlaybackDeviceTest) {
+                        Directory appDocDir =
+                            await getApplicationDocumentsDirectory();
+                        String p = path.join(
+                            appDocDir.path, 'Agora.io-Interactions.mp3');
+
+                        final file = File(p);
+                        if (!(await file.exists())) {
+                          await file.create();
+                          ByteData data = await rootBundle.load(
+                              "assets/audio_mixing/Agora.io-Interactions.mp3");
+                          List<int> bytes = data.buffer.asUint8List(
+                              data.offsetInBytes, data.lengthInBytes);
+                          await file.writeAsBytes(bytes);
+                        }
+
+                        await _audioDeviceManager.startPlaybackDeviceTest(p);
+                      } else {
+                        await _audioDeviceManager.stopPlaybackDeviceTest();
+                      }
+
+                      setState(() {});
+                    },
+              child: Text(
+                  '${_isStartPlaybackDeviceTest ? 'Stop' : 'Start'} playback device test'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: isJoined
+                  ? null
+                  : () async {
+                      _isStartAudioDeviceLoopbackTest =
+                          !_isStartAudioDeviceLoopbackTest;
+
+                      if (_isStartAudioDeviceLoopbackTest) {
+                        await _audioDeviceManager
+                            .startAudioDeviceLoopbackTest(1000);
+                      } else {
+                        await _audioDeviceManager.stopAudioDeviceLoopbackTest();
+                      }
+
+                      setState(() {});
+                    },
+              child: Text(
+                  '${_isStartAudioDeviceLoopbackTest ? 'Stop' : 'Start'} audio device loopback test'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: isJoined
+                  ? null
+                  : () async {
+                      _isStartLastmileProbeTest = !_isStartLastmileProbeTest;
+
+                      if (_isStartLastmileProbeTest) {
+                        LastmileProbeConfig config = const LastmileProbeConfig(
+                          probeUplink: true,
+                          probeDownlink: true,
+                          expectedUplinkBitrate: 100000,
+                          expectedDownlinkBitrate: 100000,
+                        );
+                        await _engine.startLastmileProbeTest(config);
+                      } else {
+                        await _engine.stopLastmileProbeTest();
+                      }
+
+                      setState(() {});
+                    },
+              child: Text(
+                  '${_isStartLastmileProbeTest ? 'Stop' : 'Start'} lastmile probeTest test'),
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 304 - 0
lib/examples/advanced/process_audio_raw_data/process_audio_raw_data.dart

@@ -0,0 +1,304 @@
+import 'dart:io';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:path/path.dart' as path;
+
+/// ProcessVideoRawData Example
+class ProcessAudioRawData extends StatefulWidget {
+  /// Construct the [ProcessAudioRawData]
+  const ProcessAudioRawData({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<ProcessAudioRawData> {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+
+  bool isJoined = false, switchCamera = true, switchRender = true;
+  Set<int> remoteUid = {};
+  late TextEditingController _controller;
+  bool _isUseFlutterTexture = false;
+  ChannelProfileType _channelProfileType =
+      ChannelProfileType.channelProfileLiveBroadcasting;
+  late File _audioFile;
+  late File _playbackAudioFile;
+  late AudioFrameObserver _audioFrameObserver;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: config.channelId);
+
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    _stopAudioFrameRecord();
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+    ));
+    await _engine.setLogFilter(LogFilterType.logFilterError);
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUid.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUid.removeWhere((element) => element == rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+          remoteUid.clear();
+        });
+      },
+    ));
+
+    await _engine.enableVideo();
+
+    _audioFrameObserver = AudioFrameObserver(
+      onRecordAudioFrame: (channelId, audioFrame) async {
+        debugPrint(
+            '[onRecordAudioFrame] channelId: $channelId, audioFrame: ${audioFrame.toJson()}');
+        if (!isJoined) {
+          return;
+        }
+        if (audioFrame.buffer != null) {
+          await _audioFile.writeAsBytes(audioFrame.buffer!.toList(),
+              mode: FileMode.append, flush: true);
+        }
+      },
+      onPlaybackAudioFrame: (String channelId, AudioFrame audioFrame) async {
+        debugPrint(
+            '[onPlaybackAudioFrame] channelId: $channelId, audioFrame: ${audioFrame.toJson()}');
+        if (!isJoined) {
+          return;
+        }
+        if (audioFrame.buffer != null) {
+          await _playbackAudioFile.writeAsBytes(audioFrame.buffer!.toList(),
+              mode: FileMode.append, flush: true);
+        }
+      },
+    );
+
+    await _engine.setVideoEncoderConfiguration(
+      const VideoEncoderConfiguration(
+        dimensions: VideoDimensions(width: 640, height: 360),
+        frameRate: 15,
+        bitrate: 800,
+      ),
+    );
+
+    await _engine.startPreview();
+
+    await _startAudioFrameRecord();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _startAudioFrameRecord() async {
+    Directory appDocDir = Platform.isAndroid
+        ? (await getExternalStorageDirectory())!
+        : await getApplicationDocumentsDirectory();
+
+    _audioFile = File(path.join(appDocDir.absolute.path, 'record_audio.raw'));
+    if (await _audioFile.exists()) {
+      await _audioFile.delete();
+    }
+    await _audioFile.create();
+    logSink
+        .log('onRecordAudioFrame file output to: ${_audioFile.absolute.path}');
+
+    _playbackAudioFile = File(path.join(
+      appDocDir.absolute.path,
+      'playback_audio.raw',
+    ));
+    if (await _playbackAudioFile.exists()) {
+      await _playbackAudioFile.delete();
+    }
+    await _playbackAudioFile.create();
+    logSink.log(
+        'onPlaybackAudioFrame file output to: ${_playbackAudioFile.absolute.path}');
+
+    _engine.getMediaEngine().registerAudioFrameObserver(_audioFrameObserver);
+    await _engine.setPlaybackAudioFrameParameters(
+        sampleRate: 32000,
+        channel: 1,
+        mode: RawAudioFrameOpModeType.rawAudioFrameOpModeReadOnly,
+        samplesPerCall: 1024);
+    await _engine.setRecordingAudioFrameParameters(
+        sampleRate: 32000,
+        channel: 1,
+        mode: RawAudioFrameOpModeType.rawAudioFrameOpModeReadOnly,
+        samplesPerCall: 1024);
+  }
+
+  void _stopAudioFrameRecord() {
+    _engine.getMediaEngine().unregisterAudioFrameObserver(_audioFrameObserver);
+  }
+
+  Future<void> _joinChannel() async {
+    await _engine.joinChannel(
+      token: config.token,
+      channelId: _controller.text,
+      uid: config.uid,
+      options: ChannelMediaOptions(
+        channelProfile: _channelProfileType,
+        clientRoleType: ClientRoleType.clientRoleBroadcaster,
+      ),
+    );
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  Future<void> _switchCamera() async {
+    await _engine.switchCamera();
+    setState(() {
+      switchCamera = !switchCamera;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return const Center(
+          child: Text('No Preview'),
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        final channelProfileType = [
+          ChannelProfileType.channelProfileLiveBroadcasting,
+          ChannelProfileType.channelProfileCommunication,
+        ];
+        final items = channelProfileType
+            .map((e) => DropdownMenuItem(
+                  child: Text(
+                    e.toString().split('.')[1],
+                  ),
+                  value: e,
+                ))
+            .toList();
+
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            if (!kIsWeb &&
+                (defaultTargetPlatform == TargetPlatform.android ||
+                    defaultTargetPlatform == TargetPlatform.iOS))
+              Row(
+                mainAxisSize: MainAxisSize.min,
+                mainAxisAlignment: MainAxisAlignment.start,
+                children: [
+                  const Text(
+                      'Rendered by SurfaceView \n(default TextureView): '),
+                  Switch(
+                    value: _isUseFlutterTexture,
+                    onChanged: isJoined
+                        ? null
+                        : (changed) {
+                            setState(() {
+                              _isUseFlutterTexture = changed;
+                            });
+                          },
+                  )
+                ],
+              ),
+            const SizedBox(
+              height: 20,
+            ),
+            const Text('Channel Profile: '),
+            DropdownButton<ChannelProfileType>(
+              items: items,
+              value: _channelProfileType,
+              onChanged: isJoined
+                  ? null
+                  : (v) {
+                      setState(() {
+                        _channelProfileType = v!;
+                      });
+                    },
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            if (defaultTargetPlatform == TargetPlatform.android ||
+                defaultTargetPlatform == TargetPlatform.iOS) ...[
+              const SizedBox(
+                height: 20,
+              ),
+              ElevatedButton(
+                onPressed: _switchCamera,
+                child: Text('Camera ${switchCamera ? 'front' : 'rear'}'),
+              ),
+            ],
+          ],
+        );
+      },
+    );
+    // if (!_isInit) return Container();
+  }
+}

+ 262 - 0
lib/examples/advanced/process_video_raw_data/process_video_raw_data.dart

@@ -0,0 +1,262 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:video_raw_data/video_raw_data.dart';
+
+/// ProcessVideoRawData Example
+/// 
+/// Demonstrate how to process video raw data in C++, check `VideoRawDataController` 
+/// implementation in https://github.com/AgoraIO-Extensions/RawDataPluginSample/tree/main/frameworks/flutter/video_raw_data
+class ProcessVideoRawData extends StatefulWidget {
+  /// Construct the [ProcessVideoRawData]
+  const ProcessVideoRawData({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<ProcessVideoRawData> {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+
+  bool isJoined = false, switchCamera = true, switchRender = true;
+  Set<int> remoteUid = {};
+  late TextEditingController _controller;
+  bool _isUseFlutterTexture = false;
+  ChannelProfileType _channelProfileType =
+      ChannelProfileType.channelProfileLiveBroadcasting;
+
+  final VideoRawDataController _videoRawDataController =
+      VideoRawDataController();
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: config.channelId);
+
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    _videoRawDataController.dispose();
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+    ));
+    await _engine.setLogFilter(LogFilterType.logFilterError);
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUid.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUid.removeWhere((element) => element == rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+          remoteUid.clear();
+        });
+      },
+    ));
+
+    await _engine.enableVideo();
+
+    final nativeHandle = await _engine.getNativeHandle();
+
+    _videoRawDataController.initialize(nativeHandle);
+
+    await _engine.startPreview();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _joinChannel() async {
+    await _engine.joinChannel(
+      token: config.token,
+      channelId: _controller.text,
+      uid: config.uid,
+      options: ChannelMediaOptions(
+        channelProfile: _channelProfileType,
+        clientRoleType: ClientRoleType.clientRoleBroadcaster,
+      ),
+    );
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  Future<void> _switchCamera() async {
+    await _engine.switchCamera();
+    setState(() {
+      switchCamera = !switchCamera;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return Stack(
+          children: [
+            AgoraVideoView(
+              controller: VideoViewController(
+                rtcEngine: _engine,
+                canvas: const VideoCanvas(uid: 0),
+                useFlutterTexture: _isUseFlutterTexture,
+              ),
+            ),
+            Align(
+              alignment: Alignment.topLeft,
+              child: SingleChildScrollView(
+                scrollDirection: Axis.horizontal,
+                child: Row(
+                  children: List.of(remoteUid.map(
+                    (e) => SizedBox(
+                      width: 120,
+                      height: 120,
+                      child: AgoraVideoView(
+                        controller: VideoViewController.remote(
+                          rtcEngine: _engine,
+                          canvas: VideoCanvas(uid: e),
+                          connection:
+                              RtcConnection(channelId: _controller.text),
+                          useFlutterTexture: _isUseFlutterTexture,
+                        ),
+                      ),
+                    ),
+                  )),
+                ),
+              ),
+            )
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        final channelProfileType = [
+          ChannelProfileType.channelProfileLiveBroadcasting,
+          ChannelProfileType.channelProfileCommunication,
+        ];
+        final items = channelProfileType
+            .map((e) => DropdownMenuItem(
+                  child: Text(
+                    e.toString().split('.')[1],
+                  ),
+                  value: e,
+                ))
+            .toList();
+
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            if (!kIsWeb &&
+                (defaultTargetPlatform == TargetPlatform.android ||
+                    defaultTargetPlatform == TargetPlatform.iOS))
+              Row(
+                mainAxisSize: MainAxisSize.min,
+                mainAxisAlignment: MainAxisAlignment.start,
+                children: [
+                  const Text(
+                      'Rendered by SurfaceView \n(default TextureView): '),
+                  Switch(
+                    value: _isUseFlutterTexture,
+                    onChanged: isJoined
+                        ? null
+                        : (changed) {
+                            setState(() {
+                              _isUseFlutterTexture = changed;
+                            });
+                          },
+                  )
+                ],
+              ),
+            const SizedBox(
+              height: 20,
+            ),
+            const Text('Channel Profile: '),
+            DropdownButton<ChannelProfileType>(
+              items: items,
+              value: _channelProfileType,
+              onChanged: isJoined
+                  ? null
+                  : (v) {
+                      setState(() {
+                        _channelProfileType = v!;
+                      });
+                    },
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            if (defaultTargetPlatform == TargetPlatform.android ||
+                defaultTargetPlatform == TargetPlatform.iOS) ...[
+              const SizedBox(
+                height: 20,
+              ),
+              ElevatedButton(
+                onPressed: _switchCamera,
+                child: Text('Camera ${switchCamera ? 'front' : 'rear'}'),
+              ),
+            ],
+          ],
+        );
+      },
+    );
+  }
+}

+ 180 - 0
lib/examples/advanced/push_audio_frame/push_audio_frame.dart

@@ -0,0 +1,180 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+
+/// PushAudioFrame Example
+class PushAudioFrame extends StatefulWidget {
+  /// Construct the [PushAudioFrame]
+  const PushAudioFrame({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<PushAudioFrame> {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+
+  bool isJoined = false, switchCamera = true, switchRender = true;
+  Set<int> remoteUid = {};
+  late TextEditingController _controller;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: config.channelId);
+
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+    ));
+    await _engine.setLogFilter(LogFilterType.logFilterError);
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUid.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUid.removeWhere((element) => element == rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+          remoteUid.clear();
+        });
+      },
+    ));
+
+    await _engine
+        .getMediaEngine()
+        .setExternalAudioSource(enabled: true, sampleRate: 48000, channels: 1);
+
+    await _engine.enableAudio();
+    await _engine.setAudioProfile(
+        profile: AudioProfileType.audioProfileMusicHighQuality);
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _joinChannel() async {
+    await _engine.joinChannel(
+      token: config.token,
+      channelId: _controller.text,
+      uid: config.uid,
+      options: const ChannelMediaOptions(
+        channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+        clientRoleType: ClientRoleType.clientRoleBroadcaster,
+      ),
+    );
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  Future<void> _pushAudioFrame() async {
+    // Invalid implementation
+    // ByteData data = await rootBundle.load("assets/Agora.io-Interactions.wav");
+    // Uint8List bytes =
+    //     data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
+
+    // final frame = AudioFrame(
+    //   type: AudioFrameType.frameTypePcm16,
+    //   samplesPerChannel: 480,
+    //   channels: 1,
+    //   samplesPerSec: 48000,
+    //   buffer: bytes,
+    //   bytesPerSample: BytesPerSample.twoBytesPerSample,
+    //   renderTimeMs: DateTime.now().millisecondsSinceEpoch,
+    // );
+
+    // await _engine.getMediaEngine().pushAudioFrame(
+    //     type: MediaSourceType.audioPlayoutSource, frame: frame);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return const Center(
+          child: Text('No Preview'),
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: _pushAudioFrame,
+              child: const Text('Push Audio Frame'),
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 222 - 0
lib/examples/advanced/push_encoded_video_frame/push_encoded_video_frame.dart

@@ -0,0 +1,222 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:permission_handler/permission_handler.dart';
+
+/// PushEncodedVideoFrame Example
+class PushEncodedVideoFrame extends StatefulWidget {
+  /// Construct the [PushEncodedVideoFrame]
+  const PushEncodedVideoFrame({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<PushEncodedVideoFrame> {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+  bool isJoined = false;
+  Set<int> remoteUids = {};
+  late final TextEditingController _channelIdController;
+  final TextEditingController _controller = TextEditingController();
+
+  @override
+  void initState() {
+    super.initState();
+    _channelIdController = TextEditingController(text: config.channelId);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    _dispose();
+    super.dispose();
+  }
+
+  Future<void> _dispose() async {
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+      onUserJoined:
+          (RtcConnection connection, int remoteUid, int elapsed) async {
+        await _engine.setRemoteVideoSubscriptionOptions(
+            uid: remoteUid,
+            options: const VideoSubscriptionOptions(encodedFrameOnly: true));
+      },
+    ));
+
+    await _engine.getMediaEngine().setExternalVideoSource(
+        enabled: true,
+        useTexture: false,
+        sourceType: ExternalVideoSourceType.encodedVideoFrame,
+        encodedVideoOption:
+            const SenderOptions(codecType: VideoCodecType.videoCodecGeneric));
+
+    // enable video module and set up video encoding configs
+    await _engine.enableVideo();
+
+    _engine
+        .getMediaEngine()
+        .registerVideoEncodedFrameObserver(VideoEncodedFrameObserver(
+      onEncodedVideoFrameReceived:
+          (uid, imageBuffer, length, videoEncodedFrameInfo) {
+        debugPrint(
+            '[onEncodedVideoFrameReceived] uid: $uid imageBuffer: $imageBuffer, length: $length, videoEncodedFrameInfo: ${videoEncodedFrameInfo.toJson()}');
+        try {
+          _showMyDialog(uid, utf8.decode(imageBuffer));
+        } catch (e, stacktrace) {
+          debugPrint('${e.toString()}\nstacktrace: $stacktrace');
+        }
+      },
+    ));
+
+    // make this room live broadcasting room
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _joinChannel() async {
+    if (defaultTargetPlatform == TargetPlatform.android) {
+      await [Permission.microphone, Permission.camera].request();
+    }
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: _channelIdController.text,
+        uid: config.uid,
+        options: const ChannelMediaOptions());
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  Future<void> _showMyDialog(int uid, String data) async {
+    return showDialog(
+      context: context,
+      barrierDismissible: false, // user must tap button!
+      builder: (BuildContext context) {
+        return AlertDialog(
+          title: Text('Receive from uid:$uid'),
+          content: SingleChildScrollView(
+            child: ListBody(
+              children: <Widget>[Text(data)],
+            ),
+          ),
+          actions: <Widget>[
+            TextButton(
+              child: const Text('Ok'),
+              onPressed: () {
+                Navigator.of(context).pop();
+              },
+            ),
+          ],
+        );
+      },
+    );
+  }
+
+  Future<void> _onPressSend() async {
+    if (_controller.text.isEmpty) {
+      return;
+    }
+
+    final data = Uint8List.fromList(utf8.encode(_controller.text));
+    await _engine.getMediaEngine().pushEncodedVideoImage(
+        imageBuffer: data,
+        length: data.length,
+        videoEncodedFrameInfo: const EncodedVideoFrameInfo(
+            framesPerSecond: 60,
+            codecType: VideoCodecType.videoCodecGeneric,
+            frameType: VideoFrameType.videoFrameTypeKeyFrame));
+
+    _controller.clear();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+
+        return const Center(
+          child: Text('No Preview'),
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextField(
+              controller: _channelIdController,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            if (isJoined)
+              Row(
+                mainAxisSize: MainAxisSize.max,
+                children: [
+                  Expanded(
+                      child: TextField(
+                          controller: _controller,
+                          decoration: const InputDecoration(
+                            hintText: 'Input Message',
+                          ))),
+                  ElevatedButton(
+                    onPressed: _onPressSend,
+                    child: const Text('Send'),
+                  ),
+                ],
+              )
+          ],
+        );
+      },
+    );
+  }
+}

+ 197 - 0
lib/examples/advanced/push_video_frame/push_video_frame.dart

@@ -0,0 +1,197 @@
+import 'dart:typed_data';
+import 'dart:ui';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+/// PushVideoFrame Example
+class PushVideoFrame extends StatefulWidget {
+  /// Construct the [PushVideoFrame]
+  const PushVideoFrame({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<PushVideoFrame> {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+
+  bool isJoined = false, switchCamera = true, switchRender = true;
+  Set<int> remoteUid = {};
+  late TextEditingController _controller;
+
+  late final Uint8List _imageByteData;
+  late final int _imageWidth;
+  late final int _imageHeight;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: config.channelId);
+
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+    ));
+    await _engine.setLogFilter(LogFilterType.logFilterError);
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUid.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUid.removeWhere((element) => element == rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+          remoteUid.clear();
+        });
+      },
+    ));
+
+    await _engine
+        .getMediaEngine()
+        .setExternalVideoSource(enabled: true, useTexture: false);
+
+    await _engine.enableVideo();
+
+    await _loadImageByteData();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _joinChannel() async {
+    await _engine.joinChannel(
+      token: config.token,
+      channelId: _controller.text,
+      uid: config.uid,
+      options: const ChannelMediaOptions(
+        channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+        clientRoleType: ClientRoleType.clientRoleBroadcaster,
+      ),
+    );
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  Future<void> _loadImageByteData() async {
+    ByteData data = await rootBundle.load("assets/agora-logo.png");
+    Uint8List bytes =
+        data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
+
+    final image = await decodeImageFromList(bytes);
+
+    final byteData =
+        await image.toByteData(format: ImageByteFormat.rawStraightRgba);
+    _imageByteData = byteData!.buffer.asUint8List();
+    _imageWidth = image.width;
+    _imageHeight = image.height;
+    image.dispose();
+  }
+
+  Future<void> _pushVideoFrame() async {
+    await _engine.getMediaEngine().pushVideoFrame(
+        frame: ExternalVideoFrame(
+            type: VideoBufferType.videoBufferRawData,
+            format: VideoPixelFormat.videoPixelRgba,
+            buffer: _imageByteData,
+            stride: _imageWidth,
+            height: _imageHeight,
+            timestamp: DateTime.now().millisecondsSinceEpoch));
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return const Center(
+          child: Text('No Preview'),
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            SizedBox(
+              height: 100,
+              width: 100,
+              child: Image.asset('assets/agora-logo.png'),
+            ),
+            const Text('Push Image as Video Frame'),
+            ElevatedButton(
+              onPressed: isJoined ? _pushVideoFrame : null,
+              child: const Text('Push Video Frame'),
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 265 - 0
lib/examples/advanced/rtmp_streaming/rtmp_streaming.dart

@@ -0,0 +1,265 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+
+/// RtmpStreaming Example
+class RtmpStreaming extends StatefulWidget {
+  /// Construct the [RtmpStreaming]
+  const RtmpStreaming({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _RtmpStreamingState();
+}
+
+class _RtmpStreamingState extends State<RtmpStreaming> {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+  String channelId = config.channelId;
+  bool isJoined = false;
+  bool switchCamera = true;
+  late TextEditingController _channelIdController;
+  late TextEditingController _rtmpUrlController;
+  late final TextEditingController _channelUidController;
+
+  bool _isStreaming = false;
+  int _remoteUid = 0;
+
+  @override
+  void initState() {
+    super.initState();
+    _channelIdController = TextEditingController(text: channelId);
+    _channelUidController = TextEditingController(text: '1001');
+    _rtmpUrlController = TextEditingController();
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    _dispose();
+    super.dispose();
+  }
+
+  Future<void> _dispose() async {
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+        _startTranscoding();
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          _remoteUid = rUid;
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          _remoteUid = 0;
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) async {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+
+        if (_isStreaming && _rtmpUrlController.text.isNotEmpty) {
+          await _engine.stopRtmpStream(_rtmpUrlController.text);
+          _isStreaming = false;
+        }
+
+        setState(() {
+          isJoined = false;
+        });
+      },
+      onRtmpStreamingStateChanged: (String url, RtmpStreamPublishState state,
+          RtmpStreamPublishErrorType errCode) {
+        logSink.log(
+            '[onRtmpStreamingStateChanged] url: $url state: $state, errCode: $errCode');
+      },
+      onRtmpStreamingEvent: (String url, RtmpStreamingEvent eventCode) {
+        logSink.log('[onRtmpStreamingEvent] url: $url eventCode: $eventCode');
+      },
+    ));
+
+    await _engine.enableVideo();
+    await _engine
+        .setChannelProfile(ChannelProfileType.channelProfileLiveBroadcasting);
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    await _engine.startPreview();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  void _joinChannel() async {
+    final uid = int.tryParse(_channelUidController.text);
+    if (uid == null) return;
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: _channelIdController.text,
+        uid: uid,
+        options: const ChannelMediaOptions());
+  }
+
+  void _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  Future<void> _startTranscoding({bool isRemoteUser = false}) async {
+    final uid = int.tryParse(_channelUidController.text);
+    if (uid == null) return;
+
+    if (_isStreaming && !isRemoteUser) return;
+    final streamUrl = _rtmpUrlController.text;
+
+    _isStreaming = true;
+
+    final List<TranscodingUser> transcodingUsers = [
+      TranscodingUser(
+        uid: uid,
+        x: 0,
+        y: 0,
+        width: 360,
+        height: 640,
+        audioChannel: 0,
+        alpha: 1.0,
+      )
+    ];
+
+    int width = 360;
+    int height = 640;
+
+    if (isRemoteUser) {
+      transcodingUsers.add(TranscodingUser(
+        uid: _remoteUid,
+        x: 360,
+        y: 0,
+        width: 360,
+        height: 640,
+        audioChannel: 0,
+        alpha: 1.0,
+      ));
+
+      width = 720;
+      height = 640;
+    }
+
+    final liveTranscoding = LiveTranscoding(
+      transcodingUsers: transcodingUsers,
+      userCount: transcodingUsers.length,
+      width: width,
+      height: height,
+      videoBitrate: 400,
+      videoCodecProfile: VideoCodecProfileType.videoCodecProfileHigh,
+      videoGop: 30,
+      videoFramerate: 15,
+      lowLatency: false,
+      audioSampleRate: AudioSampleRateType.audioSampleRate44100,
+      audioBitrate: 48,
+      audioChannels: 1,
+      audioCodecProfile: AudioCodecProfileType.audioCodecProfileLcAac,
+    );
+
+    try {
+      await _engine.startRtmpStreamWithTranscoding(
+          url: streamUrl, transcoding: liveTranscoding);
+    } catch (e) {
+      logSink.log('startRtmpStreamWithTranscoding error: ${e.toString()}');
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return Stack(
+          children: [
+            AgoraVideoView(
+              controller: VideoViewController(
+                rtcEngine: _engine,
+                canvas: const VideoCanvas(uid: 0),
+              ),
+            ),
+            if (_remoteUid != 0)
+              Align(
+                alignment: Alignment.topLeft,
+                child: SizedBox(
+                  width: 120,
+                  height: 120,
+                  child: _remoteUid != 0
+                      ? AgoraVideoView(
+                          controller: VideoViewController.remote(
+                          rtcEngine: _engine,
+                          canvas: VideoCanvas(uid: _remoteUid),
+                          connection: RtcConnection(
+                              channelId: _channelIdController.text),
+                        ))
+                      : Container(
+                          color: Colors.grey[200],
+                        ),
+                ),
+              ),
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisAlignment: MainAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextField(
+              controller: _channelIdController,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            TextField(
+              controller: _channelUidController,
+              decoration: const InputDecoration(
+                hintText: 'Enter channel uid',
+              ),
+            ),
+            TextField(
+              controller: _rtmpUrlController,
+              decoration: const InputDecoration(hintText: 'Input rtmp url'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 580 - 0
lib/examples/advanced/screen_sharing/screen_sharing.dart

@@ -0,0 +1,580 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/components/rgba_image.dart';
+import 'package:agora_rtc_engine_example/components/basic_video_configuration_widget.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:agora_rtc_engine_example/components/remote_video_views_widget.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+/// ScreenSharing Example
+class ScreenSharing extends StatefulWidget {
+  /// Construct the [ScreenSharing]
+  const ScreenSharing({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<ScreenSharing> with KeepRemoteVideoViewsMixin {
+  late final RtcEngineEx _engine;
+  bool _isReadyPreview = false;
+  String channelId = config.channelId;
+  bool isJoined = false;
+  late TextEditingController _controller;
+  late final TextEditingController _localUidController;
+  late final TextEditingController _screenShareUidController;
+
+  bool _isScreenShared = false;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: channelId);
+    _localUidController = TextEditingController(text: '1000');
+    _screenShareUidController = TextEditingController(text: '1001');
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _engine.release();
+  }
+
+  _initEngine() async {
+    _engine = createAgoraRtcEngineEx();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+    await _engine.setLogLevel(LogLevel.logLevelError);
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+        onError: (ErrorCodeType err, String msg) {
+      logSink.log('[onError] err: $err, msg: $msg');
+    }, onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+      logSink.log(
+          '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+      setState(() {
+        isJoined = true;
+      });
+    }, onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+      logSink.log(
+          '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+      setState(() {
+        isJoined = false;
+      });
+    }, onLocalVideoStateChanged: (VideoSourceType source,
+            LocalVideoStreamState state, LocalVideoStreamError error) {
+      logSink.log(
+          '[onLocalVideoStateChanged] source: $source, state: $state, error: $error');
+      if (!(source == VideoSourceType.videoSourceScreen ||
+          source == VideoSourceType.videoSourceScreenPrimary)) {
+        return;
+      }
+
+      switch (state) {
+        case LocalVideoStreamState.localVideoStreamStateCapturing:
+        case LocalVideoStreamState.localVideoStreamStateEncoding:
+          setState(() {
+            _isScreenShared = true;
+          });
+          break;
+        case LocalVideoStreamState.localVideoStreamStateStopped:
+        case LocalVideoStreamState.localVideoStreamStateFailed:
+          setState(() {
+            _isScreenShared = false;
+          });
+          break;
+        default:
+          break;
+      }
+    }));
+
+    await _engine.enableVideo();
+    await _engine.startPreview();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  void _joinChannel() async {
+    final localUid = int.tryParse(_localUidController.text);
+    if (localUid != null) {
+      await _engine.joinChannelEx(
+          token: '',
+          connection:
+              RtcConnection(channelId: _controller.text, localUid: localUid),
+          options: const ChannelMediaOptions(
+            publishCameraTrack: true,
+            publishMicrophoneTrack: true,
+            clientRoleType: ClientRoleType.clientRoleBroadcaster,
+          ));
+    }
+
+    final shareShareUid = int.tryParse(_screenShareUidController.text);
+    if (shareShareUid != null) {
+      await _engine.joinChannelEx(
+          token: '',
+          connection: RtcConnection(
+              channelId: _controller.text, localUid: shareShareUid),
+          options: const ChannelMediaOptions(
+            autoSubscribeVideo: true,
+            autoSubscribeAudio: true,
+            publishScreenTrack: true,
+            publishSecondaryScreenTrack: true,
+            publishCameraTrack: false,
+            publishMicrophoneTrack: false,
+            publishScreenCaptureAudio: true,
+            publishScreenCaptureVideo: true,
+            clientRoleType: ClientRoleType.clientRoleBroadcaster,
+          ));
+    }
+  }
+
+  Future<void> _updateScreenShareChannelMediaOptions() async {
+    final shareShareUid = int.tryParse(_screenShareUidController.text);
+    if (shareShareUid == null) return;
+    await _engine.updateChannelMediaOptionsEx(
+      options: const ChannelMediaOptions(
+        publishScreenTrack: true,
+        publishSecondaryScreenTrack: true,
+        publishCameraTrack: false,
+        publishMicrophoneTrack: false,
+        publishScreenCaptureAudio: true,
+        publishScreenCaptureVideo: true,
+        clientRoleType: ClientRoleType.clientRoleBroadcaster,
+      ),
+      connection:
+          RtcConnection(channelId: _controller.text, localUid: shareShareUid),
+    );
+  }
+
+  _leaveChannel() async {
+    await _engine.stopScreenCapture();
+    await _engine.leaveChannel();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        final children = <Widget>[
+          Expanded(
+            flex: 1,
+            child: AspectRatio(
+              aspectRatio: 1,
+              child: AgoraVideoView(
+                  controller: VideoViewController(
+                rtcEngine: _engine,
+                canvas: const VideoCanvas(
+                  uid: 0,
+                ),
+              )),
+            ),
+          ),
+          Expanded(
+            flex: 1,
+            child: AspectRatio(
+              aspectRatio: 1,
+              child: _isScreenShared
+                  ? AgoraVideoView(
+                      controller: VideoViewController(
+                      rtcEngine: _engine,
+                      canvas: const VideoCanvas(
+                        uid: 0,
+                        sourceType: VideoSourceType.videoSourceScreen,
+                      ),
+                    ))
+                  : Container(
+                      color: Colors.grey[200],
+                      child: const Center(
+                        child: Text('Screen Sharing View'),
+                      ),
+                    ),
+            ),
+          ),
+        ];
+        Widget localVideoView;
+        if (isLayoutHorizontal) {
+          localVideoView = Row(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            mainAxisAlignment: MainAxisAlignment.start,
+            children: children,
+          );
+        } else {
+          localVideoView = Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            mainAxisAlignment: MainAxisAlignment.start,
+            children: children,
+          );
+        }
+        return Stack(
+          children: [
+            localVideoView,
+            Align(
+              alignment: Alignment.topLeft,
+              child: RemoteVideoViewsWidget(
+                key: keepRemoteVideoViewsKey,
+                rtcEngine: _engine,
+                channelId: _controller.text,
+                connectionUid: int.tryParse(_localUidController.text),
+              ),
+            )
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            TextField(
+              controller: _localUidController,
+              decoration: const InputDecoration(hintText: 'Local Uid'),
+            ),
+            TextField(
+              controller: _screenShareUidController,
+              decoration: const InputDecoration(hintText: 'Screen Sharing Uid'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            BasicVideoConfigurationWidget(
+              rtcEngine: _engine,
+              title: 'Video Encoder Configuration',
+              setConfigButtonText: const Text(
+                'setVideoEncoderConfiguration',
+                style: TextStyle(fontSize: 10),
+              ),
+              onConfigChanged: (width, height, frameRate, bitrate) {
+                _engine.setVideoEncoderConfiguration(VideoEncoderConfiguration(
+                  dimensions: VideoDimensions(width: width, height: height),
+                  frameRate: frameRate,
+                  bitrate: bitrate,
+                ));
+              },
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            if (defaultTargetPlatform == TargetPlatform.android ||
+                defaultTargetPlatform == TargetPlatform.iOS)
+              ScreenShareMobile(
+                  rtcEngine: _engine,
+                  isScreenShared: _isScreenShared,
+                  onStartScreenShared: () {
+                    if (isJoined) {
+                      _updateScreenShareChannelMediaOptions();
+                    }
+                  },
+                  onStopScreenShare: () {}),
+            if (defaultTargetPlatform == TargetPlatform.windows ||
+                defaultTargetPlatform == TargetPlatform.macOS)
+              ScreenShareDesktop(
+                  rtcEngine: _engine,
+                  isScreenShared: _isScreenShared,
+                  onStartScreenShared: () {
+                    if (isJoined) {
+                      _updateScreenShareChannelMediaOptions();
+                    }
+                  },
+                  onStopScreenShare: () {}),
+          ],
+        );
+      },
+    );
+  }
+}
+
+class ScreenShareMobile extends StatefulWidget {
+  const ScreenShareMobile(
+      {Key? key,
+      required this.rtcEngine,
+      required this.isScreenShared,
+      required this.onStartScreenShared,
+      required this.onStopScreenShare})
+      : super(key: key);
+
+  final RtcEngine rtcEngine;
+  final bool isScreenShared;
+  final VoidCallback onStartScreenShared;
+  final VoidCallback onStopScreenShare;
+
+  @override
+  State<ScreenShareMobile> createState() => _ScreenShareMobileState();
+}
+
+class _ScreenShareMobileState extends State<ScreenShareMobile>
+    implements ScreenShareInterface {
+  final MethodChannel _iosScreenShareChannel =
+      const MethodChannel('example_screensharing_ios');
+
+  @override
+  bool get isScreenShared => widget.isScreenShared;
+
+  @override
+  void onStartScreenShared() {
+    widget.onStartScreenShared();
+  }
+
+  @override
+  void onStopScreenShare() {
+    widget.onStopScreenShare();
+  }
+
+  @override
+  RtcEngine get rtcEngine => widget.rtcEngine;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        Expanded(
+          flex: 1,
+          child: ElevatedButton(
+            onPressed: !isScreenShared ? startScreenShare : stopScreenShare,
+            child: Text('${isScreenShared ? 'Stop' : 'Start'} screen share'),
+          ),
+        )
+      ],
+    );
+  }
+
+  @override
+  void startScreenShare() async {
+    if (isScreenShared) return;
+
+    await rtcEngine.startScreenCapture(
+        const ScreenCaptureParameters2(captureAudio: true, captureVideo: true));
+    await rtcEngine.startPreview(sourceType: VideoSourceType.videoSourceScreen);
+    _showRPSystemBroadcastPickerViewIfNeed();
+    onStartScreenShared();
+  }
+
+  @override
+  void stopScreenShare() async {
+    if (!isScreenShared) return;
+
+    await rtcEngine.stopScreenCapture();
+    onStopScreenShare();
+  }
+
+  Future<void> _showRPSystemBroadcastPickerViewIfNeed() async {
+    if (defaultTargetPlatform != TargetPlatform.iOS) {
+      return;
+    }
+
+    await _iosScreenShareChannel
+        .invokeMethod('showRPSystemBroadcastPickerView');
+  }
+}
+
+class ScreenShareDesktop extends StatefulWidget {
+  const ScreenShareDesktop(
+      {Key? key,
+      required this.rtcEngine,
+      required this.isScreenShared,
+      required this.onStartScreenShared,
+      required this.onStopScreenShare})
+      : super(key: key);
+
+  final RtcEngine rtcEngine;
+  final bool isScreenShared;
+  final VoidCallback onStartScreenShared;
+  final VoidCallback onStopScreenShare;
+
+  @override
+  State<ScreenShareDesktop> createState() => _ScreenShareDesktopState();
+}
+
+class _ScreenShareDesktopState extends State<ScreenShareDesktop>
+    implements ScreenShareInterface {
+  List<ScreenCaptureSourceInfo> _screenCaptureSourceInfos = [];
+  late ScreenCaptureSourceInfo _selectedScreenCaptureSourceInfo;
+
+  @override
+  bool get isScreenShared => widget.isScreenShared;
+
+  @override
+  void onStartScreenShared() {
+    widget.onStartScreenShared();
+  }
+
+  @override
+  void onStopScreenShare() {
+    widget.onStopScreenShare();
+  }
+
+  @override
+  RtcEngine get rtcEngine => widget.rtcEngine;
+
+  Future<void> _initScreenCaptureSourceInfos() async {
+    SIZE thumbSize = const SIZE(width: 50, height: 50);
+    SIZE iconSize = const SIZE(width: 50, height: 50);
+    _screenCaptureSourceInfos = await rtcEngine.getScreenCaptureSources(
+        thumbSize: thumbSize, iconSize: iconSize, includeScreen: true);
+    _selectedScreenCaptureSourceInfo = _screenCaptureSourceInfos[0];
+    setState(() {});
+  }
+
+  Widget _createDropdownButton() {
+    if (_screenCaptureSourceInfos.isEmpty) return Container();
+    return DropdownButton<ScreenCaptureSourceInfo>(
+        items: _screenCaptureSourceInfos.map((info) {
+          Widget image;
+          if (info.iconImage!.width! != 0 && info.iconImage!.height! != 0) {
+            image = Image(
+              image: RgbaImage(
+                info.iconImage!.buffer!,
+                width: info.iconImage!.width!,
+                height: info.iconImage!.height!,
+              ),
+            );
+          } else if (info.thumbImage!.width! != 0 &&
+              info.thumbImage!.height! != 0) {
+            image = Image(
+              image: RgbaImage(
+                info.thumbImage!.buffer!,
+                width: info.thumbImage!.width!,
+                height: info.thumbImage!.height!,
+              ),
+            );
+          } else {
+            image = const SizedBox(
+              width: 50,
+              height: 50,
+            );
+          }
+
+          return DropdownMenuItem(
+            value: info,
+            child: Row(
+              mainAxisSize: MainAxisSize.min,
+              mainAxisAlignment: MainAxisAlignment.start,
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: [
+                image,
+                Text('${info.sourceName}', style: const TextStyle(fontSize: 10))
+              ],
+            ),
+          );
+        }).toList(),
+        value: _selectedScreenCaptureSourceInfo,
+        onChanged: isScreenShared
+            ? null
+            : (v) {
+                setState(() {
+                  _selectedScreenCaptureSourceInfo = v!;
+                });
+              });
+  }
+
+  @override
+  void initState() {
+    super.initState();
+
+    _initScreenCaptureSourceInfos();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      mainAxisAlignment: MainAxisAlignment.start,
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        _createDropdownButton(),
+        if (_screenCaptureSourceInfos.isNotEmpty)
+          Row(
+            children: [
+              Expanded(
+                flex: 1,
+                child: ElevatedButton(
+                  onPressed:
+                      !isScreenShared ? startScreenShare : stopScreenShare,
+                  child:
+                      Text('${isScreenShared ? 'Stop' : 'Start'} screen share'),
+                ),
+              )
+            ],
+          ),
+      ],
+    );
+  }
+
+  @override
+  void startScreenShare() async {
+    if (isScreenShared) return;
+
+    final sourceId = _selectedScreenCaptureSourceInfo.sourceId;
+
+    if (_selectedScreenCaptureSourceInfo.type ==
+        ScreenCaptureSourceType.screencapturesourcetypeScreen) {
+      await rtcEngine.startScreenCaptureByDisplayId(
+          displayId: sourceId!,
+          regionRect: const Rectangle(x: 0, y: 0, width: 0, height: 0),
+          captureParams: const ScreenCaptureParameters(
+            captureMouseCursor: true,
+            frameRate: 30,
+          ));
+    } else if (_selectedScreenCaptureSourceInfo.type ==
+        ScreenCaptureSourceType.screencapturesourcetypeWindow) {
+      await rtcEngine.startScreenCaptureByWindowId(
+        windowId: sourceId!,
+        regionRect: const Rectangle(x: 0, y: 0, width: 0, height: 0),
+        captureParams: const ScreenCaptureParameters(
+          captureMouseCursor: true,
+          frameRate: 30,
+        ),
+      );
+    }
+
+    onStartScreenShared();
+  }
+
+  @override
+  void stopScreenShare() async {
+    if (!isScreenShared) return;
+
+    await rtcEngine.stopScreenCapture();
+    onStopScreenShare();
+  }
+}
+
+abstract class ScreenShareInterface {
+  void onStartScreenShared();
+
+  void onStopScreenShare();
+
+  bool get isScreenShared;
+
+  RtcEngine get rtcEngine;
+
+  void startScreenShare();
+
+  void stopScreenShare();
+}

+ 252 - 0
lib/examples/advanced/send_metadata/send_metadata.dart

@@ -0,0 +1,252 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+
+/// StreamMessage Example
+class SendMetadata extends StatefulWidget {
+  /// Construct the [StreamMessage]
+  const SendMetadata({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<SendMetadata> {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+  bool isJoined = false;
+  Set<int> remoteUids = {};
+  late final TextEditingController _channelIdController;
+  final TextEditingController _controller = TextEditingController();
+
+  @override
+  void initState() {
+    super.initState();
+    _channelIdController = TextEditingController(text: config.channelId);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    _engine.release();
+  }
+
+  void _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          remoteUids.clear();
+          isJoined = false;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUids.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUids.remove(rUid);
+        });
+      },
+    ));
+
+    _engine.registerMediaMetadataObserver(
+      observer: MetadataObserver(
+        onMetadataReceived: (metadata) {
+          logSink.log(
+              '[onMetadataReceived] metadata: ${metadata.toJson()}, metadata.buffer: ${metadata.buffer}');
+          debugPrint(
+              '[onMetadataReceived] metadata: ${metadata.toJson()}, metadata.buffer: ${metadata.buffer}');
+          debugPrint(
+              '[onMetadataReceived] metadata: ${metadata.toJson()}, String.fromCharCodes(metadata.buffer!): ${String.fromCharCodes(metadata.buffer!)}');
+
+          _showMyDialog(metadata.uid!,
+              utf8.decode(metadata.buffer!, allowMalformed: true));
+        },
+      ),
+      type: MetadataType.videoMetadata,
+    );
+
+    // enable video module and set up video encoding configs
+    await _engine.enableVideo();
+
+    // make this room live broadcasting room
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  void _joinChannel() async {
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: _channelIdController.text,
+        uid: config.uid,
+        options: const ChannelMediaOptions());
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  Future<void> _showMyDialog(int uid, String data) async {
+    return showDialog(
+      context: context,
+      barrierDismissible: false, // user must tap button!
+      builder: (BuildContext context) {
+        return AlertDialog(
+          title: Text('Receive from uid:$uid'),
+          content: SingleChildScrollView(
+            child: ListBody(
+              children: <Widget>[Text('data: $data')],
+            ),
+          ),
+          actions: <Widget>[
+            TextButton(
+              child: const Text('Ok'),
+              onPressed: () {
+                Navigator.of(context).pop();
+              },
+            ),
+          ],
+        );
+      },
+    );
+  }
+
+  Future<void> _onPressSend() async {
+    if (_controller.text.isEmpty) {
+      return;
+    }
+
+    try {
+      final data = Uint8List.fromList(utf8.encode(_controller.text));
+
+      await _engine.sendMetaData(
+          metadata: Metadata(buffer: data, size: data.length),
+          sourceType: VideoSourceType.videoSourceCamera);
+
+      _controller.clear();
+    } catch (e) {
+      logSink.log('sendMetaData error: ${e.toString()}');
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return Stack(
+          children: [
+            AgoraVideoView(
+              controller: VideoViewController(
+                rtcEngine: _engine,
+                canvas: const VideoCanvas(uid: 0),
+              ),
+            ),
+            Align(
+              alignment: Alignment.topLeft,
+              child: SingleChildScrollView(
+                scrollDirection: Axis.horizontal,
+                child: Row(
+                  children: List.of(remoteUids.map(
+                    (e) => SizedBox(
+                      width: 120,
+                      height: 120,
+                      child: AgoraVideoView(
+                        controller: VideoViewController.remote(
+                          rtcEngine: _engine,
+                          canvas: VideoCanvas(uid: e),
+                          connection: RtcConnection(
+                              channelId: _channelIdController.text),
+                        ),
+                      ),
+                    ),
+                  )),
+                ),
+              ),
+            )
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextField(
+              controller: _channelIdController,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            // if (isJoined) _renderVideo(),
+            if (isJoined)
+              Row(
+                mainAxisSize: MainAxisSize.max,
+                children: [
+                  Expanded(
+                      child: TextField(
+                          controller: _controller,
+                          decoration: const InputDecoration(
+                            hintText: 'Input Message',
+                          ))),
+                  ElevatedButton(
+                    onPressed: _onPressSend,
+                    child: const Text('Send'),
+                  ),
+                ],
+              )
+          ],
+        );
+      },
+    );
+  }
+}

+ 256 - 0
lib/examples/advanced/send_multi_camera_stream/send_multi_camera_stream.dart

@@ -0,0 +1,256 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+
+/// SendMultiCameraStream Example
+class SendMultiCameraStream extends StatefulWidget {
+  /// Construct the [JoinChannelVideo]
+  const SendMultiCameraStream({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<SendMultiCameraStream> {
+  late final RtcEngineEx _engine;
+  bool _isReadyPreview = false;
+  late final VideoDeviceManager _videoDeviceManager;
+  bool isJoined = false, switchCamera = true, switchRender = true;
+  Set<int> remoteUid = {};
+  late TextEditingController _controller;
+  List<VideoDeviceInfo> _videoDeviceInfos = [];
+  bool _isStartSecondaryCameraDevice = false;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: config.channelId);
+
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    await _leaveChannel();
+    await _engine.release();
+  }
+
+  void _initEngine() async {
+    _engine = createAgoraRtcEngineEx();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUid.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUid.removeWhere((element) => element == rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+          remoteUid.clear();
+        });
+      },
+      onVideoDeviceStateChanged: (String deviceId, MediaDeviceType deviceType,
+          MediaDeviceStateType deviceState) async {
+        logSink.log(
+            '[onVideoDeviceStateChanged] deviceId: $deviceId deviceType: $deviceType, deviceState: $deviceState');
+        _videoDeviceInfos = await _videoDeviceManager.enumerateVideoDevices();
+        setState(() {});
+      },
+    ));
+
+    _videoDeviceManager = _engine.getVideoDeviceManager();
+    _videoDeviceInfos = await _videoDeviceManager.enumerateVideoDevices();
+
+    await _engine.enableVideo();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    await _engine.startCameraCapture(
+        sourceType: VideoSourceType.videoSourceCameraPrimary,
+        config: CameraCapturerConfiguration(
+          deviceId: _videoDeviceInfos[0].deviceId,
+          format: VideoFormat(
+            width: 640,
+            height: 320,
+            fps: FrameRate.frameRateFps10.value(),
+          ),
+        ));
+
+    await _engine.startPreview();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _joinChannel() async {
+    await _engine.joinChannelEx(
+        token: '',
+        connection: RtcConnection(channelId: _controller.text, localUid: 1000),
+        options: const ChannelMediaOptions(
+            publishCameraTrack: true,
+            clientRoleType: ClientRoleType.clientRoleBroadcaster,
+            channelProfile: ChannelProfileType.channelProfileLiveBroadcasting));
+
+    if (_isStartSecondaryCameraDevice) {
+      await _engine.joinChannelEx(
+          token: '',
+          connection:
+              RtcConnection(channelId: _controller.text, localUid: 1001),
+          options: const ChannelMediaOptions(
+            publishCameraTrack: false,
+            publishSecondaryCameraTrack: true,
+            clientRoleType: ClientRoleType.clientRoleBroadcaster,
+            channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+          ));
+    }
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.stopCameraCapture(VideoSourceType.videoSourceCameraPrimary);
+    await _engine.stopCameraCapture(VideoSourceType.videoSourceCameraSecondary);
+    await _engine.leaveChannel();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        final children = <Widget>[
+          Expanded(
+            flex: 1,
+            child: AspectRatio(
+              aspectRatio: 1,
+              child: AgoraVideoView(
+                controller: VideoViewController(
+                  rtcEngine: _engine,
+                  canvas: const VideoCanvas(
+                    uid: 0,
+                    sourceType: VideoSourceType.videoSourceCameraPrimary,
+                  ),
+                ),
+              ),
+            ),
+          ),
+          Expanded(
+            child: AspectRatio(
+              aspectRatio: 1,
+              child: AgoraVideoView(
+                controller: VideoViewController(
+                  rtcEngine: _engine,
+                  canvas: const VideoCanvas(
+                    uid: 0,
+                    sourceType: VideoSourceType.videoSourceCameraSecondary,
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ];
+        if (isLayoutHorizontal) {
+          return Row(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: children,
+          );
+        }
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: children,
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: isJoined
+                  ? null
+                  : _videoDeviceInfos.length >= 2
+                      ? () {
+                          _isStartSecondaryCameraDevice =
+                              !_isStartSecondaryCameraDevice;
+
+                          if (_isStartSecondaryCameraDevice) {
+                            _engine.startCameraCapture(
+                                sourceType:
+                                    VideoSourceType.videoSourceCameraSecondary,
+                                config: CameraCapturerConfiguration(
+                                  deviceId: _videoDeviceInfos[1].deviceId,
+                                  format: VideoFormat(
+                                    width: 640,
+                                    height: 320,
+                                    fps: FrameRate.frameRateFps10.value(),
+                                  ),
+                                ));
+                          } else {
+                            _engine.stopCameraCapture(
+                                VideoSourceType.videoSourceCameraSecondary);
+                          }
+
+                          setState(() {});
+                        }
+                      : null,
+              child: Text(
+                  '${_isStartSecondaryCameraDevice ? 'Stop' : 'Start'} Secondary Camera Device'),
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 277 - 0
lib/examples/advanced/send_multi_video_stream/send_multi_video_stream.dart

@@ -0,0 +1,277 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+
+/// SendMultiVideoStream Example
+class SendMultiVideoStream extends StatefulWidget {
+  const SendMultiVideoStream({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<SendMultiVideoStream> {
+  late final RtcEngineEx _engine;
+  bool _isReadyPreview = false;
+
+  late final MediaPlayerController _mediaPlayerController;
+
+  late final TextEditingController _textEditingController;
+
+  bool _isUrlOpened = false;
+  bool _isPlaying = false;
+  bool isJoined = false;
+
+  late TextEditingController _channelIdController;
+
+  @override
+  void initState() {
+    super.initState();
+    _channelIdController = TextEditingController(text: config.channelId);
+    _textEditingController = TextEditingController(
+        text:
+            'https://agoracdn.s3.us-west-1.amazonaws.com/videos/Agora.io-Interactions.mp4');
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+
+    _dispose();
+  }
+
+  void _dispose() async {
+    await _mediaPlayerController.dispose();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngineEx();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+    ));
+
+    await _engine.enableVideo();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    await _engine.startPreview();
+
+    _mediaPlayerController = MediaPlayerController(
+        rtcEngine: _engine,
+        canvas: const VideoCanvas(
+          uid: 0,
+          sourceType: VideoSourceType.videoSourceMediaPlayer,
+        ));
+    await _mediaPlayerController.initialize();
+    _mediaPlayerController.registerPlayerSourceObserver(
+      MediaPlayerSourceObserver(
+        onCompleted: () {
+          logSink.log('[onCompleted]');
+        },
+        onPlayerSourceStateChanged:
+            (MediaPlayerState state, MediaPlayerError ec) {
+          logSink.log('[onPlayerSourceStateChanged] state: $state ec: $ec');
+          if (state == MediaPlayerState.playerStateOpenCompleted) {
+            debugPrint('src ${_mediaPlayerController.getPlaySrc()}');
+            _mediaPlayerController.play();
+            setState(() {
+              _isUrlOpened = !_isUrlOpened;
+              _isPlaying = !_isPlaying;
+            });
+          }
+        },
+        onPositionChanged: (int position) {
+          logSink.log('[onPositionChanged] position: $position');
+        },
+        onPlayerEvent:
+            (MediaPlayerEvent eventCode, int elapsedTime, String message) {
+          logSink.log(
+              '[onPlayerEvent] eventCode: $eventCode, elapsedTime: $elapsedTime, message: $message');
+        },
+      ),
+    );
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  void _open() {
+    if (!_isUrlOpened) {
+      _mediaPlayerController.open(
+          url: _textEditingController.text, startPos: 0);
+    } else {
+      if (_isPlaying) {
+        _mediaPlayerController.stop();
+      } else {
+        _mediaPlayerController.play();
+      }
+
+      setState(() {
+        _isPlaying = !_isPlaying;
+        _isUrlOpened = !_isUrlOpened;
+      });
+    }
+  }
+
+  void _joinChannel() async {
+    await _engine.joinChannelEx(
+      token: '',
+      connection: RtcConnection(
+        channelId: _channelIdController.text,
+        localUid: 123,
+      ),
+      options: const ChannelMediaOptions(
+        clientRoleType: ClientRoleType.clientRoleBroadcaster,
+        publishMicrophoneTrack: true,
+        publishCameraTrack: true,
+      ),
+    );
+
+    await _engine.joinChannelEx(
+      token: '',
+      connection: RtcConnection(
+        channelId: _channelIdController.text,
+        localUid: 456,
+      ),
+      options: ChannelMediaOptions(
+        clientRoleType: ClientRoleType.clientRoleBroadcaster,
+        publishMediaPlayerAudioTrack: true,
+        publishMediaPlayerVideoTrack: true,
+        publishMediaPlayerId: _mediaPlayerController.getMediaPlayerId(),
+      ),
+    );
+  }
+
+  void _leaveChannel() async {
+    if (_isPlaying) {
+      _mediaPlayerController.stop();
+
+      setState(() {
+        _isUrlOpened = false;
+        _isPlaying = false;
+      });
+    }
+    await _engine.leaveChannel();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        final children = <Widget>[
+          Expanded(
+            flex: 1,
+            child: AspectRatio(
+              aspectRatio: 1,
+              child: AgoraVideoView(
+                controller: VideoViewController(
+                  rtcEngine: _engine,
+                  canvas: const VideoCanvas(
+                    uid: 0,
+                    sourceType: VideoSourceType.videoSourceCamera,
+                  ),
+                ),
+              ),
+            ),
+          ),
+          Expanded(
+            child: AspectRatio(
+              aspectRatio: 1,
+              child: _isUrlOpened
+                  ? AgoraVideoView(
+                      controller: _mediaPlayerController,
+                    )
+                  : Container(
+                      color: Colors.grey[200],
+                      child: const Center(
+                        child: Text('MediaPlayer'),
+                      ),
+                    ),
+            ),
+          ),
+        ];
+        if (isLayoutHorizontal) {
+          return Row(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: children,
+          );
+        }
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: children,
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          children: [
+            Column(
+              mainAxisAlignment: MainAxisAlignment.start,
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                TextField(
+                  controller: _channelIdController,
+                  decoration: const InputDecoration(hintText: 'Channel ID'),
+                ),
+              ],
+            ),
+            Row(children: [
+              Expanded(
+                child: TextField(
+                  controller: _textEditingController,
+                  decoration: const InputDecoration(
+                    hintText: 'Media URL',
+                  ),
+                ),
+              ),
+              ElevatedButton(
+                onPressed: !isJoined ? _open : null,
+                child: Text(_isPlaying ? "Stop" : "Play"),
+              ),
+            ]),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: _isPlaying
+                        ? (isJoined ? _leaveChannel : _joinChannel)
+                        : null,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 367 - 0
lib/examples/advanced/set_beauty_effect/set_beauty_effect.dart

@@ -0,0 +1,367 @@
+// ignore_for_file: unnecessary_brace_in_string_interps
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:agora_rtc_engine_example/components/remote_video_views_widget.dart';
+import 'package:flutter/material.dart';
+
+/// SetBeautyEffect Example
+class SetBeautyEffect extends StatefulWidget {
+  /// Construct the [SetBeautyEffect]
+  const SetBeautyEffect({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<SetBeautyEffect> with KeepRemoteVideoViewsMixin {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+  bool isJoined = false;
+  late TextEditingController _channelIdController;
+  double _lighteningLevel = 0.0;
+  double _smoothnessLevel = 0.0;
+  double _rednessLevel = 0.0;
+  double _sharpnessLevel = 0.0;
+
+  LighteningContrastLevel _selectedLighteningContrastLevel =
+      LighteningContrastLevel.lighteningContrastHigh;
+  final List<LighteningContrastLevel> _lighteningContrastLevels = [
+    LighteningContrastLevel.lighteningContrastLow,
+    LighteningContrastLevel.lighteningContrastNormal,
+    LighteningContrastLevel.lighteningContrastHigh,
+  ];
+
+  @override
+  void initState() {
+    super.initState();
+    _channelIdController = TextEditingController(text: config.channelId);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  void _dispose() async {
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  void _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+      onExtensionError:
+          (String provider, String extName, int error, String msg) {
+        logSink.log(
+            '[onExtensionErrored] provider: $provider, extName: $extName, error: $error, msg: $msg');
+      },
+      onExtensionStarted: (String provider, String extName) {
+        logSink
+            .log('[onExtensionStarted] provider: $provider, extName: $extName');
+      },
+      onExtensionEvent:
+          (String provider, String extName, String key, String value) {
+        logSink
+            .log('[onExtensionEvent] provider: $provider, extName: $extName');
+      },
+      onExtensionStopped: (String provider, String extName) {
+        logSink
+            .log('[onExtensionStopped] provider: $provider, extName: $extName');
+      },
+    ));
+
+    await _engine.enableVideo();
+
+    await _engine.enableExtension(
+        provider: "agora_video_filters_clear_vision",
+        extension: "clear_vision");
+
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    await _engine.startPreview();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  void _joinChannel() async {
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: _channelIdController.text,
+        uid: 0,
+        options: const ChannelMediaOptions());
+  }
+
+  _leaveChannel() async {
+    await _engine.setBeautyEffectOptions(
+      enabled: false,
+      options: BeautyOptions(
+        lighteningContrastLevel: _selectedLighteningContrastLevel,
+        lighteningLevel: _lighteningLevel,
+        smoothnessLevel: _smoothnessLevel,
+        rednessLevel: _rednessLevel,
+        sharpnessLevel: _sharpnessLevel,
+      ),
+    );
+
+    await _engine.leaveChannel();
+
+    setState(() {
+      _lighteningLevel = 0.0;
+      _smoothnessLevel = 0.0;
+      _rednessLevel = 0.0;
+      _sharpnessLevel = 0.0;
+      _selectedLighteningContrastLevel =
+          LighteningContrastLevel.lighteningContrastHigh;
+    });
+  }
+
+  Widget _buildSpatialAudioOptions() {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.start,
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            const Text('LighteningContrastLevels: '),
+            DropdownButton<LighteningContrastLevel>(
+                items: _lighteningContrastLevels.map((v) {
+                  return DropdownMenuItem(
+                    value: v,
+                    child: Text(
+                      v.toString().split('.')[1],
+                      style: const TextStyle(fontSize: 13),
+                    ),
+                  );
+                }).toList(),
+                value: _selectedLighteningContrastLevel,
+                onChanged: (v) async {
+                  _selectedLighteningContrastLevel = v!;
+
+                  await _engine.setBeautyEffectOptions(
+                    enabled: true,
+                    options: BeautyOptions(
+                      lighteningContrastLevel: _selectedLighteningContrastLevel,
+                      lighteningLevel: _lighteningLevel,
+                      smoothnessLevel: _smoothnessLevel,
+                      rednessLevel: _rednessLevel,
+                      sharpnessLevel: _sharpnessLevel,
+                    ),
+                  );
+
+                  setState(() {});
+                }),
+          ],
+        ),
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: [
+            const Text('Lightening Level:'),
+            Slider(
+              value: _lighteningLevel,
+              min: 0,
+              max: 1,
+              divisions: 10,
+              label: 'Lightening Level',
+              onChanged: (double value) async {
+                _lighteningLevel = value;
+
+                await _engine.setBeautyEffectOptions(
+                  enabled: true,
+                  options: BeautyOptions(
+                    lighteningContrastLevel: _selectedLighteningContrastLevel,
+                    lighteningLevel: _lighteningLevel,
+                    smoothnessLevel: _smoothnessLevel,
+                    rednessLevel: _rednessLevel,
+                    sharpnessLevel: _sharpnessLevel,
+                  ),
+                );
+
+                setState(() {});
+              },
+            )
+          ],
+        ),
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: [
+            const Text('Smoothness Level:'),
+            Slider(
+              value: _smoothnessLevel,
+              min: 0,
+              max: 1,
+              divisions: 10,
+              label: 'Smoothness Level',
+              onChanged: (double value) async {
+                _smoothnessLevel = value;
+
+                await _engine.setBeautyEffectOptions(
+                  enabled: true,
+                  options: BeautyOptions(
+                    lighteningContrastLevel: _selectedLighteningContrastLevel,
+                    lighteningLevel: _lighteningLevel,
+                    smoothnessLevel: _smoothnessLevel,
+                    rednessLevel: _rednessLevel,
+                    sharpnessLevel: _sharpnessLevel,
+                  ),
+                );
+
+                setState(() {});
+              },
+            )
+          ],
+        ),
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: [
+            const Text('Redness Level:'),
+            Slider(
+              value: _rednessLevel,
+              min: 0,
+              max: 1,
+              divisions: 10,
+              label: 'Redness Level',
+              onChanged: (double value) async {
+                _rednessLevel = value;
+                await _engine.setBeautyEffectOptions(
+                  enabled: true,
+                  options: BeautyOptions(
+                    lighteningContrastLevel: _selectedLighteningContrastLevel,
+                    lighteningLevel: _lighteningLevel,
+                    smoothnessLevel: _smoothnessLevel,
+                    rednessLevel: _rednessLevel,
+                    sharpnessLevel: _sharpnessLevel,
+                  ),
+                );
+
+                setState(() {});
+              },
+            )
+          ],
+        ),
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: [
+            const Text('Sharpness Level:'),
+            Slider(
+              value: _sharpnessLevel,
+              min: 0,
+              max: 1,
+              divisions: 10,
+              label: 'Sharpness Level',
+              onChanged: (double value) async {
+                _sharpnessLevel = value;
+                await _engine.setBeautyEffectOptions(
+                  enabled: true,
+                  options: BeautyOptions(
+                    lighteningContrastLevel: _selectedLighteningContrastLevel,
+                    lighteningLevel: _lighteningLevel,
+                    smoothnessLevel: _smoothnessLevel,
+                    rednessLevel: _rednessLevel,
+                    sharpnessLevel: _sharpnessLevel,
+                  ),
+                );
+                setState(() {});
+              },
+            )
+          ],
+        ),
+      ],
+    );
+  }
+
+  Widget _buildOptions() {
+    return Column(
+      children: [
+        TextField(
+          controller: _channelIdController,
+          decoration: const InputDecoration(hintText: 'Channel ID'),
+        ),
+        Row(
+          children: [
+            Expanded(
+              flex: 1,
+              child: ElevatedButton(
+                onPressed: isJoined ? _leaveChannel : _joinChannel,
+                child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+              ),
+            )
+          ],
+        ),
+        if (isJoined) _buildSpatialAudioOptions(),
+      ],
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return Stack(
+          children: [
+            AgoraVideoView(
+              controller: VideoViewController(
+                rtcEngine: _engine,
+                canvas: const VideoCanvas(uid: 0),
+              ),
+            ),
+            Align(
+              alignment: Alignment.topLeft,
+              child: RemoteVideoViewsWidget(
+                key: keepRemoteVideoViewsKey,
+                rtcEngine: _engine,
+                channelId: _channelIdController.text,
+              ),
+            ),
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return _buildOptions();
+      },
+    );
+  }
+}

+ 219 - 0
lib/examples/advanced/set_content_inspect/set_content_inspect.dart

@@ -0,0 +1,219 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:agora_rtc_engine_example/components/remote_video_views_widget.dart';
+import 'package:flutter/material.dart';
+
+/// MultiChannel Example
+class SetContentInspect extends StatefulWidget {
+  /// Construct the [JoinChannelVideo]
+  const SetContentInspect({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<SetContentInspect> with KeepRemoteVideoViewsMixin {
+  late final RtcEngine _engine;
+
+  bool _isReadyPreview = false;
+
+  bool isJoined = false, switchCamera = true, switchRender = true;
+  Set<int> remoteUid = {};
+  late TextEditingController _controller;
+  bool _isStartContentInspect = false;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: config.channelId);
+
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    // await _localVideoController.dispose();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUid.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUid.removeWhere((element) => element == rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+          remoteUid.clear();
+        });
+      },
+      onContentInspectResult: (ContentInspectResult result) {
+        logSink.log('[onContentInspectResult] result: $result');
+      },
+    ));
+
+    await _engine.enableVideo();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    await _engine.startPreview();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _joinChannel() async {
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: _controller.text,
+        uid: config.uid,
+        options: const ChannelMediaOptions());
+  }
+
+  void _leaveChannel() async {
+    if (_isStartContentInspect) {
+      _isStartContentInspect = !_isStartContentInspect;
+      await _engine.enableContentInspect(
+          enabled: false,
+          config: const ContentInspectConfig(
+            modules: [
+              ContentInspectModule(
+                  type: ContentInspectType.contentInspectModeration,
+                  interval: 0)
+            ],
+            moduleCount: 1,
+          ));
+    }
+
+    await _engine.leaveChannel();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return Stack(
+          children: [
+            AgoraVideoView(
+              controller: VideoViewController(
+                rtcEngine: _engine,
+                canvas: const VideoCanvas(uid: 0),
+              ),
+            ),
+            Align(
+              alignment: Alignment.topLeft,
+              child: RemoteVideoViewsWidget(
+                key: keepRemoteVideoViewsKey,
+                rtcEngine: _engine,
+                channelId: _controller.text,
+              ),
+            )
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: isJoined
+                  ? () {
+                      _isStartContentInspect = !_isStartContentInspect;
+
+                      if (_isStartContentInspect) {
+                        _engine.enableContentInspect(
+                            enabled: true,
+                            config: const ContentInspectConfig(
+                              modules: [
+                                ContentInspectModule(
+                                  type: ContentInspectType
+                                      .contentInspectModeration,
+                                  interval: 2,
+                                )
+                              ],
+                              moduleCount: 1,
+                            ));
+                      } else {
+                        _engine.enableContentInspect(
+                            enabled: false,
+                            config: const ContentInspectConfig(
+                              modules: [
+                                ContentInspectModule(
+                                  type: ContentInspectType
+                                      .contentInspectModeration,
+                                  interval: 2,
+                                )
+                              ],
+                              moduleCount: 1,
+                            ));
+                      }
+
+                      setState(() {});
+                    }
+                  : null,
+              child: Text(
+                  '${_isStartContentInspect ? 'Stop' : 'Start'} ContentInspect'),
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 286 - 0
lib/examples/advanced/set_encryption/set_encryption.dart

@@ -0,0 +1,286 @@
+// ignore_for_file: unnecessary_brace_in_string_interps
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+
+/// SetEncryption Example
+// ignore: use_key_in_widget_constructors
+class SetEncryption extends StatefulWidget {
+  @override
+  State<StatefulWidget> createState() => _SetEncryptionState();
+}
+
+class _SetEncryptionState extends State<SetEncryption> {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+  String channelId = config.channelId;
+  bool isJoined = false,
+      openMicrophone = true,
+      enableSpeakerphone = true,
+      playEffect = false;
+  late TextEditingController _controller;
+  Set<int> remoteUid = {};
+
+  // Only take 3 EncryptionMode for demo purpose
+  final List<EncryptionMode> encryptionModes = [
+    EncryptionMode.aes128Gcm2,
+    EncryptionMode.aes128Xts,
+    EncryptionMode.aes256Gcm,
+  ];
+
+  late EncryptionMode _selectedEncryptionMode;
+  final TextEditingController _encryptionKey = TextEditingController();
+  late final TextEditingController _encryptionKdfSalt;
+
+  @override
+  void initState() {
+    super.initState();
+    _selectedEncryptionMode = encryptionModes[0];
+    _controller = TextEditingController(text: channelId);
+    _encryptionKdfSalt =
+        TextEditingController(text: 'EncryptionKdfSaltInBase64Strings');
+    _initEngine();
+  }
+
+  @override
+  void reassemble() {
+    super.reassemble();
+    _destroy();
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    _encryptionKey.dispose();
+    _encryptionKdfSalt.dispose();
+    _destroy();
+
+    super.dispose();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUid.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUid.remove(rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          remoteUid.clear();
+          isJoined = false;
+        });
+      },
+      onEncryptionError:
+          (RtcConnection connection, EncryptionErrorType errorType) {
+        logSink.log(
+            '[onEncryptionError] connection: ${connection.toJson()} errorType: ${errorType}');
+      },
+    ));
+
+    await _engine.enableVideo();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    await _engine.startPreview();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _destroy() async {
+    _leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _joinChannel() async {
+    final EncryptionConfig encryptionConfig = EncryptionConfig(
+      encryptionMode: _selectedEncryptionMode,
+      encryptionKey: _encryptionKey.text,
+      encryptionKdfSalt:
+          Uint8List.fromList(utf8.encode(_encryptionKdfSalt.text)),
+    );
+    await _engine.enableEncryption(enabled: true, config: encryptionConfig);
+
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: _controller.text,
+        uid: config.uid,
+        options: const ChannelMediaOptions());
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return Stack(
+          children: [
+            AgoraVideoView(
+              controller: VideoViewController(
+                rtcEngine: _engine,
+                canvas: const VideoCanvas(uid: 0),
+              ),
+            ),
+            Align(
+              alignment: Alignment.topLeft,
+              child: SingleChildScrollView(
+                scrollDirection: Axis.horizontal,
+                child: Row(
+                  children: List.of(remoteUid.map(
+                    (e) => SizedBox(
+                      width: 120,
+                      height: 120,
+                      child: AgoraVideoView(
+                        controller: VideoViewController.remote(
+                          rtcEngine: _engine,
+                          canvas: VideoCanvas(uid: e),
+                          connection:
+                              RtcConnection(channelId: _controller.text),
+                        ),
+                      ),
+                    ),
+                  )),
+                ),
+              ),
+            )
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        final dropDownMenus = <DropdownMenuItem<EncryptionMode>>[];
+        for (var v in encryptionModes) {
+          dropDownMenus.add(DropdownMenuItem(
+            child: Text(
+              '$v',
+              style: const TextStyle(fontSize: 12),
+            ),
+            value: v,
+          ));
+        }
+
+        return Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+              // onChanged: (text) {
+              //   setState(() {
+              //     channelId = text;
+              //   });
+              // },
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            const Text('Encryption Mode: '),
+            Row(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              mainAxisAlignment: MainAxisAlignment.start,
+              children: [
+                DropdownButton<EncryptionMode>(
+                  items: dropDownMenus,
+                  value: _selectedEncryptionMode,
+                  onChanged: isJoined
+                      ? null
+                      : (v) {
+                          setState(() {
+                            _selectedEncryptionMode = v!;
+                          });
+                        },
+                ),
+              ],
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            const Text('Input Encryption Key: '),
+            Row(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              mainAxisAlignment: MainAxisAlignment.start,
+              children: [
+                Expanded(
+                  child: TextField(
+                    controller: _encryptionKey,
+                    readOnly: isJoined,
+                    decoration:
+                        const InputDecoration(hintText: 'Encryption Key'),
+                  ),
+                )
+              ],
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            const Text('Input EncryptionKdfSalt: '),
+            Row(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              mainAxisAlignment: MainAxisAlignment.start,
+              mainAxisSize: MainAxisSize.min,
+              children: [
+                Expanded(
+                  child: TextField(
+                    controller: _encryptionKdfSalt,
+                    readOnly: isJoined,
+                  ),
+                ),
+              ],
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 199 - 0
lib/examples/advanced/set_video_encoder_configuration/set_video_encoder_configuration.dart

@@ -0,0 +1,199 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:agora_rtc_engine_example/components/remote_video_views_widget.dart';
+import 'package:flutter/material.dart';
+
+/// SetVideoEncoderConfiguration Example
+class SetVideoEncoderConfiguration extends StatefulWidget {
+  /// Construct the [SetVideoEncoderConfiguration]
+  const SetVideoEncoderConfiguration({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _SetVideoEncoderConfigurationState();
+}
+
+class _SetVideoEncoderConfigurationState
+    extends State<SetVideoEncoderConfiguration> with KeepRemoteVideoViewsMixin {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+  String channelId = config.channelId;
+  bool isJoined = false;
+  bool switchCamera = true;
+  late TextEditingController _channelIdController;
+  int _selectedDimensionIndex = 0;
+  List<VideoDimensions> dimensions = const [
+    VideoDimensions(width: 640, height: 480),
+    VideoDimensions(width: 480, height: 480),
+    VideoDimensions(width: 480, height: 240),
+  ];
+  @override
+  void initState() {
+    super.initState();
+    _channelIdController = TextEditingController(text: channelId);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {});
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {});
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+    ));
+
+    await _engine.enableVideo();
+    await _engine.startPreview();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _joinChannel() async {
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: channelId,
+        uid: config.uid,
+        options: const ChannelMediaOptions());
+    await setVideoEncoderConfiguration(dim: _selectedDimensionIndex);
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  Future<void> setVideoEncoderConfiguration({int dim = 0}) async {
+    if (dim >= dimensions.length) {
+      logSink.log("Invalid dimension choice!");
+      return;
+    }
+
+    VideoEncoderConfiguration config = VideoEncoderConfiguration(
+      dimensions: dimensions[dim],
+      frameRate: FrameRate.frameRateFps15.value(),
+      bitrate: 0,
+      minBitrate: -1,
+      orientationMode: OrientationMode.orientationModeAdaptive,
+      degradationPreference: DegradationPreference.maintainFramerate,
+      mirrorMode: VideoMirrorModeType.videoMirrorModeAuto,
+    );
+    await _engine.setVideoEncoderConfiguration(config);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final dimesionsMenus = <DropdownMenuItem<int>>[];
+    for (int i = 0; i < dimensions.length; i++) {
+      final e = dimensions[i];
+      dimesionsMenus.add(DropdownMenuItem<int>(
+        value: i,
+        child: Text(
+          'width: ${e.width}, height: ${e.height}',
+          style: const TextStyle(fontSize: 13),
+        ),
+      ));
+    }
+
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return Stack(
+          children: [
+            AgoraVideoView(
+              controller: VideoViewController(
+                  rtcEngine: _engine, canvas: const VideoCanvas(uid: 0)),
+            ),
+            Align(
+              alignment: Alignment.topLeft,
+              child: RemoteVideoViewsWidget(
+                key: keepRemoteVideoViewsKey,
+                rtcEngine: _engine,
+                channelId: _channelIdController.text,
+              ),
+            ),
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisAlignment: MainAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextField(
+              controller: _channelIdController,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+              onChanged: (text) {
+                setState(() {
+                  channelId = text;
+                });
+              },
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            const Text('Video dimensions: '),
+            DropdownButton<int>(
+              value: _selectedDimensionIndex,
+              items: dimesionsMenus,
+              onChanged: (value) {
+                setState(() {
+                  _selectedDimensionIndex = value!;
+                });
+
+                setVideoEncoderConfiguration(dim: _selectedDimensionIndex);
+              },
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 345 - 0
lib/examples/advanced/spatial_audio_with_media_player/spatial_audio_with_media_player.dart

@@ -0,0 +1,345 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+
+/// SpatialAudioWithMediaPlayer Example
+class SpatialAudioWithMediaPlayer extends StatefulWidget {
+  const SpatialAudioWithMediaPlayer({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<SpatialAudioWithMediaPlayer> {
+  late final RtcEngineEx _engine;
+  bool _isReadyPreview = false;
+
+  late final MediaPlayerController _mediaPlayerController;
+
+  late final TextEditingController _textEditingController;
+
+  bool _isUrlOpened = false;
+  bool _isPlaying = false;
+  bool isJoined = false;
+
+  static const int _uid = 123;
+  static const int _uidMpk = 67890;
+
+  late TextEditingController _channelIdController;
+
+  @override
+  void initState() {
+    super.initState();
+    _channelIdController = TextEditingController(text: config.channelId);
+    _textEditingController = TextEditingController(
+        text:
+            'https://agoracdn.s3.us-west-1.amazonaws.com/videos/Agora.io-Interactions.mp4');
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+
+    _dispose();
+  }
+
+  void _dispose() async {
+    await _engine.getLocalSpatialAudioEngine().release();
+    await _mediaPlayerController.dispose();
+    
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngineEx();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+      audioScenario: AudioScenarioType.audioScenarioGameStreaming,
+    ));
+
+    await _engine.getLocalSpatialAudioEngine().initialize();
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+
+        List<double> f1 = [0.0, 0.0, 0.0];
+        List<double> f2 = [1.0, 0.0, 0.0];
+        List<double> f3 = [0.0, 1.0, 0.0];
+        List<double> f4 = [0.0, 0.0, 1.0];
+
+        _engine.getLocalSpatialAudioEngine().updateSelfPositionEx(
+            position: f1,
+            axisForward: f2,
+            axisRight: f3,
+            axisUp: f4,
+            connection: connection);
+
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+    ));
+
+    await _engine.enableAudio();
+    await _engine.enableVideo();
+    await _engine.enableSpatialAudio(true);
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    await _engine.startPreview();
+
+    _mediaPlayerController = MediaPlayerController(
+        rtcEngine: _engine,
+        canvas: const VideoCanvas(
+          uid: 0,
+          sourceType: VideoSourceType.videoSourceMediaPlayer,
+        ));
+    await _mediaPlayerController.initialize();
+    _mediaPlayerController.registerPlayerSourceObserver(
+      MediaPlayerSourceObserver(
+        onCompleted: () {
+          logSink.log('[onCompleted]');
+        },
+        onPlayerSourceStateChanged:
+            (MediaPlayerState state, MediaPlayerError ec) {
+          logSink.log('[onPlayerSourceStateChanged] state: $state ec: $ec');
+          if (state == MediaPlayerState.playerStateOpenCompleted) {
+            debugPrint('src ${_mediaPlayerController.getPlaySrc()}');
+            _mediaPlayerController.play();
+            setState(() {
+              _isUrlOpened = !_isUrlOpened;
+              _isPlaying = !_isPlaying;
+            });
+          }
+        },
+        onPositionChanged: (int position) {
+          logSink.log('[onPositionChanged] position: $position');
+        },
+        onPlayerEvent:
+            (MediaPlayerEvent eventCode, int elapsedTime, String message) {
+          logSink.log(
+              '[onPlayerEvent] eventCode: $eventCode, elapsedTime: $elapsedTime, message: $message');
+        },
+      ),
+    );
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _open() async {
+    if (!_isUrlOpened) {
+      await _mediaPlayerController.open(
+          url: _textEditingController.text, startPos: 0);
+      await _mediaPlayerController.adjustPlayoutVolume(0);
+    } else {
+      if (_isPlaying) {
+        await _mediaPlayerController.stop();
+      } else {
+        await _mediaPlayerController.play();
+      }
+
+      setState(() {
+        _isPlaying = !_isPlaying;
+        _isUrlOpened = !_isUrlOpened;
+      });
+    }
+  }
+
+  void _joinChannel() async {
+    await _engine.joinChannelEx(
+      token: '',
+      connection: RtcConnection(
+        channelId: _channelIdController.text,
+        localUid: _uid,
+      ),
+      options: const ChannelMediaOptions(
+        clientRoleType: ClientRoleType.clientRoleBroadcaster,
+        autoSubscribeAudio: true,
+        autoSubscribeVideo: true,
+        enableAudioRecordingOrPlayout: true,
+        // publishMicrophoneTrack: true,
+        publishCameraTrack: true,
+      ),
+    );
+
+    await _engine.joinChannelEx(
+      token: '',
+      connection: RtcConnection(
+        channelId: _channelIdController.text,
+        localUid: _uidMpk,
+      ),
+      options: ChannelMediaOptions(
+        clientRoleType: ClientRoleType.clientRoleBroadcaster,
+        autoSubscribeAudio: false,
+        autoSubscribeVideo: false,
+        enableAudioRecordingOrPlayout: false,
+        publishMediaPlayerAudioTrack: true,
+        publishMediaPlayerVideoTrack: true,
+        publishMediaPlayerId: _mediaPlayerController.getMediaPlayerId(),
+      ),
+    );
+  }
+
+  void _leaveChannel() async {
+    if (_isPlaying) {
+      _mediaPlayerController.stop();
+
+      setState(() {
+        _isUrlOpened = false;
+        _isPlaying = false;
+      });
+    }
+    await _engine.leaveChannel();
+  }
+
+  Future<void> _onLeftLocationPress() async {
+    List<double> f1 = [0.0, -1.0, 0.0];
+    await _engine.getLocalSpatialAudioEngine().updateRemotePositionEx(
+        uid: _uidMpk,
+        posInfo:
+            RemoteVoicePositionInfo(position: f1, forward: [0.0, 0.0, 0.0]),
+        connection: RtcConnection(
+          channelId: _channelIdController.text,
+          localUid: _uid,
+        ));
+  }
+
+  Future<void> _onRightLocationPress() async {
+    List<double> f1 = [0.0, 1.0, 0.0];
+    await _engine.getLocalSpatialAudioEngine().updateRemotePositionEx(
+        uid: _uidMpk,
+        posInfo:
+            RemoteVoicePositionInfo(position: f1, forward: [0.0, 0.0, 0.0]),
+        connection: RtcConnection(
+          channelId: _channelIdController.text,
+          localUid: _uid,
+        ));
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        final children = <Widget>[
+          Expanded(
+            flex: 1,
+            child: AspectRatio(
+              aspectRatio: 1,
+              child: AgoraVideoView(
+                controller: VideoViewController(
+                  rtcEngine: _engine,
+                  canvas: const VideoCanvas(
+                    uid: 0,
+                    sourceType: VideoSourceType.videoSourceCamera,
+                  ),
+                ),
+              ),
+            ),
+          ),
+          Expanded(
+            child: AspectRatio(
+              aspectRatio: 1,
+              child: _isUrlOpened
+                  ? AgoraVideoView(
+                      controller: _mediaPlayerController,
+                    )
+                  : Container(
+                      color: Colors.grey[200],
+                      child: const Center(
+                        child: Text('MediaPlayer'),
+                      ),
+                    ),
+            ),
+          ),
+        ];
+        if (isLayoutHorizontal) {
+          return Row(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: children,
+          );
+        }
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: children,
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          children: [
+            Column(
+              mainAxisAlignment: MainAxisAlignment.start,
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                TextField(
+                  controller: _channelIdController,
+                  decoration: const InputDecoration(hintText: 'Channel ID'),
+                ),
+              ],
+            ),
+            Row(children: [
+              Expanded(
+                child: TextField(
+                  controller: _textEditingController,
+                  decoration: const InputDecoration(
+                    hintText: 'Media URL',
+                  ),
+                ),
+              ),
+              ElevatedButton(
+                onPressed: !isJoined ? _open : null,
+                child: Text(_isPlaying ? "Stop" : "Play"),
+              ),
+            ]),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: _isPlaying
+                        ? (isJoined ? _leaveChannel : _joinChannel)
+                        : null,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: isJoined ? _onLeftLocationPress : null,
+              child: const Text('Left Location'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: isJoined ? _onRightLocationPress : null,
+              child: const Text('Right Location'),
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 324 - 0
lib/examples/advanced/start_direct_cdn_streaming/start_direct_cdn_streaming.dart

@@ -0,0 +1,324 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+
+/// StartDirectCDNStreaming Example
+class StartDirectCDNStreaming extends StatefulWidget {
+  /// Construct the [JoinChannelVideo]
+  const StartDirectCDNStreaming({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<StartDirectCDNStreaming> {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+
+  bool isJoined = false, switchCamera = true, switchRender = true;
+  Set<int> remoteUid = {};
+  late TextEditingController _controller;
+  late TextEditingController _publishUrlController;
+  late TextEditingController _heightController;
+  late TextEditingController _widthController;
+  late TextEditingController _fpsController;
+  bool _isStartDirectCDNStreaming = false;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: config.channelId);
+    _publishUrlController = TextEditingController(
+        text: 'rtmp://push.alexmk.name/live/agora_rtc_engine');
+    _widthController = TextEditingController(text: '360');
+    _heightController = TextEditingController(text: '240');
+    _fpsController = TextEditingController(text: '30');
+
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    _controller.dispose();
+    _publishUrlController.dispose();
+    _widthController.dispose();
+    _heightController.dispose();
+    _fpsController.dispose();
+
+    if (_isStartDirectCDNStreaming) {
+      await _engine.stopDirectCdnStreaming();
+    }
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUid.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUid.removeWhere((element) => element == rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+          remoteUid.clear();
+        });
+      },
+    ));
+
+    await _engine.enableVideo();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    await _engine.startPreview();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _joinChannel() async {
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: _controller.text,
+        uid: config.uid,
+        options: const ChannelMediaOptions());
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  Future<void> _startDirectCdnStreaming() async {
+    await _engine.startPreview();
+    await _engine.setDirectCdnStreamingAudioConfiguration(
+        AudioProfileType.audioProfileDefault);
+    await _engine
+        .setDirectCdnStreamingVideoConfiguration(VideoEncoderConfiguration(
+      codecType: VideoCodecType.videoCodecH264,
+      dimensions: VideoDimensions(
+        width: int.parse(_widthController.text),
+        height: int.parse(_heightController.text),
+      ),
+      frameRate: int.parse(_fpsController.text),
+      bitrate: 2260,
+      minBitrate: defaultMinBitrate,
+      orientationMode: OrientationMode.orientationModeFixedLandscape,
+      degradationPreference: DegradationPreference.maintainQuality,
+      mirrorMode: VideoMirrorModeType.videoMirrorModeDisabled,
+    ));
+    await _engine.startDirectCdnStreaming(
+        eventHandler: DirectCdnStreamingEventHandler(
+          onDirectCdnStreamingStateChanged: (state, error, message) {
+            logSink.log(
+                '[onDirectCdnStreamingStateChanged] state: $state, error: $error, message: $message');
+            if (state ==
+                DirectCdnStreamingState.directCdnStreamingStateRunning) {
+              setState(() {
+                _isStartDirectCDNStreaming = true;
+              });
+            } else if (state ==
+                    DirectCdnStreamingState.directCdnStreamingStateStopped ||
+                state ==
+                    DirectCdnStreamingState.directCdnStreamingStateFailed) {
+              setState(() {
+                _isStartDirectCDNStreaming = false;
+              });
+            }
+          },
+          onDirectCdnStreamingStats: (DirectCdnStreamingStats stats) {
+            logSink.log('[onDirectCdnStreamingStats] stats: ${stats.toJson()}');
+          },
+        ),
+        publishUrl: _publishUrlController.text,
+        options: const DirectCdnStreamingMediaOptions(
+          publishCameraTrack: true,
+          publishMicrophoneTrack: true,
+        ));
+  }
+
+  Future<void> _stopDirectCdnStreaming() async {
+    if (_isStartDirectCDNStreaming) {
+      await _engine.stopDirectCdnStreaming();
+      setState(() {
+        _isStartDirectCDNStreaming = !_isStartDirectCDNStreaming;
+      });
+    }
+  }
+
+  Future<void> _switchCamera() async {
+    await _engine.switchCamera();
+    setState(() {
+      switchCamera = !switchCamera;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return Stack(
+          children: [
+            AgoraVideoView(
+              controller: VideoViewController(
+                rtcEngine: _engine,
+                canvas: const VideoCanvas(uid: 0),
+              ),
+            ),
+            Align(
+              alignment: Alignment.topLeft,
+              child: SingleChildScrollView(
+                scrollDirection: Axis.horizontal,
+                child: Row(
+                  children: List.of(remoteUid.map(
+                    (e) => SizedBox(
+                      width: 120,
+                      height: 120,
+                      child: AgoraVideoView(
+                        controller: VideoViewController.remote(
+                          rtcEngine: _engine,
+                          canvas: VideoCanvas(uid: e),
+                          connection:
+                              RtcConnection(channelId: _controller.text),
+                        ),
+                      ),
+                    ),
+                  )),
+                ),
+              ),
+            )
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            TextField(
+              controller: _publishUrlController,
+              decoration: const InputDecoration(hintText: 'Publish Url'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            Row(
+              children: [
+                const Text('width: '),
+                Expanded(
+                  child: TextField(
+                    readOnly: _isStartDirectCDNStreaming,
+                    controller: _widthController,
+                    decoration: const InputDecoration(
+                      hintText: 'width',
+                      border: OutlineInputBorder(),
+                    ),
+                  ),
+                ),
+                const SizedBox(
+                  width: 10,
+                ),
+                const Text('heigth: '),
+                Expanded(
+                  child: TextField(
+                    readOnly: _isStartDirectCDNStreaming,
+                    controller: _heightController,
+                    decoration: const InputDecoration(
+                      hintText: 'height',
+                      border: OutlineInputBorder(),
+                    ),
+                  ),
+                ),
+                const SizedBox(
+                  width: 10,
+                ),
+                const Text('fps: '),
+                Expanded(
+                  child: TextField(
+                    readOnly: _isStartDirectCDNStreaming,
+                    controller: _fpsController,
+                    decoration: const InputDecoration(
+                      hintText: 'fps',
+                      border: OutlineInputBorder(),
+                    ),
+                  ),
+                ),
+              ],
+            ),
+            ElevatedButton(
+              onPressed: _isStartDirectCDNStreaming ? null : _switchCamera,
+              child: Text('Camera ${switchCamera ? 'front' : 'rear'}'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: _isStartDirectCDNStreaming
+                        ? _stopDirectCdnStreaming
+                        : _startDirectCdnStreaming,
+                    child: Text(
+                        '${_isStartDirectCDNStreaming ? 'Stop' : 'Start'}DirectCdnStreaming'),
+                  ),
+                )
+              ],
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 656 - 0
lib/examples/advanced/start_local_video_transcoder/start_local_video_transcoder.dart

@@ -0,0 +1,656 @@
+import 'dart:io';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:path/path.dart' as path;
+import 'package:path_provider/path_provider.dart';
+
+/// StartLocalVideoTranscoder Example
+class StartLocalVideoTranscoder extends StatefulWidget {
+  /// Construct the [StartLocalVideoTranscoder]
+  const StartLocalVideoTranscoder({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<StartLocalVideoTranscoder> {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+  late final VideoDeviceManager _videoDeviceManager;
+
+  bool isJoined = false, switchCamera = true, switchRender = true;
+  Set<int> remoteUid = {};
+  late TextEditingController _controller;
+  late TextEditingController _mediaPlayerUrlController;
+  late MediaPlayerController _mediaPlayerController;
+  MediaPlayerSourceObserver? _mediaPlayerSourceObserver;
+  List<TranscodingVideoStream> transcodingVideoStreams = [];
+  List<VideoDeviceInfo> _videoDevices = [];
+  bool _isStartLocalvideoTranscoder = false;
+  bool _isSecondaryCameraSource = false;
+  bool _isPrimaryScreenSource = false;
+  bool _isMediaPlayerSource = false;
+  bool _isRtcImagePngSource = false;
+  bool _isRtcImageJpegSource = false;
+  bool _isRtcImageGifSource = false;
+  String _pngFilePath = '';
+  String _jpgFilePath = '';
+  String _gifFilePath = '';
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: config.channelId);
+    _mediaPlayerUrlController = TextEditingController(
+        text:
+            'https://agoracdn.s3.us-west-1.amazonaws.com/videos/Agora.io-Interactions.mp4');
+
+    _initEngine();
+    _initImageFiles();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    await _stopLocalVideoTranscoder();
+    await _mediaPlayerController.dispose();
+
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUid.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUid.removeWhere((element) => element == rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+          remoteUid.clear();
+        });
+      },
+    ));
+
+    _videoDeviceManager = _engine.getVideoDeviceManager();
+
+    if (!(defaultTargetPlatform == TargetPlatform.android ||
+        defaultTargetPlatform == TargetPlatform.iOS)) {
+      _videoDevices = await _videoDeviceManager.enumerateVideoDevices();
+    }
+
+    _mediaPlayerController = MediaPlayerController(
+        rtcEngine: _engine, canvas: const VideoCanvas(uid: 0));
+    await _mediaPlayerController.initialize();
+
+    await _engine.enableVideo();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    await _engine.startPreview();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _initImageFiles() async {
+    _pngFilePath = await _getFilePath('agora-logo.png');
+    _jpgFilePath = await _getFilePath('jpg.jpg');
+    _gifFilePath = await _getFilePath('gif.gif');
+
+    setState(() {});
+  }
+
+  Future<void> _joinChannel() async {
+    await _engine.joinChannel(
+      token: config.token,
+      channelId: _controller.text,
+      uid: config.uid,
+      options: const ChannelMediaOptions(
+        publishCameraTrack: false,
+        publishSecondaryCameraTrack: false,
+        publishTranscodedVideoTrack: true,
+      ),
+    );
+  }
+
+  Future<void> _leaveChannel() async {
+    // await _stopLocalVideoTranscoder();
+    await _engine.leaveChannel();
+  }
+
+  LocalTranscoderConfiguration _createLocalTranscoderConfiguration() {
+    return LocalTranscoderConfiguration(
+      streamCount: transcodingVideoStreams.length,
+      videoInputStreams: transcodingVideoStreams,
+      videoOutputConfiguration: const VideoEncoderConfiguration(
+        dimensions: VideoDimensions(width: 640, height: 320),
+      ),
+    );
+  }
+
+  Future<String> _getFilePath(String fileName) async {
+    ByteData data = await rootBundle.load("assets/$fileName");
+    List<int> bytes =
+        data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
+
+    Directory appDocDir = await getApplicationDocumentsDirectory();
+    String p = path.join(appDocDir.path, fileName);
+    final file = File(p);
+    if (!(await file.exists())) {
+      await file.create();
+      await file.writeAsBytes(bytes);
+    }
+
+    return p;
+  }
+
+  Future<void> _startLocalVideoTranscoder() async {
+    if (!(defaultTargetPlatform == TargetPlatform.android ||
+        defaultTargetPlatform == TargetPlatform.iOS)) {
+      transcodingVideoStreams.add(const TranscodingVideoStream(
+          sourceType: VideoSourceType.videoSourceCameraPrimary,
+          width: 640,
+          height: 320));
+
+      final config = CameraCapturerConfiguration(
+        format: const VideoFormat(width: 640, height: 320, fps: 30),
+        deviceId: _videoDevices[0].deviceId,
+      );
+
+      await _engine.startCameraCapture(
+          sourceType: VideoSourceType.videoSourceCameraPrimary, config: config);
+    } else {
+      transcodingVideoStreams.add(const TranscodingVideoStream(
+          sourceType: VideoSourceType.videoSourceCameraPrimary,
+          width: 160,
+          height: 320));
+    }
+
+    await _engine
+        .startLocalVideoTranscoder(_createLocalTranscoderConfiguration());
+    await _engine.startPreview(
+        sourceType: VideoSourceType.videoSourceTranscoded);
+  }
+
+  Future<void> _stopLocalVideoTranscoder() async {
+    if (_isMediaPlayerSource) {
+      await _mediaPlayerController.stop();
+    }
+
+    if (_isSecondaryCameraSource) {
+      await _engine
+          .stopCameraCapture(VideoSourceType.videoSourceCameraSecondary);
+    }
+
+    if (_isPrimaryScreenSource) {
+      await _engine.stopScreenCapture();
+    }
+
+    await _engine.stopCameraCapture(VideoSourceType.videoSourceCameraPrimary);
+    await _engine.stopLocalVideoTranscoder();
+    transcodingVideoStreams.clear();
+    _isSecondaryCameraSource = false;
+    _isPrimaryScreenSource = false;
+    _isMediaPlayerSource = false;
+    _isRtcImagePngSource = false;
+    _isRtcImageJpegSource = false;
+    _isRtcImageGifSource = false;
+  }
+
+  Widget _transcoderOptions() {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      mainAxisAlignment: MainAxisAlignment.start,
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        const SizedBox(
+          height: 20,
+        ),
+        const Text('Start Primary Camera Source By Default'),
+        ElevatedButton(
+          onPressed: () async {
+            _isStartLocalvideoTranscoder = !_isStartLocalvideoTranscoder;
+
+            if (_isStartLocalvideoTranscoder) {
+              await _startLocalVideoTranscoder();
+            } else {
+              await _stopLocalVideoTranscoder();
+            }
+
+            setState(() {});
+          },
+          child: Text(
+              '${_isStartLocalvideoTranscoder ? 'Stop' : 'Start'}LocalVideoTranscoder'),
+        ),
+        const SizedBox(
+          height: 20,
+        ),
+        if (!(defaultTargetPlatform == TargetPlatform.android ||
+            defaultTargetPlatform == TargetPlatform.iOS)) ...[
+          Wrap(
+            children: [
+              const Text('SecondaryCameraSource:'),
+              Switch(
+                value: _isSecondaryCameraSource,
+                onChanged: _isStartLocalvideoTranscoder &&
+                        _videoDevices.length >= 2
+                    ? (v) async {
+                        if (!v) {
+                          await _engine.stopCameraCapture(
+                              VideoSourceType.videoSourceCameraSecondary);
+                          transcodingVideoStreams.removeWhere((element) =>
+                              element.sourceType ==
+                              VideoSourceType.videoSourceCameraSecondary);
+                        } else {
+                          transcodingVideoStreams.add(
+                              const TranscodingVideoStream(
+                                  sourceType: VideoSourceType
+                                      .videoSourceCameraSecondary,
+                                  width: 360,
+                                  height: 240));
+
+                          final config = CameraCapturerConfiguration(
+                            format: const VideoFormat(
+                                width: 640, height: 320, fps: 30),
+                            deviceId: _videoDevices[1].deviceId,
+                          );
+
+                          await _engine.startCameraCapture(
+                              sourceType:
+                                  VideoSourceType.videoSourceCameraSecondary,
+                              config: config);
+                        }
+
+                        await _engine.updateLocalTranscoderConfiguration(
+                            _createLocalTranscoderConfiguration());
+
+                        setState(() {
+                          _isSecondaryCameraSource = !_isSecondaryCameraSource;
+                        });
+                      }
+                    : null,
+              )
+            ],
+          ),
+          const SizedBox(
+            height: 20,
+          ),
+          Wrap(
+            children: [
+              const Text('PrimaryScreenSource:'),
+              Switch(
+                value: _isPrimaryScreenSource,
+                onChanged: _isStartLocalvideoTranscoder
+                    ? (v) async {
+                        if (!v) {
+                          await _engine.stopScreenCapture();
+                          transcodingVideoStreams.removeWhere((element) =>
+                              element.sourceType ==
+                              VideoSourceType.videoSourceScreen);
+                        } else {
+                          SIZE t = const SIZE(width: 360, height: 240);
+
+                          SIZE s = const SIZE(width: 360, height: 240);
+
+                          var info = await _engine.getScreenCaptureSources(
+                              thumbSize: t, iconSize: s, includeScreen: true);
+
+                          if (info.isNotEmpty) {
+                            final item = info[0];
+                            if (item.type ==
+                                ScreenCaptureSourceType
+                                    .screencapturesourcetypeWindow) {
+                              await _engine.startScreenCaptureByWindowId(
+                                windowId: item.sourceId!,
+                                regionRect: const Rectangle(
+                                    x: 0, y: 0, width: 0, height: 0),
+                                captureParams: const ScreenCaptureParameters(),
+                              );
+                            } else if (item.type ==
+                                ScreenCaptureSourceType
+                                    .screencapturesourcetypeScreen) {
+                              await _engine.startScreenCaptureByDisplayId(
+                                displayId: item.sourceId!,
+                                regionRect: const Rectangle(
+                                    x: 0, y: 0, width: 0, height: 0),
+                                captureParams: const ScreenCaptureParameters(
+                                    captureMouseCursor: true, frameRate: 30),
+                              );
+                            }
+                            transcodingVideoStreams.add(
+                                const TranscodingVideoStream(
+                                    sourceType:
+                                        VideoSourceType.videoSourceScreen,
+                                    x: 110,
+                                    y: 110,
+                                    width: 200,
+                                    height: 200,
+                                    zOrder: 20));
+                          } else {
+                            logSink.log('No Screen Capture Sources Avaliable');
+                          }
+                        }
+
+                        await _engine.updateLocalTranscoderConfiguration(
+                            _createLocalTranscoderConfiguration());
+
+                        setState(() {
+                          _isPrimaryScreenSource = !_isPrimaryScreenSource;
+                        });
+                      }
+                    : null,
+              )
+            ],
+          ),
+        ],
+        Wrap(
+          children: [
+            const Text('MediaPlayerSource:'),
+            Switch(
+              value: _isMediaPlayerSource,
+              onChanged: _isStartLocalvideoTranscoder
+                  ? (v) async {
+                      if (!v) {
+                        _mediaPlayerController.unregisterPlayerSourceObserver(
+                            _mediaPlayerSourceObserver!);
+                        await _mediaPlayerController.stop();
+                        transcodingVideoStreams.removeWhere((element) =>
+                            element.sourceType ==
+                            VideoSourceType.videoSourceMediaPlayer);
+                      } else {
+                        _mediaPlayerSourceObserver ??=
+                            MediaPlayerSourceObserver(
+                          onPlayerSourceStateChanged: (state, ec) {
+                            if (state ==
+                                MediaPlayerState.playerStateOpenCompleted) {
+                              _mediaPlayerController.play();
+                            }
+                          },
+                        );
+
+                        _mediaPlayerController.registerPlayerSourceObserver(
+                            _mediaPlayerSourceObserver!);
+
+                        _mediaPlayerController.open(
+                          url: _mediaPlayerUrlController.text,
+                          startPos: 0,
+                        );
+
+                        transcodingVideoStreams.add(TranscodingVideoStream(
+                          sourceType: VideoSourceType.videoSourceMediaPlayer,
+                          mediaPlayerId:
+                              _mediaPlayerController.getMediaPlayerId(),
+                          width: 360,
+                          height: 240,
+                          zOrder: 10,
+                          alpha: 1,
+                        ));
+                      }
+
+                      await _engine.updateLocalTranscoderConfiguration(
+                          _createLocalTranscoderConfiguration());
+
+                      setState(() {
+                        _isMediaPlayerSource = !_isMediaPlayerSource;
+                      });
+                    }
+                  : null,
+            )
+          ],
+        ),
+        TextField(
+          controller: _mediaPlayerUrlController,
+          decoration: const InputDecoration(hintText: 'Media Player Url'),
+        ),
+        SizedBox(
+          width: 150,
+          height: 100,
+          child: _isMediaPlayerSource
+              ? AgoraVideoView(
+                  controller: _mediaPlayerController,
+                )
+              : Container(
+                  color: Colors.grey[200],
+                  alignment: Alignment.center,
+                  child: const Text('MediaPlayer source'),
+                ),
+        ),
+        const SizedBox(
+          height: 20,
+        ),
+        Wrap(
+          children: [
+            const Text('RtcImagePngSource:'),
+            Switch(
+              value: _isRtcImagePngSource,
+              onChanged: _isStartLocalvideoTranscoder
+                  ? (v) async {
+                      if (!v) {
+                        transcodingVideoStreams.removeWhere((element) =>
+                            element.sourceType ==
+                            VideoSourceType.videoSourceRtcImagePng);
+                      } else {
+                        transcodingVideoStreams.add(TranscodingVideoStream(
+                            sourceType: VideoSourceType.videoSourceRtcImagePng,
+                            imageUrl: _pngFilePath,
+                            x: 220,
+                            y: 60,
+                            width: 200,
+                            height: 200));
+                      }
+
+                      await _engine.updateLocalTranscoderConfiguration(
+                          _createLocalTranscoderConfiguration());
+
+                      setState(() {
+                        _isRtcImagePngSource = !_isRtcImagePngSource;
+                      });
+                    }
+                  : null,
+            )
+          ],
+        ),
+        if (_pngFilePath.isNotEmpty)
+          SizedBox(
+            height: 100,
+            width: 100,
+            child: Image.file(File(_pngFilePath)),
+          ),
+        const SizedBox(
+          height: 20,
+        ),
+        Wrap(
+          children: [
+            const Text('RtcImageJpegSource:'),
+            Switch(
+              value: _isRtcImageJpegSource,
+              onChanged: _isStartLocalvideoTranscoder
+                  ? (v) async {
+                      if (!v) {
+                        transcodingVideoStreams.removeWhere((element) =>
+                            element.sourceType ==
+                            VideoSourceType.videoSourceRtcImageJpeg);
+                      } else {
+                        transcodingVideoStreams.add(TranscodingVideoStream(
+                            sourceType: VideoSourceType.videoSourceRtcImageJpeg,
+                            imageUrl: _jpgFilePath,
+                            x: 360,
+                            y: 0,
+                            width: 100,
+                            height: 200));
+                      }
+
+                      await _engine.updateLocalTranscoderConfiguration(
+                          _createLocalTranscoderConfiguration());
+
+                      setState(() {
+                        _isRtcImageJpegSource = !_isRtcImageJpegSource;
+                      });
+                    }
+                  : null,
+            )
+          ],
+        ),
+        if (_jpgFilePath.isNotEmpty)
+          SizedBox(
+            height: 100,
+            width: 100,
+            child: Image.file(File(_jpgFilePath)),
+          ),
+        const SizedBox(
+          height: 20,
+        ),
+        Wrap(
+          children: [
+            const Text('RtcImageGifSource:'),
+            Switch(
+              value: _isRtcImageGifSource,
+              onChanged: _isStartLocalvideoTranscoder
+                  ? (v) async {
+                      if (!v) {
+                        transcodingVideoStreams.removeWhere((element) =>
+                            element.sourceType ==
+                            VideoSourceType.videoSourceRtcImageGif);
+                      } else {
+                        transcodingVideoStreams.add(TranscodingVideoStream(
+                          sourceType: VideoSourceType.videoSourceRtcImageGif,
+                          imageUrl: _gifFilePath,
+                          x: 360,
+                          y: 0,
+                          width: 360,
+                          height: 240,
+                        ));
+                      }
+
+                      await _engine.updateLocalTranscoderConfiguration(
+                          _createLocalTranscoderConfiguration());
+
+                      setState(() {
+                        _isRtcImageGifSource = !_isRtcImageGifSource;
+                      });
+                    }
+                  : null,
+            )
+          ],
+        ),
+        if (_gifFilePath.isNotEmpty)
+          SizedBox(
+            height: 100,
+            width: 100,
+            child: Image.file(File(_gifFilePath)),
+          ),
+      ],
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return Stack(
+          children: [
+            AgoraVideoView(
+              controller: VideoViewController(
+                rtcEngine: _engine,
+                canvas: const VideoCanvas(
+                  uid: 0,
+                  sourceType: VideoSourceType.videoSourceTranscoded,
+                  renderMode: RenderModeType.renderModeFit,
+                ),
+              ),
+            ),
+            Align(
+              alignment: Alignment.topLeft,
+              child: SingleChildScrollView(
+                scrollDirection: Axis.horizontal,
+                child: Row(
+                  children: List.of(remoteUid.map(
+                    (e) => SizedBox(
+                      width: 120,
+                      height: 120,
+                      child: AgoraVideoView(
+                        controller: VideoViewController.remote(
+                          rtcEngine: _engine,
+                          canvas: VideoCanvas(uid: e),
+                          connection:
+                              RtcConnection(channelId: _controller.text),
+                        ),
+                      ),
+                    ),
+                  )),
+                ),
+              ),
+            )
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            if (_isReadyPreview) _transcoderOptions(),
+          ],
+        );
+      },
+    );
+  }
+}

+ 224 - 0
lib/examples/advanced/start_rhythm_player/start_rhythm_player.dart

@@ -0,0 +1,224 @@
+// ignore_for_file: unnecessary_brace_in_string_interps
+
+import 'dart:io';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:path/path.dart' as path;
+
+/// StartRhythmPlayer Example
+class StartRhythmPlayer extends StatefulWidget {
+  /// Construct the [StartRhythmPlayer]
+  const StartRhythmPlayer({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<StartRhythmPlayer> {
+  late final RtcEngine _engine;
+
+  bool isJoined = false;
+  late TextEditingController _controller0;
+
+  final Set<int> _remoteUids = {};
+
+  double _beatsPerMeasure = 4;
+  double _beatsPerMinute = 60;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller0 = TextEditingController(text: config.channelId);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    await _engine.stopRhythmPlayer();
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          _remoteUids.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          _remoteUids.remove(rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+    ));
+
+    await _engine.enableAudio();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+  }
+
+  Future<String> _getFilePath(String fileName) async {
+    ByteData data = await rootBundle.load("assets/$fileName");
+    List<int> bytes =
+        data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
+
+    Directory appDocDir = await getApplicationDocumentsDirectory();
+    String p = path.join(appDocDir.path, fileName);
+    final file = File(p);
+    if (!(await file.exists())) {
+      await file.create();
+      await file.writeAsBytes(bytes);
+    }
+
+    return p;
+  }
+
+  void _joinChannel() async {
+    final sound1 = await _getFilePath('dang.mp3');
+    final sound2 = await _getFilePath('ding.mp3');
+    await _engine.startRhythmPlayer(
+      sound1: sound1,
+      sound2: sound2,
+      config: AgoraRhythmPlayerConfig(
+          beatsPerMeasure: _beatsPerMeasure.toInt(),
+          beatsPerMinute: _beatsPerMinute.toInt()),
+    );
+
+    await _engine.joinChannel(
+      token: config.token,
+      channelId: _controller0.text,
+      uid: 0,
+      options: const ChannelMediaOptions(publishRhythmPlayerTrack: true),
+    );
+  }
+
+  _leaveChannel() async {
+    await _engine.stopRhythmPlayer();
+    await _engine.leaveChannel();
+    setState(() {
+      _beatsPerMeasure = 4;
+      _beatsPerMinute = 60;
+    });
+  }
+
+  Widget _buildRhythmPlayerOptions() {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.start,
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Row(
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: [
+            const Text('Beats Per Measure:'),
+            Slider(
+              value: _beatsPerMeasure,
+              min: 1,
+              max: 9,
+              divisions: 9,
+              label: 'Beats Per Measure',
+              onChanged: (double value) async {
+                setState(() {
+                  _beatsPerMeasure = value;
+                });
+                await _engine.configRhythmPlayer(
+                  AgoraRhythmPlayerConfig(
+                      beatsPerMeasure: _beatsPerMeasure.toInt(),
+                      beatsPerMinute: _beatsPerMinute.toInt()),
+                );
+              },
+            )
+          ],
+        ),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: [
+            const Text('Beats Per Minute:'),
+            Slider(
+              value: _beatsPerMinute,
+              min: 60,
+              max: 360,
+              divisions: 60,
+              label: 'Beats Per Minute',
+              onChanged: (double value) async {
+                setState(() {
+                  _beatsPerMinute = value;
+                });
+                await _engine.configRhythmPlayer(
+                  AgoraRhythmPlayerConfig(
+                      beatsPerMeasure: _beatsPerMeasure.toInt(),
+                      beatsPerMinute: _beatsPerMinute.toInt()),
+                );
+              },
+            )
+          ],
+        ),
+      ],
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: [
+        Column(
+          children: [
+            TextField(
+              controller: _controller0,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            if (isJoined) _buildRhythmPlayerOptions(),
+          ],
+        ),
+      ],
+    );
+  }
+}

+ 219 - 0
lib/examples/advanced/stream_message/stream_message.dart

@@ -0,0 +1,219 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:agora_rtc_engine_example/components/remote_video_views_widget.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:permission_handler/permission_handler.dart';
+
+/// StreamMessage Example
+class StreamMessage extends StatefulWidget {
+  /// Construct the [StreamMessage]
+  const StreamMessage({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<StreamMessage> with KeepRemoteVideoViewsMixin {
+  late final RtcEngine _engine;
+  bool _isReadyPreview = false;
+  bool isJoined = false;
+  late final TextEditingController _channelIdController;
+  final TextEditingController _controller = TextEditingController();
+
+  @override
+  void initState() {
+    super.initState();
+    _channelIdController = TextEditingController(text: config.channelId);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+      onStreamMessage: (RtcConnection connection, int remoteUid, int streamId,
+          Uint8List data, int length, int sentTs) {
+        logSink.log(
+            '[onStreamMessage] connection: ${connection.toJson()} remoteUid: $remoteUid, streamId: $streamId, data: $data, length: $length, sentTs: $sentTs');
+        _showMyDialog(remoteUid, streamId, utf8.decode(data));
+      },
+      onStreamMessageError: (RtcConnection connection, int remoteUid,
+          int streamId, ErrorCodeType code, int missed, int cached) {
+        logSink.log(
+            '[onStreamMessageError] connection: ${connection.toJson()} remoteUid: $remoteUid, streamId: $streamId, code: $code, missed: $missed, cached: $cached');
+      },
+    ));
+
+    // enable video module and set up video encoding configs
+    await _engine.enableVideo();
+
+    // make this room live broadcasting room
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _joinChannel() async {
+    if (defaultTargetPlatform == TargetPlatform.android) {
+      await [Permission.microphone, Permission.camera].request();
+    }
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: _channelIdController.text,
+        uid: config.uid,
+        options: const ChannelMediaOptions());
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  Future<void> _showMyDialog(int uid, int streamId, String data) async {
+    return showDialog(
+      context: context,
+      barrierDismissible: false, // user must tap button!
+      builder: (BuildContext context) {
+        return AlertDialog(
+          title: Text('Receive from uid:$uid'),
+          content: SingleChildScrollView(
+            child: ListBody(
+              children: <Widget>[Text('StreamId $streamId:$data')],
+            ),
+          ),
+          actions: <Widget>[
+            TextButton(
+              child: const Text('Ok'),
+              onPressed: () {
+                Navigator.of(context).pop();
+              },
+            ),
+          ],
+        );
+      },
+    );
+  }
+
+  Future<void> _onPressSend() async {
+    if (_controller.text.isEmpty) {
+      return;
+    }
+
+    try {
+      final streamId = await _engine.createDataStream(
+          const DataStreamConfig(syncWithAudio: false, ordered: false));
+      final data = Uint8List.fromList(utf8.encode(_controller.text));
+      await _engine.sendStreamMessage(
+          streamId: streamId, data: data, length: data.length);
+      _controller.clear();
+    } catch (e) {
+      logSink.log('sendStreamMessage error: ${e.toString()}');
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+
+        return Stack(
+          children: [
+            AgoraVideoView(
+              controller: VideoViewController(
+                  rtcEngine: _engine, canvas: const VideoCanvas(uid: 0)),
+            ),
+            Align(
+              alignment: Alignment.topLeft,
+              child: RemoteVideoViewsWidget(
+                key: keepRemoteVideoViewsKey,
+                rtcEngine: _engine,
+                channelId: _controller.text,
+                connectionUid: int.tryParse(_controller.text),
+              ),
+            )
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextField(
+              controller: _channelIdController,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            if (isJoined)
+              Row(
+                mainAxisSize: MainAxisSize.max,
+                children: [
+                  Expanded(
+                      child: TextField(
+                          controller: _controller,
+                          decoration: const InputDecoration(
+                            hintText: 'Input Message',
+                          ))),
+                  ElevatedButton(
+                    onPressed: _onPressSend,
+                    child: const Text('Send'),
+                  ),
+                ],
+              )
+          ],
+        );
+      },
+    );
+  }
+}

+ 333 - 0
lib/examples/advanced/take_snapshot/take_snapshot.dart

@@ -0,0 +1,333 @@
+import 'dart:io';
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:path/path.dart' as path;
+
+/// TakeSnapshot Example
+class TakeSnapshot extends StatefulWidget {
+  /// Construct the [TakeSnapshot]
+  const TakeSnapshot({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<TakeSnapshot> {
+  late final RtcEngine _engine;
+  late final RtcEngineEventHandler _eventHandler;
+  bool _isReadyPreview = false;
+
+  bool isJoined = false, switchCamera = true, switchRender = true;
+  Set<int> remoteUid = {};
+  late TextEditingController _controller;
+  int _selectedUid = config.uid;
+  String _snapshotPath = '';
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: config.channelId);
+
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    _engine.unregisterEventHandler(_eventHandler);
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _eventHandler = RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUid.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUid.removeWhere((element) => element == rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+          remoteUid.clear();
+        });
+      },
+      onSnapshotTaken: (RtcConnection connection, int uid, String filePath,
+          int width, int height, int errCode) async {
+        logSink.log(
+            '[onSnapshotTaken] connection: ${connection.toJson()}, uid: $uid, filePath: $filePath, width: $width, height: $height, errCode: $errCode');
+
+        if (_snapshotPath.isNotEmpty) {
+          final preSnapshotFile = File(_snapshotPath);
+          if (await preSnapshotFile.exists()) {
+            await preSnapshotFile.delete();
+          }
+        }
+
+        setState(() {
+          _snapshotPath = filePath;
+        });
+      },
+    );
+    _engine.registerEventHandler(_eventHandler);
+
+    await _engine.enableVideo();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+
+    await _engine.startPreview();
+
+    setState(() {
+      _isReadyPreview = true;
+    });
+  }
+
+  Future<void> _joinChannel() async {
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: _controller.text,
+        uid: config.uid,
+        options: const ChannelMediaOptions());
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.leaveChannel();
+    setState(() {
+      _snapshotPath = '';
+    });
+  }
+
+  Future<void> _takeSnapshot() async {
+    Directory appDocDir = defaultTargetPlatform == TargetPlatform.android
+        ? (await getExternalStorageDirectory())!
+        : await getApplicationDocumentsDirectory();
+    String p = path.join(appDocDir.path,
+        'snapshot_${_selectedUid}_${DateTime.now().millisecondsSinceEpoch}.jpeg');
+
+    await _engine.takeSnapshot(uid: _selectedUid, filePath: p);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        if (!_isReadyPreview) return Container();
+        return _renderVideo();
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            if (_isReadyPreview)
+              UidDropdown(
+                  rtcEngine: _engine,
+                  initialUid: config.uid,
+                  onUidSelected: (v) {
+                    setState(() {
+                      _selectedUid = v;
+                    });
+                  }),
+            const SizedBox(
+              height: 20,
+            ),
+            ElevatedButton(
+              onPressed: isJoined ? _takeSnapshot : null,
+              child: const Text('Take Snapshot'),
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            _snapshotPath.isNotEmpty
+                ? Image.file(
+                    File(_snapshotPath),
+                    width: 150,
+                    height: 200,
+                  )
+                : Container(
+                    width: 150,
+                    height: 200,
+                    color: Colors.blueGrey.shade100,
+                    child: const Text('Snapshot'),
+                  ),
+          ],
+        );
+      },
+    );
+  }
+
+  Widget _renderVideo() {
+    return Stack(
+      children: [
+        AgoraVideoView(
+          controller: VideoViewController(
+            rtcEngine: _engine,
+            canvas: const VideoCanvas(uid: 0),
+          ),
+        ),
+        Align(
+          alignment: Alignment.topLeft,
+          child: SingleChildScrollView(
+            scrollDirection: Axis.horizontal,
+            child: Row(
+                children: List.of(
+              remoteUid.map((e) => Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    mainAxisAlignment: MainAxisAlignment.start,
+                    children: [
+                      SizedBox(
+                        width: 150,
+                        height: 150,
+                        child: AgoraVideoView(
+                          controller: VideoViewController.remote(
+                            rtcEngine: _engine,
+                            canvas: VideoCanvas(uid: e),
+                            connection:
+                                RtcConnection(channelId: _controller.text),
+                          ),
+                        ),
+                      ),
+                      Container(
+                        color: Colors.white,
+                        child: Text('uid: $e'),
+                      )
+                    ],
+                  )),
+            )),
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+class UidDropdown extends StatefulWidget {
+  const UidDropdown(
+      {Key? key,
+      required this.rtcEngine,
+      required this.initialUid,
+      required this.onUidSelected})
+      : super(key: key);
+
+  final RtcEngine rtcEngine;
+
+  final ValueChanged<int> onUidSelected;
+
+  final int initialUid;
+
+  @override
+  State<UidDropdown> createState() => _UidDropdownState();
+}
+
+class _UidDropdownState extends State<UidDropdown> {
+  late final RtcEngineEventHandler _eventHandler;
+  final Set<int> _remoteUids = {};
+  late int _selectedRemoteUid;
+
+  @override
+  void initState() {
+    super.initState();
+
+    _remoteUids.add(widget.initialUid);
+    _selectedRemoteUid = _remoteUids.first;
+    _eventHandler = RtcEngineEventHandler(
+      onUserJoined: (connection, remoteUid, elapsed) {
+        logSink.log('_UidDropdownState onUserJoined');
+        setState(() {
+          _remoteUids.add(remoteUid);
+        });
+      },
+      onUserOffline: (RtcConnection connection, int remoteUid,
+          UserOfflineReasonType reason) {
+        setState(() {
+          _remoteUids.remove(remoteUid);
+        });
+      },
+    );
+    widget.rtcEngine.registerEventHandler(_eventHandler);
+  }
+
+  @override
+  void dispose() {
+    widget.rtcEngine.unregisterEventHandler(_eventHandler);
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.start,
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        const Text('Uids: '),
+        DropdownButton<int>(
+            items: _remoteUids.map((uid) {
+              return DropdownMenuItem(
+                value: uid,
+                child: Text('$uid'),
+              );
+            }).toList(),
+            value: _selectedRemoteUid,
+            onChanged: (v) {
+              _selectedRemoteUid = v!;
+              widget.onUidSelected(_selectedRemoteUid);
+              setState(() {});
+            }),
+      ],
+    );
+  }
+}

+ 202 - 0
lib/examples/advanced/voice_changer/voice_changer.config.dart

@@ -0,0 +1,202 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+
+/// VoiceChangeConfig
+// ignore: constant_identifier_names
+const VoiceChangeConfig = [
+  {
+    'alertTitle': 'Set Chat Beautifier',
+    'options': [
+      {'text': 'Off', 'type': VoiceBeautifierPreset.voiceBeautifierOff},
+      {
+        'text': 'FemaleFresh',
+        'type': VoiceBeautifierPreset.chatBeautifierFresh
+      },
+      {
+        'text': 'FemaleVitality',
+        'type': VoiceBeautifierPreset.chatBeautifierVitality,
+      },
+      {
+        'text': 'Vigorous',
+        'type': VoiceBeautifierPreset.chatBeautifierMagnetic,
+      },
+    ],
+  },
+  {
+    'alertTitle': 'Set Timbre Transformation',
+    'options': [
+      {'text': 'Off', 'type': VoiceBeautifierPreset.voiceBeautifierOff},
+      {
+        'text': 'Vigorous',
+        'type': VoiceBeautifierPreset.timbreTransformationVigorous,
+      },
+      {'text': 'Deep', 'type': VoiceBeautifierPreset.timbreTransformationDeep},
+      {
+        'text': 'Mellow',
+        'type': VoiceBeautifierPreset.timbreTransformationMellow,
+      },
+      {
+        'text': 'Falsetto',
+        'type': VoiceBeautifierPreset.timbreTransformationFalsetto,
+      },
+      {'text': 'Full', 'type': VoiceBeautifierPreset.timbreTransformationFull},
+      {
+        'text': 'Clear',
+        'type': VoiceBeautifierPreset.timbreTransformationClear
+      },
+      {
+        'text': 'Resounding',
+        'type': VoiceBeautifierPreset.timbreTransformationResounding,
+      },
+      {
+        'text': 'Ringing',
+        'type': VoiceBeautifierPreset.timbreTransformationRinging,
+      },
+    ],
+  },
+  {
+    'alertTitle': 'Set Style Transformation',
+    'options': [
+      {'text': 'Off', 'type': AudioEffectPreset.audioEffectOff},
+      {'text': 'Pop', 'type': AudioEffectPreset.styleTransformationPopular},
+      {'text': 'R&B', 'type': AudioEffectPreset.styleTransformationRnb},
+    ],
+  },
+  {
+    'alertTitle': 'Set Voice Changer',
+    'options': [
+      {'text': 'Off', 'type': AudioEffectPreset.audioEffectOff},
+      {
+        'text': 'FxUncle',
+        'type': AudioEffectPreset.voiceChangerEffectUncle,
+      },
+      {
+        'text': 'Old Man',
+        'type': AudioEffectPreset.voiceChangerEffectOldman,
+      },
+      {
+        'text': 'Baby Boy',
+        'type': AudioEffectPreset.voiceChangerEffectBoy,
+      },
+      {
+        'text': 'FxSister',
+        'type': AudioEffectPreset.voiceChangerEffectSister,
+      },
+      {
+        'text': 'Baby Girl',
+        'type': AudioEffectPreset.voiceChangerEffectGirl,
+      },
+      {
+        'text': 'ZhuBaJie',
+        'type': AudioEffectPreset.voiceChangerEffectPigking,
+      },
+      {'text': 'Hulk', 'type': AudioEffectPreset.voiceChangerEffectHulk},
+    ],
+  },
+  {
+    'alertTitle': 'Set Room Acoustics',
+    'options': [
+      {'text': 'Off', 'type': AudioEffectPreset.audioEffectOff},
+      {'text': 'KTV', 'type': AudioEffectPreset.roomAcousticsKtv},
+      {'text': 'Concert', 'type': AudioEffectPreset.roomAcousticsVocalConcert},
+      {'text': 'Studio', 'type': AudioEffectPreset.roomAcousticsStudio},
+      {'text': 'Phonograph', 'type': AudioEffectPreset.roomAcousticsPhonograph},
+      {
+        'text': 'Virtual Stereo',
+        'type': AudioEffectPreset.roomAcousticsVirtualStereo,
+      },
+      {'text': 'Spacial', 'type': AudioEffectPreset.roomAcousticsSpacial},
+      {'text': 'Ethereal', 'type': AudioEffectPreset.roomAcousticsEthereal},
+      {
+        'text': '3D Voice',
+        'type': AudioEffectPreset.roomAcoustics3dVoice,
+      },
+    ],
+  },
+  {
+    'alertTitle': 'Set Pitch Correction',
+    'options': [
+      {'text': 'Off', 'type': AudioEffectPreset.audioEffectOff},
+      {'text': 'Pitch Correction', 'type': AudioEffectPreset.pitchCorrection},
+    ],
+  },
+];
+
+/// FreqOptions
+// ignore: constant_identifier_names
+const FreqOptions = [
+  {
+    'text': '31Hz',
+    'type': AudioEqualizationBandFrequency.audioEqualizationBand31
+  },
+  {
+    'text': '62Hz',
+    'type': AudioEqualizationBandFrequency.audioEqualizationBand62
+  },
+  {
+    'text': '125Hz',
+    'type': AudioEqualizationBandFrequency.audioEqualizationBand125
+  },
+  {
+    'text': '250Hz',
+    'type': AudioEqualizationBandFrequency.audioEqualizationBand250
+  },
+  {
+    'text': '500Hz',
+    'type': AudioEqualizationBandFrequency.audioEqualizationBand500
+  },
+  {
+    'text': '1KHz',
+    'type': AudioEqualizationBandFrequency.audioEqualizationBand1k
+  },
+  {
+    'text': '2KHz',
+    'type': AudioEqualizationBandFrequency.audioEqualizationBand2k
+  },
+  {
+    'text': '4KHz',
+    'type': AudioEqualizationBandFrequency.audioEqualizationBand4k
+  },
+  {
+    'text': '8KHz',
+    'type': AudioEqualizationBandFrequency.audioEqualizationBand8k
+  },
+  {
+    'text': '16KHz',
+    'type': AudioEqualizationBandFrequency.audioEqualizationBand16k
+  },
+];
+
+/// ReverbKeyOptions
+// ignore: constant_identifier_names
+const ReverbKeyOptions = [
+  {
+    'text': 'Dry Level',
+    'type': AudioReverbType.audioReverbDryLevel,
+    'min': -20.0,
+    'max': 10.0
+  },
+  {
+    'text': 'Wet Level',
+    'type': AudioReverbType.audioReverbWetLevel,
+    'min': -20.0,
+    'max': 10.0
+  },
+  {
+    'text': 'Room Size',
+    'type': AudioReverbType.audioReverbRoomSize,
+    'min': 0.0,
+    'max': 100.0
+  },
+  {
+    'text': 'Wet Delay',
+    'type': AudioReverbType.audioReverbWetDelay,
+    'min': 0.0,
+    'max': 200.0
+  },
+  {
+    'text': 'Strength',
+    'type': AudioReverbType.audioReverbStrength,
+    'min': 0.0,
+    'max': 100.0
+  },
+];

+ 378 - 0
lib/examples/advanced/voice_changer/voice_changer.dart

@@ -0,0 +1,378 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/examples/advanced/voice_changer/voice_changer.config.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/material.dart';
+
+/// VoiceChanger Example
+class VoiceChanger extends StatefulWidget {
+  /// Construct the [VoiceChanger]
+  const VoiceChanger({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<VoiceChanger> {
+  late final RtcEngine _engine;
+  bool isJoined = false;
+  List<int> remoteUids = [];
+  int? uidMySelf;
+  int? selectedVoiceToolBtn;
+  AudioEffectPreset currentAudioEffectPreset = AudioEffectPreset.audioEffectOff;
+
+  bool isEnableSlider1 = false, isEnableSlider2 = false;
+  String sliderTitle1 = '', sliderTitle2 = '';
+  double minimumValue1 = 0,
+      maximumValue1 = 0,
+      minimumValue2 = 0,
+      maximumValue2 = 0;
+  double? sliderValue1, sliderValue2;
+  Map selectedFreq = FreqOptions[0];
+  Map selectedReverbKey = ReverbKeyOptions[0];
+
+  double _voicePitchValue = 0.5;
+  double bandGainValue = 0;
+  double reverbValue = 1;
+
+  late final TextEditingController _channelId;
+  VoiceBeautifierPreset _selectedVoiceBeautifierPreset =
+      VoiceBeautifierPreset.voiceBeautifierOff;
+
+  AudioEffectPreset _selectedAudioEffectPreset =
+      AudioEffectPreset.audioEffectOff;
+
+  AudioReverbType _selectedAudioReverbType =
+      AudioReverbType.audioReverbDryLevel;
+
+  final List<VoiceBeautifierPreset> _voiceBeautifierPresets = [
+    VoiceBeautifierPreset.voiceBeautifierOff,
+    VoiceBeautifierPreset.chatBeautifierMagnetic,
+    VoiceBeautifierPreset.chatBeautifierFresh,
+  ];
+
+  final List<AudioEffectPreset> _audioEffectPresets = [
+    AudioEffectPreset.audioEffectOff,
+    AudioEffectPreset.roomAcousticsKtv,
+    AudioEffectPreset.roomAcousticsVocalConcert,
+  ];
+
+  final List<AudioReverbType> _audioReverbTypes = [
+    AudioReverbType.audioReverbDryLevel,
+    AudioReverbType.audioReverbWetLevel,
+    AudioReverbType.audioReverbRoomSize,
+  ];
+
+  final Map<AudioReverbType, List<double>> _audioReverbTypeRanges = {
+    AudioReverbType.audioReverbDryLevel: [-20.0, 10.0, 0.0],
+    AudioReverbType.audioReverbWetLevel: [-20.0, 10.0, 0.0],
+    AudioReverbType.audioReverbRoomSize: [0.0, 100.0, 0.0],
+  };
+
+  late double _selectedAudioReverbTypeValue;
+
+  final List<AudioEqualizationBandFrequency> _audioEqualizationBandFrequencys =
+      [
+    AudioEqualizationBandFrequency.audioEqualizationBand31,
+    AudioEqualizationBandFrequency.audioEqualizationBand62,
+    AudioEqualizationBandFrequency.audioEqualizationBand125,
+  ];
+
+  late AudioEqualizationBandFrequency _selectedAudioEqualizationBandFrequencys;
+
+  late double _selectedAudioEqualizationBandFrequencyValue;
+
+  bool _setVoiceBeautifierPresetOnly = false;
+
+  @override
+  void initState() {
+    super.initState();
+    _channelId = TextEditingController(text: config.channelId);
+
+    _selectedVoiceBeautifierPreset = _voiceBeautifierPresets[0];
+    _selectedAudioEffectPreset = _audioEffectPresets[0];
+    _selectedAudioReverbType = _audioReverbTypes[0];
+    _selectedAudioReverbTypeValue =
+        _audioReverbTypeRanges[_selectedAudioReverbType]![2];
+    _selectedAudioEqualizationBandFrequencys =
+        _audioEqualizationBandFrequencys[0];
+    _selectedAudioEqualizationBandFrequencyValue = 0.0;
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {});
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {});
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+    ));
+
+    await _engine.enableAudio();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: _channelId.text,
+        uid: 0,
+        options: const ChannelMediaOptions(autoSubscribeAudio: true));
+  }
+
+  DropdownButton _createDropdownButton<T>(
+      List<T> enums, ValueGetter value, ValueChanged<T?> onChanged) {
+    return DropdownButton<T>(
+        items: enums.map((e) {
+          return DropdownMenuItem(
+            value: e,
+            child: Text('$e'),
+          );
+        }).toList(),
+        value: value(),
+        onChanged: onChanged);
+  }
+
+  Widget _presets() {
+    if (_setVoiceBeautifierPresetOnly) {
+      return Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          const Text('Select VoiceBeautifierPreset: '),
+          _createDropdownButton<VoiceBeautifierPreset>(
+            _voiceBeautifierPresets,
+            () => _selectedVoiceBeautifierPreset,
+            (v) async {
+              setState(() {
+                _selectedVoiceBeautifierPreset = v!;
+              });
+            },
+          ),
+          ElevatedButton(
+            onPressed: () async {
+              await _engine
+                  .setVoiceBeautifierPreset(_selectedVoiceBeautifierPreset);
+            },
+            child: const Text('setVoiceBeautifierPreset'),
+          ),
+        ],
+      );
+    }
+
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            const Text('Select AudioEffectPreset: '),
+            _createDropdownButton<AudioEffectPreset>(
+              _audioEffectPresets,
+              () => _selectedAudioEffectPreset,
+              (v) async {
+                setState(() {
+                  _selectedAudioEffectPreset = v!;
+                });
+              },
+            ),
+          ],
+        ),
+        ElevatedButton(
+          onPressed: () async {
+            await _engine.setAudioEffectPreset(_selectedAudioEffectPreset);
+          },
+          child: const Text('setAudioEffectPreset'),
+        ),
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            const Text('Select AudioReverbType: '),
+            _createDropdownButton<AudioReverbType>(
+              _audioReverbTypes,
+              () => _selectedAudioReverbType,
+              (v) {
+                setState(() {
+                  _selectedAudioReverbType = v!;
+                  _selectedAudioReverbTypeValue =
+                      _audioReverbTypeRanges[_selectedAudioReverbType]![2];
+                });
+              },
+            ),
+          ],
+        ),
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            const Text('Select Local Voice Reverb Value: '),
+            Slider(
+              value: _selectedAudioReverbTypeValue,
+              min: _audioReverbTypeRanges[_selectedAudioReverbType]![0],
+              max: _audioReverbTypeRanges[_selectedAudioReverbType]![1],
+              divisions: 10,
+              label: 'AudioReverbType Value $_selectedAudioReverbTypeValue',
+              onChanged: (double value) {
+                setState(() {
+                  _selectedAudioReverbTypeValue = value;
+                });
+              },
+            ),
+          ],
+        ),
+        ElevatedButton(
+            onPressed: () async {
+              await _engine.setLocalVoiceReverb(
+                  reverbKey: _selectedAudioReverbType,
+                  value: _selectedAudioReverbTypeValue.toInt());
+            },
+            child: const Text('setLocalVoiceReverb')),
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            const Text('Select AudioEqualizationBandFrequency: '),
+            _createDropdownButton<AudioEqualizationBandFrequency>(
+              _audioEqualizationBandFrequencys,
+              () => _selectedAudioEqualizationBandFrequencys,
+              (v) async {
+                setState(() {
+                  _selectedAudioEqualizationBandFrequencys = v!;
+                });
+              },
+            ),
+          ],
+        ),
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            const Text('Select AudioEqualizationBandFrequency Value:'),
+            Slider(
+              value: _selectedAudioEqualizationBandFrequencyValue,
+              min: -15.0,
+              max: 15.0,
+              divisions: 10,
+              label:
+                  'AudioEqualizationBandFrequency Value: $_selectedAudioEqualizationBandFrequencyValue',
+              onChanged: (double value) {
+                setState(() {
+                  _selectedAudioEqualizationBandFrequencyValue = value;
+                });
+              },
+            ),
+          ],
+        ),
+        ElevatedButton(
+            onPressed: () async {
+              await _engine.setLocalVoiceEqualization(
+                  bandFrequency: _selectedAudioEqualizationBandFrequencys,
+                  bandGain:
+                      _selectedAudioEqualizationBandFrequencyValue.toInt());
+            },
+            child: const Text('setLocalVoiceEqualization')),
+        Row(
+          children: [
+            const Text('Pitch:'),
+            Slider(
+              value: _voicePitchValue,
+              min: 0.5,
+              max: 2,
+              divisions: 10,
+              label: 'Pitch $_voicePitchValue',
+              onChanged: (double value) {
+                setState(() {
+                  _voicePitchValue = value;
+                });
+              },
+            )
+          ],
+        ),
+        ElevatedButton(
+          onPressed: () async {
+            await _engine.setLocalVoicePitch(_voicePitchValue);
+          },
+          child: const Text('setLocalVoicePitch'),
+        ),
+      ],
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: [
+        Column(
+          children: [
+            if (!isJoined)
+              Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  TextField(
+                    controller: _channelId,
+                  ),
+                  Row(mainAxisSize: MainAxisSize.min, children: [
+                    const Text('setVoiceBeautifierPreset Only'),
+                    Switch(
+                      value: _setVoiceBeautifierPresetOnly,
+                      onChanged: !isJoined
+                          ? (v) {
+                              setState(() {
+                                _setVoiceBeautifierPresetOnly = v;
+                              });
+                            }
+                          : null,
+                    )
+                  ]),
+                  Row(
+                    children: [
+                      Expanded(
+                        flex: 1,
+                        child: ElevatedButton(
+                          onPressed: _initEngine,
+                          child: const Text('Join channel'),
+                        ),
+                      )
+                    ],
+                  ),
+                ],
+              ),
+            if (isJoined)
+              Expanded(
+                  child: SingleChildScrollView(
+                child: _presets(),
+              )),
+          ],
+        ),
+      ],
+    );
+  }
+}

+ 12 - 0
lib/examples/basic/index.dart

@@ -0,0 +1,12 @@
+import 'package:agora_rtc_engine_example/examples/basic/string_uid/string_uid.dart';
+
+import 'join_channel_audio/join_channel_audio.dart';
+import 'join_channel_video/join_channel_video.dart';
+
+/// Data source for basic examples
+final basic = [
+  {'name': 'Basic'},
+  {'name': 'JoinChannelAudio', 'widget': const JoinChannelAudio()},
+  {'name': 'JoinChannelVideo', 'widget': const JoinChannelVideo()},
+  {'name': 'StringUid', 'widget': const StringUid()}
+];

+ 321 - 0
lib/examples/basic/join_channel_audio/join_channel_audio.dart

@@ -0,0 +1,321 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:permission_handler/permission_handler.dart';
+
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+
+/// JoinChannelAudio Example
+class JoinChannelAudio extends StatefulWidget {
+  /// Construct the [JoinChannelAudio]
+  const JoinChannelAudio({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<JoinChannelAudio> {
+  late final RtcEngine _engine;
+  String channelId = config.channelId;
+  bool isJoined = false,
+      openMicrophone = true,
+      enableSpeakerphone = true,
+      playEffect = false;
+  bool _enableInEarMonitoring = false;
+  double _recordingVolume = 100,
+      _playbackVolume = 100,
+      _inEarMonitoringVolume = 100;
+  late TextEditingController _controller;
+  ChannelProfileType _channelProfileType =
+      ChannelProfileType.channelProfileLiveBroadcasting;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: channelId);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+    ));
+
+    await _engine.enableAudio();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+    await _engine.setAudioProfile(
+      profile: AudioProfileType.audioProfileDefault,
+      scenario: AudioScenarioType.audioScenarioGameStreaming,
+    );
+  }
+
+  _joinChannel() async {
+    if (defaultTargetPlatform == TargetPlatform.android) {
+      await Permission.microphone.request();
+    }
+
+    await _engine.joinChannel(
+        token: config.token,
+        channelId: _controller.text,
+        uid: config.uid,
+        options: ChannelMediaOptions(
+          channelProfile: _channelProfileType,
+          clientRoleType: ClientRoleType.clientRoleBroadcaster,
+        ));
+  }
+
+  _leaveChannel() async {
+    await _engine.leaveChannel();
+    setState(() {
+      isJoined = false;
+      openMicrophone = true;
+      enableSpeakerphone = true;
+      playEffect = false;
+      _enableInEarMonitoring = false;
+      _recordingVolume = 100;
+      _playbackVolume = 100;
+      _inEarMonitoringVolume = 100;
+    });
+  }
+
+  _switchMicrophone() async {
+    // await await _engine.muteLocalAudioStream(!openMicrophone);
+    await _engine.enableLocalAudio(!openMicrophone);
+    setState(() {
+      openMicrophone = !openMicrophone;
+    });
+  }
+
+  _switchSpeakerphone() async {
+    await _engine.setEnableSpeakerphone(!enableSpeakerphone);
+    setState(() {
+      enableSpeakerphone = !enableSpeakerphone;
+    });
+  }
+
+  _switchEffect() async {
+    if (playEffect) {
+      await _engine.stopEffect(1);
+      setState(() {
+        playEffect = false;
+      });
+    } else {
+      final path =
+          (await _engine.getAssetAbsolutePath("assets/Sound_Horizon.mp3"))!;
+      await _engine.playEffect(
+          soundId: 1,
+          filePath: path,
+          loopCount: 0,
+          pitch: 1,
+          pan: 1,
+          gain: 100,
+          publish: true);
+      // .then((value) {
+      setState(() {
+        playEffect = true;
+      });
+    }
+  }
+
+  _onChangeInEarMonitoringVolume(double value) async {
+    _inEarMonitoringVolume = value;
+    await _engine.setInEarMonitoringVolume(_inEarMonitoringVolume.toInt());
+    setState(() {});
+  }
+
+  _toggleInEarMonitoring(value) async {
+    try {
+      await _engine.enableInEarMonitoring(
+          enabled: value,
+          includeAudioFilters: EarMonitoringFilterType.earMonitoringFilterNone);
+      _enableInEarMonitoring = value;
+      setState(() {});
+    } catch (e) {
+      // Do nothing
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final channelProfileType = [
+      ChannelProfileType.channelProfileLiveBroadcasting,
+      ChannelProfileType.channelProfileCommunication,
+    ];
+    final items = channelProfileType
+        .map((e) => DropdownMenuItem(
+              child: Text(
+                e.toString().split('.')[1],
+              ),
+              value: e,
+            ))
+        .toList();
+
+    return Stack(
+      children: [
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            const Text('Channel Profile: '),
+            DropdownButton<ChannelProfileType>(
+                items: items,
+                value: _channelProfileType,
+                onChanged: isJoined
+                    ? null
+                    : (v) async {
+                        setState(() {
+                          _channelProfileType = v!;
+                        });
+                      }),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+          ],
+        ),
+        Align(
+            alignment: Alignment.bottomRight,
+            child: Padding(
+              child: Column(
+                mainAxisSize: MainAxisSize.min,
+                crossAxisAlignment: CrossAxisAlignment.end,
+                children: [
+                  ElevatedButton(
+                    onPressed: _switchMicrophone,
+                    child: Text('Microphone ${openMicrophone ? 'on' : 'off'}'),
+                  ),
+                  ElevatedButton(
+                    onPressed: isJoined ? _switchSpeakerphone : null,
+                    child:
+                        Text(enableSpeakerphone ? 'Speakerphone' : 'Earpiece'),
+                  ),
+                  if (!kIsWeb)
+                    ElevatedButton(
+                      onPressed: isJoined ? _switchEffect : null,
+                      child: Text('${playEffect ? 'Stop' : 'Play'} effect'),
+                    ),
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.end,
+                    children: [
+                      const Text('RecordingVolume:'),
+                      Slider(
+                        value: _recordingVolume,
+                        min: 0,
+                        max: 400,
+                        divisions: 5,
+                        label: 'RecordingVolume',
+                        onChanged: isJoined
+                            ? (double value) async {
+                                setState(() {
+                                  _recordingVolume = value;
+                                });
+                                await _engine
+                                    .adjustRecordingSignalVolume(value.toInt());
+                              }
+                            : null,
+                      )
+                    ],
+                  ),
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.end,
+                    children: [
+                      const Text('PlaybackVolume:'),
+                      Slider(
+                        value: _playbackVolume,
+                        min: 0,
+                        max: 400,
+                        divisions: 5,
+                        label: 'PlaybackVolume',
+                        onChanged: isJoined
+                            ? (double value) async {
+                                setState(() {
+                                  _playbackVolume = value;
+                                });
+                                await _engine
+                                    .adjustPlaybackSignalVolume(value.toInt());
+                              }
+                            : null,
+                      )
+                    ],
+                  ),
+                  Column(
+                    mainAxisSize: MainAxisSize.min,
+                    crossAxisAlignment: CrossAxisAlignment.end,
+                    children: [
+                      Row(mainAxisSize: MainAxisSize.min, children: [
+                        const Text('InEar Monitoring Volume:'),
+                        Switch(
+                          value: _enableInEarMonitoring,
+                          onChanged: isJoined ? _toggleInEarMonitoring : null,
+                          activeTrackColor: Colors.grey[350],
+                          activeColor: Colors.white,
+                        )
+                      ]),
+                      if (_enableInEarMonitoring)
+                        SizedBox(
+                            width: 300,
+                            child: Slider(
+                              value: _inEarMonitoringVolume,
+                              min: 0,
+                              max: 100,
+                              divisions: 5,
+                              label:
+                                  'InEar Monitoring Volume $_inEarMonitoringVolume',
+                              onChanged: isJoined
+                                  ? _onChangeInEarMonitoringVolume
+                                  : null,
+                            ))
+                    ],
+                  ),
+                ],
+              ),
+              padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
+            ))
+      ],
+    );
+  }
+}

+ 291 - 0
lib/examples/basic/join_channel_video/join_channel_video.dart

@@ -0,0 +1,291 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/components/basic_video_configuration_widget.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+/// MultiChannel Example
+class JoinChannelVideo extends StatefulWidget {
+  /// Construct the [JoinChannelVideo]
+  const JoinChannelVideo({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<JoinChannelVideo> {
+  late final RtcEngine _engine;
+
+  bool isJoined = false, switchCamera = true, switchRender = true;
+  Set<int> remoteUid = {};
+  late TextEditingController _controller;
+  bool _isUseFlutterTexture = false;
+  bool _isUseAndroidSurfaceView = false;
+  ChannelProfileType _channelProfileType =
+      ChannelProfileType.channelProfileLiveBroadcasting;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: config.channelId);
+
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
+        logSink.log(
+            '[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
+        setState(() {
+          remoteUid.add(rUid);
+        });
+      },
+      onUserOffline:
+          (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
+        logSink.log(
+            '[onUserOffline] connection: ${connection.toJson()}  rUid: $rUid reason: $reason');
+        setState(() {
+          remoteUid.removeWhere((element) => element == rUid);
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+          remoteUid.clear();
+        });
+      },
+    ));
+
+    await _engine.enableVideo();
+  }
+
+  Future<void> _joinChannel() async {
+    await _engine.joinChannel(
+      token: config.token,
+      channelId: _controller.text,
+      uid: config.uid,
+      options: ChannelMediaOptions(
+        channelProfile: _channelProfileType,
+        clientRoleType: ClientRoleType.clientRoleBroadcaster,
+      ),
+    );
+  }
+
+  Future<void> _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  Future<void> _switchCamera() async {
+    await _engine.switchCamera();
+    setState(() {
+      switchCamera = !switchCamera;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ExampleActionsWidget(
+      displayContentBuilder: (context, isLayoutHorizontal) {
+        return Stack(
+          children: [
+            AgoraVideoView(
+              controller: VideoViewController(
+                rtcEngine: _engine,
+                canvas: const VideoCanvas(uid: 0),
+                useFlutterTexture: _isUseFlutterTexture,
+                useAndroidSurfaceView: _isUseAndroidSurfaceView,
+              ),
+              onAgoraVideoViewCreated: (viewId) {
+                _engine.startPreview();
+              },
+            ),
+            Align(
+              alignment: Alignment.topLeft,
+              child: SingleChildScrollView(
+                scrollDirection: Axis.horizontal,
+                child: Row(
+                  children: List.of(remoteUid.map(
+                    (e) => SizedBox(
+                      width: 120,
+                      height: 120,
+                      child: AgoraVideoView(
+                        controller: VideoViewController.remote(
+                          rtcEngine: _engine,
+                          canvas: VideoCanvas(uid: e),
+                          connection:
+                              RtcConnection(channelId: _controller.text),
+                          useFlutterTexture: _isUseFlutterTexture,
+                          useAndroidSurfaceView: _isUseAndroidSurfaceView,
+                        ),
+                      ),
+                    ),
+                  )),
+                ),
+              ),
+            )
+          ],
+        );
+      },
+      actionsBuilder: (context, isLayoutHorizontal) {
+        final channelProfileType = [
+          ChannelProfileType.channelProfileLiveBroadcasting,
+          ChannelProfileType.channelProfileCommunication,
+        ];
+        final items = channelProfileType
+            .map((e) => DropdownMenuItem(
+                  child: Text(
+                    e.toString().split('.')[1],
+                  ),
+                  value: e,
+                ))
+            .toList();
+
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextField(
+              controller: _controller,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            if (!kIsWeb &&
+                (defaultTargetPlatform == TargetPlatform.android ||
+                    defaultTargetPlatform == TargetPlatform.iOS))
+              Row(
+                mainAxisSize: MainAxisSize.min,
+                mainAxisAlignment: MainAxisAlignment.start,
+                children: [
+                  if (defaultTargetPlatform == TargetPlatform.iOS)
+                    Column(
+                        mainAxisAlignment: MainAxisAlignment.start,
+                        crossAxisAlignment: CrossAxisAlignment.start,
+                        mainAxisSize: MainAxisSize.min,
+                        children: [
+                          const Text('Rendered by Flutter texture: '),
+                          Switch(
+                            value: _isUseFlutterTexture,
+                            onChanged: isJoined
+                                ? null
+                                : (changed) {
+                                    setState(() {
+                                      _isUseFlutterTexture = changed;
+                                    });
+                                  },
+                          )
+                        ]),
+                  if (defaultTargetPlatform == TargetPlatform.android)
+                    Column(
+                        mainAxisAlignment: MainAxisAlignment.start,
+                        crossAxisAlignment: CrossAxisAlignment.start,
+                        mainAxisSize: MainAxisSize.min,
+                        children: [
+                          const Text('Rendered by Android SurfaceView: '),
+                          Switch(
+                            value: _isUseAndroidSurfaceView,
+                            onChanged: isJoined
+                                ? null
+                                : (changed) {
+                                    setState(() {
+                                      _isUseAndroidSurfaceView = changed;
+                                    });
+                                  },
+                          ),
+                        ]),
+                ],
+              ),
+            const SizedBox(
+              height: 20,
+            ),
+            const Text('Channel Profile: '),
+            DropdownButton<ChannelProfileType>(
+              items: items,
+              value: _channelProfileType,
+              onChanged: isJoined
+                  ? null
+                  : (v) {
+                      setState(() {
+                        _channelProfileType = v!;
+                      });
+                    },
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            BasicVideoConfigurationWidget(
+              rtcEngine: _engine,
+              title: 'Video Encoder Configuration',
+              setConfigButtonText: const Text(
+                'setVideoEncoderConfiguration',
+                style: TextStyle(fontSize: 10),
+              ),
+              onConfigChanged: (width, height, frameRate, bitrate) {
+                _engine.setVideoEncoderConfiguration(VideoEncoderConfiguration(
+                  dimensions: VideoDimensions(width: width, height: height),
+                  frameRate: frameRate,
+                  bitrate: bitrate,
+                ));
+              },
+            ),
+            const SizedBox(
+              height: 20,
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+            if (defaultTargetPlatform == TargetPlatform.android ||
+                defaultTargetPlatform == TargetPlatform.iOS) ...[
+              const SizedBox(
+                height: 20,
+              ),
+              ElevatedButton(
+                onPressed: _switchCamera,
+                child: Text('Camera ${switchCamera ? 'front' : 'rear'}'),
+              ),
+            ],
+          ],
+        );
+      },
+    );
+    // if (!_isInit) return Container();
+  }
+}

+ 142 - 0
lib/examples/basic/string_uid/string_uid.dart

@@ -0,0 +1,142 @@
+// ignore_for_file: unnecessary_brace_in_string_interps
+
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
+import 'package:agora_rtc_engine_example/components/log_sink.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:permission_handler/permission_handler.dart';
+
+/// StringUid Example
+class StringUid extends StatefulWidget {
+  /// Construct the [StringUid]
+  const StringUid({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _State();
+}
+
+class _State extends State<StringUid> {
+  late final RtcEngine _engine;
+  bool isJoined = false;
+  late TextEditingController _controller0, _controller1;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller0 = TextEditingController(text: config.channelId);
+    _controller1 = TextEditingController(text: config.stringUid);
+    _initEngine();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _dispose();
+  }
+
+  Future<void> _dispose() async {
+    await _engine.leaveChannel();
+    await _engine.release();
+  }
+
+  Future<void> _initEngine() async {
+    _engine = createAgoraRtcEngine();
+    await _engine.initialize(RtcEngineContext(
+      appId: config.appId,
+      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+    ));
+
+    _engine.registerEventHandler(RtcEngineEventHandler(
+      onError: (ErrorCodeType err, String msg) {
+        logSink.log('[onError] err: $err, msg: $msg');
+      },
+      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
+        logSink.log(
+            '[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
+        setState(() {
+          isJoined = true;
+        });
+      },
+      onLeaveChannel: (RtcConnection connection, RtcStats stats) {
+        logSink.log(
+            '[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
+        setState(() {
+          isJoined = false;
+        });
+      },
+    ));
+
+    await _engine.enableAudio();
+    await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
+  }
+
+  void _joinChannel() async {
+    if (defaultTargetPlatform == TargetPlatform.android) {
+      await Permission.microphone.request();
+    }
+    await _engine.joinChannelWithUserAccount(
+        token: config.token,
+        channelId: _controller0.text,
+        userAccount: _controller1.text);
+  }
+
+  _leaveChannel() async {
+    await _engine.leaveChannel();
+  }
+
+  _getUserInfo() async {
+    final userInfo = await _engine.getUserInfoByUserAccount(_controller1.text);
+    // .then((userInfo) {
+    logSink.log('getUserInfoByUserAccount ${userInfo.toJson()}');
+    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+      content: Text('${userInfo.toJson()}'),
+    ));
+    // }).catchError((err) {
+    //   logSink.log('getUserInfoByUserAccount ${err}');
+    // });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: [
+        Column(
+          children: [
+            TextField(
+              controller: _controller0,
+              decoration: const InputDecoration(hintText: 'Channel ID'),
+            ),
+            TextField(
+              controller: _controller1,
+              decoration: const InputDecoration(hintText: 'String User ID'),
+            ),
+            Row(
+              children: [
+                Expanded(
+                  flex: 1,
+                  child: ElevatedButton(
+                    onPressed: isJoined ? _leaveChannel : _joinChannel,
+                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
+                  ),
+                )
+              ],
+            ),
+          ],
+        ),
+        Align(
+          alignment: Alignment.bottomRight,
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              ElevatedButton(
+                onPressed: _getUserInfo,
+                child: const Text('Get userInfo'),
+              ),
+            ],
+          ),
+        )
+      ],
+    );
+  }
+}

+ 110 - 39
lib/main.dart

@@ -1,59 +1,130 @@
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
+import 'package:permission_handler/permission_handler.dart';
 
-import 'pages/advanced/index.dart';
-import 'pages/basic/index.dart';
+import 'examples/advanced/index.dart';
+import 'examples/basic/index.dart';
+import 'config/agora.config.dart' as config;
+import 'components/log_sink.dart';
 
-void main() => runApp(MyApp());
+void main() => runApp(const MyApp());
 
 /// This widget is the root of your application.
-class MyApp extends StatelessWidget {
+class MyApp extends StatefulWidget {
+  /// Construct the [MyApp]
+  const MyApp({Key? key}) : super(key: key);
 
-  final _DATA = [...Basic, ...Advanced];
+  @override
+  State<MyApp> createState() => _MyAppState();
+}
+
+class _MyAppState extends State<MyApp> {
+  final _data = [...basic, ...advanced];
+
+  bool _showPerformanceOverlay = false;
+
+  bool _isConfigInvalid() {
+    return config.appId == '<YOUR_APP_ID>' ||
+        config.token == '<YOUR_TOKEN>' ||
+        config.channelId == '<YOUR_CHANNEL_ID>';
+  }
 
   @override
-  Widget build(BuildContext context) {
+  void initState() {
+    super.initState();
+
+    _requestPermissionIfNeed();
+  }
+
+  Future<void> _requestPermissionIfNeed() async {
+    if (defaultTargetPlatform == TargetPlatform.android) {
+      await [Permission.microphone, Permission.camera].request();
+    }
+  }
 
+  @override
+  Widget build(BuildContext context) {
     return MaterialApp(
-      debugShowCheckedModeBanner: false,
+      showPerformanceOverlay: _showPerformanceOverlay,
       theme: ThemeData(
         primarySwatch: Colors.blue,
       ),
       home: Scaffold(
         appBar: AppBar(
-          title: const Text('玻璃心'),
-        ),
-        body: ListView.builder(
-          itemCount: _DATA.length,
-          itemBuilder: (context, index) {
-            return _DATA[index]['widget'] == null
-                ? Ink(
-                    color: Colors.grey,
-                    child: ListTile(
-                      title: Text(_DATA[index]['name'] as String,
-                          style: TextStyle(fontSize: 24, color: Colors.white)),
-                    ),
-                  )
-                : ListTile(
-                    onTap: () {
-                      Navigator.push(
-                          context,
-                          MaterialPageRoute(
-                              builder: (context) => Scaffold(
-                                    appBar: AppBar(
-                                      title:
-                                          Text(_DATA[index]['name'] as String),
-                                    ),
-                                    body: _DATA[index]['widget'] as Widget?,
-                                  )));
-                    },
-                    title: Text(
-                      _DATA[index]['name'] as String,
-                      style: TextStyle(fontSize: 24, color: Colors.black),
-                    ),
-                  );
-          },
+          title: const Text('APIExample'),
+          actions: [
+            ToggleButtons(
+              color: Colors.grey[300],
+              selectedColor: Colors.white,
+              renderBorder: false,
+              children: const [
+                Icon(
+                  Icons.data_thresholding_outlined,
+                )
+              ],
+              isSelected: [_showPerformanceOverlay],
+              onPressed: (index) {
+                setState(() {
+                  _showPerformanceOverlay = !_showPerformanceOverlay;
+                });
+              },
+            )
+          ],
         ),
+        body: _isConfigInvalid()
+            ? const InvalidConfigWidget()
+            : ListView.builder(
+                itemCount: _data.length,
+                itemBuilder: (context, index) {
+                  return _data[index]['widget'] == null
+                      ? Ink(
+                          color: Colors.grey,
+                          child: ListTile(
+                            title: Text(_data[index]['name'] as String,
+                                style: const TextStyle(
+                                    fontSize: 24, color: Colors.white)),
+                          ),
+                        )
+                      : ListTile(
+                          onTap: () {
+                            Navigator.push(
+                                context,
+                                MaterialPageRoute(
+                                    builder: (context) => Scaffold(
+                                          appBar: AppBar(
+                                            title: Text(
+                                                _data[index]['name'] as String),
+                                            // ignore: prefer_const_literals_to_create_immutables
+                                            actions: [const LogActionWidget()],
+                                          ),
+                                          body:
+                                              _data[index]['widget'] as Widget?,
+                                        )));
+                          },
+                          title: Text(
+                            _data[index]['name'] as String,
+                            style: const TextStyle(
+                                fontSize: 24, color: Colors.black),
+                          ),
+                        );
+                },
+              ),
       ),
     );
   }
 }
+
+/// This widget is used to indicate the configuration is invalid
+class InvalidConfigWidget extends StatelessWidget {
+  /// Construct the [InvalidConfigWidget]
+  const InvalidConfigWidget({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      color: Colors.red,
+      child: const Text(
+          'Make sure you set the correct appId, token, channelId, etc.. in the lib/config/agora.config.dart file.'),
+    );
+  }
+}

+ 18 - 11
lib/model/user.dart

@@ -1,28 +1,35 @@
 /// User model类
 /// Time:
 /// Copyright © 2021 liuyuqi.gov@msn.cn. All Rights Reserved.
-class User {
-  String userid;
-  String userName;
-  String token;
+class UserModel {
+  String userid = "";
+  String userName = "";
+  String token = "";
 
-  User.fromJson(Map<String, dynamic> json) {
+  UserModel.fromJson(Map<String, dynamic> json) {
     userid = json['userid'];
     userName = json['userName'];
     token = json['token'];
   }
 
-  Map<String,dynamic> toJson(User user) {
+  Map<String, dynamic> toJson(UserModel user) {
     return {
       'userid': userid,
       'userName': userName,
       'token': token,
     };
+  }
 }
 
-class Users {
-  List<User> users;
- List<User>  fromJson(){
-   
- }
+class UserEntity {
+  List<UserModel> users = [];
+
+  UserEntity.fromJson(Map<String, dynamic> json) {
+    if (json['users'] != null) {
+      users = List<UserModel>.empty();
+      json['users'].forEach((v) {
+        users.add(UserModel.fromJson(v));
+      });
+    }
+  }
 }

+ 0 - 192
lib/pages/advanced/create_stream_data.dart

@@ -1,192 +0,0 @@
-import 'dart:developer';
-
-import 'package:agora_rtc_engine/agora_rtc_engine.dart';
-import 'package:agora_rtc_engine/rtc_engine.dart';
-import 'package:agora_rtc_engine/rtc_local_view.dart' as RtcLocalView;
-import 'package:agora_rtc_engine/rtc_remote_view.dart' as RtcRemoteView;
-import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:permission_handler/permission_handler.dart';
-
-/// CreateStreamData Example
-class CreateStreamData extends StatefulWidget {
-  @override
-  State<StatefulWidget> createState() => _State();
-}
-
-class _State extends State<CreateStreamData> {
-  late final RtcEngine _engine;
-  bool isJoined = false;
-  int? remoteUid;
-
-  @override
-  void initState() {
-    super.initState();
-  }
-
-  @override
-  void dispose() {
-    super.dispose();
-    _engine.destroy();
-  }
-
-  _initEngine() async {
-    if (defaultTargetPlatform == TargetPlatform.android) {
-      await Permission.microphone.request();
-    }
-    _engine = await RtcEngine.createWithContext(RtcEngineContext(config.appId));
-    this._addListener();
-
-    // enable video module and set up video encoding configs
-    await _engine.enableVideo();
-
-    // make this room live broadcasting room
-    await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
-    await _engine.setClientRole(ClientRole.Broadcaster);
-
-    // Set audio route to speaker
-    await _engine.setDefaultAudioRoutetoSpeakerphone(true);
-
-    // start joining channel
-    // 1. Users can only see each other after they join the
-    // same channel successfully using the same app id.
-    // 2. If app certificate is turned on at dashboard, token is needed
-    // when joining channel. The channel name and uid used to calculate
-    // the token has to match the ones used for channel join
-    await _engine.joinChannel(config.token, config.channelId, null, 0, null);
-  }
-
-  Future<void> _showMyDialog(int uid, int streamId, String data) async {
-    return showDialog(
-      context: context,
-      barrierDismissible: false, // user must tap button!
-      builder: (BuildContext context) {
-        return AlertDialog(
-          title: Text('Receive from uid:${uid}'),
-          content: SingleChildScrollView(
-            child: ListBody(
-              children: <Widget>[Text('StreamId ${streamId}:${data}')],
-            ),
-          ),
-          actions: <Widget>[
-            TextButton(
-              child: Text('Ok'),
-              onPressed: () {
-                Navigator.of(context).pop();
-              },
-            ),
-          ],
-        );
-      },
-    );
-  }
-
-  _addListener() {
-    _engine.setEventHandler(RtcEngineEventHandler(warning: (warningCode) {
-      log('Warning ${warningCode}');
-    }, error: (errorCode) {
-      log('Warning ${errorCode}');
-    }, joinChannelSuccess: (channel, uid, elapsed) {
-      log('joinChannelSuccess ${channel} ${uid} ${elapsed}');
-      ;
-      setState(() {
-        isJoined = true;
-      });
-    }, userJoined: (uid, elapsed) {
-      log('userJoined $uid $elapsed');
-      this.setState(() {
-        remoteUid = uid;
-      });
-    }, userOffline: (uid, reason) {
-      log('userOffline $uid $reason');
-      this.setState(() {
-        remoteUid = null;
-      });
-    }, streamMessage: (int uid, int streamId, String data) {
-      _showMyDialog(uid, streamId, data);
-      log('streamMessage $uid $streamId $data');
-    }, streamMessageError:
-        (int uid, int streamId, ErrorCode error, int missed, int cached) {
-      log('streamMessage $uid $streamId $error $missed $cached');
-    }));
-  }
-
-  _onPressSend() async {
-    if (_controller.text.length == 0) {
-      return;
-    }
-
-    var streamId = await _engine
-        .createDataStreamWithConfig(DataStreamConfig(false, false));
-    if (streamId != null) {
-      _engine.sendStreamMessage(streamId, _controller.text);
-    }
-    _controller.clear();
-  }
-
-  final TextEditingController _controller = TextEditingController();
-
-  @override
-  Widget build(BuildContext context) {
-    return Stack(
-      children: [
-        Column(
-          children: [
-            !isJoined
-                ? Row(
-                    children: [
-                      Expanded(
-                        flex: 1,
-                        child: ElevatedButton(
-                          onPressed: _initEngine,
-                          child: Text('Join channel'),
-                        ),
-                      )
-                    ],
-                  )
-                : _renderVideo(),
-            if (isJoined)
-              Row(
-                mainAxisSize: MainAxisSize.max,
-                children: [
-                  Expanded(
-                      child: TextField(
-                          controller: _controller,
-                          decoration: InputDecoration(
-                            hintText: 'Input Message',
-                          ))),
-                  ElevatedButton(
-                    onPressed: _onPressSend,
-                    child: Text('Send'),
-                  ),
-                ],
-              )
-          ],
-        ),
-      ],
-    );
-  }
-
-  _renderVideo() {
-    return Row(children: [
-      Expanded(
-          child: AspectRatio(
-        aspectRatio: 1,
-        child: RtcLocalView.SurfaceView(),
-      )),
-      Expanded(
-        child: AspectRatio(
-          aspectRatio: 1,
-          child: remoteUid != null
-              ? RtcRemoteView.SurfaceView(
-                  uid: remoteUid!,
-                )
-              : Container(
-                  color: Colors.grey[200],
-                ),
-        ),
-      )
-    ]);
-  }
-}

+ 0 - 21
lib/pages/advanced/index.dart

@@ -1,21 +0,0 @@
-import 'package:agora_rtc_engine_example/pages/advanced/create_stream_data.dart';
-import 'package:agora_rtc_engine_example/pages/advanced/live_streaming.dart';
-import 'package:agora_rtc_engine_example/pages/advanced/media_channel_relay.dart';
-import 'package:agora_rtc_engine_example/pages/advanced/multi_channel.dart';
-import 'package:agora_rtc_engine_example/pages/advanced/voice_change.dart';
-
-/// Data source for advanced examples
-final Advanced = [
-  {'name': 'Advanced'},
-  {'name': 'MultiChannel', 'widget': MultiChannel()},
-  {'name': 'LiveStreaming', 'widget': LiveStreaming()},
-  {
-    'name': 'CreateStreamData',
-    'widget': CreateStreamData(),
-  },
-  {
-    'name': 'MediaChannelRelay',
-    'widget': MediaChannelRelay(),
-  },
-  {'name': 'VoiceChange', 'widget': VoiceChange()},
-];

+ 0 - 236
lib/pages/advanced/live_streaming.dart

@@ -1,236 +0,0 @@
-import 'dart:developer';
-
-import 'package:agora_rtc_engine/rtc_engine.dart';
-import 'package:agora_rtc_engine/rtc_local_view.dart' as RtcLocalView;
-import 'package:agora_rtc_engine/rtc_remote_view.dart' as RtcRemoteView;
-import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/scheduler.dart';
-import 'package:permission_handler/permission_handler.dart';
-
-/// LiveStreaming Example
-class LiveStreaming extends StatefulWidget {
-  @override
-  State<StatefulWidget> createState() => _State();
-}
-
-class _State extends State<LiveStreaming> {
-  late final RtcEngine _engine;
-  bool isJoined = false;
-  ClientRole role = ClientRole.Audience;
-  int? remoteUid;
-  bool isLowAudio = true;
-
-  @override
-  void initState() {
-    super.initState();
-    SchedulerBinding.instance?.addPostFrameCallback((timeStamp) {
-      _showMyDialog();
-    });
-  }
-
-  @override
-  void dispose() {
-    super.dispose();
-    _engine.destroy();
-  }
-
-  Future<void> _showMyDialog() async {
-    return showDialog(
-      context: context,
-      barrierDismissible: false, // user must tap button!
-      builder: (BuildContext context) {
-        return AlertDialog(
-          title: Text('AlertDialog Title'),
-          content: SingleChildScrollView(
-            child: ListBody(
-              children: <Widget>[Text('Please choose role')],
-            ),
-          ),
-          actions: <Widget>[
-            TextButton(
-              child: Text('Broadcaster'),
-              onPressed: () {
-                this.setState(() {
-                  role = ClientRole.Broadcaster;
-                  Navigator.of(context).pop();
-                });
-              },
-            ),
-            TextButton(
-              child: Text('Audience'),
-              onPressed: () {
-                this.setState(() {
-                  role = ClientRole.Audience;
-                  Navigator.of(context).pop();
-                });
-              },
-            ),
-          ],
-        );
-      },
-    );
-  }
-
-  _initEngine() async {
-    if (defaultTargetPlatform == TargetPlatform.android) {
-      await Permission.microphone.request();
-    }
-    _engine = await RtcEngine.createWithContext(RtcEngineContext(config.appId));
-
-    this._addListener();
-
-    // enable video module and set up video encoding configs
-    await _engine.enableVideo();
-
-    // make this room live broadcasting room
-    await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
-    await this._updateClientRole(role);
-
-    // Set audio route to speaker
-    await _engine.setDefaultAudioRoutetoSpeakerphone(true);
-
-    // start joining channel
-    // 1. Users can only see each other after they join the
-    // same channel successfully using the same app id.
-    // 2. If app certificate is turned on at dashboard, token is needed
-    // when joining channel. The channel name and uid used to calculate
-    // the token has to match the ones used for channel join
-    await _engine.joinChannel(config.token, config.channelId, null, 0, null);
-  }
-
-  _addListener() {
-    _engine.setEventHandler(RtcEngineEventHandler(warning: (warningCode) {
-      log('Warning ${warningCode}');
-    }, error: (errorCode) {
-      log('Warning ${errorCode}');
-    }, joinChannelSuccess: (channel, uid, elapsed) {
-      log('joinChannelSuccess ${channel} ${uid} ${elapsed}');
-      setState(() {
-        isJoined = true;
-      });
-    }, userJoined: (uid, elapsed) {
-      log('userJoined $uid $elapsed');
-      this.setState(() {
-        remoteUid = uid;
-      });
-    }, userOffline: (uid, reason) {
-      log('userOffline $uid $reason');
-      this.setState(() {
-        remoteUid = null;
-      });
-    }));
-  }
-
-  _updateClientRole(ClientRole role) async {
-    var option;
-    if (role == ClientRole.Broadcaster) {
-      await _engine.setVideoEncoderConfiguration(VideoEncoderConfiguration(
-          dimensions: VideoDimensions(width: 640, height: 360),
-          frameRate: VideoFrameRate.Fps30,
-          orientationMode: VideoOutputOrientationMode.Adaptative));
-      // enable camera/mic, this will bring up permission dialog for first time
-      await _engine.enableLocalAudio(true);
-      await _engine.enableLocalVideo(true);
-    } else {
-      // You have to provide client role options if set to audience
-      option = ClientRoleOptions(
-          audienceLatencyLevel: isLowAudio
-              ? AudienceLatencyLevelType.LowLatency
-              : AudienceLatencyLevelType.UltraLowLatency);
-    }
-    await _engine.setClientRole(role, option);
-  }
-
-  _onPressToggleRole() {
-    this.setState(() {
-      role = role == ClientRole.Audience
-          ? ClientRole.Broadcaster
-          : ClientRole.Audience;
-      _updateClientRole(role);
-    });
-  }
-
-  _onPressToggleLatencyLevel(value) {
-    this.setState(() {
-      isLowAudio = !isLowAudio;
-      _engine.setClientRole(
-          ClientRole.Audience,
-          ClientRoleOptions(
-              audienceLatencyLevel: isLowAudio
-                  ? AudienceLatencyLevelType.LowLatency
-                  : AudienceLatencyLevelType.UltraLowLatency));
-    });
-  }
-
-  _renderToolBar() {
-    return Positioned(
-      child: Column(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          ElevatedButton(
-            child: Text('Toggle Role'),
-            onPressed: _onPressToggleRole,
-          ),
-          Container(
-            color: Colors.white,
-            child: Row(mainAxisSize: MainAxisSize.min, children: [
-              Text('Toggle Audience Latency Level'),
-              Switch(
-                value: isLowAudio,
-                onChanged: _onPressToggleLatencyLevel,
-                activeTrackColor: Colors.grey[350],
-                activeColor: Colors.white,
-              ),
-            ]),
-          )
-        ],
-      ),
-      left: 10,
-      bottom: 10,
-    );
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Stack(
-      children: [
-        Column(
-          children: [
-            Row(
-              children: [
-                if (!isJoined)
-                  Expanded(
-                    flex: 1,
-                    child: ElevatedButton(
-                      onPressed: _initEngine,
-                      child: Text('Join channel'),
-                    ),
-                  )
-              ],
-            ),
-            if (isJoined) _renderVideo(),
-          ],
-        ),
-        if (isJoined) _renderToolBar(),
-      ],
-    );
-  }
-
-  _renderVideo() {
-    return Expanded(
-      child: Stack(
-        children: [
-          role == ClientRole.Broadcaster
-              ? RtcLocalView.SurfaceView()
-              : remoteUid != null
-                  ? RtcRemoteView.SurfaceView(
-                      uid: remoteUid!,
-                    )
-                  : Container()
-        ],
-      ),
-    );
-  }
-}

+ 0 - 193
lib/pages/advanced/media_channel_relay.dart

@@ -1,193 +0,0 @@
-import 'dart:developer';
-
-import 'package:agora_rtc_engine/rtc_engine.dart';
-import 'package:agora_rtc_engine/rtc_local_view.dart' as RtcLocalView;
-import 'package:agora_rtc_engine/rtc_remote_view.dart' as RtcRemoteView;
-import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:permission_handler/permission_handler.dart';
-
-/// MediaChannelRelay Example
-class MediaChannelRelay extends StatefulWidget {
-  @override
-  State<StatefulWidget> createState() => _State();
-}
-
-class _State extends State<MediaChannelRelay> {
-  late final RtcEngine _engine;
-  bool isJoined = false;
-  int? remoteUid;
-  bool isRelaying = false;
-
-  @override
-  void initState() {
-    super.initState();
-  }
-
-  @override
-  void dispose() {
-    super.dispose();
-    _engine.destroy();
-  }
-
-  _initEngine() async {
-    if (defaultTargetPlatform == TargetPlatform.android) {
-      await Permission.microphone.request();
-    }
-    _engine = await RtcEngine.createWithContext(RtcEngineContext(config.appId));
-    this._addListener();
-
-    // enable video module and set up video encoding configs
-    await _engine.enableVideo();
-
-    // make this room live broadcasting room
-    await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
-    await _engine.setClientRole(ClientRole.Broadcaster);
-
-    // Set audio route to speaker
-    await _engine.setDefaultAudioRoutetoSpeakerphone(true);
-
-    // start joining channel
-    // 1. Users can only see each other after they join the
-    // same channel successfully using the same app id.
-    // 2. If app certificate is turned on at dashboard, token is needed
-    // when joining channel. The channel name and uid used to calculate
-    // the token has to match the ones used for channel join
-    await _engine.joinChannel(config.token, config.channelId, null, 0, null);
-  }
-
-  _addListener() {
-    _engine.setEventHandler(RtcEngineEventHandler(warning: (warningCode) {
-      log('Warning ${warningCode}');
-    }, error: (errorCode) {
-      log('Warning ${errorCode}');
-    }, joinChannelSuccess: (channel, uid, elapsed) {
-      log('joinChannelSuccess ${channel} ${uid} ${elapsed}');
-      ;
-      setState(() {
-        isJoined = true;
-      });
-    }, userJoined: (uid, elapsed) {
-      log('userJoined $uid $elapsed');
-      this.setState(() {
-        remoteUid = uid;
-      });
-    }, userOffline: (uid, reason) {
-      log('userOffline $uid $reason');
-      this.setState(() {
-        remoteUid = null;
-      });
-    }, channelMediaRelayStateChanged:
-        (ChannelMediaRelayState state, ChannelMediaRelayError code) {
-      switch (state) {
-        case ChannelMediaRelayState.Idle:
-          log('ChannelMediaRelayState.Idle $code');
-          this.setState(() {
-            isRelaying = false;
-          });
-          break;
-        case ChannelMediaRelayState.Connecting:
-          log('ChannelMediaRelayState.Connecting $code)');
-          break;
-        case ChannelMediaRelayState.Running:
-          log('ChannelMediaRelayState.Running $code)');
-          this.setState(() {
-            isRelaying = true;
-          });
-          break;
-        case ChannelMediaRelayState.Failure:
-          log('ChannelMediaRelayState.Failure $code)');
-          this.setState(() {
-            isRelaying = false;
-          });
-          break;
-        default:
-          log('default $code)');
-          break;
-      }
-    }));
-  }
-
-  _onPressRelayOrStop() async {
-    if (isRelaying) {
-      await _engine.stopChannelMediaRelay();
-      return;
-    }
-    if (_controller.text.length == 0) {
-      return;
-    }
-
-    await _engine.startChannelMediaRelay(ChannelMediaRelayConfiguration(
-        ChannelMediaInfo(config.channelId, 0, token: config.token),
-        [ChannelMediaInfo('', 0, token: '')]));
-
-    _controller.clear();
-  }
-
-  final TextEditingController _controller = TextEditingController();
-
-  @override
-  Widget build(BuildContext context) {
-    return Stack(
-      children: [
-        Column(
-          children: [
-            !isJoined
-                ? Row(
-                    children: [
-                      Expanded(
-                        flex: 1,
-                        child: ElevatedButton(
-                          onPressed: _initEngine,
-                          child: Text('Join channel'),
-                        ),
-                      )
-                    ],
-                  )
-                : _renderVideo(),
-            if (isJoined)
-              Row(
-                mainAxisSize: MainAxisSize.max,
-                children: [
-                  Expanded(
-                      child: TextField(
-                          controller: _controller,
-                          decoration: InputDecoration(
-                            hintText: 'Enter target relay channel name',
-                          ))),
-                  ElevatedButton(
-                    onPressed: _onPressRelayOrStop,
-                    child: Text(!isRelaying ? 'Relay' : 'Stop'),
-                  ),
-                ],
-              )
-          ],
-        ),
-      ],
-    );
-  }
-
-  _renderVideo() {
-    return Row(children: [
-      Expanded(
-          child: AspectRatio(
-        aspectRatio: 1,
-        child: RtcLocalView.SurfaceView(),
-      )),
-      Expanded(
-        child: AspectRatio(
-          aspectRatio: 1,
-          child: remoteUid != null
-              ? RtcRemoteView.SurfaceView(
-                  uid: remoteUid!,
-                )
-              : Container(
-                  color: Colors.grey[200],
-                ),
-        ),
-      )
-    ]);
-  }
-}

+ 0 - 278
lib/pages/advanced/multi_channel.dart

@@ -1,278 +0,0 @@
-import 'dart:developer';
-
-import 'package:agora_rtc_engine/rtc_channel.dart';
-import 'package:agora_rtc_engine/rtc_engine.dart';
-import 'package:agora_rtc_engine/rtc_local_view.dart' as RtcLocalView;
-import 'package:agora_rtc_engine/rtc_remote_view.dart' as RtcRemoteView;
-import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:permission_handler/permission_handler.dart';
-
-const _channelId0 = 'channel0';
-const _channelId1 = 'channel1';
-
-/// MultiChannel Example
-class MultiChannel extends StatefulWidget {
-  @override
-  State<StatefulWidget> createState() => _State();
-}
-
-class _State extends State<MultiChannel> {
-  late final RtcEngine _engine;
-  late final RtcChannel _channel0, _channel1;
-  String? renderChannelId;
-  bool isJoined0 = false, isJoined1 = false;
-  List<int> remoteUid0 = [], remoteUid1 = [];
-
-  @override
-  void initState() {
-    super.initState();
-    this._initEngine();
-  }
-
-  @override
-  void dispose() {
-    super.dispose();
-    _engine.destroy();
-  }
-
-  _initEngine() async {
-    _engine = await RtcEngine.createWithContext(RtcEngineContext(config.appId));
-
-    await _engine.enableVideo();
-    await _engine.startPreview();
-    await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
-    await _engine.setClientRole(ClientRole.Broadcaster);
-  }
-
-  _joinChannel0() async {
-    if (defaultTargetPlatform == TargetPlatform.android) {
-      await [Permission.microphone, Permission.camera].request();
-    }
-
-    _channel0 = await RtcChannel.create(_channelId0);
-    this._addListener(_channel0);
-
-    await _channel0.setClientRole(ClientRole.Broadcaster);
-    await _channel0.joinChannel(
-        null,
-        null,
-        0,
-        ChannelMediaOptions(
-          publishLocalAudio: false,
-          publishLocalVideo: false,
-        ));
-  }
-
-  _joinChannel1() async {
-    if (defaultTargetPlatform == TargetPlatform.android) {
-      await [Permission.microphone, Permission.camera].request();
-    }
-
-    _channel1 = await RtcChannel.create(_channelId1);
-    this._addListener(_channel1);
-
-    await _channel1.setClientRole(ClientRole.Broadcaster);
-    await _channel1.joinChannel(
-        null,
-        null,
-        0,
-        ChannelMediaOptions(
-          publishLocalAudio: false,
-          publishLocalVideo: false,
-        ));
-  }
-
-  _addListener(RtcChannel channel) {
-    String channelId = channel.channelId;
-    channel.setEventHandler(
-        RtcChannelEventHandler(joinChannelSuccess: (channel, uid, elapsed) {
-      log('joinChannelSuccess ${channel} ${uid} ${elapsed}');
-      if (channelId == _channelId0) {
-        setState(() {
-          isJoined0 = true;
-        });
-      } else if (channelId == _channelId1) {
-        setState(() {
-          isJoined1 = true;
-        });
-      }
-    }, userJoined: (uid, elapsed) {
-      log('userJoined ${channel.channelId} $uid $elapsed');
-    }, userOffline: (uid, reason) {
-      log('userOffline ${channel.channelId} $uid $reason');
-    }, leaveChannel: (stats) {
-      log('leaveChannel ${channel.channelId} ${stats.toJson()}');
-      if (channelId == _channelId0) {
-        this.setState(() {
-          isJoined0 = false;
-          remoteUid0.clear();
-        });
-      } else if (channelId == _channelId1) {
-        this.setState(() {
-          isJoined1 = false;
-          remoteUid1.clear();
-        });
-      }
-    }, remoteVideoStateChanged: (uid, state, reason, elapsed) {
-      log('remoteVideoStateChanged ${uid} ${state} ${reason} ${elapsed}');
-      if (state == VideoRemoteState.Starting) {
-        if (channelId == _channelId0) {
-          this.setState(() {
-            remoteUid0.add(uid);
-          });
-        } else if (channelId == _channelId1) {
-          this.setState(() {
-            remoteUid1.add(uid);
-          });
-        }
-      } else if (state == VideoRemoteState.Stopped) {
-        if (channelId == _channelId0) {
-          this.setState(() {
-            remoteUid0.removeWhere((element) => element == uid);
-          });
-        } else if (channelId == _channelId1) {
-          this.setState(() {
-            remoteUid1.removeWhere((element) => element == uid);
-          });
-        }
-      }
-    }));
-  }
-
-  _publishChannel0() async {
-    await _channel1.unpublish();
-    await _channel0.publish();
-  }
-
-  _publishChannel1() async {
-    await _channel0.unpublish();
-    await _channel1.publish();
-  }
-
-  _leaveChannel0() async {
-    await _channel0.leaveChannel();
-  }
-
-  _leaveChannel1() async {
-    await _channel1.leaveChannel();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Stack(
-      children: [
-        Column(
-          children: [
-            Row(
-              children: [
-                Expanded(
-                  flex: 1,
-                  child: ElevatedButton(
-                    onPressed: () {
-                      if (isJoined0) {
-                        this._leaveChannel0();
-                      } else {
-                        this._joinChannel0();
-                      }
-                    },
-                    child: Text('${isJoined0 ? 'Leave' : 'Join'} $_channelId0'),
-                  ),
-                )
-              ],
-            ),
-            Row(
-              children: [
-                Expanded(
-                  flex: 1,
-                  child: ElevatedButton(
-                    onPressed: () {
-                      if (isJoined1) {
-                        this._leaveChannel1();
-                      } else {
-                        this._joinChannel1();
-                      }
-                    },
-                    child: Text('${isJoined1 ? 'Leave' : 'Join'} $_channelId1'),
-                  ),
-                )
-              ],
-            ),
-            _renderVideo(),
-          ],
-        ),
-        Align(
-          alignment: Alignment.bottomRight,
-          child: Column(
-            mainAxisSize: MainAxisSize.min,
-            children: [
-              ElevatedButton(
-                onPressed: this._publishChannel0,
-                child: Text('Publish ${_channelId0}'),
-              ),
-              ElevatedButton(
-                onPressed: () {
-                  setState(() {
-                    renderChannelId = _channelId0;
-                  });
-                },
-                child: Text('Render ${_channelId0}'),
-              ),
-              ElevatedButton(
-                onPressed: this._publishChannel1,
-                child: Text('Publish ${_channelId1}'),
-              ),
-              ElevatedButton(
-                onPressed: () {
-                  setState(() {
-                    renderChannelId = _channelId1;
-                  });
-                },
-                child: Text('Render ${_channelId1}'),
-              ),
-            ],
-          ),
-        )
-      ],
-    );
-  }
-
-  _renderVideo() {
-    List<int>? remoteUid = null;
-    if (renderChannelId == _channelId0) {
-      remoteUid = remoteUid0;
-    } else if (renderChannelId == _channelId1) {
-      remoteUid = remoteUid1;
-    }
-    return Expanded(
-      child: Stack(
-        children: [
-          RtcLocalView.SurfaceView(
-            channelId: renderChannelId,
-          ),
-          if (remoteUid != null)
-            Align(
-              alignment: Alignment.topLeft,
-              child: SingleChildScrollView(
-                scrollDirection: Axis.horizontal,
-                child: Row(
-                  children: List.of(remoteUid.map(
-                    (e) => Container(
-                      width: 120,
-                      height: 120,
-                      child: RtcRemoteView.SurfaceView(
-                        uid: e,
-                        channelId: renderChannelId,
-                      ),
-                    ),
-                  )),
-                ),
-              ),
-            )
-        ],
-      ),
-    );
-  }
-}

+ 0 - 481
lib/pages/advanced/voice_change.dart

@@ -1,481 +0,0 @@
-import 'dart:developer';
-
-import 'package:agora_rtc_engine/agora_rtc_engine.dart';
-import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
-import 'package:agora_rtc_engine_example/config/voice_changer.config.dart';
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:permission_handler/permission_handler.dart';
-
-/// VoiceChange Example
-class VoiceChange extends StatefulWidget {
-  @override
-  State<StatefulWidget> createState() => _State();
-}
-
-class _State extends State<VoiceChange> {
-  late final RtcEngine _engine;
-  bool isJoined = false;
-  List<int> remoteUids = [];
-  int? uidMySelf;
-  int? selectedVoiceToolBtn;
-  AudioEffectPreset currentAudioEffectPreset = AudioEffectPreset.AudioEffectOff;
-
-  bool isEnableSlider1 = false, isEnableSlider2 = false;
-  String sliderTitle1 = '', sliderTitle2 = '';
-  double minimumValue1 = 0,
-      maximumValue1 = 0,
-      minimumValue2 = 0,
-      maximumValue2 = 0;
-  double? sliderValue1, sliderValue2;
-  Map selectedFreq = FreqOptions[0];
-  Map selectedReverbKey = ReverbKeyOptions[0];
-
-  double voicePitchValue = 0.5;
-  double bandGainValue = 0;
-  double reverbValue = 1;
-
-  @override
-  void initState() {
-    super.initState();
-  }
-
-  @override
-  void dispose() {
-    super.dispose();
-    _engine.destroy();
-  }
-
-  _initEngine() async {
-    if (defaultTargetPlatform == TargetPlatform.android) {
-      await Permission.microphone.request();
-    }
-    _engine = await RtcEngine.createWithContext(RtcEngineContext(config.appId));
-    this._addListener();
-
-    // make this room live broadcasting room
-    await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
-    await _engine.setClientRole(ClientRole.Broadcaster);
-
-    // Set audio route to speaker
-    await _engine.setDefaultAudioRoutetoSpeakerphone(true);
-
-    // start joining channel
-    // 1. Users can only see each other after they join the
-    // same channel successfully using the same app id.
-    // 2. If app certificate is turned on at dashboard, token is needed
-    // when joining channel. The channel name and uid used to calculate
-    // the token has to match the ones used for channel join
-    await _engine.joinChannel(config.token, config.channelId, null, 0, null);
-  }
-
-  _addListener() {
-    _engine.setEventHandler(RtcEngineEventHandler(warning: (warningCode) {
-      log('Warning ${warningCode}');
-    }, error: (errorCode) {
-      log('Warning ${errorCode}');
-    }, joinChannelSuccess: (channel, uid, elapsed) {
-      log('joinChannelSuccess ${channel} ${uid} ${elapsed}');
-      ;
-      setState(() {
-        isJoined = true;
-        uidMySelf = uid;
-      });
-    }, userJoined: (uid, elapsed) {
-      log('userJoined $uid $elapsed');
-      this.setState(() {
-        remoteUids.add(uid);
-      });
-    }, userOffline: (uid, reason) {
-      log('userOffline $uid $reason');
-      this.setState(() {
-        remoteUids.remove(uid);
-      });
-    }));
-  }
-
-  _onPressBFButton(dynamic type, int index) async {
-    switch (index) {
-      case 0:
-      case 1:
-        await _engine.setVoiceBeautifierPreset(type as VoiceBeautifierPreset);
-        this._updateSliderUI(AudioEffectPreset.AudioEffectOff);
-        break;
-      case 2:
-      case 3:
-      case 4:
-      case 5:
-        await _engine.setAudioEffectPreset(type as AudioEffectPreset);
-        this._updateSliderUI(type);
-        break;
-      default:
-        break;
-    }
-  }
-
-  _updateSliderUI(AudioEffectPreset type) {
-    this.setState(() {
-      currentAudioEffectPreset = type;
-      switch (type) {
-        case AudioEffectPreset.roomAcoustics3dVoice:
-          isEnableSlider1 = true;
-          isEnableSlider2 = false;
-          sliderTitle1 = 'Cycle';
-          minimumValue1 = 1;
-          sliderValue1 = 1;
-          maximumValue1 = 3;
-          break;
-        case AudioEffectPreset.pitchCorrection:
-          isEnableSlider1 = true;
-          isEnableSlider2 = true;
-          sliderTitle1 = 'Tonic Mode';
-          sliderTitle2 = 'Tonic Pitch';
-          minimumValue1 = 1;
-          sliderValue1 = 1;
-          maximumValue1 = 3;
-          minimumValue2 = 1;
-          sliderValue2 = 1;
-          maximumValue2 = 12;
-          break;
-        default:
-          isEnableSlider1 = false;
-          isEnableSlider2 = false;
-          break;
-      }
-    });
-  }
-
-  _onAudioEffectUpdate({
-    double? value1,
-    double? value2,
-  }) async {
-    this.setState(() {
-      if (value1 != null) {
-        sliderValue1 = value1;
-      }
-      if (value2 != null) {
-        sliderValue2 = value2;
-      }
-      _engine.setAudioEffectParameters(
-          currentAudioEffectPreset,
-          (isEnableSlider1 ? sliderValue1 ?? minimumValue1 : 0).toInt(),
-          (isEnableSlider2 ? sliderValue2 ?? minimumValue2 : 0).toInt());
-    });
-  }
-
-  _onPressChangeFreq() {
-    return showDialog(
-      context: context,
-      barrierDismissible: false,
-      builder: (BuildContext context) {
-        return AlertDialog(
-          title: Text('Set Band Frequency'),
-          actions: <Widget>[
-            for (var freqOpt in FreqOptions)
-              TextButton(
-                child: Text(freqOpt['text'] as String),
-                onPressed: () {
-                  setState(() {
-                    selectedFreq = freqOpt;
-                    _engine.setLocalVoiceEqualization(
-                        freqOpt['type'] as AudioEqualizationBandFrequency,
-                        bandGainValue.toInt());
-                  });
-                  Navigator.of(context).pop();
-                },
-              ),
-          ],
-        );
-      },
-    );
-  }
-
-  _onPressChangeReverbKey() {
-    return showDialog(
-      context: context,
-      barrierDismissible: false,
-      builder: (BuildContext context) {
-        return AlertDialog(
-          title: Text('Set Reverb Key'),
-          actions: <Widget>[
-            for (var reverbKey in ReverbKeyOptions)
-              TextButton(
-                child: Text(reverbKey['text'] as String),
-                onPressed: () {
-                  setState(() {
-                    selectedReverbKey = reverbKey;
-                  });
-                  Navigator.of(context).pop();
-                },
-              ),
-          ],
-        );
-      },
-    );
-  }
-
-  _renderToolBar() {
-    return Padding(
-        padding: EdgeInsets.symmetric(horizontal: 8, vertical: 0),
-        child: Column(
-          children: [
-            Row(children: [
-              Text('Voice Beautifier & Effects Preset',
-                  style: TextStyle(fontWeight: FontWeight.bold))
-            ]),
-            Container(
-                width: MediaQuery.of(context).size.width,
-                child: Wrap(children: [
-                  for (var i = 0; i < VoiceChangeConfig.length; i++)
-                    _renderBtnItem(VoiceChangeConfig[i], i)
-                ])),
-            if (isEnableSlider1)
-              Row(
-                mainAxisAlignment: MainAxisAlignment.end,
-                mainAxisSize: MainAxisSize.max,
-                children: [
-                  Expanded(
-                    child: Text(sliderTitle1),
-                    flex: 1,
-                  ),
-                  Expanded(
-                    child: Slider(
-                      value: sliderValue1!,
-                      min: minimumValue1,
-                      max: maximumValue1,
-                      divisions: 5,
-                      label: sliderTitle1,
-                      onChanged: (double value) {
-                        _onAudioEffectUpdate(value1: value);
-                      },
-                    ),
-                    flex: 2,
-                  )
-                ],
-              ),
-            if (isEnableSlider2)
-              Row(
-                mainAxisAlignment: MainAxisAlignment.end,
-                mainAxisSize: MainAxisSize.max,
-                children: [
-                  Expanded(child: Text(sliderTitle2), flex: 1),
-                  Expanded(
-                      child: Slider(
-                        value: sliderValue2!,
-                        min: minimumValue2,
-                        max: maximumValue2,
-                        divisions: 5,
-                        label: sliderTitle1,
-                        onChanged: (double value) {
-                          _onAudioEffectUpdate(value2: value);
-                        },
-                      ),
-                      flex: 2)
-                ],
-              ),
-            Row(children: [
-              Text('Customize Voice Effects',
-                  style: TextStyle(fontWeight: FontWeight.bold))
-            ]),
-            Row(
-              mainAxisAlignment: MainAxisAlignment.spaceBetween,
-              children: [
-                Text('Pitch:'),
-                Slider(
-                  value: voicePitchValue,
-                  min: 0.5,
-                  max: 2,
-                  divisions: 5,
-                  onChanged: (double value) {
-                    setState(() {
-                      voicePitchValue = value;
-                    });
-                    _engine.setLocalVoicePitch(value);
-                  },
-                )
-              ],
-            ),
-            Row(
-              mainAxisAlignment: MainAxisAlignment.spaceBetween,
-              children: [
-                Text('BandFreq'),
-                TextButton(
-                  child: Text(selectedFreq['text']),
-                  onPressed: _onPressChangeFreq,
-                )
-              ],
-            ),
-            Row(
-              mainAxisAlignment: MainAxisAlignment.spaceBetween,
-              children: [
-                Text('BandGain:'),
-                Slider(
-                  value: bandGainValue,
-                  min: 0,
-                  max: 9,
-                  divisions: 5,
-                  onChanged: (double value) async {
-                    this.setState(() {
-                      bandGainValue = value;
-                    });
-                    _engine.setLocalVoiceEqualization(
-                        selectedFreq['type'], value.toInt());
-                  },
-                )
-              ],
-            ),
-            Row(
-              mainAxisAlignment: MainAxisAlignment.spaceBetween,
-              children: [
-                Text('BandKey'),
-                TextButton(
-                  child: Text(selectedReverbKey['text']),
-                  onPressed: _onPressChangeReverbKey,
-                )
-              ],
-            ),
-            Row(
-              mainAxisAlignment: MainAxisAlignment.spaceBetween,
-              children: [
-                Text('ReverbValue:'),
-                Slider(
-                  value: reverbValue,
-                  min: selectedReverbKey['min'],
-                  max: selectedReverbKey['max'],
-                  divisions: 5,
-                  onChanged: (double value) async {
-                    setState(() {
-                      reverbValue = value;
-                    });
-                    await _engine.setLocalVoiceReverb(
-                        selectedReverbKey['type'], value.toInt());
-                  },
-                )
-              ],
-            ),
-          ],
-        ));
-  }
-
-  _renderBtnItem(Map<String, dynamic> config, int index) {
-    return _CusBtn(
-        config['alertTitle'], selectedVoiceToolBtn != index, config['options'],
-        (type) {
-      setState(() {
-        selectedVoiceToolBtn = index;
-      });
-      _onPressBFButton(type, index);
-    });
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Stack(
-      children: [
-        Column(
-          children: [
-            !isJoined
-                ? Row(
-                    children: [
-                      Expanded(
-                        flex: 1,
-                        child: ElevatedButton(
-                          onPressed: _initEngine,
-                          child: Text('Join channel'),
-                        ),
-                      )
-                    ],
-                  )
-                : _renderUserUid(),
-            if (isJoined) _renderToolBar()
-          ],
-        ),
-      ],
-    );
-  }
-
-  _renderUserUid() {
-    final size = MediaQuery.of(context).size;
-    var list = [uidMySelf, ...remoteUids];
-    return Container(
-      width: size.width,
-      height: 200,
-      child: ListView.builder(
-        itemCount: list.length,
-        itemBuilder: (context, index) {
-          return Center(
-            child: Padding(
-              child: Text(
-                'AUDIO ONLY ${index == 0 ? 'LOCAL' : 'REMOTE'} UID: ${list[index]}',
-                style: TextStyle(fontSize: 14, color: Colors.black),
-              ),
-              padding: EdgeInsets.all(4.0),
-            ),
-          );
-        },
-      ),
-    );
-  }
-}
-
-class _CusBtn extends StatefulWidget {
-  String alertTitle;
-  dynamic options;
-  bool isOff = false;
-  void Function(dynamic type) onPressed;
-
-  _CusBtn(this.alertTitle, this.isOff, this.options, this.onPressed);
-
-  @override
-  State<StatefulWidget> createState() => _CusBtnState(isOff);
-}
-
-class _CusBtnState extends State<_CusBtn> {
-  String title = "Off";
-  bool isEnable;
-
-  _CusBtnState(this.isEnable);
-
-  @override
-  void didUpdateWidget(covariant _CusBtn oldWidget) {
-    super.didUpdateWidget(oldWidget);
-    this.setState(() {
-      isEnable = !widget.isOff;
-    });
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return TextButton(
-      child: Text(isEnable ? title : 'Off'),
-      onPressed: _showMyDialog,
-    );
-  }
-
-  Future<void> _showMyDialog() async {
-    return showDialog(
-      context: context,
-      barrierDismissible: false,
-      builder: (BuildContext context) {
-        return AlertDialog(
-          title: Text(widget.alertTitle),
-          actions: <Widget>[
-            for (var option in widget.options)
-              TextButton(
-                child: Text(option['text']),
-                onPressed: () {
-                  setState(() {
-                    isEnable = true;
-                    title = option['text'];
-                    widget.onPressed(option['type']);
-                  });
-                  Navigator.of(context).pop();
-                },
-              ),
-          ],
-        );
-      },
-    );
-  }
-}

+ 0 - 11
lib/pages/basic/index.dart

@@ -1,11 +0,0 @@
-import 'package:agora_rtc_engine_example/pages/basic/join_channel_audio.dart';
-import 'package:agora_rtc_engine_example/pages/basic/join_channel_video.dart';
-import 'package:agora_rtc_engine_example/pages/basic/string_uid.dart';
-
-/// Data source for basic examples
-final Basic = [
-  {'name': 'Basic'},
-  {'name': '语音聊天', 'widget': JoinChannelAudio()},
-  {'name': '视频聊天', 'widget': JoinChannelVideo()},
-  {'name': 'StringUid', 'widget': StringUid()}
-];

+ 0 - 266
lib/pages/basic/join_channel_audio.dart

@@ -1,266 +0,0 @@
-import 'dart:async';
-import 'dart:developer';
-
-import 'package:agora_rtc_engine/agora_rtc_engine.dart';
-import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:permission_handler/permission_handler.dart';
-
-/// JoinChannelAudio Example
-class JoinChannelAudio extends StatefulWidget {
-  @override
-  State<StatefulWidget> createState() => _State();
-}
-
-class _State extends State<JoinChannelAudio> {
-  late final RtcEngine _engine;
-  String channelId = config.channelId;
-  bool isJoined = false,
-      openMicrophone = true,
-      enableSpeakerphone = true,
-      playEffect = false;
-  bool _enableInEarMonitoring = false;
-  double _recordingVolume = 0, _playbackVolume = 0, _inEarMonitoringVolume = 0;
-  TextEditingController? _controller;
-
-  @override
-  void initState() {
-    super.initState();
-    _controller = TextEditingController(text: channelId);
-    this._initEngine();
-  }
-
-  @override
-  void dispose() {
-    super.dispose();
-    _engine.destroy();
-  }
-
-  _initEngine() async {
-    _engine = await RtcEngine.createWithContext(RtcEngineContext(config.appId));
-    this._addListeners();
-
-    await _engine.enableAudio();
-    await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
-    await _engine.setClientRole(ClientRole.Broadcaster);
-  }
-
-  _addListeners() {
-    _engine.setEventHandler(RtcEngineEventHandler(
-      joinChannelSuccess: (channel, uid, elapsed) {
-        log('joinChannelSuccess $channel $uid $elapsed');
-        setState(() {
-          isJoined = true;
-        });
-      },
-      leaveChannel: (stats) async {
-        log('leaveChannel ${stats.toJson()}');
-        setState(() {
-          isJoined = false;
-        });
-      },
-    ));
-  }
-
-  _joinChannel() async {
-    if (defaultTargetPlatform == TargetPlatform.android) {
-      await Permission.microphone.request();
-    }
-
-    await _engine
-        .joinChannel(config.token, config.channelId, null, config.uid)
-        .catchError((onError) {
-      print('error ${onError.toString()}');
-    });
-  }
-
-  _leaveChannel() async {
-    await _engine.leaveChannel();
-  }
-
-  _switchMicrophone() {
-    _engine.enableLocalAudio(!openMicrophone).then((value) {
-      setState(() {
-        openMicrophone = !openMicrophone;
-      });
-    }).catchError((err) {
-      log('enableLocalAudio $err');
-    });
-  }
-
-  _switchSpeakerphone() {
-    _engine.setEnableSpeakerphone(!enableSpeakerphone).then((value) {
-      setState(() {
-        enableSpeakerphone = !enableSpeakerphone;
-      });
-    }).catchError((err) {
-      log('setEnableSpeakerphone $err');
-    });
-  }
-
-  _switchEffect() async {
-    if (playEffect) {
-      _engine.stopEffect(1).then((value) {
-        setState(() {
-          playEffect = false;
-        });
-      }).catchError((err) {
-        log('stopEffect $err');
-      });
-    } else {
-      _engine
-          .playEffect(
-              1,
-              await (_engine.getAssetAbsolutePath("assets/Sound_Horizon.mp3")
-                  as FutureOr<String>),
-              -1,
-              1,
-              1,
-              100,
-              true)
-          .then((value) {
-        setState(() {
-          playEffect = true;
-        });
-      }).catchError((err) {
-        log('playEffect $err');
-      });
-    }
-  }
-
-  _onChangeInEarMonitoringVolume(double value) {
-    setState(() {
-      _inEarMonitoringVolume = value;
-    });
-    _engine.setInEarMonitoringVolume(value.toInt());
-  }
-
-  _toggleInEarMonitoring(value) {
-    setState(() {
-      _enableInEarMonitoring = value;
-    });
-    _engine.enableInEarMonitoring(value);
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Stack(
-      children: [
-        Column(
-          children: [
-            TextField(
-              controller: _controller,
-              decoration: InputDecoration(hintText: 'Channel ID'),
-              onChanged: (text) {
-                setState(() {
-                  channelId = text;
-                });
-              },
-            ),
-            Row(
-              children: [
-                Expanded(
-                  flex: 1,
-                  child: ElevatedButton(
-                    onPressed:
-                        isJoined ? this._leaveChannel : this._joinChannel,
-                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
-                  ),
-                )
-              ],
-            ),
-          ],
-        ),
-        Align(
-            alignment: Alignment.bottomRight,
-            child: Padding(
-              child: Column(
-                mainAxisSize: MainAxisSize.min,
-                crossAxisAlignment: CrossAxisAlignment.end,
-                children: [
-                  ElevatedButton(
-                    onPressed: this._switchMicrophone,
-                    child: Text('Microphone ${openMicrophone ? 'on' : 'off'}'),
-                  ),
-                  ElevatedButton(
-                    onPressed: this._switchSpeakerphone,
-                    child:
-                        Text(enableSpeakerphone ? 'Speakerphone' : 'Earpiece'),
-                  ),
-                  ElevatedButton(
-                    onPressed: this._switchEffect,
-                    child: Text('${playEffect ? 'Stop' : 'Play'} effect'),
-                  ),
-                  Row(
-                    mainAxisAlignment: MainAxisAlignment.end,
-                    children: [
-                      Text('RecordingVolume:'),
-                      Slider(
-                        value: _recordingVolume,
-                        min: 0,
-                        max: 400,
-                        divisions: 5,
-                        label: 'RecordingVolume',
-                        onChanged: (double value) {
-                          setState(() {
-                            _recordingVolume = value;
-                          });
-                          _engine.adjustRecordingSignalVolume(value.toInt());
-                        },
-                      )
-                    ],
-                  ),
-                  Row(
-                    mainAxisAlignment: MainAxisAlignment.end,
-                    children: [
-                      Text('PlaybackVolume:'),
-                      Slider(
-                        value: _playbackVolume,
-                        min: 0,
-                        max: 400,
-                        divisions: 5,
-                        label: 'PlaybackVolume',
-                        onChanged: (double value) {
-                          setState(() {
-                            _playbackVolume = value;
-                          });
-                          _engine.adjustPlaybackSignalVolume(value.toInt());
-                        },
-                      )
-                    ],
-                  ),
-                  Column(
-                    mainAxisSize: MainAxisSize.min,
-                    crossAxisAlignment: CrossAxisAlignment.end,
-                    children: [
-                      Row(mainAxisSize: MainAxisSize.min, children: [
-                        Text('InEar Monitoring Volume:'),
-                        Switch(
-                          value: _enableInEarMonitoring,
-                          onChanged: _toggleInEarMonitoring,
-                          activeTrackColor: Colors.grey[350],
-                          activeColor: Colors.white,
-                        )
-                      ]),
-                      if (_enableInEarMonitoring)
-                        Container(
-                            width: 300,
-                            child: Slider(
-                              value: _inEarMonitoringVolume,
-                              min: 0,
-                              max: 100,
-                              divisions: 5,
-                              label: 'InEar Monitoring Volume',
-                              onChanged: _onChangeInEarMonitoringVolume,
-                            ))
-                    ],
-                  ),
-                ],
-              ),
-              padding: EdgeInsets.symmetric(vertical: 20, horizontal: 0),
-            ))
-      ],
-    );
-  }
-}

+ 0 - 181
lib/pages/basic/join_channel_video.dart

@@ -1,181 +0,0 @@
-import 'dart:developer';
-
-import 'package:agora_rtc_engine/rtc_engine.dart';
-import 'package:agora_rtc_engine/rtc_local_view.dart' as RtcLocalView;
-import 'package:agora_rtc_engine/rtc_remote_view.dart' as RtcRemoteView;
-import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:permission_handler/permission_handler.dart';
-
-/// MultiChannel Example
-class JoinChannelVideo extends StatefulWidget {
-  @override
-  State<StatefulWidget> createState() => _State();
-}
-
-class _State extends State<JoinChannelVideo> {
-  late final RtcEngine _engine;
-  String channelId = config.channelId;
-  bool isJoined = false, switchCamera = true, switchRender = true;
-  List<int> remoteUid = [];
-  TextEditingController? _controller;
-
-  @override
-  void initState() {
-    super.initState();
-    _controller = TextEditingController(text: channelId);
-    this._initEngine();
-  }
-
-  @override
-  void dispose() {
-    super.dispose();
-    _engine.destroy();
-  }
-
-  _initEngine() async {
-    _engine = await RtcEngine.createWithContext(RtcEngineContext(config.appId));
-    this._addListeners();
-
-    await _engine.enableVideo();
-    await _engine.startPreview();
-    await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
-    await _engine.setClientRole(ClientRole.Broadcaster);
-  }
-
-  _addListeners() {
-    _engine.setEventHandler(RtcEngineEventHandler(
-      joinChannelSuccess: (channel, uid, elapsed) {
-        log('joinChannelSuccess ${channel} ${uid} ${elapsed}');
-        setState(() {
-          isJoined = true;
-        });
-      },
-      userJoined: (uid, elapsed) {
-        log('userJoined  ${uid} ${elapsed}');
-        setState(() {
-          remoteUid.add(uid);
-        });
-      },
-      userOffline: (uid, reason) {
-        log('userOffline  ${uid} ${reason}');
-        setState(() {
-          remoteUid.removeWhere((element) => element == uid);
-        });
-      },
-      leaveChannel: (stats) {
-        log('leaveChannel ${stats.toJson()}');
-        setState(() {
-          isJoined = false;
-          remoteUid.clear();
-        });
-      },
-    ));
-  }
-
-  _joinChannel() async {
-    if (defaultTargetPlatform == TargetPlatform.android) {
-      await [Permission.microphone, Permission.camera].request();
-    }
-    await _engine.joinChannel(config.token, channelId, null, config.uid);
-  }
-
-  _leaveChannel() async {
-    await _engine.leaveChannel();
-  }
-
-  _switchCamera() {
-    _engine.switchCamera().then((value) {
-      setState(() {
-        switchCamera = !switchCamera;
-      });
-    }).catchError((err) {
-      log('switchCamera $err');
-    });
-  }
-
-  _switchRender() {
-    setState(() {
-      switchRender = !switchRender;
-      remoteUid = List.of(remoteUid.reversed);
-    });
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Stack(
-      children: [
-        Column(
-          children: [
-            TextField(
-              controller: _controller,
-              decoration: InputDecoration(hintText: 'Channel ID'),
-              onChanged: (text) {
-                setState(() {
-                  channelId = text;
-                });
-              },
-            ),
-            Row(
-              children: [
-                Expanded(
-                  flex: 1,
-                  child: ElevatedButton(
-                    onPressed:
-                        isJoined ? this._leaveChannel : this._joinChannel,
-                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
-                  ),
-                )
-              ],
-            ),
-            _renderVideo(),
-          ],
-        ),
-        Align(
-          alignment: Alignment.bottomRight,
-          child: Column(
-            mainAxisSize: MainAxisSize.min,
-            children: [
-              ElevatedButton(
-                onPressed: this._switchCamera,
-                child: Text('Camera ${switchCamera ? 'front' : 'rear'}'),
-              ),
-            ],
-          ),
-        )
-      ],
-    );
-  }
-
-  _renderVideo() {
-    return Expanded(
-      child: Stack(
-        children: [
-          RtcLocalView.SurfaceView(),
-          Align(
-            alignment: Alignment.topLeft,
-            child: SingleChildScrollView(
-              scrollDirection: Axis.horizontal,
-              child: Row(
-                children: List.of(remoteUid.map(
-                  (e) => GestureDetector(
-                    onTap: this._switchRender,
-                    child: Container(
-                      width: 120,
-                      height: 120,
-                      child: RtcRemoteView.SurfaceView(
-                        uid: e,
-                      ),
-                    ),
-                  ),
-                )),
-              ),
-            ),
-          )
-        ],
-      ),
-    );
-  }
-}

+ 0 - 138
lib/pages/basic/string_uid.dart

@@ -1,138 +0,0 @@
-import 'dart:developer';
-
-import 'package:agora_rtc_engine/rtc_engine.dart';
-import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:permission_handler/permission_handler.dart';
-
-/// MultiChannel Example
-class StringUid extends StatefulWidget {
-  @override
-  State<StatefulWidget> createState() => _State();
-}
-
-class _State extends State<StringUid> {
-  late final RtcEngine _engine;
-  String channelId = config.channelId;// 频道id
-  String stringUid = config.stringUid;// 用户id
-  bool isJoined = false; // user 是否加入 频道
-  TextEditingController? _controller0, _controller1;
-
-  @override
-  void initState() {
-    super.initState();
-    _controller0 = TextEditingController(text: channelId);
-    _controller1 = TextEditingController(text: stringUid);
-    this._initEngine();
-  }
-
-  @override
-  void dispose() {
-    super.dispose();
-    _engine.destroy();
-  }
-/// 初始化IM
-  _initEngine() async {
-    _engine = await RtcEngine.createWithContext(RtcEngineContext(config.appId));
-    this._addListeners();
-
-    await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
-    await _engine.setClientRole(ClientRole.Broadcaster);
-  }
-
-  _addListeners() {
-    _engine.setEventHandler(RtcEngineEventHandler(
-      joinChannelSuccess: (channel, uid, elapsed) {
-        log('joinChannelSuccess ${channel} ${uid} ${elapsed}');
-        setState(() {
-          isJoined = true;
-        });
-      },
-      leaveChannel: (stats) {
-        log('leaveChannel ${stats.toJson()}');
-        setState(() {
-          isJoined = false;
-        });
-      },
-    ));
-  }
-
-  _joinChannel() async {
-    if (defaultTargetPlatform == TargetPlatform.android) {
-      await Permission.microphone.request();
-    }
-    await _engine.joinChannelWithUserAccount(
-        config.token, channelId, stringUid);
-  }
-
-  _leaveChannel() async {
-    await _engine.leaveChannel();
-  }
-  /// 获取当前用户信息
-  _getUserInfo() {
-    _engine.getUserInfoByUserAccount(stringUid).then((userInfo) {
-      log('getUserInfoByUserAccount ${userInfo.toJson()}');
-      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
-        content: Text('${userInfo.toJson()}'),
-      ));
-    }).catchError((err) {
-      log('getUserInfoByUserAccount ${err}');
-    });
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Stack(
-      children: [
-        Column(
-          children: [
-            TextField(
-              controller: _controller0,
-              decoration: InputDecoration(hintText: '聊天室'),
-              onChanged: (text) {
-                setState(() {
-                  channelId = text;
-                });
-              },
-            ),
-            TextField(
-              controller: _controller1,
-              decoration: InputDecoration(hintText: '用户名'),
-              onChanged: (text) {
-                setState(() {
-                  stringUid = text;
-                });
-              },
-            ),
-            Row(
-              children: [
-                Expanded(
-                  flex: 1,
-                  child: ElevatedButton(
-                    onPressed:
-                        isJoined ? this._leaveChannel : this._joinChannel,
-                    child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
-                  ),
-                )
-              ],
-            ),
-          ],
-        ),
-        Align(
-          alignment: Alignment.bottomRight,
-          child: Column(
-            mainAxisSize: MainAxisSize.min,
-            children: [
-              ElevatedButton(
-                onPressed: this._getUserInfo,
-                child: Text('Get userInfo'),
-              ),
-            ],
-          ),
-        )
-      ],
-    );
-  }
-}

+ 14 - 2
pubspec.yaml

@@ -3,15 +3,21 @@ description: Demonstrates how to use the agora_rtc_engine plugin.
 publish_to: 'none'
 
 environment:
-  sdk: '>=2.12.0 <3.0.0'
+  sdk: ">=2.16.1 <3.0.0"
 
 dependencies:
   flutter:
     sdk: flutter
   agora_rtc_engine: ^6.2.0
   cupertino_icons: ^1.0.2
-  permission_handler: ^10.1.0
+  permission_handler: ^10.2.0
+  path_provider: ^2.0.8
 
+  video_raw_data:
+    git:
+      url: https://github.com/AgoraIO-Extensions/RawDataPluginSample.git
+      path: frameworks/flutter/video_raw_data
+      ref: c1e6426a2aa4381b23e4633a6ef4a9b3b075fa1f
 dev_dependencies:
   integration_test:
     sdk: flutter
@@ -24,3 +30,9 @@ flutter:
   uses-material-design: true
   assets:
     - assets/Sound_Horizon.mp3
+    - assets/audio_mixing/Agora.io-Interactions.mp3
+    - assets/agora-logo.png
+    - assets/dang.mp3
+    - assets/ding.mp3
+    - assets/gif.gif
+    - assets/jpg.jpg