feat(calls): Щас попробую легонечко с data channel побурмалдить

This commit is contained in:
Jganenok
2026-06-12 22:19:57 +07:00
parent 42f1fd5292
commit d7dcb076c3
7 changed files with 77 additions and 87 deletions
+64
View File
@@ -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<CallSessionState>.broadcast();
final _remoteStream = StreamController<MediaStream>.broadcast();
final _info = StreamController<void>.broadcast();
final _kometDetected = StreamController<void>.broadcast();
Stream<CallSessionState> get stateStream => _state.stream;
Stream<MediaStream> get remoteStreamStream => _remoteStream.stream;
@@ -111,6 +118,9 @@ class CallSession {
Stream<void> get infoUpdates => _info.stream;
Stream<void> 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<void> _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<void> _setupSfu() async {
if (_pc != null) {
await _pc!.close();
_pc = null;
_probeChannel = null;
_remoteDescSet = false;
_pendingCandidates.clear();
for (final track in _localStream?.getTracks() ?? <MediaStreamTrack>[]) {
@@ -1018,6 +1077,10 @@ class CallSession {
Future<void> _dispose() async {
_levelTimer?.cancel();
try {
await _probeChannel?.close();
} catch (_) {}
_probeChannel = null;
for (final track in _localStream?.getTracks() ?? <MediaStreamTrack>[]) {
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<String, dynamic> msg, List iceServers) {
-2
View File
@@ -1,2 +0,0 @@
export 'file_log_output_stub.dart'
if (dart.library.io) 'file_log_output_io.dart';
-61
View File
@@ -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<String> _buffer = [];
static final RegExp _ansi = RegExp('\x1B\\[[0-9;]*m');
String? get path => _path;
Future<void> 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<void> destroy() async {
try {
await _raf?.close();
} catch (_) {}
_raf = null;
}
}
-14
View File
@@ -1,14 +0,0 @@
import 'package:logger/logger.dart';
class FileLogOutput extends LogOutput {
FileLogOutput._();
static final FileLogOutput instance = FileLogOutput._();
String? get path => null;
Future<void> start() async {}
@override
void output(OutputEvent event) {}
}
+1 -7
View File
@@ -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<void> startFileLogging() => FileLogOutput.instance.start();
String? get logFilePath => FileLogOutput.instance.path;
int _importanceSortKey(Level level) {
final v = level.value;
if (v >= 5999) {
@@ -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<CallScreen>
CallSession? _session;
StreamSubscription<CallSessionState>? _stateSub;
StreamSubscription<void>? _infoSub;
StreamSubscription<void>? _kometSub;
StreamSubscription<MediaStream>? _remoteStreamSub;
CallSessionState _state = CallSessionState.connecting;
bool _incomingPending = false;
@@ -212,12 +214,21 @@ class _CallScreenState extends State<CallScreen>
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<CallScreen>
void dispose() {
_stateSub?.cancel();
_infoSub?.cancel();
_kometSub?.cancel();
_remoteStreamSub?.cancel();
_dotsController.dispose();
_videoController.dispose();
-3
View File
@@ -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<Locale> _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) {