Реализованы голосовые звонки 1:1 поверх инфраструктуры VK/OK
(flutter_webrtc + сигналинг ws2 wss://videowebrtc.okcdn.ru). Протокол
восстановлен реверсом захваченных пакетов и декомпиляцией
ru.ok.android.externcalls.sdk.
Ядро (lib/core/calls/):
- conversation_params.dart — декодер vcp (LZ4-блок), с unit-тестом
- ws2_signaling.dart — клиент сигналинга ws2 (transmit-data, ICE,
accept-call, hangup, change-media-settings, ping/pong)
- call_session.dart — WebRTC-контроллер для звонящего и вызываемого
- call_controller.dart — глобальный оркестратор: пуш opcode 137,
исходящие, приём/отклонение, управление активной сессией
Бэкенд:
- CallsModule.initiateCall — инициация исходящего (opcode 78)
- общий LZ4-block декодер вынесен в core/protocol/lz4_block.dart
(используется и транспортом, и vcp)
UI / обвязка:
- CallScreen переписан под живой CallSession (+ кнопка «свернуть»
без завершения звонка, возврат повторным нажатием 📞)
- кнопка звонка в шапке диалога
- инициализация CallController на логине, показ входящего по пушу
- дев-тумблер: принудительный сигнал состояния микрофона без
изменения реального аудиотрека
Прочее:
- зависимость flutter_webrtc ^0.12.5
- Android: RECORD_AUDIO / MODIFY_AUDIO_SETTINGS / BLUETOOTH
48 lines
2.2 KiB
Dart
48 lines
2.2 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:komet/core/calls/conversation_params.dart';
|
|
|
|
void main() {
|
|
// Реальный `vcp` из захвата входящего звонка (docs/PCAPdroid_10_июн._15_32_04).
|
|
const sampleVcp =
|
|
'532:8Ux7InRrbiI6IjZ5OHFHbkx4czJ0TXk5d1dOZjZFVms2OEN6QlR3Vmg3OGxBaDZZem4zems9Iiwid3NlIjoid3NzOi8vdmlkZW93ZWJydGMub2tjZG4ucnUvd3My'
|
|
'JwD6B2lwIjpbIjE1NS4yMTIuMjA0LjExIiwRAIA5NiJdLCJ3dFMAT2h0dHBVAAWQOjIzNDU2L3d0gQAfdFoAFzh2Y2FbAFVjYWxsc6oAESIgAA9NAAAsOTWoAADSAAnKABEzuQDwFHNyY3AiOiJvbmVfbWUi'
|
|
'LCJldCI6MTc4MTA5NDc1Mywic3RufwBWc3R1bjo/AJA1LjgyOjE5MzBWACF0ciMAP3R1ciMAAx0sGgAoMTQ9AEB1IjoicwDyBDEyMzM3Mzo5MTAyMTUzNDUyOTdeAAChAPAMOVhWbTduUnoxMEFuVkZWN2t0M003aGdDL3hR0wGgaXYiOmZhbHNlfQ==';
|
|
|
|
test('decodes the captured vcp blob', () {
|
|
final params = ConversationParams.decode(sampleVcp);
|
|
|
|
expect(params, isNotNull);
|
|
expect(params!.token, '6y8qGnLxs2tMy9wWNf6EVk68CzBTwVh78lAh6Yzn3zk=');
|
|
expect(params.wsEndpoint, 'wss://videowebrtc.okcdn.ru/ws2');
|
|
expect(params.wtEndpoint, 'https://videowebrtc.okcdn.ru:23456/wt');
|
|
expect(params.callsApiEndpoint, 'https://calls.okcdn.ru');
|
|
expect(params.clientType, 'one_me');
|
|
expect(params.expiresAt, 1781094753);
|
|
expect(params.stun, 'stun:155.212.205.82:19302');
|
|
expect(params.turn, [
|
|
'turn:155.212.205.82:19302',
|
|
'turn:155.212.205.14:19302',
|
|
]);
|
|
expect(params.turnUser, '1781123373:910215345297');
|
|
expect(params.turnPassword, '9XVm7nRz10AnVFV7kt3M7hgC/xQ=');
|
|
expect(params.isVideo, false);
|
|
});
|
|
|
|
test('builds ice servers for flutter_webrtc', () {
|
|
final params = ConversationParams.decode(sampleVcp)!;
|
|
final ice = params.iceServers;
|
|
|
|
expect(ice, hasLength(2));
|
|
expect(ice[0]['urls'], 'stun:155.212.205.82:19302');
|
|
expect(ice[1]['urls'], isA<List<String>>());
|
|
expect(ice[1]['username'], '1781123373:910215345297');
|
|
expect(ice[1]['credential'], '9XVm7nRz10AnVFV7kt3M7hgC/xQ=');
|
|
});
|
|
|
|
test('rejects malformed input', () {
|
|
expect(ConversationParams.decode('not-a-vcp'), isNull);
|
|
expect(ConversationParams.decode(''), isNull);
|
|
expect(ConversationParams.decode('0:'), isNull);
|
|
});
|
|
}
|