Files
Qlyra/lib/core/calls/sfu_data_channel.dart
T

347 lines
9.0 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import '../utils/logger.dart';
class MsgpackWriter {
final BytesBuilder _out = BytesBuilder();
Uint8List takeBytes() => _out.takeBytes();
void raw(int byte) => _out.addByte(byte);
void nil() => raw(0xC0);
void boolean(bool value) => raw(value ? 0xC3 : 0xC2);
void integer(int value) {
if (value >= 0) {
if (value < 0x80) return raw(value);
if (value <= 0xFF) {
raw(0xCC);
return raw(value);
}
if (value <= 0xFFFF) {
raw(0xCD);
return _uint(value, 2);
}
if (value <= 0xFFFFFFFF) {
raw(0xCE);
return _uint(value, 4);
}
raw(0xCF);
return _uint(value, 8);
}
if (value >= -32) return raw(0xE0 | (value + 32));
if (value >= -128) {
raw(0xD0);
return _uint(value & 0xFF, 1);
}
if (value >= -32768) {
raw(0xD1);
return _uint(value & 0xFFFF, 2);
}
if (value >= -2147483648) {
raw(0xD2);
return _uint(value & 0xFFFFFFFF, 4);
}
raw(0xD3);
_uint(value, 8);
}
void string(String value) {
final bytes = utf8.encode(value);
final length = bytes.length;
if (length < 32) {
raw(0xA0 | length);
} else if (length <= 0xFF) {
raw(0xD9);
raw(length);
} else if (length <= 0xFFFF) {
raw(0xDA);
_uint(length, 2);
} else {
raw(0xDB);
_uint(length, 4);
}
_out.add(bytes);
}
void arrayHeader(int length) {
if (length < 16) return raw(0x90 | length);
if (length <= 0xFFFF) {
raw(0xDC);
return _uint(length, 2);
}
raw(0xDD);
_uint(length, 4);
}
void _uint(int value, int bytes) {
for (var shift = (bytes - 1) * 8; shift >= 0; shift -= 8) {
raw((value >> shift) & 0xFF);
}
}
}
class MsgpackReader {
final Uint8List _data;
int _pos = 0;
MsgpackReader(this._data);
bool get exhausted => _pos >= _data.length;
bool get nextIsString {
final b = _data[_pos];
return (b & 0xE0) == 0xA0 || b == 0xD9 || b == 0xDA || b == 0xDB;
}
int readInt() {
final b = _data[_pos++];
if (b < 0x80) return b;
if (b >= 0xE0) return b - 256;
switch (b) {
case 0xCC:
return _uint(1);
case 0xCD:
return _uint(2);
case 0xCE:
return _uint(4);
case 0xCF:
return _uint(8);
case 0xD0:
final v = _uint(1);
return v >= 0x80 ? v - 0x100 : v;
case 0xD1:
final v = _uint(2);
return v >= 0x8000 ? v - 0x10000 : v;
case 0xD2:
final v = _uint(4);
return v >= 0x80000000 ? v - 0x100000000 : v;
case 0xD3:
return _uint(8);
}
throw FormatException('не целое: 0x${b.toRadixString(16)}');
}
String readString() {
final b = _data[_pos++];
int length;
if ((b & 0xE0) == 0xA0) {
length = b & 0x1F;
} else if (b == 0xD9) {
length = _uint(1);
} else if (b == 0xDA) {
length = _uint(2);
} else if (b == 0xDB) {
length = _uint(4);
} else {
throw FormatException('не строка: 0x${b.toRadixString(16)}');
}
final value = utf8.decode(_data.sublist(_pos, _pos + length));
_pos += length;
return value;
}
int readMapHeader() {
final b = _data[_pos++];
if ((b & 0xF0) == 0x80) return b & 0x0F;
if (b == 0xDE) return _uint(2);
if (b == 0xDF) return _uint(4);
throw FormatException('не map: 0x${b.toRadixString(16)}');
}
int readArrayHeader() {
final b = _data[_pos++];
if ((b & 0xF0) == 0x90) return b & 0x0F;
if (b == 0xDC) return _uint(2);
if (b == 0xDD) return _uint(4);
throw FormatException('не array: 0x${b.toRadixString(16)}');
}
int _uint(int bytes) {
var value = 0;
for (var i = 0; i < bytes; i++) {
value = (value << 8) | _data[_pos++];
}
return value;
}
}
class SfuLayoutItem {
final String trackKey;
final int width;
final int height;
const SfuLayoutItem({
required this.trackKey,
this.width = 640,
this.height = 360,
});
}
class SfuCommandChannel {
static const int _commandDisplayLayout = 0;
static const int _fitMode = 0;
static const int _notifyAliases = 1;
static const int _notifySlots = 2;
static const int _notifyAudioLevels = 6;
RTCDataChannel? _command;
int _sequence = 1;
final Map<int, String> _aliases = {};
final _slots = StreamController<Map<String, int>>.broadcast();
final _levels = StreamController<Map<String, int>>.broadcast();
Stream<Map<String, int>> get slotUpdates => _slots.stream;
Stream<Map<String, int>> get audioLevels => _levels.stream;
void bind(RTCDataChannel channel) {
if (channel.label == 'producerCommand') {
_command = channel;
channel.onMessage = _onCommandReply;
return;
}
if (channel.label != 'producerNotification') return;
channel.onMessage = _onNotification;
}
bool get ready => _command?.state == RTCDataChannelState.RTCDataChannelOpen;
Future<bool> sendDisplayLayout(
List<SfuLayoutItem> items, {
bool snapshot = true,
}) async {
final channel = _command;
if (channel == null) return false;
if (channel.state != RTCDataChannelState.RTCDataChannelOpen) {
logger.w('[call][sfu] producerCommand не открыт, слои не отправлены');
return false;
}
final writer = MsgpackWriter()
..integer(_commandDisplayLayout)
..integer(0)
..integer(_sequence++)
..boolean(snapshot);
if (items.isEmpty) {
writer.nil();
} else {
writer.arrayHeader(items.length * 2);
for (final item in items) {
writer
..string(item.trackKey)
..integer(0)
..nil()
..integer(item.width)
..integer(item.height)
..integer(_fitMode);
}
}
writer.nil();
final payload = writer.takeBytes();
try {
await channel.send(RTCDataChannelMessage.fromBinary(payload));
logger.i(
'[call][sfu] update-display-layout: '
'${items.map((i) => i.trackKey).join(', ')} '
'raw=${payload.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}',
);
return true;
} catch (e) {
logger.w('[call][sfu] update-display-layout failed: $e');
return false;
}
}
void _onCommandReply(RTCDataChannelMessage message) {
if (!message.isBinary) return;
final bytes = message.binary;
final head = bytes.length > 32 ? bytes.sublist(0, 32) : bytes;
final hex = head.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
try {
final reader = MsgpackReader(bytes);
final type = reader.readInt();
final version = reader.readInt();
final error = reader.readInt();
if (error != 0) {
logger.w(
'[call][sfu] command reply type=$type version=$version '
'ERROR=$error raw=$hex',
);
return;
}
logger.i('[call][sfu] command reply type=$type ok raw=$hex');
} catch (e) {
logger.w('[call][sfu] command reply parse failed: $e raw=$hex');
}
}
int _dumped = 0;
void _onNotification(RTCDataChannelMessage message) {
if (!message.isBinary) return;
if (_dumped < 12) {
_dumped++;
final bytes = message.binary;
final head = bytes.length > 64 ? bytes.sublist(0, 64) : bytes;
logger.i(
'[call][sfu] notify raw len=${bytes.length} '
'${head.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}',
);
}
final bytes = message.binary;
if (bytes.isEmpty) return;
final type = bytes[0];
final reader = MsgpackReader(Uint8List.sublistView(bytes, 1));
try {
switch (type) {
case _notifyAliases:
final count = reader.readMapHeader();
for (var i = 0; i < count; i++) {
final key = reader.readString();
_aliases[reader.readInt()] = key;
}
break;
case _notifySlots:
final count = reader.readArrayHeader();
final slots = <String, int>{};
for (var i = 0; i < count; i++) {
final key = _aliases[reader.readInt()];
if (key != null) slots[key] = i;
}
logger.i('[call][sfu] slots: $slots');
if (!_slots.isClosed) _slots.add(slots);
break;
case _notifyAudioLevels:
final count = reader.readMapHeader();
final levels = <String, int>{};
for (var i = 0; i < count; i++) {
final key = _aliases[reader.readInt()];
final level = reader.readInt();
if (key != null) levels[key] = level;
}
if (!_levels.isClosed) _levels.add(levels);
break;
}
} catch (e) {
logger.w('[call][sfu] notify type=$type parse failed: $e');
}
}
Future<void> dispose() async {
_command = null;
_aliases.clear();
if (!_slots.isClosed) await _slots.close();
if (!_levels.isClosed) await _levels.close();
}
}