Neda/Front/lib/services/ptt_service.dart
2026-03-05 23:08:50 +03:30

169 lines
5.4 KiB
Dart

// LiveKit PTT protocol:
// connect(url, token) → join LiveKit room (muted by default)
// startSpeaking() → enable microphone → LiveKit publishes audio track
// stopSpeaking() → disable microphone → LiveKit unpublishes track
// Remote audio is played automatically by LiveKit SDK
// ActiveSpeakersChangedEvent fires whenever a participant starts/stops speaking
import 'dart:async';
import 'package:livekit_client/livekit_client.dart';
import '../config/app_config.dart';
enum PttState { idle, connected, speaking, receiving }
class PttService {
Room? _room;
EventsListener<RoomEvent>? _listener;
final _stateCtrl = StreamController<PttState>.broadcast();
final _speakerCtrl = StreamController<String?>.broadcast();
final _errorCtrl = StreamController<String>.broadcast();
PttState _state = PttState.idle;
String? _speakerName;
Stream<PttState> get stateStream => _stateCtrl.stream;
Stream<String?> get speakerStream => _speakerCtrl.stream;
Stream<String> get errorStream => _errorCtrl.stream;
PttState get currentState => _state;
String? get currentSpeaker => _speakerName;
void _setState(PttState s) {
_state = s;
_stateCtrl.add(s);
}
// ── Connect ──────────────────────────────────────────────────────────────
Future<bool> connect(String url, String token) async {
if (AppConfig.debug) {
await Future.delayed(const Duration(milliseconds: 400));
_setState(PttState.connected);
return true;
}
try {
_room = Room(
roomOptions: const RoomOptions(
adaptiveStream: false,
dynacast: false,
),
);
_listener = _room!.createListener();
_listener!
..on<ActiveSpeakersChangedEvent>(
(e) => _onSpeakersChanged(e.speakers),
)
..on<RoomDisconnectedEvent>((_) {
_setState(PttState.idle);
});
await _room!.connect(url, token);
// Start muted — mic only enabled when PTT is pressed
await _room!.localParticipant?.setMicrophoneEnabled(false);
_setState(PttState.connected);
return true;
} catch (e) {
_errorCtrl.add('اتصال به سرور برقرار نشد');
return false;
}
}
// ── Speakers detection ────────────────────────────────────────────────────
void _onSpeakersChanged(List<Participant> speakers) {
// Ignore while I'm the one speaking
if (_state == PttState.speaking) return;
final remoteSpeakers =
speakers.whereType<RemoteParticipant>().toList();
if (remoteSpeakers.isNotEmpty) {
final p = remoteSpeakers.first;
_speakerName = p.name.isNotEmpty ? p.name : p.identity;
_speakerCtrl.add(_speakerName);
_setState(PttState.receiving);
} else if (_state == PttState.receiving) {
_speakerName = null;
_speakerCtrl.add(null);
_setState(PttState.connected);
}
}
// ── PTT ───────────────────────────────────────────────────────────────────
Future<void> startSpeaking() async {
if (_state != PttState.connected) return;
if (AppConfig.debug) {
_setState(PttState.speaking);
return;
}
try {
await _room?.localParticipant?.setMicrophoneEnabled(true);
_setState(PttState.speaking);
} catch (_) {
_errorCtrl.add('خطا در فعال‌سازی میکروفون');
}
}
Future<void> stopSpeaking() async {
if (_state != PttState.speaking) return;
if (AppConfig.debug) {
_setState(PttState.connected);
_debugSimulateIncoming();
return;
}
try {
await _room?.localParticipant?.setMicrophoneEnabled(false);
_setState(PttState.connected);
} catch (_) {
_setState(PttState.connected);
}
}
// ── Debug helper ──────────────────────────────────────────────────────────
Future<void> _debugSimulateIncoming() async {
await Future.delayed(const Duration(milliseconds: 800));
if (_state != PttState.connected) return;
_speakerName = 'کاربر تست';
_speakerCtrl.add(_speakerName);
_setState(PttState.receiving);
await Future.delayed(const Duration(seconds: 2));
if (_state != PttState.receiving) return;
_speakerName = null;
_speakerCtrl.add(null);
_setState(PttState.connected);
}
// ── Disconnect / Dispose ──────────────────────────────────────────────────
Future<void> disconnect() async {
if (AppConfig.debug) {
_setState(PttState.idle);
return;
}
try {
await _room?.localParticipant?.setMicrophoneEnabled(false);
} catch (_) {}
await _listener?.dispose();
await _room?.disconnect();
_room = null;
_listener = null;
_setState(PttState.idle);
}
Future<void> dispose() async {
await disconnect();
await _stateCtrl.close();
await _speakerCtrl.close();
await _errorCtrl.close();
}
}