Browse Source

update client tool

arthur.wu 2 years ago
parent
commit
d7554df9d0

+ 1 - 1
Tools/TestTools/client/.vscode/launch.json

@@ -14,7 +14,7 @@
                 "--web-port",
                 "8086",
                 "--web-hostname",
-                "127.0.0.1"
+                "192.168.6.175"
             ]
         },
 

+ 10 - 5
Tools/TestTools/client/lib/ConsultationList.dart

@@ -44,7 +44,9 @@ class _ConsultationListState extends State<ConsultationList> {
                           leading: ExcludeSemantics(
                             child: Icon(Icons.person),
                           ),
-                          title: Text(widget.consultations[index].id),
+                          title: Text(widget.consultations[index].id +
+                              " - " +
+                              widget.consultations[index].expertName),
                           subtitle:
                               Text(widget.consultations[index].patientName),
                           onTap: (() =>
@@ -182,14 +184,16 @@ class _ConsultationListState extends State<ConsultationList> {
 class Consultation {
   final String id;
   final String patientName;
+  final String expertName;
 
-  Consultation({required this.id, required this.patientName});
+  Consultation(
+      {required this.id, required this.patientName, required this.expertName});
 
   factory Consultation.fromJson(Map<String, dynamic> json) {
     var item = Consultation(
-      id: json['ConsultationCode'] as String,
-      patientName: json['PatientName'] as String,
-    );
+        id: json['ConsultationCode'] as String,
+        patientName: json['PatientName'] as String,
+        expertName: json['ExpertUserName'] as String);
 
     return item;
   }
@@ -198,6 +202,7 @@ class Consultation {
     return {
       'ConsultationCode': id,
       'PatientName': patientName,
+      'ExpertUserName': expertName,
     };
   }
 

+ 12 - 19
Tools/TestTools/client/lib/Services/ConsultationService.dart

@@ -1,4 +1,5 @@
 import 'dart:convert';
+import 'dart:html';
 import 'dart:js_util';
 import 'package:get_it/get_it.dart';
 import 'package:http/http.dart' as http;
@@ -13,28 +14,20 @@ import 'package:web_socket_channel/web_socket_channel.dart';
 import 'AppSettings.dart';
 import 'UserService.dart';
 import 'package:intl/intl.dart';
+import 'package:event/event.dart';
 
-class ConsultationService {
-  Future<Expert> apply(String host, String userName, String password) async {
-    var client = http.Client();
-    var now = DateTime.now();
-    var date = DateTime.utc(
-        now.year, now.month, now.day, now.hour, now.minute, now.second);
-    var data = new List<DataItemDTO>.empty();
-    //data.add(new DataItemDTO("key", "value"));
-    var requet = new ApplyConsultationRequest(
-        "expertusercode", "d", "s", date, data, "", "");
-    var body = sprintf(
-        '{"jsonrpc": "2.0", "method": "CommonLoginAsync", "params": [{"AnyAccount": "%s", "AnyCode": "", "Password": "%s", }], "id": 1 }',
-        [userName, password]);
-    final response = await client
-        .post(Uri.parse(AppSettings.host + '/ILoginService'), body: body);
-    print('response.body' + response.body);
-    final parsed = jsonDecode(response.body);
+class NotificationReceivedArgs extends EventArgs {
+  Map<String, dynamic> jsonMessage;
 
-    var record = new Expert(code: "code", userName: "accessToken");
+  NotificationReceivedArgs(this.jsonMessage);
+}
+
+class ConsultationService {
+  Event<NotificationReceivedArgs> NotificationReceived =
+      Event<NotificationReceivedArgs>();
 
-    return record; //TODO
+  RaiseConsultationNotificationReceived(Map<String, dynamic> jsonMessage) {
+    NotificationReceived.broadcast(NotificationReceivedArgs(jsonMessage));
   }
 
   Future<AppConsultationDataModel> LoadDataAsync() async {

+ 42 - 0
Tools/TestTools/client/lib/Services/UserService.dart

@@ -1,3 +1,4 @@
+import 'dart:async';
 import 'dart:convert';
 import 'dart:js_util';
 import 'package:get_it/get_it.dart';
@@ -18,6 +19,7 @@ class UserService {
   late WebSocketChannel? Channel = WebSocketChannel.connect(
     Uri.parse('ws://192.168.6.80:9301?token=${currentUser.accessToken}'),
   ); //TODO
+
   User? getCurrentUser() {
     //if (currentUser != null) //TODO workaround
     //{
@@ -73,6 +75,8 @@ class UserService {
         var jsonUser = user.toJson();
 
         this.storage.setItem(UserStroageKey, jsonUser);
+        var userAgenter = UserAgentClient(client, token);
+
         print('jsonUser:' + jsonUser.toString());
         var url = Uri.parse(AppSettings.host);
         print("object" + url.host);
@@ -99,6 +103,44 @@ class UserService {
   }
 }
 
+class UserAgentClient extends http.BaseClient {
+  //final String userAgent;
+  final http.Client _inner;
+  final String token;
+  late Timer? _timer;
+  UserAgentClient(this._inner, this.token) {
+    //cancelTimer();
+    final Duration duration = Duration(seconds: 300);
+    _timer = Timer(duration, () => Run());
+  }
+
+  Future<http.StreamedResponse> send(http.BaseRequest request) {
+    //request.headers['user-agent'] = userAgent;
+    return _inner.send(request);
+  }
+
+  void Run() {
+    //cancelTimer();
+    var request =
+        http.Request('POST', Uri.parse(AppSettings.host + '/IUserService'));
+    request.body =
+        '{"jsonrpc": "2.0", "method": "HeartRateAsync", "params": [{"Token": "$token" }], "id": 1 }';
+    var response = send(request);
+
+    response.asStream().listen((event) {
+      print('heartrate response:${event.toString()}');
+    });
+    response.then(
+        (value) => {print('heartrate result:' + value.stream.toString())});
+    //var parsed = jsonDecode(response.then((value) => {print(value)}));
+    //return parsed['result'] as bool;
+  }
+
+  void cancelTimer() {
+    _timer?.cancel();
+  }
+}
+
 class JsonRpcResult {}
 
 class User {

+ 8 - 1
Tools/TestTools/client/lib/UserView.dart

@@ -8,6 +8,7 @@ import 'package:get_it/get_it.dart';
 import 'package:sprintf/sprintf.dart';
 import 'package:ustest/ConsultationList.dart';
 import 'package:ustest/Services/UserService.dart';
+import 'package:ustest/meeting.dart';
 
 import 'Services/ConsultationService.dart';
 
@@ -160,6 +161,11 @@ class _UserView extends State<UserView> {
                           var disconnectNotification =
                               FinishNotifyRecordsMessage.fromJson(
                                   messageObject);
+                          var consultationService =
+                              GetIt.instance.get<ConsultationService>();
+                          consultationService
+                              .RaiseConsultationNotificationReceived(
+                                  messageObject);
                           print(
                               "FinishNotifyRecordsMessage.NotificationType:${disconnectNotification.notificationType},  ${disconnectNotification.codes}");
                         }
@@ -171,7 +177,8 @@ class _UserView extends State<UserView> {
                         child: new Text(message),
                       );
                     })
-                : Text('no notification')
+                : Text('no notification'),
+            //MeetingPage()
           ],
         ),
       ),

+ 932 - 0
Tools/TestTools/client/lib/meeting.dart

@@ -0,0 +1,932 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:get_it/get_it.dart';
+import 'package:ustest/Services/UserService.dart';
+import 'package:tencent_trtc_cloud/trtc_cloud_video_view.dart';
+import 'package:tencent_trtc_cloud/trtc_cloud.dart';
+import 'package:tencent_trtc_cloud/tx_beauty_manager.dart';
+import 'package:tencent_trtc_cloud/tx_device_manager.dart';
+import 'package:tencent_trtc_cloud/tx_audio_effect_manager.dart';
+import 'package:tencent_trtc_cloud/trtc_cloud_def.dart';
+import 'package:tencent_trtc_cloud/trtc_cloud_listener.dart';
+import 'package:provider/provider.dart';
+import 'Services/ConsultationService.dart';
+import 'models/meeting.dart';
+import 'tool.dart';
+
+const iosAppGroup = 'group.com.tencent.comm.trtc.demo';
+const iosExtensionName = 'TRTC Demo Screen';
+
+/// Meeting Page
+class MeetingPage extends StatefulWidget {
+  @override
+  State<StatefulWidget> createState() => MeetingPageState();
+}
+
+class MeetingPageState extends State<MeetingPage> with WidgetsBindingObserver {
+  final _scaffoldKey = GlobalKey<ScaffoldState>();
+  var meetModel;
+  var userInfo = {}; //Multiplayer video user list
+
+  bool isOpenMic = true; //whether turn on the microphone
+  bool isOpenCamera = true; //whether turn on the video
+  bool isFrontCamera = true; //front camera
+  bool isSpeak = true;
+  bool isDoubleTap = false;
+  bool isShowingWindow = false;
+  int? localViewId;
+  bool isShowBeauty = true; //whether enable beauty settings
+  String curBeauty = 'pitu';
+  double curBeautyValue = 6; //The default beauty value is 6
+  String doubleUserId = "";
+  String doubleUserIdType = "";
+
+  late TRTCCloud trtcCloud;
+  late TXDeviceManager txDeviceManager;
+  late TXBeautyManager txBeautyManager;
+  late TXAudioEffectManager txAudioManager;
+
+  List userList = [];
+  List userListLast = [];
+  List screenUserList = [];
+  int? meetId;
+  int quality = TRTCCloudDef.TRTC_AUDIO_QUALITY_DEFAULT;
+
+  late ScrollController scrollControl;
+  @override
+  initState() {
+    super.initState();
+    WidgetsBinding.instance!.addObserver(this);
+    meetModel = context.read<MeetingModel>();
+    var userSetting = meetModel.getUserSetting();
+    meetId = userSetting["meetId"];
+    var userService = GetIt.instance.get<UserService>();
+    //userService.currentUser?.userName
+    userInfo['userId'] = userSetting["userId"];
+    isOpenCamera = userSetting["enabledCamera"];
+    isOpenMic = userSetting["enabledMicrophone"];
+    var consultationService = GetIt.instance.get<ConsultationService>();
+    consultationService.NotificationReceived.subscribe(
+      (args) {
+        iniRoom(0, '');
+        initScrollListener();
+      },
+    );
+  }
+
+  iniRoom(int sdkAppId, String userSig) async {
+    // Create TRTCCloud singleton
+    trtcCloud = (await TRTCCloud.sharedInstance())!;
+    // Tencent Cloud Audio Effect Management Module
+    txDeviceManager = trtcCloud.getDeviceManager();
+    // Beauty filter and animated effect parameter management
+    txBeautyManager = trtcCloud.getBeautyManager();
+    // Tencent Cloud Audio Effect Management Module
+    txAudioManager = trtcCloud.getAudioEffectManager();
+    // Register event callback
+    trtcCloud.registerListener(onRtcListener);
+
+    // Enter the room
+    enterRoom(sdkAppId, userSig);
+
+    initData();
+
+    //Set beauty effect
+    txBeautyManager.setBeautyStyle(TRTCCloudDef.TRTC_BEAUTY_STYLE_NATURE);
+    txBeautyManager.setBeautyLevel(6);
+  }
+
+  @override
+  void didChangeAppLifecycleState(AppLifecycleState state) {
+    switch (state) {
+      case AppLifecycleState.inactive:
+        break;
+      case AppLifecycleState
+          .resumed: //Switch from the background to the foreground, and the interface is visible
+        if (!kIsWeb && Platform.isAndroid) {
+          userListLast = jsonDecode(jsonEncode(userList));
+          userList = [];
+          screenUserList = MeetingTool.getScreenList(userList);
+          this.setState(() {});
+
+          const timeout = const Duration(milliseconds: 100); //10ms
+          Timer(timeout, () {
+            userList = userListLast;
+            screenUserList = MeetingTool.getScreenList(userList);
+            this.setState(() {});
+          });
+        }
+        break;
+      case AppLifecycleState.paused: // Interface invisible, background
+        break;
+      case AppLifecycleState.detached:
+        break;
+    }
+  }
+
+  // Enter the trtc room
+  enterRoom(int sdkAppId, String userSig) async {
+    try {
+      userInfo['userSig'] = userSig;
+      meetModel.setUserInfo(userInfo);
+    } catch (err) {
+      userInfo['userSig'] = '';
+      print(err);
+    }
+
+    await trtcCloud.enterRoom(
+        TRTCParams(
+            sdkAppId: sdkAppId,
+            userId: userInfo['userId'],
+            userSig: userInfo['userSig'],
+            role: TRTCCloudDef.TRTCRoleAnchor,
+            roomId: meetId!),
+        TRTCCloudDef.TRTC_APP_SCENE_LIVE);
+  }
+
+  initData() async {
+    if (isOpenCamera) {
+      userList.add({
+        'userId': userInfo['userId'],
+        'type': 'video',
+        'visible': true,
+        'size': {'width': 0, 'height': 0}
+      });
+    } else {
+      userList.add({
+        'userId': userInfo['userId'],
+        'type': 'video',
+        'visible': false,
+        'size': {'width': 0, 'height': 0}
+      });
+    }
+    if (isOpenMic) {
+      if (kIsWeb) {
+        Future.delayed(Duration(seconds: 2), () {
+          trtcCloud.startLocalAudio(quality);
+        });
+      } else {
+        await trtcCloud.startLocalAudio(quality);
+      }
+    }
+
+    screenUserList = MeetingTool.getScreenList(userList);
+    meetModel.setList(userList);
+    this.setState(() {});
+  }
+
+  destoryRoom() async {
+    trtcCloud.unRegisterListener(onRtcListener);
+    await trtcCloud.exitRoom();
+    await TRTCCloud.destroySharedInstance();
+  }
+
+  @override
+  dispose() {
+    WidgetsBinding.instance!.removeObserver(this);
+    destoryRoom();
+    scrollControl.dispose();
+    super.dispose();
+  }
+
+  /// Event callbacks
+  onRtcListener(type, param) async {
+    if (type == TRTCCloudListener.onError) {
+      if (param['errCode'] == -1308) {
+        MeetingTool.toast('Failed to start screen recording', context);
+        await trtcCloud.stopScreenCapture();
+        userList[0]['visible'] = true;
+        isShowingWindow = false;
+        this.setState(() {});
+        trtcCloud.startLocalPreview(isFrontCamera, localViewId);
+      } else {
+        showErrordDialog(param['errMsg']);
+      }
+    }
+    if (type == TRTCCloudListener.onEnterRoom) {
+      if (param > 0) {
+        MeetingTool.toast('Enter room success', context);
+      }
+    }
+    if (type == TRTCCloudListener.onExitRoom) {
+      if (param > 0) {
+        MeetingTool.toast('Exit room success', context);
+      }
+    }
+    // Remote user entry
+    if (type == TRTCCloudListener.onRemoteUserEnterRoom) {
+      userList.add({
+        'userId': param,
+        'type': 'video',
+        'visible': false,
+        'size': {'width': 0, 'height': 0}
+      });
+      screenUserList = MeetingTool.getScreenList(userList);
+      this.setState(() {});
+      meetModel.setList(userList);
+    }
+    // Remote user leaves room
+    if (type == TRTCCloudListener.onRemoteUserLeaveRoom) {
+      String userId = param['userId'];
+      for (var i = 0; i < userList.length; i++) {
+        if (userList[i]['userId'] == userId) {
+          userList.removeAt(i);
+        }
+      }
+      //The user who is amplifying the video exit room
+      if (doubleUserId == userId) {
+        isDoubleTap = false;
+      }
+      screenUserList = MeetingTool.getScreenList(userList);
+      this.setState(() {});
+      meetModel.setList(userList);
+    }
+    if (type == TRTCCloudListener.onUserVideoAvailable) {
+      String userId = param['userId'];
+
+      if (param['available']) {
+        for (var i = 0; i < userList.length; i++) {
+          if (userList[i]['userId'] == userId &&
+              userList[i]['type'] == 'video') {
+            userList[i]['visible'] = true;
+          }
+        }
+      } else {
+        for (var i = 0; i < userList.length; i++) {
+          if (userList[i]['userId'] == userId &&
+              userList[i]['type'] == 'video') {
+            if (isDoubleTap &&
+                doubleUserId == userList[i]['userId'] &&
+                doubleUserIdType == userList[i]['type']) {
+              doubleTap(userList[i]);
+            }
+            trtcCloud.stopRemoteView(
+                userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);
+            userList[i]['visible'] = false;
+          }
+        }
+      }
+
+      screenUserList = MeetingTool.getScreenList(userList);
+      this.setState(() {});
+      meetModel.setList(userList);
+    }
+
+    if (type == TRTCCloudListener.onUserSubStreamAvailable) {
+      String userId = param["userId"];
+      if (param["available"]) {
+        userList.add({
+          'userId': userId,
+          'type': 'subStream',
+          'visible': true,
+          'size': {'width': 0, 'height': 0}
+        });
+      } else {
+        for (var i = 0; i < userList.length; i++) {
+          if (userList[i]['userId'] == userId &&
+              userList[i]['type'] == 'subStream') {
+            if (isDoubleTap &&
+                doubleUserId == userList[i]['userId'] &&
+                doubleUserIdType == userList[i]['type']) {
+              doubleTap(userList[i]);
+            }
+            trtcCloud.stopRemoteView(
+                userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SUB);
+            userList.removeAt(i);
+          }
+        }
+      }
+      screenUserList = MeetingTool.getScreenList(userList);
+      this.setState(() {});
+      meetModel.setList(userList);
+    }
+  }
+
+  // Screen scrolling left and right event
+  initScrollListener() {
+    scrollControl = ScrollController();
+    double lastOffset = 0;
+    scrollControl.addListener(() async {
+      double screenWidth = MediaQuery.of(context).size.width;
+      int pageSize = (scrollControl.offset / screenWidth).ceil();
+
+      if (lastOffset < scrollControl.offset) {
+        scrollControl.animateTo(pageSize * screenWidth,
+            duration: Duration(milliseconds: 100), curve: Curves.ease);
+        if (scrollControl.offset == pageSize * screenWidth) {
+          //Slide from left to right
+          for (var i = 1; i < pageSize * MeetingTool.screenLen; i++) {
+            await trtcCloud.stopRemoteView(
+                userList[i]['userId'],
+                userList[i]['type'] == "video"
+                    ? TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG
+                    : TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SUB);
+          }
+        }
+      } else {
+        scrollControl.animateTo((pageSize - 1) * screenWidth,
+            duration: Duration(milliseconds: 100), curve: Curves.ease);
+        if (scrollControl.offset == pageSize * screenWidth) {
+          var pageScreen = screenUserList[pageSize];
+          int initI = 0;
+          if (pageSize == 0) {
+            initI = 1;
+          }
+          for (var i = initI; i < pageScreen.length; i++) {
+            await trtcCloud.startRemoteView(
+                pageScreen[i]['userId'],
+                pageScreen[i]['type'] == "video"
+                    ? TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG
+                    : TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SUB,
+                pageScreen[i]['viewId']);
+          }
+        }
+      }
+      lastOffset = scrollControl.offset;
+    });
+  }
+
+  Future<bool?> showErrordDialog(errorMsg) {
+    return showDialog<bool>(
+      context: context,
+      barrierDismissible: false,
+      builder: (context) {
+        return AlertDialog(
+          title: Text("Tips"),
+          content: Text(errorMsg),
+          actions: <Widget>[
+            TextButton(
+              child: Text("Confirm"),
+              onPressed: () {
+                // Navigator.push(
+                //   context,
+                //   MaterialPageRoute(
+                //     builder: (context) => IndexPage(),
+                //   ),
+                // );
+              },
+            ),
+          ],
+        );
+      },
+    );
+  }
+
+  Future<bool?> showExitMeetingConfirmDialog() {
+    return showDialog<bool>(
+      context: context,
+      builder: (context) {
+        return AlertDialog(
+          title: Text("Tips"),
+          content: Text("Are you sure to exit the meeting?"),
+          actions: <Widget>[
+            TextButton(
+              child: Text("Cancel"),
+              onPressed: () => Navigator.of(context).pop(),
+            ),
+            TextButton(
+              child: Text("Confirm"),
+              onPressed: () {
+                Navigator.of(context).pop(true);
+              },
+            ),
+          ],
+        );
+      },
+    );
+  }
+
+  // Double click zoom in and zoom out
+  doubleTap(item) async {
+    Size screenSize = MediaQuery.of(context).size;
+    if (isDoubleTap) {
+      userList.remove(item);
+      isDoubleTap = false;
+      doubleUserId = "";
+      doubleUserIdType = "";
+      item['size'] = {'width': 0, 'height': 0};
+    } else {
+      userList.remove(item);
+      isDoubleTap = true;
+      doubleUserId = item['userId'];
+      doubleUserIdType = item['type'];
+      item['size'] = {'width': screenSize.width, 'height': screenSize.height};
+    }
+    // userself
+    if (item['userId'] == userInfo['userId']) {
+      userList.insert(0, item);
+      if (!kIsWeb && Platform.isIOS) {
+        await trtcCloud.stopLocalPreview();
+      }
+    } else {
+      userList.add(item);
+      if (item['type'] == 'video') {
+        await trtcCloud.stopRemoteView(
+            item['userId'], TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG);
+      } else {
+        await trtcCloud.stopRemoteView(
+            item['userId'], TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SUB);
+      }
+      if (isDoubleTap) {
+        userList[0]['visible'] = false;
+      } else {
+        if (!kIsWeb && Platform.isIOS) {
+          await trtcCloud.stopLocalPreview();
+        }
+        if (isOpenCamera) {
+          userList[0]['visible'] = true;
+        }
+      }
+    }
+
+    this.setState(() {});
+  }
+
+  startShare({String shareUserId = '', String shareUserSig = ''}) async {
+    if (shareUserId == '') await trtcCloud.stopLocalPreview();
+    trtcCloud.startScreenCapture(
+      TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SUB,
+      TRTCVideoEncParam(
+        videoFps: 10,
+        videoResolution: TRTCCloudDef.TRTC_VIDEO_RESOLUTION_1280_720,
+        videoBitrate: 1600,
+        videoResolutionMode: TRTCCloudDef.TRTC_VIDEO_RESOLUTION_MODE_PORTRAIT,
+      ),
+      appGroup: iosAppGroup,
+      shareUserId: shareUserId,
+      shareUserSig: shareUserSig,
+    );
+  }
+
+  onShareClick() async {
+    // if (kIsWeb) {
+    //   String shareUserId = 'share-' + userInfo['userId'];
+    //   String shareUserSig = await GenerateTestUserSig.genTestSig(shareUserId);
+    //   await startShare(shareUserId: shareUserId, shareUserSig: shareUserSig);
+    // } else if (!kIsWeb && Platform.isAndroid) {
+    //   if (!isShowingWindow) {
+    //     await startShare();
+    //     userList[0]['visible'] = false;
+    //     this.setState(() {
+    //       isShowingWindow = true;
+    //       isOpenCamera = false;
+    //     });
+    //   } else {
+    //     await trtcCloud.stopScreenCapture();
+    //     userList[0]['visible'] = true;
+    //     trtcCloud.startLocalPreview(isFrontCamera, localViewId);
+    //     this.setState(() {
+    //       isShowingWindow = false;
+    //       isOpenCamera = true;
+    //     });
+    //   }
+    // } else {
+    //   await startShare();
+    //   //The screen sharing function can only be tested on the real machine
+    //   ReplayKitLauncher.launchReplayKitBroadcast(iosExtensionName);
+    //   this.setState(() {
+    //     isOpenCamera = false;
+    //   });
+    // }
+    //TODO
+  }
+
+  Widget renderView(item, valueKey, width, height) {
+    if (item['visible']) {
+      return GestureDetector(
+          key: valueKey,
+          onDoubleTap: () {
+            doubleTap(item);
+          },
+          child: TRTCCloudVideoView(
+              key: valueKey,
+              viewType: TRTCCloudDef.TRTC_VideoView_TextureView,
+              // This parameter is required for rendering desktop.(Android/iOS/web no need)
+              textureParam: CustomRender(
+                userId: item['userId'],
+                isLocal: item['userId'] == userInfo['userId'] ? true : false,
+                streamType: item['type'] == 'video'
+                    ? TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG
+                    : TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SUB,
+                width: 72,
+                height: 120,
+              ),
+              onViewCreated: (viewId) async {
+                if (item['userId'] == userInfo['userId']) {
+                  await trtcCloud.startLocalPreview(isFrontCamera, viewId);
+                  setState(() {
+                    localViewId = viewId;
+                  });
+                } else {
+                  trtcCloud.startRemoteView(
+                      item['userId'],
+                      item['type'] == 'video'
+                          ? TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG
+                          : TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SUB,
+                      viewId);
+                }
+                item['viewId'] = viewId;
+              }));
+    } else {
+      return Container(
+        alignment: Alignment.center,
+        child: ClipOval(
+          child: Image.asset('images/avatar3_100.20191230.png', scale: 3.5),
+        ),
+      );
+    }
+  }
+
+  /// The user name and sound are displayed on the video layer
+  Widget videoVoice(item) {
+    return Positioned(
+      child: new Container(
+          child: Row(children: <Widget>[
+        Text(
+          item['userId'] == userInfo['userId']
+              ? item['userId'] + "(me)"
+              : item['userId'],
+          style: TextStyle(color: Colors.white),
+        ),
+        Container(
+          margin: EdgeInsets.only(left: 10),
+          child: Icon(
+            Icons.signal_cellular_alt,
+            color: Colors.white,
+            size: 20,
+          ),
+        ),
+      ])),
+      left: 24.0,
+      bottom: 80.0,
+    );
+  }
+
+  Widget topSetting() {
+    return new Align(
+        child: new Container(
+          margin: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
+          child: new Row(
+            mainAxisAlignment: MainAxisAlignment.spaceAround,
+            children: <Widget>[
+              IconButton(
+                  icon: Icon(
+                    isSpeak ? Icons.volume_up : Icons.hearing,
+                    color: Colors.white,
+                    size: 36.0,
+                  ),
+                  onPressed: () async {
+                    if (isSpeak) {
+                      txDeviceManager.setAudioRoute(
+                          TRTCCloudDef.TRTC_AUDIO_ROUTE_EARPIECE);
+                    } else {
+                      txDeviceManager
+                          .setAudioRoute(TRTCCloudDef.TRTC_AUDIO_ROUTE_SPEAKER);
+                    }
+                    setState(() {
+                      isSpeak = !isSpeak;
+                    });
+                  }),
+              IconButton(
+                  icon: Icon(
+                    Icons.camera_alt,
+                    color: Colors.white,
+                    size: 36.0,
+                  ),
+                  onPressed: () async {
+                    if (isFrontCamera) {
+                      txDeviceManager.switchCamera(false);
+                    } else {
+                      txDeviceManager.switchCamera(true);
+                    }
+                    setState(() {
+                      isFrontCamera = !isFrontCamera;
+                    });
+                  }),
+              Text(meetId.toString(),
+                  style: TextStyle(fontSize: 20, color: Colors.white)),
+              ElevatedButton(
+                //color: Colors.red,
+                //textColor: Colors.white,
+                onPressed: () async {
+                  bool? delete = await showExitMeetingConfirmDialog();
+                  if (delete != null) {
+                    Navigator.pop(context);
+                  }
+                },
+                child: Text(
+                  "Exit Meeting",
+                  style: TextStyle(fontSize: 16.0),
+                ),
+              )
+            ],
+          ),
+          height: 50.0,
+          color: Color.fromRGBO(200, 200, 200, 0.4),
+        ),
+        alignment: Alignment.topCenter);
+  }
+
+  ///Beauty setting floating layer
+  Widget beautySetting() {
+    return Positioned(
+      bottom: 80,
+      child: Offstage(
+        offstage: isShowBeauty,
+        child: Container(
+          padding: EdgeInsets.all(10),
+          color: Color.fromRGBO(0, 0, 0, 0.8),
+          height: 100,
+          width: MediaQuery.of(context).size.width,
+          child: Column(
+            children: [
+              Row(children: [
+                Expanded(
+                  flex: 2,
+                  child: Slider(
+                    value: curBeautyValue,
+                    min: 0,
+                    max: 9,
+                    divisions: 9,
+                    onChanged: (double value) {
+                      if (curBeauty == 'smooth' ||
+                          curBeauty == 'nature' ||
+                          curBeauty == 'pitu') {
+                        txBeautyManager.setBeautyLevel(value.round());
+                      } else if (curBeauty == 'whitening') {
+                        txBeautyManager.setWhitenessLevel(value.round());
+                      } else if (curBeauty == 'ruddy') {
+                        txBeautyManager.setRuddyLevel(value.round());
+                      }
+                      this.setState(() {
+                        curBeautyValue = value;
+                      });
+                    },
+                  ),
+                ),
+                Text(curBeautyValue.round().toString(),
+                    textAlign: TextAlign.center,
+                    style: TextStyle(color: Colors.white)),
+              ]),
+              Padding(
+                padding: EdgeInsets.only(top: 10),
+                child: Row(
+                  children: [
+                    GestureDetector(
+                      child: Container(
+                        alignment: Alignment.centerLeft,
+                        width: 80.0,
+                        child: Text(
+                          'Smooth',
+                          style: TextStyle(
+                              color: curBeauty == 'smooth'
+                                  ? Color.fromRGBO(64, 158, 255, 1)
+                                  : Colors.white),
+                        ),
+                      ),
+                      onTap: () => this.setState(() {
+                        txBeautyManager.setBeautyStyle(
+                            TRTCCloudDef.TRTC_BEAUTY_STYLE_SMOOTH);
+                        curBeauty = 'smooth';
+                        curBeautyValue = 6;
+                      }),
+                    ),
+                    GestureDetector(
+                      child: Container(
+                        alignment: Alignment.centerLeft,
+                        width: 80.0,
+                        child: Text(
+                          'Nature',
+                          style: TextStyle(
+                              color: curBeauty == 'nature'
+                                  ? Color.fromRGBO(64, 158, 255, 1)
+                                  : Colors.white),
+                        ),
+                      ),
+                      onTap: () => this.setState(() {
+                        txBeautyManager.setBeautyStyle(
+                            TRTCCloudDef.TRTC_BEAUTY_STYLE_NATURE);
+                        curBeauty = 'nature';
+                        curBeautyValue = 6;
+                      }),
+                    ),
+                    GestureDetector(
+                      child: Container(
+                        alignment: Alignment.centerLeft,
+                        width: 80.0,
+                        child: Text(
+                          'Pitu',
+                          style: TextStyle(
+                              color: curBeauty == 'pitu'
+                                  ? Color.fromRGBO(64, 158, 255, 1)
+                                  : Colors.white),
+                        ),
+                      ),
+                      onTap: () => this.setState(() {
+                        txBeautyManager.setBeautyStyle(
+                            TRTCCloudDef.TRTC_BEAUTY_STYLE_PITU);
+                        curBeauty = 'pitu';
+                        curBeautyValue = 6;
+                      }),
+                    ),
+                    GestureDetector(
+                      child: Container(
+                        alignment: Alignment.centerLeft,
+                        width: 50.0,
+                        child: Text(
+                          'Ruddy',
+                          style: TextStyle(
+                              color: curBeauty == 'ruddy'
+                                  ? Color.fromRGBO(64, 158, 255, 1)
+                                  : Colors.white),
+                        ),
+                      ),
+                      onTap: () => this.setState(() {
+                        txBeautyManager.setRuddyLevel(0);
+                        curBeauty = 'ruddy';
+                        curBeautyValue = 0;
+                      }),
+                    ),
+                  ],
+                ),
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget bottomSetting() {
+    return new Align(
+        child: new Container(
+          padding: EdgeInsets.fromLTRB(0, 0, 0, 20),
+          child: new Row(
+            mainAxisAlignment: MainAxisAlignment.spaceAround,
+            children: <Widget>[
+              IconButton(
+                  icon: Icon(
+                    isOpenMic ? Icons.mic : Icons.mic_off,
+                    color: Colors.white,
+                    size: 36.0,
+                  ),
+                  onPressed: () {
+                    if (isOpenMic) {
+                      trtcCloud.stopLocalAudio();
+                    } else {
+                      trtcCloud.startLocalAudio(quality);
+                    }
+                    setState(() {
+                      isOpenMic = !isOpenMic;
+                    });
+                  }),
+              IconButton(
+                  icon: Icon(
+                    isOpenCamera ? Icons.videocam : Icons.videocam_off,
+                    color: Colors.white,
+                    size: 36.0,
+                  ),
+                  onPressed: () {
+                    if (isOpenCamera) {
+                      userList[0]['visible'] = false;
+                      trtcCloud.stopLocalPreview();
+                      if (isDoubleTap &&
+                          doubleUserId == userList[0]['userId']) {
+                        doubleTap(userList[0]);
+                      }
+                    } else {
+                      userList[0]['visible'] = true;
+                    }
+                    setState(() {
+                      isOpenCamera = !isOpenCamera;
+                    });
+                  }),
+              IconButton(
+                  icon: Icon(
+                    Icons.face,
+                    color: Colors.white,
+                    size: 36.0,
+                  ),
+                  onPressed: () {
+                    this.setState(() {
+                      if (isShowBeauty) {
+                        isShowBeauty = false;
+                      } else {
+                        isShowBeauty = true;
+                      }
+                    });
+                  }),
+              IconButton(
+                  icon: Icon(
+                    Icons.people,
+                    color: Colors.white,
+                    size: 36.0,
+                  ),
+                  onPressed: () {
+                    Navigator.pushNamed(context, '/memberList');
+                  }),
+              IconButton(
+                icon: Icon(
+                  Icons.share_rounded,
+                  color: Colors.white,
+                  size: 36.0,
+                ),
+                onPressed: () {
+                  this.onShareClick();
+                },
+              ),
+              //SettingPage(),
+            ],
+          ),
+          height: 70.0,
+          color: Color.fromRGBO(200, 200, 200, 0.4),
+        ),
+        alignment: Alignment.bottomCenter);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      key: _scaffoldKey,
+      body: WillPopScope(
+        onWillPop: () async {
+          trtcCloud.exitRoom();
+          return true;
+        },
+        child: Stack(
+          children: <Widget>[
+            ListView.builder(
+                scrollDirection: Axis.horizontal,
+                physics: new ClampingScrollPhysics(),
+                itemCount: screenUserList.length,
+                cacheExtent: 0,
+                controller: scrollControl,
+                itemBuilder: (BuildContext context, index) {
+                  var item = screenUserList[index];
+                  return Container(
+                    width: MediaQuery.of(context).size.width,
+                    height: MediaQuery.of(context).size.height,
+                    color: Color.fromRGBO(19, 41, 75, 1),
+                    child: Wrap(
+                      children: List.generate(
+                        item.length,
+                        (index) => LayoutBuilder(
+                          key: ValueKey(item[index]['userId'] +
+                              item[index]['type'] +
+                              item[index]['size']['width'].toString()),
+                          builder: (BuildContext context,
+                              BoxConstraints constraints) {
+                            Size size = MeetingTool.getViewSize(
+                                MediaQuery.of(context).size,
+                                userList.length,
+                                index,
+                                item.length);
+                            double width = size.width;
+                            double height = size.height;
+                            if (isDoubleTap) {
+                              //Set the width and height of other video rendering to 1, otherwise the video will not be streamed
+                              if (item[index]['size']['width'] == 0) {
+                                width = 1;
+                                height = 1;
+                              }
+                            }
+                            ValueKey valueKey = ValueKey(item[index]['userId'] +
+                                item[index]['type'] +
+                                (isDoubleTap ? "1" : "0"));
+                            if (item[index]['size']['width'] > 0) {
+                              width = double.parse(
+                                  item[index]['size']['width'].toString());
+                              height = double.parse(
+                                  item[index]['size']['height'].toString());
+                            }
+                            return Container(
+                              key: valueKey,
+                              height: height,
+                              width: width,
+                              child: Stack(
+                                key: valueKey,
+                                children: <Widget>[
+                                  renderView(
+                                      item[index], valueKey, width, height),
+                                  videoVoice(item[index])
+                                ],
+                              ),
+                            );
+                          },
+                        ),
+                      ),
+                    ),
+                  );
+                }),
+            topSetting(),
+            beautySetting(),
+            bottomSetting()
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 154 - 0
Tools/TestTools/client/lib/member_list.dart

@@ -0,0 +1,154 @@
+import 'package:flutter/material.dart';
+import 'package:tencent_trtc_cloud/trtc_cloud.dart';
+import 'package:provider/provider.dart';
+import 'package:ustest/models/meeting.dart';
+import 'package:ustest/tool.dart';
+
+/// Member list page
+class MemberListPage extends StatefulWidget {
+  @override
+  State<StatefulWidget> createState() => MemberListPageState();
+}
+
+class MemberListPageState extends State<MemberListPage> {
+  late TRTCCloud trtcCloud;
+  var meetModel;
+  var userInfo;
+  List micList = [];
+  var micMap = {};
+  @override
+  initState() {
+    super.initState();
+    initRoom();
+    meetModel = context.read<MeetingModel>();
+    userInfo = meetModel.getUserInfo();
+    micList = meetModel.getList();
+  }
+
+  initRoom() async {
+    trtcCloud = (await TRTCCloud.sharedInstance())!;
+  }
+
+  @override
+  dispose() {
+    super.dispose();
+    micList = [];
+    micMap = {};
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Member List'),
+        centerTitle: true,
+        elevation: 0,
+        backgroundColor: Color.fromRGBO(14, 25, 44, 1),
+      ),
+      body: Container(
+        color: Color.fromRGBO(14, 25, 44, 1),
+        padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 10.0),
+        child: Stack(
+          children: <Widget>[
+            Container(
+              padding: const EdgeInsets.symmetric(horizontal: 10.0),
+              child: Consumer<MeetingModel>(
+                builder: (context, meet, child) {
+                  List newList = [];
+                  meet.userList.forEach((item) {
+                    if (item['type'] == 'video') {
+                      newList.add(item);
+                    }
+                  });
+                  micList = newList;
+                  return ListView(
+                    children: newList
+                        .map<Widget>((item) => Container(
+                              key: ValueKey(item['userId']),
+                              height: 50,
+                              child: Row(
+                                children: [
+                                  Expanded(
+                                    flex: 1,
+                                    child: Text(item['userId'],
+                                        style: TextStyle(
+                                            color: Colors.white, fontSize: 16)),
+                                  ),
+                                  Expanded(
+                                    flex: 1,
+                                    child: Offstage(
+                                      offstage:
+                                          item['userId'] == userInfo['userId'],
+                                      child: IconButton(
+                                          icon: Icon(
+                                            micMap[item['userId']] == null
+                                                ? Icons.mic
+                                                : Icons.mic_off,
+                                            color: Colors.white,
+                                            size: 36.0,
+                                          ),
+                                          onPressed: () {
+                                            if (micMap[item['userId']] ==
+                                                null) {
+                                              micMap[item['userId']] = true;
+                                              trtcCloud.muteRemoteAudio(
+                                                  item['userId'], true);
+                                            } else {
+                                              micMap.remove(item['userId']);
+                                              trtcCloud.muteRemoteAudio(
+                                                  item['userId'], false);
+                                            }
+                                            this.setState(() {});
+                                          }),
+                                    ),
+                                  ),
+                                ],
+                              ),
+                            ))
+                        .toList(),
+                  );
+                },
+              ),
+            ),
+            new Align(
+                child: new Container(
+                  // grey box
+                  child: new Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceAround,
+                    children: <Widget>[
+                      ElevatedButton(
+                        //color: Color.fromRGBO(245, 108, 108, 1),
+                        onPressed: () {
+                          trtcCloud.muteAllRemoteAudio(true);
+                          MeetingTool.toast('Total silence', context);
+                          for (var i = 0; i < micList.length; i++) {
+                            micMap[micList[i]['userId']] = true;
+                          }
+                          this.setState(() {});
+                        },
+                        child: Text('Total silence',
+                            style: TextStyle(color: Colors.white)),
+                      ),
+                      ElevatedButton(
+                        //color: Color.fromRGBO(64, 158, 255, 1),
+                        onPressed: () {
+                          trtcCloud.muteAllRemoteAudio(false);
+                          MeetingTool.toast('Lift all bans', context);
+                          this.setState(() {
+                            micMap = {};
+                          });
+                        },
+                        child: Text('Lift all bans',
+                            style: TextStyle(color: Colors.white)),
+                      ),
+                    ],
+                  ),
+                  height: 50.0,
+                ),
+                alignment: Alignment.bottomCenter),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 45 - 0
Tools/TestTools/client/lib/models/meeting.dart

@@ -0,0 +1,45 @@
+import 'dart:collection';
+
+import 'package:flutter/foundation.dart';
+
+class MeetingModel extends ChangeNotifier {
+  /// Internal, private state of the cart.
+  List _userList = [];
+  Map _userInfo = {};
+  Map _userSetting = {};
+
+  /// An unmodifiable view of the items in the cart.
+  UnmodifiableListView get userList => UnmodifiableListView(_userList);
+
+  void setList(list) {
+    _userList = list;
+    notifyListeners();
+  }
+
+  void setUserInfo(userInfo) {
+    _userInfo = userInfo;
+  }
+
+  void setUserSettig(userSetting) {
+    _userSetting = userSetting;
+  }
+
+  getUserSetting() {
+    return _userSetting;
+  }
+
+  getUserInfo() {
+    return _userInfo;
+  }
+
+  getList() {
+    return _userList;
+  }
+
+  /// Removes all items from the cart.
+  void removeAll() {
+    _userList.clear();
+    // This call tells the widgets that are listening to this model to rebuild.
+    notifyListeners();
+  }
+}

+ 42 - 0
Tools/TestTools/client/lib/tool.dart

@@ -0,0 +1,42 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_styled_toast/flutter_styled_toast.dart';
+
+class MeetingTool {
+  static toast(text, context) {
+    showToast(text, context: context, position: StyledToastPosition.center);
+  }
+
+  static int screenLen = 4;
+  static getScreenList(list) {
+    int len = screenLen;
+    List<List> result = [];
+    int index = 1;
+    while (true) {
+      if (index * len < list.length) {
+        List temp = list.skip((index - 1) * len).take(len).toList();
+        result.add(temp);
+        index++;
+        continue;
+      }
+      List temp = list.skip((index - 1) * len).toList();
+      result.add(temp);
+      break;
+    }
+    return result;
+  }
+
+  static Size getViewSize(
+      Size screenSize, int listLength, int index, int total) {
+    if (listLength < 5) {
+      if (total == 1) {
+        return screenSize;
+      }
+      if (total == 2) {
+        return Size(screenSize.width, screenSize.height / 2);
+      }
+    }
+    return Size(screenSize.width / 2, screenSize.height / 2);
+  }
+}

+ 48 - 1
Tools/TestTools/client/pubspec.lock

@@ -71,6 +71,13 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.5"
+  event:
+    dependency: "direct main"
+    description:
+      name: event
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.2"
   fake_async:
     dependency: transitive
     description:
@@ -111,6 +118,18 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.5.1"
+  flutter_localizations:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_styled_toast:
+    dependency: "direct main"
+    description:
+      name: flutter_styled_toast
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.3"
   flutter_test:
     dependency: "direct dev"
     description: flutter
@@ -163,6 +182,13 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.6.4"
+  json_annotation:
+    dependency: transitive
+    description:
+      name: json_annotation
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.7.0"
   localstorage:
     dependency: "direct main"
     description:
@@ -191,6 +217,13 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.8.0"
+  nested:
+    dependency: transitive
+    description:
+      name: nested
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.0"
   path:
     dependency: transitive
     description:
@@ -261,6 +294,13 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "4.2.3"
+  provider:
+    dependency: "direct main"
+    description:
+      name: provider
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "6.0.4"
   sentry:
     dependency: transitive
     description:
@@ -315,6 +355,13 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.0.0"
+  tencent_trtc_cloud:
+    dependency: "direct main"
+    description:
+      name: tencent_trtc_cloud
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.4.0"
   term_glyph:
     dependency: transitive
     description:
@@ -372,5 +419,5 @@ packages:
     source: hosted
     version: "0.2.0+2"
 sdks:
-  dart: ">=2.17.0-0 <3.0.0"
+  dart: ">=2.17.0 <3.0.0"
   flutter: ">=2.0.0"

+ 4 - 0
Tools/TestTools/client/pubspec.yaml

@@ -38,6 +38,10 @@ dependencies:
   autocomplete_textfield: ^2.0.1
   flutter_combo_box: ^0.0.2+5
   intl: ^0.17.0
+  tencent_trtc_cloud: ^2.4.0
+  provider: ^6.0.4
+  event: ^2.1.2
+  flutter_styled_toast: ^2.1.3
   
 
 dev_dependencies: