diff --git a/flutter_hbb/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so b/flutter_hbb/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
deleted file mode 120000
index 0684cadc9..000000000
--- a/flutter_hbb/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../target/aarch64-linux-android/release/librustdesk.so
\ No newline at end of file
diff --git a/flutter_hbb/android/app/src/main/jniLibs/x86_64/librustdesk.so b/flutter_hbb/android/app/src/main/jniLibs/x86_64/librustdesk.so
deleted file mode 120000
index a4e9274f6..000000000
--- a/flutter_hbb/android/app/src/main/jniLibs/x86_64/librustdesk.so
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../target/x86_64-linux-android/release/librustdesk.so
\ No newline at end of file
diff --git a/flutter_hbb/lib/home_page.dart b/flutter_hbb/lib/home_page.dart
index 8dcb57cc2..1de6ee6af 100644
--- a/flutter_hbb/lib/home_page.dart
+++ b/flutter_hbb/lib/home_page.dart
@@ -172,6 +172,9 @@ class _HomePageState extends State<HomePage> {
   }
 
   Widget getPeers() {
+    if (!FFI.ffiModel.initialized) {
+      return Container();
+    }
     final cards = <Widget>[];
     var peers = FFI.peers();
     peers.forEach((p) {
@@ -211,6 +214,8 @@ class _HomePageState extends State<HomePage> {
 
 void showServer(BuildContext context) {
   final formKey = GlobalKey<FormState>();
+  final id0 = FFI.getByName('custom-rendezvous-server');
+  final relay0 = FFI.getByName('relay-server');
   var id = '';
   var relay = '';
   showAlertDialog(
@@ -222,31 +227,23 @@ void showServer(BuildContext context) {
                 child:
                     Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
                   TextFormField(
+                    initialValue: id0,
                     decoration: InputDecoration(
                       labelText: 'ID Server',
                     ),
-                    validator: (value) {
-                      if (value.isEmpty) {
-                        return 'Please enter valid server address';
-                      }
-                      return null;
-                    },
+                    validator: validate,
                     onSaved: (String value) {
-                      id = value;
+                      id = value.trim();
                     },
                   ),
                   TextFormField(
+                    initialValue: relay0,
                     decoration: InputDecoration(
                       labelText: 'Relay Server',
                     ),
-                    validator: (value) {
-                      if (value.isEmpty) {
-                        return 'Please enter valid server address';
-                      }
-                      return null;
-                    },
+                    validator: validate,
                     onSaved: (String value) {
-                      relay = value;
+                      relay = value.trim();
                     },
                   ),
                 ])),
@@ -263,6 +260,9 @@ void showServer(BuildContext context) {
                 onPressed: () {
                   if (formKey.currentState.validate()) {
                     formKey.currentState.save();
+                    if (id != id0)
+                      FFI.setByName('custom-rendezvous-server', id);
+                    if (relay != relay0) FFI.setByName('relay-server', relay);
                     Navigator.pop(context);
                   }
                 },
@@ -273,3 +273,12 @@ void showServer(BuildContext context) {
       () async => true,
       true);
 }
+
+String validate(value) {
+  value = value.trim();
+  if (value.isEmpty) {
+    return null;
+  }
+  final res = FFI.getByName('test_if_valid_server', value);
+  return res.isEmpty ? null : res;
+}
diff --git a/flutter_hbb/lib/model.dart b/flutter_hbb/lib/model.dart
index 17ddbc000..ef755bd14 100644
--- a/flutter_hbb/lib/model.dart
+++ b/flutter_hbb/lib/model.dart
@@ -1,6 +1,8 @@
 import 'package:ffi/ffi.dart';
 import 'package:flutter/gestures.dart';
 import 'package:path_provider/path_provider.dart';
+import 'package:flutter_sound/flutter_sound.dart';
+import 'package:device_info/device_info.dart';
 import 'dart:io';
 import 'dart:math';
 import 'dart:ffi';
@@ -31,6 +33,7 @@ class FfiModel with ChangeNotifier {
   bool _waitForImage;
   bool _initialized = false;
   final _permissions = Map<String, bool>();
+  final _audioPlayer = FlutterSoundPlayer();
 
   get permissions => _permissions;
   get initialized => _initialized;
@@ -40,6 +43,7 @@ class FfiModel with ChangeNotifier {
     clear();
     () async {
       await FFI.init();
+      await _audioPlayer.openAudioSession();
       _initialized = true;
       notifyListeners();
     }();
@@ -63,11 +67,17 @@ class FfiModel with ChangeNotifier {
     _permissions.clear();
   }
 
-  void update(
+  Future<Null> stopAudio() async {
+    final st = await _audioPlayer.getPlayerState();
+    if (st != PlayerState.isPlaying) return;
+    await _audioPlayer.stopPlayer();
+  }
+
+  Future<Null> update(
       String id,
       BuildContext context,
       void Function(Map<String, dynamic> evt, String id, BuildContext context)
-          handleMsgbox) {
+          handleMsgbox) async {
     var pos;
     for (;;) {
       var evt = FFI.popEvent();
@@ -88,7 +98,15 @@ class FfiModel with ChangeNotifier {
       } else if (name == 'permission') {
         FFI.ffiModel.updatePermission(evt);
       } else if (name == "audio_format") {
-        //
+        try {
+          var s = int.parse(evt['sample_rate']);
+          var c = int.parse(evt['channels']);
+          await stopAudio();
+          await _audioPlayer.startPlayerFromStream(
+              codec: Codec.opusWebM, numChannels: c, sampleRate: s);
+        } catch (e) {
+          print('audio_format: $e');
+        }
       }
     }
     if (pos != null) FFI.cursorModel.updateCursorPosition(pos);
@@ -108,9 +126,22 @@ class FfiModel with ChangeNotifier {
           try {
             // my throw exception, because the listener maybe already dispose
             FFI.imageModel.update(image);
-          } catch (e) {}
+          } catch (e) {
+            print('update image: $e');
+          }
         });
       }
+      var frame = FFI._getAudio();
+      if (frame != null && frame != nullptr) {
+        final ref = frame.ref;
+        final bytes = Uint8List.sublistView(ref.data.asTypedList(ref.len));
+        try {
+          await _audioPlayer.feedFromStream(bytes);
+        } catch (e) {
+          print('play audio frame: $e');
+        }
+        FFI._freeRgba(frame);
+      }
     }
   }
 
@@ -355,7 +386,9 @@ class CursorModel with ChangeNotifier {
       try {
         // my throw exception, because the listener maybe already dispose
         notifyListeners();
-      } catch (e) {}
+      } catch (e) {
+        print('notify cursor: $e');
+      }
     });
   }
 
@@ -465,7 +498,7 @@ class FFI {
               Peer.fromJson(s[0] as String, s[1] as Map<String, dynamic>))
           .toList();
     } catch (e) {
-      print(e);
+      print('peers(): $e');
     }
     return [];
   }
@@ -493,7 +526,7 @@ class FFI {
       Map<String, dynamic> event = json.decode(s);
       return event;
     } catch (e) {
-      print(e);
+      print('popEvent(): $e');
     }
     return null;
   }
@@ -543,6 +576,10 @@ class FFI {
     _getRgba = dylib.lookupFunction<F5, F5>('get_rgba');
     _getAudio = dylib.lookupFunction<F5, F5>('get_audio');
     _dir = (await getApplicationDocumentsDirectory()).path;
+    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
+    AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
+    print(
+        '${androidInfo.product} ${androidInfo.brand} ${androidInfo.device} ${androidInfo.model} ${androidInfo.brand} ${androidInfo.manufacturer}');
     setByName('init', _dir);
   }
 }
diff --git a/flutter_hbb/pubspec.lock b/flutter_hbb/pubspec.lock
index 4a14ca263..f65d572b2 100644
--- a/flutter_hbb/pubspec.lock
+++ b/flutter_hbb/pubspec.lock
@@ -43,6 +43,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.15.0-nullsafety.3"
+  convert:
+    dependency: transitive
+    description:
+      name: convert
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.5"
   csslib:
     dependency: transitive
     description:
@@ -57,6 +71,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.0.0"
+  device_info:
+    dependency: "direct main"
+    description:
+      name: device_info
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
+  device_info_platform_interface:
+    dependency: transitive
+    description:
+      name: device_info_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.1"
   fake_async:
     dependency: transitive
     description:
@@ -90,6 +118,27 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.2.0"
+  flutter_sound:
+    dependency: "direct main"
+    description:
+      name: flutter_sound
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.4.2+1"
+  flutter_sound_platform_interface:
+    dependency: transitive
+    description:
+      name: flutter_sound_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.4.2+1"
+  flutter_sound_web:
+    dependency: transitive
+    description:
+      name: flutter_sound_web
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.4.2+1"
   flutter_spinkit:
     dependency: transitive
     description:
@@ -135,6 +184,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.6.2"
+  logger:
+    dependency: transitive
+    description:
+      name: logger
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.7.0+2"
   matcher:
     dependency: transitive
     description:
@@ -233,6 +289,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.5"
+  recase:
+    dependency: transitive
+    description:
+      name: recase
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -266,6 +329,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.1.0-nullsafety.1"
+  synchronized:
+    dependency: transitive
+    description:
+      name: synchronized
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.0+2"
   term_glyph:
     dependency: transitive
     description:
@@ -294,6 +364,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.3.0-nullsafety.3"
+  uuid:
+    dependency: transitive
+    description:
+      name: uuid
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.2"
   vector_math:
     dependency: transitive
     description:
diff --git a/flutter_hbb/pubspec.yaml b/flutter_hbb/pubspec.yaml
index 2d6d6af40..89e034813 100644
--- a/flutter_hbb/pubspec.yaml
+++ b/flutter_hbb/pubspec.yaml
@@ -34,6 +34,8 @@ dependencies:
   flutter_easyloading: ^2.1.3
   tuple: ^1.0.1
   wakelock: ^0.2.1+1
+  flutter_sound: ^6.4.2+1
+  device_info: ^1.0.0 
 
 dev_dependencies:
   flutter_test: