Explorar o código

feat(dispatcher): multi-user room isolation + Flutter dispatcher client

Server:
- dispatcher.py: HTTP API on :9100, POST /connect creates unique room
- Each app gets own room, worker auto-spawns per room
- PUBLIC_LIVEKIT_URL returns external URL for apps
- No LiveKit server API dependency (roomCreate token)

Flutter:
- recording_notifier calls dispatcher before LiveKit connect
- Gets room + url + token from dispatcher response
- Falls back to hardcoded room if dispatcher unreachable
- LiveKitService.connect accepts optional token from dispatcher
- SettingsService.getDispatcherUrl() with default :9100
- http package added for REST API calls
wenhongquan hai 3 semanas
pai
achega
821da4ce0c

+ 30 - 3
flutter_asr_client/lib/pages/recording/notifiers/recording_notifier.dart

@@ -1,7 +1,9 @@
 import 'dart:async';
+import 'dart:convert';
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:http/http.dart' as http;
 import 'package:uuid/uuid.dart';
 
 import 'package:asr_client/models/conversation.dart';
@@ -116,9 +118,10 @@ final class RecordingNotifier extends AutoDisposeAsyncNotifier<RecordingState> {
       _messageSub?.cancel();
     });
 
-    final roomName = 'asr-test';
     final identity = 'user-${_uuid.v4().substring(0, 6)}';
-    final lkUrl = await ref.read(settingsServiceProvider).getLiveKitUrl();
+    final settingsService = ref.read(settingsServiceProvider);
+    final lkUrl = await settingsService.getLiveKitUrl();
+    final dispatcherUrl = await settingsService.getDispatcherUrl();
 
     final initialState = const RecordingState(
       projectName: 'G15 沈海高速改扩建 · K1120+300',
@@ -126,8 +129,32 @@ final class RecordingNotifier extends AutoDisposeAsyncNotifier<RecordingState> {
       templateName: '模板 · 普通混凝土坍落度 GB/T 50080',
     );
 
+    String roomName = 'asr-test';
+    String token = '';
+    String url = lkUrl;
+
+    // Try dispatcher for isolated room
+    if (dispatcherUrl.isNotEmpty) {
+      try {
+        final resp = await http.post(
+          Uri.parse('$dispatcherUrl/connect'),
+          headers: {'Content-Type': 'application/json'},
+          body: jsonEncode({'identity': identity}),
+        ).timeout(const Duration(seconds: 5));
+        if (resp.statusCode == 200) {
+          final data = jsonDecode(resp.body) as Map<String, dynamic>;
+          roomName = data['room'] as String;
+          token = data['token'] as String;
+          url = data['url'] as String;
+          debugPrint('[notifier] dispatcher: room=$roomName url=$url');
+        }
+      } catch (e) {
+        debugPrint('[notifier] dispatcher failed: $e, using fallback');
+      }
+    }
+
     try {
-      await _liveKit.connect(url: lkUrl, room: roomName, identity: identity);
+      await _liveKit.connect(url: url, room: roomName, identity: identity, token: token);
     } on Exception catch (e) {
       return initialState.copyWith(
         errorMessage: '无法连接 LiveKit: $e',

+ 4 - 3
flutter_asr_client/lib/services/livekit_service.dart

@@ -11,7 +11,7 @@ abstract interface class LiveKitService {
   Stream<ServerMessage> get messageStream;
   bool get isConnected;
 
-  Future<void> connect({required String url, required String room, required String identity});
+  Future<void> connect({required String url, required String room, required String identity, String token = ''});
   Future<void> disconnect();
 }
 
@@ -34,10 +34,11 @@ final class LiveKitServiceImpl implements LiveKitService {
     required String url,
     required String room,
     required String identity,
+    String token = '',
   }) async {
     if (_connected) return;
 
-    final token = _makeToken(room, identity);
+    final effectiveToken = token.isNotEmpty ? token : _makeToken(room, identity);
     debugPrint('[lk] connecting to $url room=$room');
 
     // Listen for room events (including connection failures)
@@ -68,7 +69,7 @@ final class LiveKitServiceImpl implements LiveKitService {
     try {
       await _room.connect(
         '$url',
-        token,
+        effectiveToken,
       );
     } catch (e, st) {
       debugPrint('[lk] connect failed: $e');

+ 8 - 1
flutter_asr_client/lib/services/settings_service.dart

@@ -10,6 +10,7 @@ abstract interface class SettingsService {
   Future<void> setPort(int port);
   String getWebSocketUrl(String host, int port);
   Future<String> getLiveKitUrl();
+  Future<String> getDispatcherUrl();
   Future<void> setLiveKitUrl(String url);
 }
 
@@ -61,10 +62,15 @@ final class SharedPreferencesSettingsService implements SettingsService {
 
   @override
   Future<String> getLiveKitUrl() async {
-    final prefs = await _preferences;
+    final prefs = await SharedPreferences.getInstance();
     return prefs.getString(_lkUrlKey) ?? AppConstants.defaultLiveKitUrl;
   }
 
+  Future<String> getDispatcherUrl() async {
+    final prefs = await SharedPreferences.getInstance();
+    return prefs.getString(_dispatcherUrlKey) ?? 'http://200.200.18.11:9100';
+  }
+
   @override
   Future<void> setLiveKitUrl(String url) async {
     final prefs = await _preferences;
@@ -74,4 +80,5 @@ final class SharedPreferencesSettingsService implements SettingsService {
   static const _hostKey = 'asr_host';
   static const _portKey = 'asr_port';
   static const _lkUrlKey = 'asr_livekit_url';
+  static const _dispatcherUrlKey = 'asr_dispatcher_url';
 }

+ 1 - 1
flutter_asr_client/pubspec.lock

@@ -219,7 +219,7 @@ packages:
     source: hosted
     version: "14.8.1"
   http:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: http
       sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"

+ 1 - 0
flutter_asr_client/pubspec.yaml

@@ -20,6 +20,7 @@ dependencies:
   uuid: ^4.5.3
   livekit_client: ^2.8.1
   dart_jsonwebtoken: ^3.4.1
+  http: ^1.2.0
 
 dev_dependencies:
   flutter_test: