瀏覽代碼

硬件检测基本可用版本

gavin.chen 2 年之前
父節點
當前提交
08b6b015dd

+ 1 - 1
README.md

@@ -1,3 +1,3 @@
 # fis_lib_basictest
 
-这是Flyinsono的基础设置界面,目前基础设置包含的内容为,网络测试、麦克风测试、扬声器测试、摄像头测试。
+这是 Flyinsono 的基础设置界面,目前基础设置包含的内容为,网络测试、麦克风测试、扬声器测试、摄像头测试。

二進制
assets/testspeak.mp3


+ 10 - 0
lib/hardware_detection/bindings.dart

@@ -0,0 +1,10 @@
+import 'package:get/get.dart';
+
+import 'controller.dart';
+
+class HardwareDetectionBinding implements Bindings {
+  @override
+  void dependencies() {
+    Get.lazyPut<HardwareDetectionController>(() => HardwareDetectionController());
+  }
+}

+ 427 - 0
lib/hardware_detection/controller.dart

@@ -0,0 +1,427 @@
+import 'dart:async';
+import 'package:camera/camera.dart';
+import 'package:get/get.dart';
+import 'package:tencent_trtc_cloud/trtc_cloud.dart';
+import 'package:tencent_trtc_cloud/tx_device_manager.dart';
+import 'package:flutter_sound_platform_interface/flutter_sound_recorder_platform_interface.dart';
+import 'package:flutter_sound/flutter_sound.dart';
+import 'package:audio_session/audio_session.dart';
+import 'package:flutter/foundation.dart' show kIsWeb;
+import 'index.dart';
+
+class HardwareDetectionController extends GetxController {
+  HardwareDetectionController();
+
+  final state = HardwareDetectionState();
+
+  /// 相机控制器需要的参数
+  late List<CameraDescription> _cameras;
+  late CameraController cameraController;
+
+  /// 扬声器控制器需要的参数
+  final FlutterSoundPlayer flutterSound = FlutterSoundPlayer();
+
+  /// 麦克风控制器需要的参数
+  Codec _codec = Codec.aacMP4;
+  String _mPath = 'tau_file.mp4';
+  final FlutterSoundRecorder flutterRecorder = FlutterSoundRecorder();
+
+  /// 初始化设备列表
+  Future<void> _intialize() async {
+    await _initCameras();
+    await _initMicrophones();
+    await _initSpeakers();
+    print("总设备数:${state.deviceList.length}");
+    state.cameraList = state.deviceList
+        .where((element) => element.type == HardwareDeviceType.camera)
+        .toList();
+    state.microphoneList = state.deviceList
+        .where((element) => element.type == HardwareDeviceType.microphone)
+        .toList();
+    state.speakerList = state.deviceList
+        .where((element) => element.type == HardwareDeviceType.speaker)
+        .toList();
+    if (state.cameraList.isNotEmpty) state.currentCamera = state.cameraList[0];
+    if (state.microphoneList.isNotEmpty)
+      state.currentMicrophone = state.microphoneList[0];
+    if (state.speakerList.isNotEmpty)
+      state.currentSpeaker = state.speakerList[0];
+  }
+
+  /// 初始化摄像头列表
+  Future<void> _initCameras() async {
+    List<HardwareDevice> _cameraList = await _trtcGetCameras();
+    _cameras = await availableCameras();
+    print("初始摄像头数量:${_cameras.length}");
+    for (var i = 0; i < _cameras.length; i++) {
+      for (var j = 0; j < _cameraList.length; j++) {
+        if (_cameras[i].name == _cameraList[j].name) {
+          state.deviceList.add(_cameraList[j]);
+          break;
+        }
+      }
+    }
+  }
+
+  /// 初始化麦克风列表
+  Future<void> _initMicrophones() async {
+    List<HardwareDevice> _microphoneList = await _trtcGetMicrophones();
+    state.deviceList.addAll(_microphoneList);
+    await _initTheRecorder();
+  }
+
+  /// 初始化扬声器列表
+  Future<void> _initSpeakers() async {
+    List<HardwareDevice> _speakerList = await _trtcGetSpeakers();
+    state.deviceList.addAll(_speakerList);
+    await _initThePlayer();
+  }
+
+  // 刷新设备列表
+  void refreshDevices() {
+    ///TODO 刷新设备列表
+  }
+
+  /// 设置当前选中的摄像头可用性
+  void setCameraAvailable(bool available) {
+    state.cameraAvailable = available;
+    stopDetectingCamera();
+  }
+
+  /// 设置当前选中的麦克风可用性
+  void setMicrophoneAvailable(bool available) {
+    state.microphoneAvailable = available;
+    stopDetectingMicrophone();
+  }
+
+  /// 设置当前选中的扬声器可用性
+  void setSpeakerAvailable(bool available) {
+    state.speakerAvailable = available;
+    stopDetectingSpeaker();
+  }
+
+  /// 开始检测摄像头
+  void startDetectingCamera() {
+    state.cameraAvailable = null;
+    state.detectingCamera = true;
+    _openSelectedCamera();
+  }
+
+  /// 结束检测摄像头
+  void stopDetectingCamera() {
+    state.detectingCamera = false;
+    _stopCamera();
+  }
+
+  /// 开始检测麦克风
+  void startDetectingMicrophone() {
+    state.microphoneAvailable = null;
+    state.detectingMicrophone = true;
+    _startRecordMicrophone();
+  }
+
+  /// 结束检测麦克风
+  void stopDetectingMicrophone() {
+    state.detectingMicrophone = false;
+    _stopRecordMicrophone();
+  }
+
+  /// 开始检测扬声器
+  void startDetectingSpeaker() {
+    state.speakerAvailable = null;
+    state.detectingSpeaker = true;
+    _startPlaySound();
+  }
+
+  /// 结束检测扬声器
+  void stopDetectingSpeaker() {
+    state.detectingSpeaker = false;
+    _stopPlaySound();
+  }
+
+  /// 选择摄像头
+  void selectCameraById(String deviceId) {
+    if (state.currentCamera != null && deviceId == state.currentCamera!.id)
+      return;
+    state.cameraList.forEach((element) {
+      if (element.id == deviceId) {
+        state.currentCamera = element;
+        state.cameraAvailable = null;
+        print("选择摄像头: ${element.name} id: ${element.id}");
+      }
+    });
+    if (state.currentCamera == null) return;
+
+    /// 如果当前正在检测摄像头,则刷新
+    if (state.detectingCamera) {
+      _stopCamera();
+      _openSelectedCamera();
+    }
+  }
+
+  /// 选择麦克风 [web 无法修改麦克风]
+  void selectMicrophoneById(String deviceId) {
+    state.microphoneList.forEach((element) {
+      if (element.id == deviceId) {
+        state.currentMicrophone = element;
+        state.microphoneAvailable = null;
+        print("选择麦克风: ${element.name} id: ${element.id}");
+      }
+    });
+    if (state.currentMicrophone == null) return;
+  }
+
+  /// 选择扬声器
+  void selectSpeakerById(String deviceId) {
+    state.speakerList.forEach((element) {
+      if (element.id == deviceId) {
+        state.currentSpeaker = element;
+        state.speakerAvailable = null;
+        print("选择扬声器: ${element.name} id: ${element.id}");
+      }
+    });
+    if (state.currentSpeaker == null) return;
+  }
+
+  /// 刷新扬声器列表
+  Future<void> refreshSpeakerList() async {
+    if (state.detectingSpeaker) stopDetectingSpeaker();
+    state.speakerList = [];
+    state.speakerAvailable = null;
+    await Future.delayed(Duration(milliseconds: 100));
+    state.speakerList = await _trtcGetSpeakers();
+    for (int i = 0; i < state.deviceList.length; i++) {
+      if (state.deviceList[i].type == HardwareDeviceType.speaker) {
+        state.deviceList.removeAt(i);
+        i--;
+      }
+    }
+    state.deviceList.addAll(state.speakerList);
+    print("当前总设备数量: ${state.deviceList.length}");
+  }
+
+  /// 刷新摄像头列表
+  Future<void> refreshCameraList() async {
+    if (state.detectingCamera) stopDetectingCamera();
+    state.cameraList = [];
+    state.cameraAvailable = null;
+    await Future.delayed(Duration(milliseconds: 100));
+    state.cameraList = await _trtcGetCameras();
+    for (int i = 0; i < state.deviceList.length; i++) {
+      if (state.deviceList[i].type == HardwareDeviceType.camera) {
+        state.deviceList.removeAt(i);
+        i--;
+      }
+    }
+    state.deviceList.addAll(state.cameraList);
+    print("当前总设备数量: ${state.deviceList.length}");
+  }
+
+  /// 刷新麦克风列表
+  Future<void> refreshMicrophoneList() async {
+    if (state.detectingMicrophone) stopDetectingMicrophone();
+    state.microphoneList = [];
+    state.microphoneAvailable = null;
+    await Future.delayed(Duration(milliseconds: 100));
+    state.microphoneList = await _trtcGetMicrophones();
+    for (int i = 0; i < state.deviceList.length; i++) {
+      if (state.deviceList[i].type == HardwareDeviceType.microphone) {
+        state.deviceList.removeAt(i);
+        i--;
+      }
+    }
+    state.deviceList.addAll(state.microphoneList);
+    print("当前总设备数量: ${state.deviceList.length}");
+  }
+
+  /// 调用 TRTC 获取摄像头列表
+  Future<List<HardwareDevice>> _trtcGetCameras() async {
+    List<HardwareDevice> deviceList = [];
+    TRTCCloud trtcCloud = (await TRTCCloud.sharedInstance())!;
+    TXDeviceManager deviceManager = trtcCloud.getDeviceManager();
+    final List<dynamic>? cameras = await deviceManager.getDevicesList(2);
+    if (cameras != null && cameras.isNotEmpty) {
+      cameras.forEach((camera) {
+        deviceList.add(HardwareDevice(
+            id: camera["deviceId"],
+            name: camera["label"],
+            type: HardwareDeviceType.camera));
+      });
+    }
+    return deviceList;
+  }
+
+  /// 调用 TRTC 获取麦克风列表
+  Future<List<HardwareDevice>> _trtcGetMicrophones() async {
+    List<HardwareDevice> deviceList = [];
+    TRTCCloud trtcCloud = (await TRTCCloud.sharedInstance())!;
+    TXDeviceManager deviceManager = trtcCloud.getDeviceManager();
+    final List<dynamic>? microphones = await deviceManager.getDevicesList(0);
+    if (microphones != null && microphones.isNotEmpty) {
+      microphones.forEach((microphone) {
+        deviceList.add(HardwareDevice(
+            id: microphone["deviceId"],
+            name: microphone["label"],
+            type: HardwareDeviceType.microphone));
+      });
+    }
+    return deviceList;
+  }
+
+  /// 调用 TRTC 获取扬声器列表
+  Future<List<HardwareDevice>> _trtcGetSpeakers() async {
+    List<HardwareDevice> deviceList = [];
+    TRTCCloud trtcCloud = (await TRTCCloud.sharedInstance())!;
+    TXDeviceManager deviceManager = trtcCloud.getDeviceManager();
+    final List<dynamic>? speakers = await deviceManager.getDevicesList(1);
+    if (speakers != null && speakers.isNotEmpty) {
+      speakers.forEach((speaker) {
+        deviceList.add(HardwareDevice(
+            id: speaker["deviceId"],
+            name: speaker["label"],
+            type: HardwareDeviceType.speaker));
+      });
+    }
+    return deviceList;
+  }
+
+  /// 打开当前选中的摄像头
+  _openSelectedCamera() {
+    bool found = false;
+    int cameraIndex = 0;
+    for (var i = 0; i < _cameras.length; i++) {
+      if (_cameras[i].name == state.currentCamera?.name) {
+        cameraIndex = i;
+        found = true;
+        break;
+      }
+    }
+    if (!found) {
+      print("未找到当前选中的摄像头");
+      return;
+    }
+    cameraController =
+        CameraController(_cameras[cameraIndex], ResolutionPreset.max);
+    cameraController.initialize().then((_) {
+      state.isDisplayCameraWindow = true;
+    }).catchError((Object e) {
+      if (e is CameraException) {
+        switch (e.code) {
+          case 'CameraAccessDenied':
+            // Handle access errors here.
+            break;
+          default:
+            // Handle other errors here.
+            break;
+        }
+      }
+    });
+  }
+
+  /// 关闭摄像头并释放
+  _stopCamera() {
+    state.isDisplayCameraWindow = false;
+    if (cameraController.value.isStreamingImages) {
+      cameraController.stopImageStream();
+    }
+    cameraController.dispose();
+  }
+
+  /// 使用当前的扬声器播放测试音频
+  Future<void> _startPlaySound() async {
+    await flutterSound.startPlayer(
+        fromURI: "assets/testspeak.mp3",
+        codec: Codec.aacADTS,
+        whenFinished: () {
+          print("播放完成");
+        });
+  }
+
+  /// 停止播放音频
+  _stopPlaySound() {
+    print("停止播放音频");
+    flutterSound.stopPlayer();
+  }
+
+  /// 初始化播放器
+  Future<void> _initThePlayer() async {
+    await flutterSound.openPlayer();
+    await flutterSound
+        .setSubscriptionDuration(const Duration(milliseconds: 100));
+    state.isDisplaySpeakerWave = true;
+  }
+
+  // 开始采集麦克风音频
+  void _startRecordMicrophone() async {
+    await flutterRecorder.startRecorder(
+        toFile: _mPath, codec: _codec, audioSource: AudioSource.microphone);
+  }
+
+  /// 停止采集麦克风音频
+  void _stopRecordMicrophone() async {
+    await flutterRecorder.stopRecorder();
+  }
+
+  /// 初始化麦克风录音器
+  Future<void> _initTheRecorder() async {
+    await flutterRecorder.openRecorder();
+    if (!await flutterRecorder.isEncoderSupported(_codec) && kIsWeb) {
+      _codec = Codec.opusWebM;
+      _mPath = 'tau_file.webm';
+      if (!await flutterRecorder.isEncoderSupported(_codec) && kIsWeb) {
+        state.isDisplayMicrophoneWave = true;
+        return;
+      }
+    }
+    // 设置采样率
+    await flutterRecorder
+        .setSubscriptionDuration(const Duration(milliseconds: 50));
+    final session = await AudioSession.instance;
+    await session.configure(AudioSessionConfiguration(
+      avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
+      avAudioSessionCategoryOptions:
+          AVAudioSessionCategoryOptions.allowBluetooth |
+              AVAudioSessionCategoryOptions.defaultToSpeaker,
+      avAudioSessionMode: AVAudioSessionMode.spokenAudio,
+      avAudioSessionRouteSharingPolicy:
+          AVAudioSessionRouteSharingPolicy.defaultPolicy,
+      avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
+      androidAudioAttributes: const AndroidAudioAttributes(
+        contentType: AndroidAudioContentType.speech,
+        flags: AndroidAudioFlags.none,
+        usage: AndroidAudioUsage.voiceCommunication,
+      ),
+      androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
+      androidWillPauseWhenDucked: true,
+    ));
+    state.isDisplayMicrophoneWave = true;
+  }
+
+  /// 在 widget 内存中分配后立即调用。
+  @override
+  void onInit() async {
+    await _intialize();
+    super.onInit();
+  }
+
+  /// 在 onInit() 之后调用 1 帧。这是进入的理想场所
+  @override
+  void onReady() {
+    super.onReady();
+  }
+
+  /// 在 [onDelete] 方法之前调用。
+  @override
+  void onClose() {
+    super.onClose();
+  }
+
+  /// dispose 释放内存
+  @override
+  void dispose() {
+    super.dispose();
+    cameraController.dispose();
+    flutterSound.closePlayer();
+    flutterRecorder.closeRecorder();
+  }
+}

+ 6 - 0
lib/hardware_detection/index.dart

@@ -0,0 +1,6 @@
+library hardware_detection;
+
+export './state.dart';
+export './controller.dart';
+export './bindings.dart';
+export './view.dart';

+ 133 - 0
lib/hardware_detection/state.dart

@@ -0,0 +1,133 @@
+import 'package:get/get.dart';
+
+class HardwareDetectionState {
+  /// *** 数据层状态 ***
+  ///
+  /// 数据层状态是指在 [Controller] 中的状态,用于控制数据的获取和存储。
+
+  /// 设备列表
+  List<HardwareDevice> _deviceList = [];
+  set deviceList(List<HardwareDevice> value) => _deviceList = value;
+  List<HardwareDevice> get deviceList => _deviceList;
+
+  /// *** UI 层状态 ***
+  ///
+  /// UI 层状态是指在 [View] 中的状态,用于控制页面的显示效果。
+  /// UI 层状态通常是通过 [Rx] 类型的变量来实现的。
+  /// 摄像头初始化完成
+  RxBool _isDisplayCameraWindow = RxBool(false);
+
+  /// 扬声器初始化完成
+  RxBool _isDisplaySpeakerWave = RxBool(false);
+
+  /// 麦克风初始化完成
+  RxBool _isDisplayMicrophoneWave = RxBool(false);
+
+  /// 摄像头列表
+  RxList<HardwareDevice> _cameraList = RxList<HardwareDevice>([]);
+
+  /// 麦克风列表
+  RxList<HardwareDevice> _microphoneList = RxList<HardwareDevice>([]);
+
+  /// 扬声器列表
+  RxList<HardwareDevice> _speakerList = RxList<HardwareDevice>([]);
+
+  /// 当前选中的摄像头
+  Rx<HardwareDevice?> _currentCamera = Rx<HardwareDevice?>(null);
+
+  /// 当前选中的麦克风
+  Rx<HardwareDevice?> _currentMicrophone = Rx<HardwareDevice?>(null);
+
+  /// 当前选中的扬声器
+  Rx<HardwareDevice?> _currentSpeaker = Rx<HardwareDevice?>(null);
+
+  /// 当前摄像头可用性
+  Rx<bool?> _cameraAvailable = Rx(null);
+
+  /// 当前麦克风可用性
+  Rx<bool?> _microphoneAvailable = Rx(null);
+
+  /// 当前扬声器可用性
+  Rx<bool?> _speakerAvailable = Rx(null);
+
+  /// 是否正在检测摄像头
+  RxBool _detectingCamera = RxBool(false);
+
+  /// 是否正在检测麦克风
+  RxBool _detectingMicrophone = RxBool(false);
+
+  /// 是否正在检测扬声器
+  RxBool _detectingSpeaker = RxBool(false);
+
+  set isDisplayMicrophoneWave(bool value) =>
+      _isDisplayMicrophoneWave.value = value;
+  bool get isDisplayMicrophoneWave => _isDisplayMicrophoneWave.value;
+
+  set isDisplaySpeakerWave(bool value) => _isDisplaySpeakerWave.value = value;
+  bool get isDisplaySpeakerWave => _isDisplaySpeakerWave.value;
+
+  set isDisplayCameraWindow(bool value) => _isDisplayCameraWindow.value = value;
+  bool get isDisplayCameraWindow => _isDisplayCameraWindow.value;
+
+  set cameraList(List<HardwareDevice> value) => _cameraList.value = value;
+  List<HardwareDevice> get cameraList => _cameraList;
+
+  set currentCamera(HardwareDevice? value) => _currentCamera = Rx(value);
+  HardwareDevice? get currentCamera => _currentCamera.value;
+
+  set microphoneList(List<HardwareDevice> value) =>
+      _microphoneList.value = value;
+  List<HardwareDevice> get microphoneList => _microphoneList;
+
+  set currentMicrophone(HardwareDevice? value) =>
+      _currentMicrophone = Rx(value);
+  HardwareDevice? get currentMicrophone => _currentMicrophone.value;
+
+  set speakerList(List<HardwareDevice> value) => _speakerList.value = value;
+  List<HardwareDevice> get speakerList => _speakerList;
+
+  set currentSpeaker(HardwareDevice? value) => _currentSpeaker = Rx(value);
+  HardwareDevice? get currentSpeaker => _currentSpeaker.value;
+
+  set cameraAvailable(bool? value) => _cameraAvailable.value = value;
+  bool? get cameraAvailable => _cameraAvailable.value;
+
+  set microphoneAvailable(bool? value) => _microphoneAvailable.value = value;
+  bool? get microphoneAvailable => _microphoneAvailable.value;
+
+  set speakerAvailable(bool? value) => _speakerAvailable.value = value;
+  bool? get speakerAvailable => _speakerAvailable.value;
+
+  set detectingCamera(bool value) => _detectingCamera.value = value;
+  bool get detectingCamera => _detectingCamera.value;
+
+  set detectingMicrophone(bool value) => _detectingMicrophone.value = value;
+  bool get detectingMicrophone => _detectingMicrophone.value;
+
+  set detectingSpeaker(bool value) => _detectingSpeaker.value = value;
+  bool get detectingSpeaker => _detectingSpeaker.value;
+}
+
+class HardwareDevice {
+  /// 设备名称
+  String name;
+
+  /// 设备id
+  String id;
+
+  /// 设备类型
+  HardwareDeviceType type;
+
+  HardwareDevice({required this.name, required this.id, required this.type});
+}
+
+enum HardwareDeviceType {
+  /// 摄像头
+  camera,
+
+  /// 扬声器
+  speaker,
+
+  /// 麦克风
+  microphone,
+}

+ 69 - 0
lib/hardware_detection/view.dart

