livekit_service.dart 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
  4. import 'package:flutter/foundation.dart';
  5. import 'package:livekit_client/livekit_client.dart';
  6. import 'package:asr_client/models/websocket_message.dart';
  7. /// Wraps LiveKit Room connection and relays transcription data messages.
  8. abstract interface class LiveKitService {
  9. Stream<ServerMessage> get messageStream;
  10. bool get isConnected;
  11. Future<void> connect({required String url, required String room, required String identity, String token = ''});
  12. Future<void> disconnect();
  13. }
  14. final class LiveKitServiceImpl implements LiveKitService {
  15. LiveKitServiceImpl({Room? room}) : _room = room ?? Room();
  16. Room _room;
  17. final _messageController = StreamController<ServerMessage>.broadcast();
  18. var _connected = false;
  19. EventsListener<RoomEvent>? _listener;
  20. @override
  21. Stream<ServerMessage> get messageStream => _messageController.stream;
  22. @override
  23. bool get isConnected => _connected;
  24. @override
  25. Future<void> connect({
  26. required String url,
  27. required String room,
  28. required String identity,
  29. String token = '',
  30. }) async {
  31. if (_connected) return;
  32. final effectiveToken = token.isNotEmpty ? token : _makeToken(room, identity);
  33. debugPrint('[lk] connecting to $url room=$room');
  34. // Listen for room events (including connection failures)
  35. _listener = _room.createListener()
  36. ..on<DataReceivedEvent>((e) {
  37. debugPrint('[lk] data received topic=${e.topic} bytes=${e.data.length}');
  38. if (e.topic == 'transcription') {
  39. try {
  40. final json = jsonDecode(utf8.decode(e.data)) as Map<String, dynamic>;
  41. final message = ServerMessage.fromJson(json);
  42. _messageController.add(message);
  43. } catch (ex) {
  44. debugPrint('[lk] bad data: $ex');
  45. }
  46. }
  47. })
  48. ..on<RoomDisconnectedEvent>((e) {
  49. debugPrint('[lk] disconnected: ${e.reason}');
  50. _connected = false;
  51. })
  52. ..on<RoomReconnectingEvent>((e) {
  53. debugPrint('[lk] reconnecting');
  54. })
  55. ..on<RoomReconnectedEvent>((e) {
  56. debugPrint('[lk] reconnected');
  57. });
  58. try {
  59. await _room.connect(
  60. url,
  61. effectiveToken,
  62. );
  63. } catch (e, st) {
  64. debugPrint('[lk] connect failed: $e');
  65. debugPrint('[lk] stack: $st');
  66. rethrow;
  67. }
  68. debugPrint('[lk] connected, publishing mic...');
  69. try {
  70. await _room.localParticipant!.setMicrophoneEnabled(true);
  71. debugPrint('[lk] mic published');
  72. } catch (e, st) {
  73. debugPrint('[lk] mic FAILED: $e\n$st');
  74. }
  75. _connected = true;
  76. _messageController.add(const ConnectedMessage(model: {}));
  77. }
  78. @override
  79. Future<void> disconnect() async {
  80. if (!_connected) return;
  81. _connected = false;
  82. _listener?.dispose();
  83. _listener = null;
  84. await _room.disconnect();
  85. // Room instances are single-use; create a fresh one for the next connect.
  86. _room = Room();
  87. }
  88. String _makeToken(String room, String identity) {
  89. // Dev token: for localhost LiveKit Server with key "devkey"
  90. // In production, tokens should be generated server-side.
  91. const key = 'devkey';
  92. const secret = 'secretsecretsecretsecretsecret12';
  93. final jwt = JWT(
  94. {
  95. 'iss': key,
  96. 'sub': identity,
  97. 'name': identity,
  98. 'nbf': DateTime.now()
  99. .subtract(const Duration(minutes: 1))
  100. .millisecondsSinceEpoch ~/
  101. 1000,
  102. 'exp': DateTime.now()
  103. .add(const Duration(hours: 6))
  104. .millisecondsSinceEpoch ~/
  105. 1000,
  106. 'video': {
  107. 'roomJoin': true,
  108. 'room': room,
  109. 'canPublish': true,
  110. 'canSubscribe': true,
  111. 'canPublishData': true,
  112. },
  113. },
  114. );
  115. return jwt.sign(SecretKey(secret));
  116. }
  117. }