Skip to content

Commit 0121443

Browse files
committed
refactor(call): generic peer_message side channel for in-call app-to-app state (WT-713)
Replace the media_state-specific request/event with a generic peer_message envelope carrying an opaque {type, data} pair. media_state (camera state, data {video}) becomes the first carried type; the client routes inbound peer_message by type and ignores unknown ones, so future in-call signals reuse the same channel without protocol changes. "message" over "signal" to avoid collision with the SIP/WS signaling plane - this is an app-to-app message.
1 parent bc56887 commit 0121443

12 files changed

Lines changed: 175 additions & 120 deletions

File tree

lib/features/call/bloc/call_bloc.dart

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ typedef OutgoingFromNumberResolver = String? Function(String? callerProvidedFrom
8181

8282
const _getUserMediaPushKitTimeout = Duration(seconds: 8);
8383

84+
/// `peer_message` type carrying the remote camera state (`data: {video: bool}`).
85+
const _mediaStatePeerMessageType = 'media_state';
86+
8487
class CallBloc extends Bloc<CallEvent, CallState> with WidgetsBindingObserver implements CallkeepDelegate {
8588
final CallLogsRepository callLogsRepository;
8689
final void Function(String callId, String callerName) _onMissedCall;
@@ -1138,7 +1141,13 @@ class CallBloc extends Bloc<CallEvent, CallState> with WidgetsBindingObserver im
11381141
final transaction = WebtritSignalingClient.generateTransactionId();
11391142
_signalingModule
11401143
.execute(
1141-
MediaStateRequest(transaction: transaction, line: call.line, callId: call.callId, media: {'video': video}),
1144+
PeerMessageRequest(
1145+
transaction: transaction,
1146+
line: call.line,
1147+
callId: call.callId,
1148+
type: _mediaStatePeerMessageType,
1149+
data: {'video': video},
1150+
),
11421151
)
11431152
?.catchError((Object e) => _logger.info('_sendMediaState: $e'));
11441153
}
@@ -4113,8 +4122,12 @@ class CallBloc extends Bloc<CallEvent, CallState> with WidgetsBindingObserver im
41134122
add(_CallSignalingEvent.updating(line: event.line, callId: event.callId));
41144123
} else if (event is UpdatedEvent) {
41154124
add(_CallSignalingEvent.updated(line: event.line, callId: event.callId));
4116-
} else if (event is MediaStateEvent) {
4117-
add(_CallSignalingEvent.mediaState(line: event.line, callId: event.callId, media: event.media));
4125+
} else if (event is PeerMessageEvent) {
4126+
if (event.type == _mediaStatePeerMessageType) {
4127+
add(_CallSignalingEvent.mediaState(line: event.line, callId: event.callId, media: event.data));
4128+
} else {
4129+
_logger.info('[SIG] PeerMessageEvent: ignoring unknown type "${event.type}"');
4130+
}
41184131
} else if (event is TransferEvent) {
41194132
add(
41204133
_CallSignalingEvent.transfer(

packages/webtrit_signaling/lib/src/events/call/call_events.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ export 'hangingup_event.dart';
77
export 'hangup_event.dart';
88
export 'holding_event.dart';
99
export 'incoming_call_event.dart';
10-
export 'media_state_event.dart';
1110
export 'missed_call_event.dart';
1211
export 'notify_event.dart';
12+
export 'peer_message_event.dart';
1313
export 'proceeding_event.dart';
1414
export 'progress_event.dart';
1515
export 'resuming_event.dart';

packages/webtrit_signaling/lib/src/events/call/media_state_event.dart

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import '../abstract_events.dart';
2+
3+
/// Peer-relayed counterpart of [PeerMessageRequest]. Carries the opaque
4+
/// [type] + [data] envelope from the remote party plus the [sender] number;
5+
/// the client interprets [type] (e.g. `media_state`) and ignores unknown ones.
6+
class PeerMessageEvent extends CallEvent {
7+
const PeerMessageEvent({
8+
super.transaction,
9+
required super.line,
10+
required super.callId,
11+
required this.type,
12+
required this.data,
13+
this.sender,
14+
});
15+
16+
final String type;
17+
final Map<String, dynamic> data;
18+
final String? sender;
19+
20+
@override
21+
List<Object?> get props => [...super.props, type, data, sender];
22+
23+
static const typeValue = 'peer_message';
24+
25+
@override
26+
Map<String, dynamic> toJson() => {
27+
...callBaseJson(typeValue),
28+
'type': type,
29+
'data': data,
30+
if (sender != null) 'sender': sender,
31+
};
32+
33+
factory PeerMessageEvent.fromJson(Map<String, dynamic> json) {
34+
final eventTypeValue = json[Event.typeKey];
35+
if (eventTypeValue != typeValue) {
36+
throw ArgumentError.value(eventTypeValue, Event.typeKey, 'Not equal $typeValue');
37+
}
38+
39+
return PeerMessageEvent(
40+
transaction: json['transaction'],
41+
line: json['line'],
42+
callId: json['call_id'],
43+
type: json['type'],
44+
data: json['data'],
45+
sender: json['sender'],
46+
);
47+
}
48+
}

packages/webtrit_signaling/lib/src/events/call_event.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ abstract class CallEvent extends LineEvent {
3939
HangupEvent.typeValue: HangupEvent.fromJson,
4040
HoldingEvent.typeValue: HoldingEvent.fromJson,
4141
IncomingCallEvent.typeValue: IncomingCallEvent.fromJson,
42-
MediaStateEvent.typeValue: MediaStateEvent.fromJson,
4342
MissedCallEvent.typeValue: MissedCallEvent.fromJson,
4443
NotifyEvent.typeValue: NotifyEvent.fromJson,
44+
PeerMessageEvent.typeValue: PeerMessageEvent.fromJson,
4545
ProceedingEvent.typeValue: ProceedingEvent.fromJson,
4646
ProgressEvent.typeValue: ProgressEvent.fromJson,
4747
ResumingEvent.typeValue: ResumingEvent.fromJson,

packages/webtrit_signaling/lib/src/requests/call/call_requests.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ export 'accept_request.dart';
22
export 'decline_request.dart';
33
export 'hangup_request.dart';
44
export 'hold_request.dart';
5-
export 'media_state_request.dart';
65
export 'outgoing_call_request.dart';
6+
export 'peer_message_request.dart';
77
export 'transfer_request.dart';
88
export 'unhold_request.dart';
99
export 'update_request.dart';

packages/webtrit_signaling/lib/src/requests/call/media_state_request.dart

Lines changed: 0 additions & 36 deletions
This file was deleted.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import '../abstract_requests.dart';
2+
3+
/// Generic in-call app-to-app side channel. The server is a dumb relay: it
4+
/// forwards the opaque [type] + [data] envelope to the peer's sessions without
5+
/// inspecting it. `media_state` (camera state) is the first carried [type];
6+
/// further types (call quality, peer mute, ...) reuse the same request.
7+
class PeerMessageRequest extends CallRequest {
8+
const PeerMessageRequest({
9+
required super.transaction,
10+
required super.line,
11+
required super.callId,
12+
required this.type,
13+
required this.data,
14+
});
15+
16+
final String type;
17+
final Map<String, dynamic> data;
18+
19+
@override
20+
List<Object?> get props => [...super.props, type, data];
21+
22+
static const typeValue = 'peer_message';
23+
24+
factory PeerMessageRequest.fromJson(Map<String, dynamic> json) {
25+
final requestTypeValue = json[Request.typeKey];
26+
if (requestTypeValue != typeValue) {
27+
throw ArgumentError.value(requestTypeValue, Request.typeKey, 'Not equal $typeValue');
28+
}
29+
30+
return PeerMessageRequest(
31+
transaction: json['transaction'],
32+
line: json['line'],
33+
callId: json['call_id'],
34+
type: json['type'],
35+
data: json['data'],
36+
);
37+
}
38+
39+
@override
40+
Map<String, dynamic> toJson() {
41+
return {
42+
Request.typeKey: typeValue,
43+
'transaction': transaction,
44+
'line': line,
45+
'call_id': callId,
46+
'type': type,
47+
'data': data,
48+
};
49+
}
50+
}

packages/webtrit_signaling/lib/src/requests/call_request.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ abstract class CallRequest extends LineRequest {
2929
DeclineRequest.typeValue: DeclineRequest.fromJson,
3030
HangupRequest.typeValue: HangupRequest.fromJson,
3131
HoldRequest.typeValue: HoldRequest.fromJson,
32-
MediaStateRequest.typeValue: MediaStateRequest.fromJson,
3332
OutgoingCallRequest.typeValue: OutgoingCallRequest.fromJson,
33+
PeerMessageRequest.typeValue: PeerMessageRequest.fromJson,
3434
TransferRequest.typeValue: TransferRequest.fromJson,
3535
UnholdRequest.typeValue: UnholdRequest.fromJson,
3636
UpdateRequest.typeValue: UpdateRequest.fromJson,

packages/webtrit_signaling/test/src/events/call/media_state_event_test.dart renamed to packages/webtrit_signaling/test/src/events/call/peer_message_event_test.dart

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,58 +6,62 @@ import 'package:webtrit_signaling/src/events/call/call_events.dart';
66
import 'package:webtrit_signaling/src/events/call_event.dart';
77

88
void main() {
9-
void testFromJson(String description, Map<String, dynamic> actual, MediaStateEvent expected) {
9+
void testFromJson(String description, Map<String, dynamic> actual, PeerMessageEvent expected) {
1010
test(description, () {
11-
expect(MediaStateEvent.fromJson(actual), equals(expected));
11+
expect(PeerMessageEvent.fromJson(actual), equals(expected));
1212
expect(CallEvent.fromJson(actual), equals(expected));
1313
});
1414
}
1515

1616
testFromJson(
17-
'MediaStateEvent: video off with sender',
17+
'PeerMessageEvent: media_state video off with sender',
1818
json.decode(r'''
1919
{
2020
"transaction": "transaction 1",
2121
"line": 0,
2222
"call_id": "qwerty",
23-
"event": "media_state",
24-
"media": {"video": false},
23+
"event": "peer_message",
24+
"type": "media_state",
25+
"data": {"video": false},
2526
"sender": "555001"
2627
}
2728
''')
2829
as Map<String, dynamic>,
29-
const MediaStateEvent(
30+
const PeerMessageEvent(
3031
transaction: 'transaction 1',
3132
line: 0,
3233
callId: 'qwerty',
33-
media: {'video': false},
34+
type: 'media_state',
35+
data: {'video': false},
3436
sender: '555001',
3537
),
3638
);
3739

3840
testFromJson(
39-
'MediaStateEvent: video on without sender and transaction',
41+
'PeerMessageEvent: media_state video on without sender and transaction',
4042
json.decode(r'''
4143
{
4244
"line": 1,
4345
"call_id": "qwerty",
44-
"event": "media_state",
45-
"media": {"video": true}
46+
"event": "peer_message",
47+
"type": "media_state",
48+
"data": {"video": true}
4649
}
4750
''')
4851
as Map<String, dynamic>,
49-
const MediaStateEvent(line: 1, callId: 'qwerty', media: {'video': true}),
52+
const PeerMessageEvent(line: 1, callId: 'qwerty', type: 'media_state', data: {'video': true}),
5053
);
5154

52-
test('MediaStateEvent: toJson round trip', () {
53-
const event = MediaStateEvent(
55+
test('PeerMessageEvent: toJson round trip', () {
56+
const event = PeerMessageEvent(
5457
transaction: 'transaction 1',
5558
line: 0,
5659
callId: 'qwerty',
57-
media: {'video': false},
60+
type: 'media_state',
61+
data: {'video': false},
5862
sender: '555001',
5963
);
6064

61-
expect(MediaStateEvent.fromJson(event.toJson()), equals(event));
65+
expect(PeerMessageEvent.fromJson(event.toJson()), equals(event));
6266
});
6367
}

0 commit comments

Comments
 (0)