Реализованы голосовые звонки 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
72 lines
2.0 KiB
Dart
72 lines
2.0 KiB
Dart
import 'dart:typed_data';
|
|
|
|
/// LZ4 block декомпрессия (без frame-заголовка).
|
|
///
|
|
/// Сервер шлёт block-формат как в транспорте (payload пакетов), так и в
|
|
/// `vcp`-параметрах звонка. dart_lz4 поддерживает только frame-формат, поэтому
|
|
/// block распаковывается вручную.
|
|
Uint8List lz4BlockDecompress(Uint8List src, int maxSize) {
|
|
var out = Uint8List(1024);
|
|
int outLen = 0;
|
|
int pos = 0;
|
|
|
|
void ensure(int extra) {
|
|
if (outLen + extra > maxSize) throw StateError('LZ4: превышен лимит');
|
|
if (outLen + extra <= out.length) return;
|
|
var newCap = out.length * 2;
|
|
while (newCap < outLen + extra) {
|
|
newCap *= 2;
|
|
}
|
|
if (newCap > maxSize) newCap = maxSize;
|
|
final grown = Uint8List(newCap);
|
|
grown.setRange(0, outLen, out);
|
|
out = grown;
|
|
}
|
|
|
|
while (pos < src.length) {
|
|
final token = src[pos++];
|
|
var litLen = token >> 4;
|
|
|
|
if (litLen == 15) {
|
|
while (pos < src.length) {
|
|
final b = src[pos++];
|
|
litLen += b;
|
|
if (b != 255) break;
|
|
}
|
|
}
|
|
|
|
if (litLen > 0) {
|
|
ensure(litLen);
|
|
out.setRange(outLen, outLen + litLen, src, pos);
|
|
outLen += litLen;
|
|
pos += litLen;
|
|
}
|
|
|
|
if (pos >= src.length) break;
|
|
|
|
if (pos + 1 >= src.length) throw StateError('LZ4: unexpected end of input');
|
|
final offset = src[pos] | (src[pos + 1] << 8);
|
|
pos += 2;
|
|
if (offset == 0) throw StateError('LZ4: offset = 0');
|
|
|
|
var matchLen = (token & 0x0F) + 4;
|
|
if ((token & 0x0F) == 0x0F) {
|
|
while (pos < src.length) {
|
|
final b = src[pos++];
|
|
matchLen += b;
|
|
if (b != 255) break;
|
|
}
|
|
}
|
|
|
|
ensure(matchLen);
|
|
final start = outLen - offset;
|
|
if (start < 0) throw StateError('LZ4: offset за пределами вывода');
|
|
for (var i = 0; i < matchLen; i++) {
|
|
out[outLen + i] = out[start + i];
|
|
}
|
|
outLen += matchLen;
|
|
}
|
|
|
|
return Uint8List.sublistView(out, 0, outLen);
|
|
}
|