@@ -0,0 +1,69 @@
+import 'package:fis_lib_basictest/hardware_detection/widgets/camera_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:fis_ui/index.dart';
+import 'package:get/get.dart';
+import 'index.dart';
+import 'widgets/widgets.dart';
+
+class HardwareDetectionPage extends GetView<HardwareDetectionController> {
+  HardwareDetectionPage({Key? key}) : super(key: key);
+  final bigTitleStyle = TextStyle(
+    fontSize: 24,
+  );
+
+  // 主视图
+  Widget _buildView() {
+    return Center(
+      child: Container(
+        padding: EdgeInsets.all(20),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Text("视频", style: bigTitleStyle),
+            _decorateLine(),
+            CameraDetection(),
+            SizedBox(height: 20),
+            CameraUI(),
+            SizedBox(height: 40),
+            Text("音频", style: bigTitleStyle),
+            _decorateLine(),
+            SpeakerDetection(),
+            SizedBox(height: 20),
+            SpeakerUI(),
+            SizedBox(height: 40),
+            MicrophoneDetection(),
+            SizedBox(height: 20),
+            MicrophoneUI(),
+            SizedBox(height: 20),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _decorateLine() {
+    return FContainer(
+      height: 8,
+      width: double.infinity,
+      margin: EdgeInsets.only(top: 5, bottom: 10, right: 200),
+      decoration: BoxDecoration(
+        color: Colors.grey[300],
+        borderRadius: BorderRadius.circular(3),
+      ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return GetBuilder<HardwareDetectionController>(
+      builder: (_) {
+        return Scaffold(
+          appBar: AppBar(title: const Text("hardware_detection")),
+          body: SafeArea(
+            child: _buildView(),
+          ),
+        );
+      },
+    );
+  }
+}

+ 104 - 0
lib/hardware_detection/widgets/camera_detection.dart

@@ -0,0 +1,104 @@
+import 'package:flutter/material.dart';
+import 'package:fis_ui/index.dart';
+import 'package:fis_ui/base_define/page.dart';
+import 'package:get/get.dart';
+
+import '../index.dart';
+import 'widgets.dart';
+
+/// 摄像头检测
+class CameraDetection extends GetView<HardwareDetectionController>
+    implements FPage {
+  CameraDetection({Key? key}) : super(key: key);
+
+  @override
+  String get pageName => "device_detection";
+
+  final smallTitleStyle = TextStyle(
+    fontSize: 14,
+    fontWeight: FontWeight.bold,
+    height: 1,
+  );
+
+  /// 副标题行高:[摄像头]、[麦克风]、[扬声器]
+  static const _subTitleRowHright = 30.0;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Container(
+              width: 250,
+              height: _subTitleRowHright,
+              child: Row(
+                children: [
+                  Text("摄像头", style: smallTitleStyle),
+                  SizedBox(width: 10),
+                  Obx(() {
+                    if (controller.state.cameraAvailable == null)
+                      return Container();
+                    return controller.state.cameraAvailable!
+                        ? CommonWidgets.deviceAvailableTip()
+                        : CommonWidgets.deviceUnavailableTip();
+                  }),
+                  Expanded(child: Container()),
+                  InkWell(
+                    onTap: controller.refreshCameraList,
+                    child: Icon(
+                      Icons.refresh,
+                      size: 18,
+                    ),
+                  )
+                ],
+              ),
+            ),
+            _cameraSelector(),
+          ],
+        ),
+        SizedBox(width: 20),
+        DetectButton(
+          deviceType: HardwareDeviceType.camera,
+          subTitleRowHright: _subTitleRowHright,
+        ),
+      ],
+    );
+  }
+
+  /// 摄像头选择器
+  Widget _cameraSelector() {
+    return Obx(
+      () {
+        List<HardwareDevice> source = controller.state.cameraList;
+        bool noCamera = source.isEmpty;
+        return noCamera
+            ? Container(
+                width: 250,
+                height: 30,
+                child: Center(
+                  child: CommonWidgets.deviceNotFoundTip(),
+                ),
+              )
+            : FSelect<HardwareDevice, String>(
+                source: source,
+                height: 30,
+                width: 250,
+                value: controller.state.currentCamera?.id,
+                optionValueExtractor: (value) {
+                  return value.id;
+                },
+                optionLabelExtractor: (value) {
+                  return value.name;
+                },
+                onSelectChanged: (value, index) {
+                  if (value == null) return;
+                  controller.selectCameraById(value);
+                },
+                fontSize: 14,
+              );
+      },
+    );
+  }
+}

+ 32 - 0
lib/hardware_detection/widgets/camera_ui.dart

@@ -0,0 +1,32 @@
+import 'package:camera/camera.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import '../index.dart';
+import 'widgets.dart';
+
+/// 摄像头预览
+class CameraUI extends GetView<HardwareDetectionController> {
+  @override
+  Widget build(BuildContext context) {
+    return Obx(
+      () => ExpandableContainer(
+        expanded: controller.state.isDisplayCameraWindow,
+        child: controller.state.isDisplayCameraWindow
+            ? CameraPreview(controller.cameraController)
+            : Container(
+                decoration: BoxDecoration(
+                  border: Border.all(color: Colors.grey[300]!),
+                  borderRadius: BorderRadius.circular(5),
+                  color: Colors.grey[200],
+                ),
+                child: Center(
+                  child: Icon(
+                    Icons.photo_camera_outlined,
+                    color: Colors.black54,
+                  ),
+                ),
+              ),
+      ),
+    );
+  }
+}

+ 56 - 0
lib/hardware_detection/widgets/common_widgets.dart

@@ -0,0 +1,56 @@
+import 'package:flutter/material.dart';
+import 'package:fis_ui/index.dart';
+
+/// 通用组件
+class CommonWidgets {
+  /// 设备异常提示
+  static FWidget deviceUnavailableTip() {
+    return FRow(
+      children: [
+        FText(
+          "该设备异常",
+          style: TextStyle(fontSize: 14, color: Colors.red, height: 1),
+        ),
+        FSizedBox(
+          width: 5,
+        ),
+        FIcon(
+          Icons.highlight_off_rounded,
+          color: Colors.red,
+          size: 18,
+        ),
+      ],
+    );
+  }
+
+  /// 设备正常提示
+  static FWidget deviceAvailableTip() {
+    return FRow(
+      crossAxisAlignment: CrossAxisAlignment.center,
+      children: [
+        FText(
+          "该设备正常",
+          style: TextStyle(fontSize: 14, color: Colors.green, height: 1),
+        ),
+        FSizedBox(
+          width: 5,
+        ),
+        FIcon(
+          Icons.check_circle_outline,
+          color: Colors.green,
+          size: 18,
+        ),
+      ],
+    );
+  }
+
+  /// 找不到设备提示
+  static FWidget deviceNotFoundTip() {
+    return FContainer(
+      child: FText(
+        "未能找到可用的设备",
+        style: TextStyle(fontSize: 14, color: Colors.grey, height: 1),
+      ),
+    );
+  }
+}

+ 202 - 0
lib/hardware_detection/widgets/detect_button.dart

@@ -0,0 +1,202 @@
+import 'package:flutter/material.dart';
+import 'package:fis_ui/index.dart';
+import 'package:fis_ui/base_define/page.dart';
+import 'package:get/get.dart';
+
+import '../index.dart';
+
+/// 设备检测按钮
+class DetectButton extends GetView<HardwareDetectionController>
+    implements FPage {
+  DetectButton({
+    Key? key,
+    required this.deviceType,
+    this.subTitleRowHright = 30.0,
+  }) : super(key: key);
+
+  @override
+  String get pageName => "detect_button";
+
+  final HardwareDeviceType deviceType;
+
+  /// 副标题行高:[是否能看到画面变化]
+  final double subTitleRowHright;
+
+  /// 检测按钮文案
+  String get detectButtonText {
+    switch (deviceType) {
+      case HardwareDeviceType.camera:
+        return "检测摄像头";
+      case HardwareDeviceType.speaker:
+        return "检测扬声器";
+      case HardwareDeviceType.microphone:
+        return "检测麦克风";
+      default:
+        return "";
+    }
+  }
+
+  /// 检测中询问文案
+  String get detectingAskText {
+    switch (deviceType) {
+      case HardwareDeviceType.camera:
+        return "是否能看到画面?";
+      case HardwareDeviceType.speaker:
+        return "是否能听到音乐?";
+      case HardwareDeviceType.microphone:
+        return "是否能看到音量条变化?";
+      default:
+        return "";
+    }
+  }
+
+  List<HardwareDevice> get deviceList {
+    switch (deviceType) {
+      case HardwareDeviceType.camera:
+        return controller.state.cameraList;
+      case HardwareDeviceType.speaker:
+        return controller.state.speakerList;
+      case HardwareDeviceType.microphone:
+        return controller.state.microphoneList;
+      default:
+        return [];
+    }
+  }
+
+  bool get detectingDevice {
+    switch (deviceType) {
+      case HardwareDeviceType.camera:
+        return controller.state.detectingCamera;
+      case HardwareDeviceType.speaker:
+        return controller.state.detectingSpeaker;
+      case HardwareDeviceType.microphone:
+        return controller.state.detectingMicrophone;
+      default:
+        return false;
+    }
+  }
+
+  Function get setDeviceAvailable {
+    switch (deviceType) {
+      case HardwareDeviceType.camera:
+        return controller.setCameraAvailable;
+      case HardwareDeviceType.speaker:
+        return controller.setSpeakerAvailable;
+      case HardwareDeviceType.microphone:
+        return controller.setMicrophoneAvailable;
+      default:
+        return () {};
+    }
+  }
+
+  Function get startDetectDevice {
+    switch (deviceType) {
+      case HardwareDeviceType.camera:
+        return controller.startDetectingCamera;
+      case HardwareDeviceType.speaker:
+        return controller.startDetectingSpeaker;
+      case HardwareDeviceType.microphone:
+        return controller.startDetectingMicrophone;
+      default:
+        return () {};
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Obx(
+      () {
+        bool noDevice = deviceList.isEmpty;
+        bool isDetectingDevice = detectingDevice;
+        if (noDevice) return Container();
+        if (isDetectingDevice) {
+          return FColumn(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              FContainer(
+                height: subTitleRowHright,
+                child: FCenter(
+                  child: FText(
+                    detectingAskText,
+                    style: TextStyle(fontSize: 14, height: 1),
+                  ),
+                ),
+              ),
+              FRow(
+                children: [
+                  FInkWell(
+                    child: Container(
+                      padding:
+                          EdgeInsets.symmetric(horizontal: 15, vertical: 5),
+                      decoration: BoxDecoration(
+                        border: Border.all(color: Colors.green, width: 2),
+                        borderRadius: BorderRadius.circular(5),
+                      ),
+                      child: FText(
+                        "是",
+                        style: TextStyle(
+                            fontSize: 14, color: Colors.green, height: 1),
+                      ),
+                    ),
+                    onTap: () {
+                      setDeviceAvailable(true);
+                    },
+                  ),
+                  FSizedBox(
+                    width: 5,
+                  ),
+                  FInkWell(
+                    child: Container(
+                      padding:
+                          EdgeInsets.symmetric(horizontal: 15, vertical: 5),
+                      decoration: BoxDecoration(
+                        border: Border.all(color: Colors.red, width: 2),
+                        borderRadius: BorderRadius.circular(5),
+                      ),
+                      child: FText(
+                        "否",
+                        style: TextStyle(
+                            fontSize: 14, color: Colors.red, height: 1),
+                      ),
+                    ),
+                    onTap: () {
+                      setDeviceAvailable(false);
+                    },
+                  ),
+                ],
+              ),
+            ],
+          );
+        } else {
+          return FColumn(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              FSizedBox(
+                height: subTitleRowHright,
+              ),
+
+              /// FIXME 检测摄像头按钮 [外部无法直接使用 FElevatedButton,因为获取不到全局 env 参数]
+              // FElevatedButton(
+              //   businessParent: this,
+              //   name: "startDetectingCamera",
+              //   child: FText(detectButtonText),
+              //   onPressed: () {
+              //     startDetectDevice();
+              //   },
+              // ),
+              FInkWell(
+                child: FText(
+                  detectButtonText,
+                  style: TextStyle(fontSize: 14, color: Colors.blue, height: 1),
+                ),
+                onTap: () {
+                  startDetectDevice();
+                },
+              ),
+            ],
+          );
+        }
+      },
+    );
+  }
+}

+ 103 - 0
lib/hardware_detection/widgets/expandable_container.dart

@@ -0,0 +1,103 @@
+import 'package:flutter/material.dart';
+
+/// 可伸缩容器
+class ExpandableContainer extends StatefulWidget {
+  final Widget child;
+  final bool expanded;
+  final double collapsedHeight;
+  final double expandedHeight;
+  final double fixedWidth;
+
+  ExpandableContainer({
+    required this.child,
+    this.expanded = false,
+    this.collapsedHeight = 40.0,
+    this.expandedHeight = 200.0,
+    this.fixedWidth = 250.0,
+  });
+
+  @override
+  _ExpandableContainerState createState() => _ExpandableContainerState();
+}
+
+class _ExpandableContainerState extends State<ExpandableContainer>
+    with SingleTickerProviderStateMixin {
+  late AnimationController _controller;
+  late Animation<double> _heightFactor;
+
+  @override
+  void initState() {
+    super.initState();
+
+    _controller = AnimationController(
+      vsync: this,
+      duration: Duration(milliseconds: 500),
+    );
+
+    final double initialHeightFactor = widget.expanded ? 1.0 : 0.0;
+    _heightFactor = _controller
+        .drive(
+          CurveTween(curve: Curves.fastOutSlowIn),
+        )
+        .drive(
+          Tween<double>(begin: initialHeightFactor, end: 1.0),
+        );
+
+    if (widget.expanded) {
+      _controller.value = 1.0;
+    }
+  }
+
+  @override
+  void didUpdateWidget(ExpandableContainer oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (widget.expanded != oldWidget.expanded) {
+      if (widget.expanded) {
+        _controller.forward();
+      } else {
+        _controller.reverse();
+      }
+    }
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    super.dispose();
+  }
+
+  void _toggleExpanded() {
+    print("toggleExpanded");
+    if (_controller.isAnimating) {
+      return;
+    }
+    final bool isExpanded = _heightFactor.value == 1.0;
+    _controller.fling(
+      velocity: isExpanded ? -2.0 : 2.0,
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      behavior: HitTestBehavior.opaque,
+      onTap: _toggleExpanded,
+      child: AnimatedSize(
+        duration: Duration(milliseconds: 500),
+        child: AnimatedBuilder(
+          animation: _heightFactor,
+          builder: (BuildContext context, Widget? child) {
+            return Container(
+              width: widget.fixedWidth,
+              height: widget.collapsedHeight +
+                  (_heightFactor.value *
+                      (widget.expandedHeight - widget.collapsedHeight)),
+              child: child,
+            );
+          },
+          child: widget.child,
+        ),
+      ),
+    );
+  }
+}

+ 104 - 0
lib/hardware_detection/widgets/microphone_detection.dart

@@ -0,0 +1,104 @@
+import 'package:flutter/material.dart';
+import 'package:fis_ui/index.dart';
+import 'package:fis_ui/base_define/page.dart';
+import 'package:get/get.dart';
+
+import '../index.dart';
+import 'widgets.dart';
+
+/// 麦克风检测
+class MicrophoneDetection extends GetView<HardwareDetectionController>
+    implements FPage {
+  MicrophoneDetection({Key? key}) : super(key: key);
+
+  @override
+  String get pageName => "device_detection";
+
+  final smallTitleStyle = TextStyle(
+    fontSize: 14,
+    fontWeight: FontWeight.bold,
+    height: 1,
+  );
+
+  /// 副标题行高:[摄像头]、[麦克风]、[扬声器]
+  static const _subTitleRowHright = 30.0;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Container(
+              width: 250,
+              height: _subTitleRowHright,
+              child: Row(
+                children: [
+                  Text("麦克风", style: smallTitleStyle),
+                  SizedBox(width: 10),
+                  Obx(() {
+                    if (controller.state.microphoneAvailable == null)
+                      return Container();
+                    return controller.state.microphoneAvailable!
+                        ? CommonWidgets.deviceAvailableTip()
+                        : CommonWidgets.deviceUnavailableTip();
+                  }),
+                  Expanded(child: Container()),
+                  InkWell(
+                    onTap: controller.refreshMicrophoneList,
+                    child: Icon(
+                      Icons.refresh,
+                      size: 18,
+                    ),
+                  )
+                ],
+              ),
+            ),
+            _microphoneSelector(),
+          ],
+        ),
+        SizedBox(width: 20),
+        DetectButton(
+          deviceType: HardwareDeviceType.microphone,
+          subTitleRowHright: _subTitleRowHright,
+        ),
+      ],
+    );
+  }
+
+  /// 麦克风选择器
+  Widget _microphoneSelector() {
+    return Obx(
+      () {
+        List<HardwareDevice> source = controller.state.microphoneList;
+        bool noMicrophone = source.isEmpty;
+        return noMicrophone
+            ? Container(
+                width: 250,
+                height: 30,
+                child: Center(
+                  child: CommonWidgets.deviceNotFoundTip(),
+                ),
+              )
+            : FSelect<HardwareDevice, String>(
+                source: source,
+                height: 30,
+                width: 250,
+                value: controller.state.currentMicrophone?.id,
+                optionValueExtractor: (value) {
+                  return value.id;
+                },
+                optionLabelExtractor: (value) {
+                  return value.name;
+                },
+                onSelectChanged: (value, index) {
+                  if (value == null) return;
+                  controller.selectMicrophoneById(value);
+                },
+                fontSize: 14,
+              );
+      },
+    );
+  }
+}

+ 33 - 0
lib/hardware_detection/widgets/microphone_ui.dart

@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import '../index.dart';
+import 'widgets.dart';
+
+class MicrophoneUI extends GetView<HardwareDetectionController> {
+  @override
+  Widget build(BuildContext context) {
+    return Obx(
+      () => controller.state.isDisplayMicrophoneWave
+          ? MicrophoneWave(
+              isRecording: controller.state.detectingMicrophone,
+              recorder: controller.flutterRecorder,
+              width: 250.0,
+              height: 60.0,
+            )
+          : Container(
+              width: 250,
+              height: 60,
+              child: Align(
+                alignment: Alignment.centerLeft,
+                child: Text(
+                  "麦克风输入:",
+                  style: TextStyle(
+                    height: 1,
+                    color: Colors.black87,
+                  ),
+                ),
+              ),
+            ),
+    );
+  }
+}

+ 150 - 0
lib/hardware_detection/widgets/microphone_wave.dart

@@ -0,0 +1,150 @@
+import 'dart:async';
+import 'package:flutter/material.dart';
+import 'package:flutter_sound/flutter_sound.dart';
+
+/// 麦克风输入可视化
+class MicrophoneWave extends StatefulWidget {
+  final FlutterSoundRecorder recorder;
+  final double width;
+  final double height;
+  final bool isRecording;
+
+  MicrophoneWave({
+    required this.recorder,
+    required this.isRecording,
+    this.width = 250.0,
+    this.height = 40.0,
+  });
+
+  @override
+  _MicrophoneWaveState createState() => _MicrophoneWaveState();
+}
+
+class _MicrophoneWaveState extends State<MicrophoneWave>
+    with SingleTickerProviderStateMixin {
+  StreamSubscription? _recorderSubscription;
+  late AnimationController _controller;
+  late Animation<double> _animation;
+  List<double> _loudnessValues = [];
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = AnimationController(
+      vsync: this,
+      duration: Duration(milliseconds: 50),
+    );
+    _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller)
+      ..addListener(() {
+        setState(() {});
+      });
+
+    /// 如果没有分贝数据,请参考 https://github.com/Canardoux/flutter_sound/issues/838
+    /// 将  flutter_sound_web 的 flutter_sound_recorder.js 从 flutter_sound_web/js/flutter_sound 复制到 flutter_sound_web/src
+    /// 监听 onProgress 回调
+    _recorderSubscription =
+        widget.recorder.onProgress!.listen((RecordingDisposition e) {
+      /// 输出响度,响度范围是0-120
+      /// 0表示没有声音,120表示最大声音
+      final loudness = ((e.decibels ?? 0) + 5) / 120;
+      setState(() {
+        _loudnessValues.add(loudness);
+      });
+      if (_loudnessValues.length > widget.width) {
+        _loudnessValues.removeAt(0);
+      }
+      _controller.forward(from: 0.0);
+    }, onError: (err) {
+      print('onError: $err');
+    });
+  }
+
+  @override
+  void didUpdateWidget(covariant MicrophoneWave oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (oldWidget.isRecording != widget.isRecording) {
+      if (widget.isRecording) {
+        _loudnessValues.clear();
+      }
+    }
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _controller.dispose();
+    _recorderSubscription?.cancel();
+    _recorderSubscription = null;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      width: widget.width,
+      height: widget.height,
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          Text(
+            "麦克风输入:",
+            style: TextStyle(
+              height: 1,
+              color: Colors.black87,
+            ),
+          ),
+          Expanded(
+              child: ClipPath(
+            child: CustomPaint(
+              painter: _MicrophoneVisualizerPainter(
+                loudnessList: _loudnessValues,
+                animationValue: _animation.value,
+                isRecording: widget.isRecording,
+              ),
+              size: Size(widget.width, widget.height),
+            ),
+          )),
+        ],
+      ),
+    );
+  }
+}
+
+class _MicrophoneVisualizerPainter extends CustomPainter {
+  final List<double> loudnessList;
+  final double animationValue;
+  final bool isRecording;
+  _MicrophoneVisualizerPainter({
+    required this.loudnessList,
+    required this.animationValue,
+    this.isRecording = false,
+  });
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final double sampleStep = 5.0;
+    final offsetX = sampleStep * animationValue;
+    final double centerY = size.height / 2;
+    Paint linePaint = Paint()
+      ..color = Colors.grey
+      ..strokeWidth = 1.0;
+
+    /// 绘制中轴线
+    canvas.drawLine(Offset(0, centerY), Offset(size.width, centerY), linePaint);
+    if (!isRecording) return;
+    linePaint = Paint()
+      ..color = Colors.blue
+      ..strokeWidth = 2.0;
+    for (int i = 0; i < loudnessList.length; i++) {
+      final loudness = loudnessList[loudnessList.length - i - 1];
+      final x = i * sampleStep + offsetX;
+      final y = centerY + loudness * centerY;
+      final y2 = centerY - loudness * centerY;
+      canvas.drawLine(Offset(x, y), Offset(x, y2), linePaint);
+    }
+  }
+
+  @override
+  bool shouldRepaint(_MicrophoneVisualizerPainter oldDelegate) {
+    return oldDelegate.animationValue != animationValue;
+  }
+}

