diff --git a/lib/core/calls/call_session.dart b/lib/core/calls/call_session.dart index f26edd3..9909b4b 100644 --- a/lib/core/calls/call_session.dart +++ b/lib/core/calls/call_session.dart @@ -84,6 +84,12 @@ class CallSession { static const double _speakLevelOn = 0.05; static const int _speakHoldTicks = 3; + RTCDataChannel? _probeChannel; + bool _peerIsKomet = false; + + static const String _probeQuestion = 'AreYouKomet?'; + static const String _probeAnswer = 'YesImKomet😎'; + bool get localVideo => _localVideo; bool get localScreen => _localScreen; MediaStream? get localVideoStream => _localVideoStream; @@ -104,6 +110,7 @@ class CallSession { final _state = StreamController.broadcast(); final _remoteStream = StreamController.broadcast(); final _info = StreamController.broadcast(); + final _kometDetected = StreamController.broadcast(); Stream get stateStream => _state.stream; Stream get remoteStreamStream => _remoteStream.stream; @@ -111,6 +118,9 @@ class CallSession { Stream get infoUpdates => _info.stream; + Stream get peerKometDetected => _kometDetected.stream; + bool get peerIsKomet => _peerIsKomet; + bool get isMuted => _muted; bool get peerMuted => _peerMuted; bool get peerVideo => _peerVideo; @@ -482,6 +492,8 @@ class CallSession { init: RTCRtpTransceiverInit(direction: TransceiverDirection.RecvOnly), ); + await _setupKometProbe(pc); + if (role == CallRole.caller) { _setState(CallSessionState.ringing); await _createAndSendOffer(); @@ -499,6 +511,7 @@ class CallSession { }); pc.onIceCandidate = _onLocalCandidate; pc.onTrack = (event) => unawaited(_onRemoteTrack(event)); + pc.onDataChannel = (channel) => _bindProbeChannel(channel, ask: false); pc.onIceConnectionState = (s) => logger.t('[call] ice $s'); pc.onConnectionState = (s) { logger.t('[call] pc $s'); @@ -534,10 +547,56 @@ class CallSession { } } + Future _setupKometProbe(RTCPeerConnection pc) async { + if (_topology == 'SERVER') return; + try { + final channel = await pc.createDataChannel( + 'komet', + RTCDataChannelInit()..ordered = true, + ); + _probeChannel = channel; + _bindProbeChannel(channel, ask: true); + } catch (_) {} + } + + void _bindProbeChannel(RTCDataChannel channel, {required bool ask}) { + channel.onMessage = (message) => _onProbeMessage(channel, message); + channel.onDataChannelState = (state) { + if (ask && state == RTCDataChannelState.RTCDataChannelOpen) { + _sendProbe(channel, _probeQuestion); + } + }; + } + + void _onProbeMessage(RTCDataChannel channel, RTCDataChannelMessage message) { + if (message.isBinary) return; + final text = message.text; + if (text == _probeQuestion) { + _sendProbe(channel, _probeAnswer); + } else if (text == _probeAnswer) { + _markPeerKomet(); + } + } + + void _sendProbe(RTCDataChannel channel, String text) { + try { + channel.send(RTCDataChannelMessage(text)); + } catch (_) {} + } + + void _markPeerKomet() { + if (_peerIsKomet) return; + _peerIsKomet = true; + logger.t('[call] peer is Komet'); + if (!_kometDetected.isClosed) _kometDetected.add(null); + _notifyInfo(); + } + Future _setupSfu() async { if (_pc != null) { await _pc!.close(); _pc = null; + _probeChannel = null; _remoteDescSet = false; _pendingCandidates.clear(); for (final track in _localStream?.getTracks() ?? []) { @@ -1018,6 +1077,10 @@ class CallSession { Future _dispose() async { _levelTimer?.cancel(); + try { + await _probeChannel?.close(); + } catch (_) {} + _probeChannel = null; for (final track in _localStream?.getTracks() ?? []) { await track.stop(); } @@ -1033,6 +1096,7 @@ class CallSession { if (!_state.isClosed) await _state.close(); if (!_remoteStream.isClosed) await _remoteStream.close(); if (!_info.isClosed) await _info.close(); + if (!_kometDetected.isClosed) await _kometDetected.close(); } void _applyConnectionInfo(Map msg, List iceServers) { diff --git a/lib/core/utils/file_log_output.dart b/lib/core/utils/file_log_output.dart deleted file mode 100644 index 01ea152..0000000 --- a/lib/core/utils/file_log_output.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'file_log_output_stub.dart' - if (dart.library.io) 'file_log_output_io.dart'; diff --git a/lib/core/utils/file_log_output_io.dart b/lib/core/utils/file_log_output_io.dart deleted file mode 100644 index dec9ee1..0000000 --- a/lib/core/utils/file_log_output_io.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'dart:io'; - -import 'package:logger/logger.dart'; -import 'package:path_provider/path_provider.dart'; - -class FileLogOutput extends LogOutput { - FileLogOutput._(); - - static final FileLogOutput instance = FileLogOutput._(); - - RandomAccessFile? _raf; - String? _path; - final List _buffer = []; - - static final RegExp _ansi = RegExp('\x1B\\[[0-9;]*m'); - - String? get path => _path; - - Future start() async { - if (_raf != null) return; - try { - final dir = await getApplicationSupportDirectory(); - final file = File('${dir.path}/komet_calls.log'); - final raf = await file.open(mode: FileMode.write); - _path = file.path; - _raf = raf; - _write('=== komet log ${file.path} ==='); - for (final line in _buffer) { - _write(line); - } - _buffer.clear(); - } catch (_) {} - } - - void _write(String line) { - try { - _raf?.writeStringSync('$line\n'); - } catch (_) {} - } - - @override - void output(OutputEvent event) { - if (_raf == null) { - for (final line in event.lines) { - if (_buffer.length < 20000) _buffer.add(line.replaceAll(_ansi, '')); - } - return; - } - for (final line in event.lines) { - _write(line.replaceAll(_ansi, '')); - } - } - - @override - Future destroy() async { - try { - await _raf?.close(); - } catch (_) {} - _raf = null; - } -} diff --git a/lib/core/utils/file_log_output_stub.dart b/lib/core/utils/file_log_output_stub.dart deleted file mode 100644 index 76a0181..0000000 --- a/lib/core/utils/file_log_output_stub.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:logger/logger.dart'; - -class FileLogOutput extends LogOutput { - FileLogOutput._(); - - static final FileLogOutput instance = FileLogOutput._(); - - String? get path => null; - - Future start() async {} - - @override - void output(OutputEvent event) {} -} diff --git a/lib/core/utils/logger.dart b/lib/core/utils/logger.dart index 8080e82..1460e91 100644 --- a/lib/core/utils/logger.dart +++ b/lib/core/utils/logger.dart @@ -3,8 +3,6 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:logger/logger.dart'; -import 'file_log_output.dart'; - Level _minimumLogLevel() { const raw = String.fromEnvironment('KOMET_LOG_LEVEL', defaultValue: ''); switch (raw.toLowerCase()) { @@ -43,13 +41,9 @@ final logger = Logger( filter: _logFilter(), level: _minimumLogLevel(), printer: KometLogPrinter(), - output: MultiOutput([ConsoleOutput(), FileLogOutput.instance]), + output: ConsoleOutput(), ); -Future startFileLogging() => FileLogOutput.instance.start(); - -String? get logFilePath => FileLogOutput.instance.path; - int _importanceSortKey(Level level) { final v = level.value; if (v >= 5999) { diff --git a/lib/frontend/screens/calls/call_screen.dart b/lib/frontend/screens/calls/call_screen.dart index 58f1b27..7e4ef5f 100644 --- a/lib/frontend/screens/calls/call_screen.dart +++ b/lib/frontend/screens/calls/call_screen.dart @@ -20,6 +20,7 @@ import '../../../core/calls/call_controller.dart'; import '../../../core/calls/call_info.dart'; import '../../../core/calls/call_session.dart'; import '../../../core/utils/format.dart'; +import '../../widgets/custom_notification.dart'; import '../../widgets/glossy_pill.dart'; const Color _kEndRed = Color(0xFFE5484D); @@ -50,6 +51,7 @@ class _CallScreenState extends State CallSession? _session; StreamSubscription? _stateSub; StreamSubscription? _infoSub; + StreamSubscription? _kometSub; StreamSubscription? _remoteStreamSub; CallSessionState _state = CallSessionState.connecting; bool _incomingPending = false; @@ -212,12 +214,21 @@ class _CallScreenState extends State setState(() {}); }); _remoteStreamSub = session.remoteStreamStream.listen(_attachStream); + _kometSub = session.peerKometDetected.listen((_) => _showKometBadge()); + if (session.peerIsKomet) { + WidgetsBinding.instance.addPostFrameCallback((_) => _showKometBadge()); + } final existing = session.remoteStream; if (existing != null) _attachStream(existing); _resolveParticipants(); _syncVideo(); } + void _showKometBadge() { + if (!mounted) return; + showCustomNotification(context, 'Этот человек использует Komet! :3'); + } + void _resolveParticipants() { final session = _session; if (session == null) return; @@ -337,6 +348,7 @@ class _CallScreenState extends State void dispose() { _stateSub?.cancel(); _infoSub?.cancel(); + _kometSub?.cancel(); _remoteStreamSub?.cancel(); _dotsController.dispose(); _videoController.dispose(); diff --git a/lib/main.dart b/lib/main.dart index 901f43d..343f7e5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,7 +11,6 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'backend/api.dart'; import 'core/cache/info_cache.dart'; -import 'core/utils/logger.dart'; import 'core/config/app_accent.dart'; import 'core/config/app_amoled.dart'; import 'core/config/app_bubble_behavior.dart'; @@ -79,8 +78,6 @@ Future _loadInitialLocale() async { void main() async { WidgetsFlutterBinding.ensureInitialized(); - await startFileLogging(); - logger.i('[log] file: ${logFilePath ?? '—'}'); await AppDatabase.init(); final activeAccountId = await TokenStorage.getActiveAccountId(); if (activeAccountId != null) {