呼出/通话中按钮、计时器、底色、60s 未接与进出场动效对齐视觉规范。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
72 lines
1.7 KiB
Dart
72 lines
1.7 KiB
Dart
import 'dart:async';
|
|
|
|
import '../brand/app_tokens.dart';
|
|
|
|
/// 一对一语音会话防护:占线、超时、重复信令、销毁时释放计时器。
|
|
/// 不自建第二套信令,只约束官方 OpenIMLive 的进出场。
|
|
enum VoiceCallPhase { idle, outgoing, incoming, inCall }
|
|
|
|
class CallSessionGuard {
|
|
CallSessionGuard({this.inviteTimeout = AppDuration.callInviteTimeout});
|
|
|
|
final Duration inviteTimeout;
|
|
|
|
VoiceCallPhase phase = VoiceCallPhase.idle;
|
|
String? roomID;
|
|
void Function()? onTimeout;
|
|
|
|
final Set<String> _seenKeys = <String>{};
|
|
Timer? _timeout;
|
|
|
|
bool get isBusy => phase != VoiceCallPhase.idle;
|
|
|
|
bool acceptSignaling({required int customType, required String roomID}) {
|
|
if (roomID.isEmpty) return false;
|
|
return _seenKeys.add('$customType:$roomID');
|
|
}
|
|
|
|
String? startOutgoing(String roomID) {
|
|
if (isBusy) return 'busy';
|
|
this.roomID = roomID;
|
|
phase = VoiceCallPhase.outgoing;
|
|
_armTimeout();
|
|
return null;
|
|
}
|
|
|
|
String? startIncoming(String roomID) {
|
|
if (isBusy) return 'busy';
|
|
this.roomID = roomID;
|
|
phase = VoiceCallPhase.incoming;
|
|
_armTimeout();
|
|
return null;
|
|
}
|
|
|
|
void markConnected() {
|
|
if (!isBusy) return;
|
|
_timeout?.cancel();
|
|
_timeout = null;
|
|
phase = VoiceCallPhase.inCall;
|
|
}
|
|
|
|
bool sameRoom(String? other) =>
|
|
other != null && other.isNotEmpty && other == roomID;
|
|
|
|
void dispose() {
|
|
_timeout?.cancel();
|
|
_timeout = null;
|
|
_seenKeys.clear();
|
|
phase = VoiceCallPhase.idle;
|
|
roomID = null;
|
|
onTimeout = null;
|
|
}
|
|
|
|
void _armTimeout() {
|
|
_timeout?.cancel();
|
|
_timeout = Timer(inviteTimeout, () {
|
|
if (phase == VoiceCallPhase.outgoing || phase == VoiceCallPhase.incoming) {
|
|
onTimeout?.call();
|
|
}
|
|
});
|
|
}
|
|
}
|