+ 109 - 0
lib/hardware_detection/widgets/speaker_detection.dart

@@ -0,0 +1,109 @@
+import 'package:flutter/material.dart';
+import 'package:fis_ui/base_define/page.dart';
+import 'package:get/get.dart';
+
+import '../index.dart';
+import 'widgets.dart';
+
+/// 扬声器检测
+class SpeakerDetection extends GetView<HardwareDetectionController>
+    implements FPage {
+  SpeakerDetection({Key? key}) : super(key: key);
+
+  @override
+  String get pageName => "device_detection";
+
+  final smallTitleStyle = TextStyle(
+    fontSize: 14,
+    fontWeight: FontWeight.bold,
+    height: 1,
+  );
+
+  /// 副标题行高:[摄像头]、[麦克风]、[扬声器]
+  static const _subTitleRowHright = 30.0;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Container(
+              height: _subTitleRowHright,
+              width: 250,
+              child: Row(
+                children: [
+                  Text("扬声器", style: smallTitleStyle),
+                  SizedBox(width: 10),
+                  Obx(() {
+                    if (controller.state.speakerAvailable == null)
+                      return Container();
+                    return controller.state.speakerAvailable!
+                        ? CommonWidgets.deviceAvailableTip()
+                        : CommonWidgets.deviceUnavailableTip();
+                  }),
+                  Expanded(child: Container()),
+                  InkWell(
+                    onTap: controller.refreshSpeakerList,
+                    child: Icon(
+                      Icons.refresh,
+                      size: 18,
+                    ),
+                  )
+                ],
+              ),
+            ),
+            _speakerSelector(),
+          ],
+        ),
+        SizedBox(width: 20),
+        DetectButton(
+          deviceType: HardwareDeviceType.speaker,
+          subTitleRowHright: _subTitleRowHright,
+        ),
+      ],
+    );
+  }
+
+  /// 扬声器选择器
+  Widget _speakerSelector() {
+    return Obx(
+      () {
+        List<HardwareDevice> source = controller.state.speakerList;
+        bool noSpeaker = source.isEmpty;
+        return noSpeaker
+            ? Container(
+                width: 250,
+                height: 30,
+                child: Center(
+                  child: CommonWidgets.deviceNotFoundTip(),
+                ),
+              )
+            : Container(
+                height: 30,
+                width: 250,
+                child: Row(
+                  children: [
+                    Expanded(
+                      child: Tooltip(
+                        message: "${source.first.name}",
+                        child: Center(
+                          child: Text(
+                            source.first.name,
+                            style: TextStyle(
+                              fontSize: 14,
+                              color: Colors.black,
+                            ),
+                            overflow: TextOverflow.ellipsis,
+                          ),
+                        ),
+                      ),
+                    ),
+                  ],
+                ),
+              );
+      },
+    );
+  }
+}

+ 18 - 0
lib/hardware_detection/widgets/speaker_ui.dart

@@ -0,0 +1,18 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import '../index.dart';
+import 'speaker_wave.dart';
+
+class SpeakerUI extends GetView<HardwareDetectionController> {
+  @override
+  Widget build(BuildContext context) {
+    return Obx(() => controller.state.isDisplaySpeakerWave
+        ? SpeakerWave(
+            audioPlayer: controller.flutterSound,
+            isPlaying: controller.state.detectingSpeaker,
+            width: 250.0,
+            height: 40.0,
+          )
+        : Container());
+  }
+}

+ 114 - 0
lib/hardware_detection/widgets/speaker_wave.dart

@@ -0,0 +1,114 @@
+import 'dart:async';
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_sound/flutter_sound.dart';
+
+/// 扬声器输出等级可视化
+class SpeakerWave extends StatefulWidget {
+  final FlutterSoundPlayer audioPlayer;
+  final double width;
+  final double height;
+  final Color color;
+  final bool isPlaying;
+
+  SpeakerWave({
+    required this.audioPlayer,
+    this.isPlaying = false,
+    this.width = 250.0,
+    this.height = 40.0,
+    this.color = Colors.blue,
+  });
+
+  @override
+  _SpeakerWaveState createState() => _SpeakerWaveState();
+}
+
+class _SpeakerWaveState extends State<SpeakerWave> {
+  StreamSubscription? _waveformSubscription;
+  int _randomWaveform = 0;
+  static const _itemHeight = 10.0;
+  static const _itemCount = 5;
+  @override
+  void initState() {
+    super.initState();
+    print("监听扬声器输出:${widget.audioPlayer}");
+
+    /// 监听扬声器输出
+    _waveformSubscription =
+        widget.audioPlayer.onProgress?.listen((PlaybackDisposition data) {
+      setState(() {
+        _randomWaveform = Random().nextInt(_itemCount);
+      });
+      if (data.duration - data.position < Duration(milliseconds: 500)) {
+        setState(() {
+          _randomWaveform = 0;
+        });
+      }
+    });
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _waveformSubscription?.cancel();
+    _waveformSubscription = null;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      width: widget.width,
+      height: widget.height,
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          Text(
+            "扬声器输出:",
+            style: TextStyle(
+              height: 1,
+              color: Colors.black87,
+            ),
+          ),
+          if (widget.isPlaying) ...[
+            for (var i = 0; i < _randomWaveform; i++)
+              Expanded(
+                child: buildActiveItem(),
+              ),
+            for (var i = 0; i < _itemCount - _randomWaveform; i++)
+              Expanded(
+                child: buildInactiveItem(),
+              ),
+          ] else ...[
+            for (var i = 0; i < _itemCount; i++)
+              Expanded(
+                child: buildInactiveItem(),
+              ),
+          ]
+        ],
+      ),
+    );
+  }
+
+  Widget buildActiveItem() {
+    return Container(
+      height: _itemHeight,
+      margin: EdgeInsets.symmetric(horizontal: 3),
+      decoration: BoxDecoration(
+        borderRadius: BorderRadius.all(Radius.circular(5)),
+        color: widget.color,
+      ),
+    );
+  }
+
+  Widget buildInactiveItem() {
+    return Container(
+      height: _itemHeight,
+      margin: EdgeInsets.symmetric(horizontal: 3),
+      decoration: BoxDecoration(
+        borderRadius: BorderRadius.all(Radius.circular(5)),
+        color: Colors.grey[300],
+      ),
+    );
+  }
+}

+ 12 - 0
lib/hardware_detection/widgets/widgets.dart

@@ -0,0 +1,12 @@
+library widgets;
+
+export 'camera_detection.dart';
+export 'microphone_detection.dart';
+export 'speaker_detection.dart';
+export 'common_widgets.dart';
+export 'detect_button.dart';
+export 'expandable_container.dart';
+export 'speaker_ui.dart';
+export 'speaker_wave.dart';
+export 'microphone_ui.dart';
+export 'microphone_wave.dart';

+ 15 - 16
lib/main.dart

@@ -1,10 +1,10 @@
 import 'dart:async';
 import 'dart:math';
-import 'dart:ui';
+
+import 'package:fis_lib_basictest/hardware_detection/index.dart';
 import 'package:fis_ui/index.dart';
-import 'package:fis_ui/widgets/functional/text.dart';
 import 'package:flutter/material.dart';
-import 'dart:ui' as ui;
+import 'package:get/get.dart';
 
 void main() {
   runApp(const MyApp());
@@ -16,20 +16,19 @@ class MyApp extends StatelessWidget {
   // This widget is the root of your application.
   @override
   Widget build(BuildContext context) {
-    return MaterialApp(
+    return GetMaterialApp(
       theme: ThemeData(
-        // This is the theme of your application.
-        //
-        // Try running your application with "flutter run". You'll see the
-        // application has a blue toolbar. Then, without quitting the app, try
-        // changing the primarySwatch below to Colors.green and then invoke
-        // "hot reload" (press "r" in the console where you ran "flutter run",
-        // or simply save your changes to "hot reload" in a Flutter IDE).
-        // Notice that the counter didn't reset back to zero; the application
-        // is not restarted.
         primarySwatch: Colors.blue,
       ),
-      home: const DrawWidgetStep2(),
+      initialRoute: '/',
+      getPages: [
+        GetPage(
+          name: '/',
+          page: () => HardwareDetectionPage(),
+          binding: HardwareDetectionBinding(),
+        ),
+        GetPage(name: '/old', page: () => DrawWidgetStep2()),
+      ],
     );
   }
 }
@@ -373,9 +372,9 @@ class DrawWidgetStep2State extends State<DrawWidgetStep2>
   FWidget _buildTestButton(String content) {
     return FContainer(
       padding: EdgeInsets.fromLTRB(10, 10, 10, 10),
-      child: FElevatedButton(
+      child: FInkWell(
         child: FText(content),
-        onPressed: () => {},
+        onTap: () => {},
       ),
     );
   }

+ 394 - 184
pubspec.lock

@@ -5,191 +5,237 @@ packages:
     dependency: transitive
     description:
       name: archive
-      url: "https://pub.dartlang.org"
+      sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "3.3.4"
+    version: "3.3.6"
   args:
     dependency: transitive
     description:
       name: args
-      url: "https://pub.dartlang.org"
+      sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.3.1"
+    version: "2.4.0"
   asn1lib:
     dependency: transitive
     description:
       name: asn1lib
-      url: "https://pub.dartlang.org"
+      sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "1.3.0"
+    version: "1.4.0"
   async:
     dependency: transitive
     description:
       name: async
-      url: "https://pub.dartlang.org"
+      sha256: "271b8899fc99f9df4f4ed419fa14e2fff392c7b2c162fbb87b222e2e963ddc73"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.9.0"
+  audio_session:
+    dependency: "direct main"
+    description:
+      name: audio_session
+      sha256: e4acc4e9eaa32436dfc5d7aed7f0a370f2d7bb27ee27de30d6c4f220c2a05c73
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.1.13"
   boolean_selector:
     dependency: transitive
     description:
       name: boolean_selector
-      url: "https://pub.dartlang.org"
+      sha256: "5bbf32bc9e518d41ec49718e2931cd4527292c9b0c6d2dffcf7fe6b9a8a8cf72"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.1.0"
+  camera:
+    dependency: "direct main"
+    description:
+      name: camera
+      sha256: ad1c53c554a2f3e5708f3b01eb738d60b902bb61f7f4ad420c65c715e65a7379
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.10.3+2"
+  camera_android:
+    dependency: transitive
+    description:
+      name: camera_android
+      sha256: df9c3376ccfce13c98a09c1d6433c64b0c6b2de81eeade8151884f8c8f08881f
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.10.4+1"
+  camera_avfoundation:
+    dependency: transitive
+    description:
+      name: camera_avfoundation
+      sha256: a1ebce7132e2c1ffd3138e672538f6f2360715e181cf5b7eb5a073f1a735235e
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.9.12"
+  camera_platform_interface:
+    dependency: transitive
+    description:
+      name: camera_platform_interface
+      sha256: "00d972adee2e8a282b4d7445e8e694aa1dc0c36b70455b99afa96fbf5e814119"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.4.1"
+  camera_web:
+    dependency: "direct main"
+    description:
+      name: camera_web
+      sha256: f7a949ce4d2e091234f2e6607557a0a65398c34c84c1161fb249498595e3b4ce
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.3.1+2"
   characters:
     dependency: transitive
     description:
       name: characters
-      url: "https://pub.dartlang.org"
+      sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.2.1"
   clock:
     dependency: transitive
     description:
       name: clock
-      url: "https://pub.dartlang.org"
+      sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.1.1"
   collection:
     dependency: transitive
     description:
       name: collection
-      url: "https://pub.dartlang.org"
+      sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "1.16.0"
+    version: "1.17.0"
   convert:
     dependency: transitive
     description:
       name: convert
-      url: "https://pub.dartlang.org"
+      sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.1.1"
   cross_file:
     dependency: transitive
     description:
       name: cross_file
-      url: "https://pub.dartlang.org"
+      sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "0.3.3+2"
+    version: "0.3.3+4"
   crypto:
     dependency: transitive
     description:
       name: crypto
-      url: "https://pub.dartlang.org"
+      sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.0.2"
   custom_pop_up_menu:
     dependency: transitive
     description:
       name: custom_pop_up_menu
-      url: "https://pub.dartlang.org"
+      sha256: "4fc58444afa5b2007e7df36fd4f2696440ffed821831b8865bb6c67168aa9c8b"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.2.2"
   dio:
     dependency: transitive
     description:
       name: dio
-      url: "https://pub.dartlang.org"
+      sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "4.0.6"
   encrypt:
     dependency: transitive
     description:
       name: encrypt
-      url: "https://pub.dartlang.org"
+      sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "5.0.1"
   fake_async:
     dependency: transitive
     description:
       name: fake_async
-      url: "https://pub.dartlang.org"
+      sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.3.1"
+  ff_stars:
+    dependency: transitive
+    description:
+      name: ff_stars
+      sha256: "4686c230267d8cab9cf7b508c1c850660001044fbca18d0e769a701c4f76b7bc"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.0.0"
   ffi:
     dependency: transitive
     description:
       name: ffi
-      url: "https://pub.dartlang.org"
+      sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "1.2.1"
+    version: "2.0.1"
   file:
     dependency: transitive
     description:
       name: file
-      url: "https://pub.dartlang.org"
+      sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "6.1.4"
   fis_common:
-    dependency: "direct main"
+    dependency: "direct overridden"
     description:
       path: "."
-      ref: b5a4fa2
-      resolved-ref: b5a4fa2a376542e313caee81b7286b985438277b
+      ref: "9cd9474"
+      resolved-ref: "9cd9474b69afacb88087b4da19628302312bca6e"
       url: "http://git.ius.plus:88/Project-Wing/fis_lib_common.git"
     source: git
     version: "0.0.2"
-  fis_i18n:
-    dependency: "direct main"
-    description:
-      path: "."
-      ref: "213eb50"
-      resolved-ref: "213eb506d9983dc8f24aaee59eca93a028814a8c"
-      url: "http://git.ius.plus:88/Project-Wing/fis_lib_i18n.git"
-    source: git
-    version: "0.0.1"
   fis_jsonrpc:
-    dependency: "direct main"
-    description:
-      path: "."
-      ref: "735163e"
-      resolved-ref: "735163e1588df56c672bc09d8952fcf6392135fa"
-      url: "http://git.ius.plus:88/Project-Wing/fis_lib_jsonrpc.git"
-    source: git
-    version: "0.0.1"
-  fis_lib_business_components:
     dependency: "direct overridden"
     description:
       path: "."
-      ref: "009ddb6"
-      resolved-ref: "009ddb6d2875df33d78eb8d32ecb52fb9756dbfa"
-      url: "http://git.ius.plus/bakamaka.guan/fis_lib_business_components.git"
-    source: git
-    version: "0.0.1"
-  fis_resource:
-    dependency: "direct main"
-    description:
-      path: "."
-      ref: "1.0.1.1"
-      resolved-ref: c52238750e3d0e987cf65467ad6e265e618e24a2
-      url: "http://git.ius.plus:88/Project-Wing/fis_lib_resource.git"
+      ref: "87bc52f"
+      resolved-ref: "87bc52f8695d66b8474006f8181919c7dd4d3174"
+      url: "http://git.ius.plus:88/Project-Wing/fis_lib_jsonrpc.git"
     source: git
     version: "0.0.1"
   fis_theme:
-    dependency: "direct main"
+    dependency: transitive
     description:
       path: "."
-      ref: "6991f8ef7f"
-      resolved-ref: "6991f8ef7f0e86a525f7cc70193f3eb97e85d824"
+      ref: "^1.0.0.2"
+      resolved-ref: "6a2455596179bc8a62092be343bfb0632c4dcf2d"
       url: "http://git.ius.plus:88/Project-Wing/fis_lib_theme.git"
     source: git
     version: "0.0.1"
   fis_ui:
-    dependency: "direct main"
+    dependency: "direct overridden"
     description:
       path: "."
-      ref: "034e87c"
-      resolved-ref: "034e87ceaf119c9d87829f143dd6728a24911676"
+      ref: e5c7cb8
+      resolved-ref: e5c7cb8c6f420b64cd9c02daaa5d6b3c93dfdef2
       url: "http://git.ius.plus:88/Project-Wing/fis_lib_ui.git"
     source: git
     version: "0.0.1"
   fis_vid:
-    dependency: transitive
+    dependency: "direct overridden"
     description:
       path: "."
-      ref: "^1.0.2"
-      resolved-ref: d226cb53baf2bd0041f408ad2b8e9c522fec3cd2
-      url: "http://git.ius.plus:88/melon.yin/fis_lib_vid.git"
+      ref: f3e6342
+      resolved-ref: f3e634268e6da52da89a1f92bbba28109346938d
+      url: "http://git.ius.plus/Project-Wing/fis_lib_vid.git"
     source: git
     version: "0.0.1"
   flutter:
@@ -201,42 +247,48 @@ packages:
     dependency: transitive
     description:
       name: flutter_keyboard_visibility
-      url: "https://pub.dartlang.org"
+      sha256: "86b71bbaffa38e885f5c21b1182408b9be6951fd125432cf6652c636254cef2d"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "5.4.0"
   flutter_keyboard_visibility_linux:
     dependency: transitive
     description:
       name: flutter_keyboard_visibility_linux
-      url: "https://pub.dartlang.org"
+      sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.0"
   flutter_keyboard_visibility_macos:
     dependency: transitive
     description:
       name: flutter_keyboard_visibility_macos
-      url: "https://pub.dartlang.org"
+      sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.0"
   flutter_keyboard_visibility_platform_interface:
     dependency: transitive
     description:
       name: flutter_keyboard_visibility_platform_interface
-      url: "https://pub.dartlang.org"
+      sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.0"
   flutter_keyboard_visibility_web:
     dependency: transitive
     description:
       name: flutter_keyboard_visibility_web
-      url: "https://pub.dartlang.org"
+      sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.0"
   flutter_keyboard_visibility_windows:
     dependency: transitive
     description:
       name: flutter_keyboard_visibility_windows
-      url: "https://pub.dartlang.org"
+      sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.0"
   flutter_localizations:
@@ -248,236 +300,324 @@ packages:
     dependency: transitive
     description:
       name: flutter_plugin_android_lifecycle
-      url: "https://pub.dartlang.org"
+      sha256: "4bef634684b2c7f3468c77c766c831229af829a0cd2d4ee6c1b99558bd14e5d2"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.0.7"
+    version: "2.0.8"
   flutter_slidable:
     dependency: transitive
     description:
       name: flutter_slidable
-      url: "https://pub.dartlang.org"
+      sha256: ab07e4c793f8d0c9c9e2062d264bd9e61cf50e3ecbbef496d4f4a4f1e705cd38
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.2.0"
+  flutter_sound:
+    dependency: "direct main"
+    description:
+      name: flutter_sound
+      sha256: "090a4694b11ecc744c2010621c4ffc5fe7c3079d304ea014961a72c7b72cfe6c"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "9.2.13"
+  flutter_sound_platform_interface:
+    dependency: transitive
+    description:
+      name: flutter_sound_platform_interface
+      sha256: "4537eaeb58a32748c42b621ad6116f7f4c6ee0a8d6ffaa501b165fe1c9df4753"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "9.2.13"
+  flutter_sound_web:
+    dependency: transitive
+    description:
+      name: flutter_sound_web
+      sha256: ad4ca92671a1879e1f613e900bbbdb8170b20d57d1e4e6363018fe56b055594f
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "9.2.13"
   flutter_test:
     dependency: "direct dev"
     description: flutter
     source: sdk
     version: "0.0.0"
-  flutter_treeview:
-    dependency: transitive
-    description:
-      name: flutter_treeview
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "1.0.7+1"
   flutter_web_plugins:
     dependency: transitive
     description: flutter
     source: sdk
     version: "0.0.0"
   get:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: get
-      url: "https://pub.dartlang.org"
+      sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "4.6.1"
+    version: "4.6.5"
   getwidget:
     dependency: transitive
     description:
       name: getwidget
-      url: "https://pub.dartlang.org"
+      sha256: "211f7955d7e46595462d2c63eabd42be9eab8081c065ebc2f213fca0535d6ead"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.1.1"
+    version: "2.0.4"
   http:
     dependency: transitive
     description:
       name: http
-      url: "https://pub.dartlang.org"
+      sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "0.13.4"
+    version: "0.13.5"
   http_parser:
     dependency: transitive
     description:
       name: http_parser
-      url: "https://pub.dartlang.org"
+      sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "4.0.2"
-  image:
-    dependency: transitive
-    description:
-      name: image
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "3.1.3"
-  image_picker_android:
-    dependency: "direct overridden"
-    description:
-      name: image_picker_android
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "0.8.4+13"
-  image_picker_platform_interface:
-    dependency: transitive
-    description:
-      name: image_picker_platform_interface
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "2.6.2"
   intl:
     dependency: transitive
     description:
       name: intl
-      url: "https://pub.dartlang.org"
+      sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.17.0"
   js:
     dependency: transitive
     description:
       name: js
-      url: "https://pub.dartlang.org"
+      sha256: a5e201311cb08bf3912ebbe9a2be096e182d703f881136ec1e81a2338a9e120d
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.6.4"
+  json_annotation:
+    dependency: transitive
+    description:
+      name: json_annotation
+      sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.8.0"
+  logger:
+    dependency: transitive
+    description:
+      name: logger
+      sha256: c40f9ef51e5bffb4ce69ad2d8c8aad7bd47ec109c090521109b63a4e2bc27191
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.2.2"
   matcher:
     dependency: transitive
     description:
       name: matcher
-      url: "https://pub.dartlang.org"
+      sha256: "80c2989398773fa06e2457e9ff08580f24e9858b28462a722241cb53e5613478"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.12.12"
   material_color_utilities:
     dependency: transitive
     description:
       name: material_color_utilities
-      url: "https://pub.dartlang.org"
+      sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "0.1.5"
+    version: "0.2.0"
   meta:
     dependency: transitive
     description:
       name: meta
-      url: "https://pub.dartlang.org"
+      sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.8.0"
+  nested:
+    dependency: transitive
+    description:
+      name: nested
+      sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.0"
   path:
     dependency: transitive
     description:
       name: path
-      url: "https://pub.dartlang.org"
+      sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.8.2"
   path_provider:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: path_provider
-      url: "https://pub.dartlang.org"
+      sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.0.2"
-  path_provider_linux:
+    version: "2.0.13"
+  path_provider_android:
     dependency: transitive
     description:
-      name: path_provider_linux
-      url: "https://pub.dartlang.org"
+      name: path_provider_android
+      sha256: "7623b7d4be0f0f7d9a8b5ee6879fc13e4522d4c875ab86801dee4af32b54b83e"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.1.7"
-  path_provider_macos:
+    version: "2.0.23"
+  path_provider_foundation:
     dependency: transitive
     description:
-      name: path_provider_macos
-      url: "https://pub.dartlang.org"
+      name: path_provider_foundation
+      sha256: eec003594f19fe2456ea965ae36b3fc967bc5005f508890aafe31fa75e41d972
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.0.6"
+    version: "2.1.2"
+  path_provider_linux:
+    dependency: transitive
+    description:
+      name: path_provider_linux
+      sha256: "525ad5e07622d19447ad740b1ed5070031f7a5437f44355ae915ff56e986429a"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.9"
   path_provider_platform_interface:
     dependency: transitive
     description:
       name: path_provider_platform_interface
-      url: "https://pub.dartlang.org"
+      sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.0.5"
+    version: "2.0.6"
   path_provider_windows:
     dependency: transitive
     description:
       name: path_provider_windows
-      url: "https://pub.dartlang.org"
+      sha256: "642ddf65fde5404f83267e8459ddb4556316d3ee6d511ed193357e25caa3632d"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.0.7"
-  petitparser:
-    dependency: transitive
-    description:
-      name: petitparser
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "5.1.0"
+    version: "2.1.4"
   platform:
     dependency: transitive
     description:
       name: platform
-      url: "https://pub.dartlang.org"
+      sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.1.0"
   plugin_platform_interface:
     dependency: transitive
     description:
       name: plugin_platform_interface
-      url: "https://pub.dartlang.org"
+      sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.4"
+  pointer_interceptor:
+    dependency: transitive
+    description:
+      name: pointer_interceptor
+      sha256: "6aa680b30d96dccef496933d00208ad25f07e047f644dc98ce03ec6141633a9a"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.1.3"
+    version: "0.9.3+4"
   pointycastle:
     dependency: transitive
     description:
       name: pointycastle
-      url: "https://pub.dartlang.org"
+      sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.6.2"
   process:
     dependency: transitive
     description:
       name: process
-      url: "https://pub.dartlang.org"
+      sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "4.2.4"
+  provider:
+    dependency: transitive
+    description:
+      name: provider
+      sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "6.0.5"
+  quiver:
+    dependency: transitive
+    description:
+      name: quiver
+      sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.2.1"
+  recase:
+    dependency: transitive
+    description:
+      name: recase
+      sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.1.0"
+  rxdart:
+    dependency: transitive
+    description:
+      name: rxdart
+      sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.27.7"
   shared_preferences:
     dependency: "direct main"
     description:
       name: shared_preferences
-      url: "https://pub.dartlang.org"
+      sha256: abce1248423109beb9ef6cb6536cbd06a676e3cf0d7a00a980b3bce793f35de0
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.5"
   shared_preferences_linux:
     dependency: transitive
     description:
       name: shared_preferences_linux
-      url: "https://pub.dartlang.org"
+      sha256: d7fb71e6e20cd3dfffcc823a28da3539b392e53ed5fc5c2b90b55fdaa8a7e8fa
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.1.1"
+    version: "2.1.4"
   shared_preferences_macos:
     dependency: transitive
     description:
       name: shared_preferences_macos
-      url: "https://pub.dartlang.org"
+      sha256: "81b6a60b2d27020eb0fc41f4cebc91353047309967901a79ee8203e40c42ed46"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.0.4"
+    version: "2.0.5"
   shared_preferences_platform_interface:
     dependency: transitive
     description:
       name: shared_preferences_platform_interface
-      url: "https://pub.dartlang.org"
+      sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.1.0"
+    version: "2.1.1"
   shared_preferences_web:
     dependency: transitive
     description:
       name: shared_preferences_web
-      url: "https://pub.dartlang.org"
+      sha256: "6737b757e49ba93de2a233df229d0b6a87728cea1684da828cbc718b65dcf9d7"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.0.4"
+    version: "2.0.5"
   shared_preferences_windows:
     dependency: transitive
     description:
       name: shared_preferences_windows
-      url: "https://pub.dartlang.org"
+      sha256: bd014168e8484837c39ef21065b78f305810ceabc1d4f90be6e3b392ce81b46d
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.1.1"
+    version: "2.1.4"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -487,70 +627,104 @@ packages:
     dependency: transitive
     description:
       name: source_span
-      url: "https://pub.dartlang.org"
+      sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "1.9.0"
+    version: "1.9.1"
   stack_trace:
     dependency: transitive
     description:
       name: stack_trace
-      url: "https://pub.dartlang.org"
+      sha256: f8d9f247e2f9f90e32d1495ff32dac7e4ae34ffa7194c5ff8fcc0fd0e52df774
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.10.0"
   stream_channel:
     dependency: transitive
     description:
       name: stream_channel
-      url: "https://pub.dartlang.org"
+      sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.1"
+  stream_transform:
+    dependency: transitive
+    description:
+      name: stream_transform
+      sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.1.0"
   string_scanner:
     dependency: transitive
     description:
       name: string_scanner
-      url: "https://pub.dartlang.org"
+      sha256: "862015c5db1f3f3c4ea3b94dc2490363a84262994b88902315ed74be1155612f"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.1.1"
   synchronized:
     dependency: transitive
     description:
       name: synchronized
-      url: "https://pub.dartlang.org"
+      sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "3.0.0"
+    version: "3.0.1"
+  tencent_trtc_cloud:
+    dependency: "direct overridden"
+    description:
+      path: "."
+      ref: "14ea31a"
+      resolved-ref: "14ea31a03cb313e1659e55624fd483efbda69a8c"
+      url: "http://git.ius.plus/melon.yin/fis_trtc_magic.git"
+    source: git
+    version: "2.4.2"
   term_glyph:
     dependency: transitive
     description:
       name: term_glyph
-      url: "https://pub.dartlang.org"
+      sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.2.1"
   test_api:
     dependency: transitive
     description:
       name: test_api
-      url: "https://pub.dartlang.org"
+      sha256: ceeddf59d613e862e77f4b506cfc2945ac9637ce0b4c00f4f4c1ac639f3e9731
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "0.4.12"
+    version: "0.4.14"
   typed_data:
     dependency: transitive
     description:
       name: typed_data
-      url: "https://pub.dartlang.org"
+      sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.3.1"
+  uuid:
+    dependency: transitive
+    description:
+      name: uuid
+      sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.0.7"
   vector_math:
     dependency: transitive
     description:
       name: vector_math
-      url: "https://pub.dartlang.org"
+      sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.1.2"
+    version: "2.1.4"
   vid:
     dependency: transitive
     description:
       path: "."
-      ref: HEAD
+      ref: b38bb1b5a4
       resolved-ref: b38bb1b5a48bc0c04dc1a4bbbd1db1421f82be17
       url: "http://git.ius.plus:88/Project-Wing/flutter_vid"
     source: git
@@ -559,30 +733,66 @@ packages:
     dependency: transitive
     description:
       name: web_socket_channel
-      url: "https://pub.dartlang.org"
+      sha256: "3a969ddcc204a3e34e863d204b29c0752716f78b6f9cc8235083208d268a4ccd"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.2.0"
+  webview_flutter:
+    dependency: transitive
+    description:
+      name: webview_flutter
+      sha256: "6886b3ceef1541109df5001054aade5ee3c36b5780302e41701c78357233721c"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.8.0"
+  webview_flutter_android:
+    dependency: transitive
+    description:
+      name: webview_flutter_android
+      sha256: "8b3b2450e98876c70bfcead876d9390573b34b9418c19e28168b74f6cb252dbd"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.10.4"
+  webview_flutter_platform_interface:
+    dependency: transitive
+    description:
+      name: webview_flutter_platform_interface
+      sha256: "812165e4e34ca677bdfbfa58c01e33b27fd03ab5fa75b70832d4b7d4ca1fa8cf"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.9.5"
+  webview_flutter_wkwebview:
+    dependency: transitive
+    description:
+      name: webview_flutter_wkwebview
+      sha256: a5364369c758892aa487cbf59ea41d9edd10f9d9baf06a94e80f1bd1b4c7bbc0
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.9.5"
+  webviewx:
+    dependency: transitive
+    description:
+      name: webviewx
+      sha256: d7a7b73e0270c9e48d211dfc4174d19212134de7e8733cdda3d6dea13d7e0177
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.2.1"
   win32:
     dependency: transitive
     description:
       name: win32
-      url: "https://pub.dartlang.org"
+      sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.6.1"
+    version: "3.1.3"
   xdg_directories:
     dependency: transitive
     description:
       name: xdg_directories
-      url: "https://pub.dartlang.org"
+      sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "0.2.0+2"
-  xml:
-    dependency: transitive
-    description:
-      name: xml
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "5.4.1"
+    version: "1.0.0"
 sdks:
-  dart: ">=2.18.2 <3.0.0"
-  flutter: ">=2.10.0"
+  dart: ">=2.18.2 <4.0.0"
+  flutter: ">=3.3.0"

+ 22 - 119
pubspec.yaml

@@ -1,20 +1,6 @@
-name: flyinsono
-description: Flyinsono flutter client.
-
-# The following line prevents the package from being accidentally published to
-# pub.dev using `pub publish`. This is preferred for private packages.
-publish_to: "none" # Remove this line if you wish to publish to pub.dev
-
-# The following defines the version and build number for your application.
-# A version number is three numbers separated by dots, like 1.2.43
-# followed by an optional build number separated by a +.
-# Both the version and the builder number may be overridden in flutter
-# build by specifying --build-name and --build-number, respectively.
-# In Android, build-name is used as versionName while build-number used as versionCode.
-# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
-# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
-# Read more about iOS versioning at
-# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+name: fis_lib_basictest
+description: Flyinsono WebRTC hardware setting capability detection library
+publish_to: "none"
 version: 1.0.0+1
 
 environment:
@@ -25,124 +11,41 @@ dependencies:
     sdk: flutter
   flutter_localizations:
     sdk: flutter
-
-  # The following adds the Cupertino Icons font to your application.
-  # Use with the CupertinoIcons class for iOS style icons.
-  # cupertino_icons: ^1.0.2
   shared_preferences: 2.0.5
+  get: ^4.6.1
+  camera: ^0.10.3+1
+  camera_web: ^0.3.1+2
+  flutter_sound: ^9.2.13
+  path_provider: ^2.0.13
+  audio_session: ^0.1.13
 
-  fis_common:
+# 需要覆盖版本号的依赖
+dependency_overrides:
+  tencent_trtc_cloud:
     git:
-      url: http://git.ius.plus:88/Project-Wing/fis_lib_common.git
-      ref: ^1.0.8
-  fis_theme:
+      url: http://git.ius.plus/melon.yin/fis_trtc_magic.git
+      ref: 14ea31a
+  fis_vid:
     git:
-      url: http://git.ius.plus:88/Project-Wing/fis_lib_theme.git
-      ref: ^1.0.0.2
+      url: http://git.ius.plus/Project-Wing/fis_lib_vid.git
+      ref: f3e6342
   fis_ui:
     git:
       url: http://git.ius.plus:88/Project-Wing/fis_lib_ui.git
-      ref: ^1.0.1-rc3
-  fis_resource:
-    git:
-      url: http://git.ius.plus:88/Project-Wing/fis_lib_resource.git
-      ref: ^1.0.1.1
-  fis_i18n:
-    git:
-      url: http://git.ius.plus:88/Project-Wing/fis_lib_i18n.git
-      ref: ^1.0.0
-  fis_jsonrpc:
-    git:
-      url: http://git.ius.plus:88/Project-Wing/fis_lib_jsonrpc.git
-      ref: ^1.1.1
-
-# 需要覆盖版本号的依赖
-dependency_overrides:
-  image_picker_android: 0.8.4+13
+      ref: e5c7cb8
   fis_common:
     git:
       url: http://git.ius.plus:88/Project-Wing/fis_lib_common.git
-      ref: b5a4fa2
-    #path: ../fis_lib_common
-  fis_i18n:
-    git:
-      url: http://git.ius.plus:88/Project-Wing/fis_lib_i18n.git
-      ref: 213eb50
-    # path: ../fis_lib_i18n
-  fis_ui:
-    git:
-      url: http://git.ius.plus:88/Project-Wing/fis_lib_ui.git
-      ref: 034e87c
-    # path: ../fis_lib_ui
+      ref: 9cd9474
   fis_jsonrpc:
     git:
       url: http://git.ius.plus:88/Project-Wing/fis_lib_jsonrpc.git
-      ref: 735163e
-    #  path: E:\ACode\fis_lib_jsonrpc
-  fis_theme:
-    git:
-      url: http://git.ius.plus:88/Project-Wing/fis_lib_theme.git
-      ref: 6991f8ef7f
-  fis_resource:
-    git:
-      url: http://git.ius.plus:88/Project-Wing/fis_lib_resource.git
-      ref: 1.0.1.1
-
-  fis_lib_business_components:
-    git:
-      url: http://git.ius.plus/bakamaka.guan/fis_lib_business_components.git
-      ref: 009ddb6
-    # path: ../fis_lib_components
-
+      ref: 87bc52f
 dev_dependencies:
   flutter_test:
     sdk: flutter
-# For information on the generic Dart part of this file, see the
-# following page: https://dart.dev/tools/pub/pubspec
-
-# The following section is specific to Flutter.
 flutter:
-  # The following line ensures that the Material Icons font is
-  # included with your application, so that you can use the icons in
-  # the material Icons class.
   uses-material-design: true
+  assets:
+    - assets/testspeak.mp3
 
-  # To add assets to your application, add an assets section, like this:
- 
-  # An image asset can refer to one or more resolution-specific "variants", see
-  # https://flutter.dev/assets-and-images/#resolution-aware.
-
-  # For details regarding adding assets from package dependencies, see
-  # https://flutter.dev/assets-and-images/#from-packages
-
-  # To add custom fonts to your application, add a fonts section here,
-  # in this "flutter" section. Each entry in this list should have a
-  # "family" key with the font family name, and a "fonts" key with a
-  # list giving the asset and other descriptors for the font. For
-  # example:
-  # fonts:
-  #   - family: Schyler
-  #     fonts:
-  #       - asset: fonts/Schyler-Regular.ttf
-  #       - asset: fonts/Schyler-Italic.ttf
-  #         style: italic
-  #   - family: Trajan Pro
-  #     fonts:
-  #       - asset: fonts/TrajanPro.ttf
-  #       - asset: fonts/TrajanPro_Bold.ttf
-  #         weight: 700
-  #
-  # fonts:
-  #   #fix issue 70101 - https://github.com/flutter/flutter/issues/70101
-  #   - family: Roboto
-  #     fonts:
-  #       - asset: assets/fonts/Flutter_Roboto.ttf
-    #icon fonts
-    # - family: Icon_Meteocons
-    #   fonts:
-    #       - asset: assets/icons/Meteocons.ttf
-    # - family: FisIcons
-    #   fonts:
-    #     - asset: assets/icons/FisIcons.ttf
-  # For details regarding fonts from package dependencies,
-  # see https://flutter.dev/custom-fonts/#from-packages

+ 0 - 30
test/widget_test.dart

@@ -1,30 +0,0 @@
-// This is a basic Flutter widget test.
-//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility in the flutter_test package. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
-
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import 'package:project/main.dart';
-
-void main() {
-  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
-    // Build our app and trigger a frame.
-    await tester.pumpWidget(const MyApp());
-
-    // Verify that our counter starts at 0.
-    expect(find.text('0'), findsOneWidget);
-    expect(find.text('1'), findsNothing);
-
-    // Tap the '+' icon and trigger a frame.
-    await tester.tap(find.byIcon(Icons.add));
-    await tester.pump();
-
-    // Verify that our counter has incremented.
-    expect(find.text('0'), findsNothing);
-    expect(find.text('1'), findsOneWidget);
-  });
-}

+ 47 - 42
web/index.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html>
-<head>
-  <!--
+  <head>
+    <!--
     If you are serving your web app in a path other than the root, change the
     href value below to reflect the base path you are serving from.
 
@@ -14,45 +14,50 @@
     This is a placeholder for base href that will be replaced by the value of
     the `--base-href` argument provided to `flutter build`.
   -->
-  <base href="$FLUTTER_BASE_HREF">
-
-  <meta charset="UTF-8">
-  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
-  <meta name="description" content="A new Flutter project.">
-
-  <!-- iOS meta tags & icons -->
-  <meta name="apple-mobile-web-app-capable" content="yes">
-  <meta name="apple-mobile-web-app-status-bar-style" content="black">
-  <meta name="apple-mobile-web-app-title" content="project">
-  <link rel="apple-touch-icon" href="icons/Icon-192.png">
-
-  <!-- Favicon -->
-  <link rel="icon" type="image/png" href="favicon.png"/>
-
-  <title>project</title>
-  <link rel="manifest" href="manifest.json">
-
-  <script>
-    // The value below is injected by flutter build, do not touch.
-    var serviceWorkerVersion = null;
-  </script>
-  <!-- This script adds the flutter initialization JS code -->
-  <script src="flutter.js" defer></script>
-</head>
-<body>
-  <script>
-    window.addEventListener('load', function(ev) {
-      // Download main.dart.js
-      _flutter.loader.loadEntrypoint({
-        serviceWorker: {
-          serviceWorkerVersion: serviceWorkerVersion,
-        }
-      }).then(function(engineInitializer) {
-        return engineInitializer.initializeEngine();
-      }).then(function(appRunner) {
-        return appRunner.runApp();
+    <base href="$FLUTTER_BASE_HREF" />
+
+    <meta charset="UTF-8" />
+    <meta content="IE=Edge" http-equiv="X-UA-Compatible" />
+    <meta name="description" content="A new Flutter project." />
+
+    <!-- iOS meta tags & icons -->
+    <meta name="apple-mobile-web-app-capable" content="yes" />
+    <meta name="apple-mobile-web-app-status-bar-style" content="black" />
+    <meta name="apple-mobile-web-app-title" content="project" />
+    <link rel="apple-touch-icon" href="icons/Icon-192.png" />
+
+    <!-- Favicon -->
+    <link rel="icon" type="image/png" href="favicon.png" />
+
+    <title>project</title>
+    <link rel="manifest" href="manifest.json" />
+    <script src="trtc/TrtcWrapper.2.0.0-dev.0.0.2.bundle.js"></script>
+    <script src="trtc/BeautyManagerWrapper.2.0.0-dev.0.0.2.bundle.js"></script>
+
+    <script>
+      // The value below is injected by flutter build, do not touch.
+      var serviceWorkerVersion = null;
+    </script>
+    <!-- This script adds the flutter initialization JS code -->
+    <script src="flutter.js" defer></script>
+  </head>
+  <body>
+    <script>
+      window.addEventListener("load", function (ev) {
+        // Download main.dart.js
+        _flutter.loader
+          .loadEntrypoint({
+            serviceWorker: {
+              serviceWorkerVersion: serviceWorkerVersion,
+            },
+          })
+          .then(function (engineInitializer) {
+            return engineInitializer.initializeEngine();
+          })
+          .then(function (appRunner) {
+            return appRunner.runApp();
+          });
       });
-    });
-  </script>
-</body>
+    </script>
+  </body>
 </html>

文件差異過大導致無法顯示
+ 16 - 0
web/trtc/BeautyManagerWrapper.2.0.0-dev.0.0.2.bundle.js


文件差異過大導致無法顯示
+ 16 - 0
web/trtc/TrtcWrapper.2.0.0-dev.0.0.2.bundle.js


部分文件因文件數量過多而無法顯示