feat(voice): add opt-in peer-to-peer calls and country rollouts (#3367)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-10 15:45:17 +02:00
1 parent e5a568acca
commit bef5e33eec
215 files changed
+15840 -2693

No files matched your search

+697
View File
@@ -0,0 +1,697 @@
%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(call_p2p_tests).
-typing([eqwalizer]).
-include_lib("eunit/include/eunit.hrl").
agreed_joiners_form_a_mesh_without_pending_connections_test() ->
with_stubs(fun() ->
State = join_all([{1, true}, {2, true}], call_state()),
?assertEqual([true, true], p2p_flags(State)),
?assertEqual(#{}, maps:get(pending_connections, State)),
?assertEqual([], drain_voice_server_updates())
end).
sfu_join_keeps_its_pending_connection_test() ->
with_stubs(fun() ->
State = join_all([{1, false}], call_state()),
?assert(maps:is_key(connection_id(1), maps:get(pending_connections, State)))
end).
race_sfu_join_after_p2p_start_is_rejected_test() ->
with_stubs(fun() ->
State = join_all([{1, true}], call_state()),
?assertEqual(
{reply, {error, voice_p2p_consent_required}, State}, join(2, false, State)
),
?assertEqual([], drain_voice_server_updates())
end).
race_p2p_join_after_sfu_start_is_rejected_test() ->
with_stubs(fun() ->
State = join_all([{2, false}], call_state()),
?assertEqual(
{reply, {error, voice_p2p_consent_required}, State}, join(1, true, State)
),
?assertEqual([], drain_voice_server_updates())
end).
race_async_join_producing_mixed_states_is_rejected_test() ->
with_stubs(fun() ->
State = join_all([{1, true}], call_state()),
?assertEqual(
{noreply, State},
call_voice:handle_join_async(
2, voice_state(2, false), session_id(2), erlang:get(call_p2p_sink), State
)
),
receive
{call_join_result, 1, Result} ->
?assertEqual({error, voice_p2p_consent_required}, Result)
after 1000 ->
error(call_join_result_not_sent)
end,
?assertEqual([], drain_voice_server_updates())
end).
fifth_joiner_over_the_cap_is_rejected_test() ->
with_stubs(fun() ->
Mesh = join_all([{UserId, true} || UserId <- [1, 2, 3, 4]], call_state()),
?assertEqual([true, true, true, true], p2p_flags(Mesh)),
?assertEqual({reply, {error, voice_channel_full}, Mesh}, join(5, true, Mesh)),
?assertEqual([], drain_voice_server_updates())
end).
race_join_past_the_configured_cap_is_rejected_test() ->
with_stubs(fun() ->
Mesh = join_all([{1, true}, {2, true}], call_state()),
{reply, ok, Full} = call:handle_call(capped_join(3, 3), from(), Mesh),
?assertEqual([true, true, true], p2p_flags(Full)),
?assertEqual(
{reply, {error, voice_channel_full}, Full},
call:handle_call(capped_join(4, 3), from(), Full)
),
?assertNot(maps:is_key(4, maps:get(voice_states, Full))),
?assertEqual(#{}, maps:get(pending_connections, Full)),
?assertEqual([], drain_voice_server_updates())
end).
join_without_a_cap_falls_back_to_the_hard_ceiling_test() ->
with_stubs(fun() ->
Mesh = join_all([{UserId, true} || UserId <- [1, 2, 3]], call_state()),
{reply, ok, Full} = call:handle_call(
{join, 4, voice_state(4, true), session_id(4), erlang:get(call_p2p_sink),
connection_id(4)},
from(),
Mesh
),
?assertEqual([true, true, true, true], p2p_flags(Full))
end).
session_p2p_grant_passes_the_cap_to_the_call_join_test() ->
with_stubs(fun() ->
meck:new(gateway_rpc_call_lookup, [passthrough, no_link]),
try
{reply, #{success := true}, _State} = dm_join(true, [voice_state(20, true)]),
?assertMatch(
[{join, 10, #{<<"p2p">> := true}, <<"sess">>, _Pid, _ConnectionId, 3}],
[
Request
|| {_Caller, {gateway_rpc_call_lookup, safe_gen_server_call, [_, Request, _]},
_Result} <- meck:history(gateway_rpc_call_lookup),
element(1, Request) =:= join
]
)
after
meck:unload(gateway_rpc_call_lookup)
end
end).
rejected_join_rolls_back_without_retrying_test() ->
with_stubs(fun() ->
erlang:put(call_p2p_join_reply, {error, voice_p2p_consent_required}),
ok = dm_voice_token:join_or_create_call(
100, 10, voice_state(10, false), <<"sess">>, self(), undefined
),
?assertEqual(
[
{voice_rejected, voice_p2p_consent_required},
{call_force_disconnect, 100, connection_id(10)}
],
drain_session_casts()
),
?assertEqual(1, meck:num_calls(call_manager, lookup, '_'))
end).
explicit_p2p_false_update_converts_test() ->
with_stubs(fun() ->
State0 = join_all([{1, true}, {2, true}], call_state()),
{reply, ok, State1} = call:handle_call(
{update_voice_state, 1, voice_state(1, false)}, from(), State0
),
?assertEqual([false, false], p2p_flags(State1)),
?assertMatch(#{<<"p2p">> := false}, receive_voice_state_update(connection_id(2))),
?assertEqual(
[connection_id(1), connection_id(2)], lists:sort(drain_voice_server_updates())
)
end).
update_keeping_p2p_keeps_the_mesh_test() ->
with_stubs(fun() ->
State0 = join_all([{1, true}, {2, true}], call_state()),
{reply, ok, State1} = call:handle_call(
{update_voice_state, 1, (voice_state(1, true))#{<<"self_mute">> => true}},
from(),
State0
),
?assertEqual([true, true], p2p_flags(State1)),
?assertEqual([], drain_voice_server_updates())
end).
stale_p2p_update_after_conversion_stays_sfu_test() ->
with_stubs(fun() ->
Mesh = join_all([{1, true}, {2, true}], call_state()),
{reply, ok, State0} = call:handle_call(
{update_voice_state, 2, voice_state(2, false)}, from(), Mesh
),
?assertEqual(2, length(drain_voice_server_updates())),
ok = flush_user_dispatches(),
{reply, ok, State1} = call:handle_call(
{update_voice_state, 1, voice_state(1, true)}, from(), State0
),
?assertEqual([false, false], p2p_flags(State1)),
?assertMatch(#{<<"p2p">> := false}, receive_voice_state_update(connection_id(1))),
?assertEqual([], drain_voice_server_updates())
end).
same_region_update_converts_a_mesh_test() ->
with_stubs(fun() ->
State0 = join_all([{1, true}, {2, true}], (call_state())#{region => <<"us-east">>}),
ok = flush_user_dispatches(),
{reply, ok, State1} = call:handle_call({update_region, <<"us-east">>}, from(), State0),
?assertEqual([false, false], p2p_flags(State1)),
?assertEqual(
[connection_id(1), connection_id(2)], lists:sort(drain_voice_server_updates())
),
?assertEqual(
[<<"us-east">>, <<"us-east">>],
[maps:get(<<"rtc_region">>, Request) || Request <- token_requests()]
),
?assertMatch(
#{voice_states := [#{<<"p2p">> := false}, #{<<"p2p">> := false}]},
receive_user_dispatch(call_update)
)
end).
same_region_update_leaves_an_sfu_call_alone_test() ->
with_stubs(fun() ->
State0 = join_all([{1, false}, {2, false}], (call_state())#{region => <<"us-east">>}),
{reply, ok, _State1} = call:handle_call({update_region, <<"us-east">>}, from(), State0),
?assertEqual([], drain_voice_server_updates())
end).
call_event_and_handoff_keep_the_p2p_flag_test() ->
with_stubs(fun() ->
State = join_all([{1, true}], call_state()),
?assertMatch(
#{voice_states := [#{<<"p2p">> := true}]}, call_state:build_call_event(State)
),
Restored = call_handoff:restore_state(call_handoff:export_state(State)),
?assertEqual([true], p2p_flags(Restored))
end).
signal_relay_allowed_test() ->
with_stubs(fun() ->
State = join_all([{1, true}, {2, true}], call_state()),
Data = #{<<"type">> => <<"answer">>, <<"sdp">> => <<"v=0">>},
{noreply, State} = call:handle_cast(
{voice_signal, session_id(1), connection_id(2), Data}, State
),
?assertEqual(
#{
<<"channel_id">> => <<"1">>,
<<"from">> => connection_id(1),
<<"user_id">> => <<"1">>,
<<"data">> => Data
},
receive_voice_signal()
)
end).
signal_relay_denied_test() ->
with_stubs(fun() ->
Mesh = join_all([{1, true}, {2, true}], call_state()),
Data = #{<<"type">> => <<"candidate">>},
Denied = [
{voice_signal, session_id(9), connection_id(2), Data},
{voice_signal, session_id(1), connection_id(1), Data},
{voice_signal, session_id(1), <<"unknown">>, Data}
],
[{noreply, Mesh} = call:handle_cast(Signal, Mesh) || Signal <- Denied],
{reply, ok, Sfu} = call:handle_call({update_region, <<"us-east">>}, from(), Mesh),
{noreply, Sfu} = call:handle_cast(
{voice_signal, session_id(1), connection_id(2), Data}, Sfu
),
assert_no_voice_signal()
end).
session_p2p_start_without_a_call_test() ->
with_stubs(fun() ->
{reply, #{success := true, connection_id := ConnectionId}, State} = dm_join(
true, not_found
),
?assertMatch(
#{
<<"p2p">> := true,
<<"p2p_initiator">> := true,
<<"p2p_participant_count">> := 1,
<<"country_code">> := <<"SE">>
},
lists:last(token_requests())
),
?assertEqual(
true, maps:get(<<"p2p">>, maps:get(ConnectionId, maps:get(dm_voice_states, State)))
),
Update = receive_session_voice_server_update(),
?assertEqual(
#{
<<"p2p">> => true,
<<"ice_servers">> => [#{<<"urls">> => [<<"stun:stun.example:3478">>]}],
<<"connection_id">> => ConnectionId,
<<"channel_id">> => <<"100">>
},
Update
)
end).
session_p2p_join_of_an_existing_mesh_test() ->
with_stubs(fun() ->
{reply, #{success := true}, _State} = dm_join(true, [voice_state(20, true)]),
?assertMatch(
#{
<<"p2p">> := true,
<<"p2p_initiator">> := false,
<<"p2p_participant_count">> := 2
},
lists:last(token_requests())
),
?assertMatch(#{<<"p2p">> := true}, receive_session_voice_server_update())
end).
session_join_from_a_second_device_counts_the_user_once_test() ->
with_stubs(fun() ->
{reply, #{success := true}, _State} = dm_join(
true, [voice_state(20, true), voice_state(10, true)]
),
?assertMatch(
#{
<<"p2p">> := true,
<<"p2p_initiator">> := false,
<<"p2p_participant_count">> := 2
},
lists:last(token_requests())
)
end).
session_join_of_an_sfu_call_does_not_ask_for_p2p_test() ->
with_stubs(fun() ->
{reply, #{success := true, connection_id := ConnectionId}, State} = dm_join(
true, [voice_state(20, false)]
),
?assertNot(maps:is_key(<<"p2p">>, lists:last(token_requests()))),
?assertEqual(
false, maps:get(<<"p2p">>, maps:get(ConnectionId, maps:get(dm_voice_states, State)))
),
Update = receive_session_voice_server_update(),
?assertEqual(<<"tok">>, maps:get(<<"token">>, Update)),
?assertNot(maps:is_key(<<"p2p">>, Update))
end).
session_join_of_a_mesh_without_agreement_is_rejected_test() ->
with_stubs(fun() ->
?assertMatch(
{reply, {error, permission_denied, voice_p2p_consent_required}, #{
dm_voice_states := #{}
}},
dm_join_reply(undefined, [voice_state(20, true)])
),
?assertEqual([], token_requests()),
assert_no_session_voice_server_update()
end).
session_bot_join_of_a_mesh_is_rejected_test() ->
with_stubs(fun() ->
erlang:put(call_p2p_call_voice_states, [voice_state(20, true)]),
State = dm_session_state(#{}),
?assertMatch(
{reply, {error, permission_denied, voice_p2p_consent_required}, _State},
dm_voice_token:get_dm_voice_token_and_create_state(
(dm_request(true, State))#{bot => true}
)
)
end).
session_join_of_a_full_mesh_is_rejected_test() ->
with_stubs(fun() ->
Mesh = [voice_state(UserId, true) || UserId <- [20, 21, 22, 23]],
?assertMatch(
{reply, {error, permission_denied, voice_channel_full}, _State},
dm_join_reply(true, Mesh)
),
?assertEqual([], token_requests())
end).
session_join_past_the_configured_cap_is_rejected_as_full_test() ->
with_stubs(fun() ->
erlang:put(call_p2p_stub_mode, channel_full),
?assertMatch(
{reply, {error, permission_denied, voice_channel_full}, #{dm_voice_states := #{}}},
dm_join_reply(true, [voice_state(20, true), voice_state(21, true)])
),
?assertMatch(
#{
<<"p2p">> := true,
<<"p2p_initiator">> := false,
<<"p2p_participant_count">> := 3
},
lists:last(token_requests())
),
assert_no_session_voice_server_update()
end).
session_declined_grant_into_a_mesh_is_rejected_test() ->
with_stubs(fun() ->
erlang:put(call_p2p_stub_mode, decline),
?assertMatch(
{reply, {error, voice_error, voice_p2p_unavailable}, #{dm_voice_states := #{}}},
dm_join_reply(true, [voice_state(20, true)])
),
?assertMatch(
#{<<"p2p">> := true, <<"p2p_initiator">> := false}, lists:last(token_requests())
),
assert_no_session_voice_server_update()
end).
session_declined_grant_without_a_call_joins_sfu_test() ->
with_stubs(fun() ->
erlang:put(call_p2p_stub_mode, decline),
{reply, #{success := true, connection_id := ConnectionId}, State} = dm_join(
true, not_found
),
?assertEqual(
false, maps:get(<<"p2p">>, maps:get(ConnectionId, maps:get(dm_voice_states, State)))
),
?assertEqual(<<"tok">>, maps:get(<<"token">>, receive_session_voice_server_update()))
end).
session_update_with_explicit_p2p_false_clears_the_flag_test() ->
with_stubs(fun() ->
?assertEqual(false, dm_updated_p2p(false)),
?assertEqual(true, dm_updated_p2p(undefined)),
?assertEqual(true, dm_updated_p2p(true))
end).
dm_updated_p2p(P2p) ->
Existing = (voice_state(10, true))#{
<<"channel_id">> => <<"100">>, <<"session_id">> => <<"sess">>
},
ConnectionId = connection_id(10),
State = dm_session_state(#{ConnectionId => Existing}),
{reply, #{success := true}, NewState} = dm_voice_connect:handle_dm_connect_or_update(
(dm_request(P2p, State))#{
connection_id => ConnectionId, viewer_stream_keys => undefined
}
),
maps:get(<<"p2p">>, maps:get(ConnectionId, maps:get(dm_voice_states, NewState))).
dm_join_reply(P2p, CallVoiceStates) ->
erlang:put(call_p2p_call_voice_states, CallVoiceStates),
State = dm_session_state(#{}),
dm_voice_token:get_dm_voice_token_and_create_state(dm_request(P2p, State)).
dm_join(P2p, CallVoiceStates) ->
Reply = dm_join_reply(P2p, CallVoiceStates),
receive
{'$gen_cast', {call_monitor, 100, _CallPid}} -> Reply
after 1000 ->
error(call_not_joined)
end.
dm_session_state(VoiceStates) ->
#{
dm_voice_states => VoiceStates,
channels => #{100 => #{<<"type">> => 1, <<"recipient_ids">> => [20]}},
user_id => 10,
id => <<"sess">>,
session_pid => erlang:get(call_p2p_sink)
}.
dm_request(P2p, State) ->
#{
user_id => 10,
channel_id => 100,
session_id => <<"sess">>,
connection_id => null,
self_mute => false,
self_deaf => false,
self_video => false,
self_stream => false,
viewer_stream_keys => [],
is_mobile => false,
latitude => null,
longitude => null,
e2ee_capable => false,
bot => false,
p2p => P2p,
country_code => <<"SE">>,
voice_states => maps:get(dm_voice_states, State),
state => State
}.
join_all(Joins, State0) ->
lists:foldl(
fun({UserId, P2p}, State) ->
{reply, ok, NewState} = join(UserId, P2p, State),
NewState
end,
State0,
Joins
).
join(UserId, P2p, State) ->
call_voice:handle_join_internal(
UserId,
voice_state(UserId, P2p),
session_id(UserId),
erlang:get(call_p2p_sink),
connection_id(UserId),
undefined,
State
).
capped_join(UserId, MaxParticipants) ->
{join, UserId, voice_state(UserId, true), session_id(UserId), erlang:get(call_p2p_sink),
connection_id(UserId), MaxParticipants}.
p2p_flags(State) ->
[
maps:get(<<"p2p">>, VoiceState)
|| {_UserId, VoiceState} <- lists:keysort(1, maps:to_list(maps:get(voice_states, State)))
].
voice_state(UserId, P2p) ->
#{
<<"user_id">> => integer_to_binary(UserId),
<<"channel_id">> => <<"1">>,
<<"connection_id">> => connection_id(UserId),
<<"session_id">> => session_id(UserId),
<<"p2p">> => P2p
}.
connection_id(UserId) ->
<<"conn-", (integer_to_binary(UserId))/binary>>.
session_id(UserId) ->
<<"sess-", (integer_to_binary(UserId))/binary>>.
from() ->
{self(), make_ref()}.
call_state() ->
#{
channel_id => 1,
message_id => 1,
region => undefined,
ringing => [],
pending_ringing => [],
recipients => [1],
voice_states => #{},
sessions => #{},
pending_connections => #{},
initiator_ready => true,
ringing_timers => #{},
idle_timer => undefined,
created_at => 0,
participants_history => sets:new(),
last_call_event => undefined
}.
token_requests() ->
[
Request
|| {_Pid, {rpc_client, call, [#{<<"type">> := <<"voice_get_token">>} = Request]}, _Result} <-
meck:history(rpc_client)
].
drain_voice_server_updates() ->
receive
{'$gen_cast', {dispatch, voice_server_update, Payload}} ->
?assertEqual(<<"tok">>, maps:get(<<"token">>, Payload)),
?assertNot(maps:is_key(<<"p2p">>, Payload)),
[maps:get(<<"connection_id">>, Payload) | drain_voice_server_updates()]
after 200 ->
[]
end.
receive_session_voice_server_update() ->
receive
{'$gen_cast', {dispatch, voice_server_update, Payload}} -> Payload
after 1000 ->
error(voice_server_update_not_dispatched)
end.
assert_no_session_voice_server_update() ->
receive
{'$gen_cast', {dispatch, voice_server_update, Payload}} ->
error({unexpected_voice_server_update, Payload})
after 100 ->
ok
end.
drain_session_casts() ->
receive
{'$gen_cast', Cast} -> [Cast | drain_session_casts()]
after 100 ->
[]
end.
receive_voice_state_update(ConnectionId) ->
case receive_user_dispatch(voice_state_update) of
#{<<"connection_id">> := ConnectionId} = VoiceState -> VoiceState;
_Other -> receive_voice_state_update(ConnectionId)
end.
receive_user_dispatch(Event) ->
receive
{user_dispatch, Event, Data} -> Data
after 1000 ->
error({user_dispatch_not_received, Event})
end.
flush_user_dispatches() ->
receive
{user_dispatch, _Event, _Data} -> flush_user_dispatches()
after 100 ->
ok
end.
receive_voice_signal() ->
receive
{'$gen_cast', {dispatch, voice_signal, Payload}} -> Payload
after 1000 ->
error(voice_signal_not_dispatched)
end.
assert_no_voice_signal() ->
receive
{'$gen_cast', {dispatch, voice_signal, Payload}} ->
error({unexpected_voice_signal, Payload})
after 100 ->
ok
end.
with_stubs(Fun) ->
Test = self(),
Sink = spawn(fun() -> sink(Test) end),
erlang:put(call_p2p_sink, Sink),
erlang:erase(call_p2p_call_voice_states),
erlang:erase(call_p2p_stub_mode),
erlang:erase(call_p2p_join_reply),
meck:new(rpc_client, [passthrough, no_link, non_strict]),
meck:expect(rpc_client, call, fun rpc_reply/1),
meck:new(presence_manager, [passthrough, no_link]),
meck:expect(presence_manager, dispatch_to_user, fun(_UserId, Event, Data) ->
Sink ! {user_dispatch, Event, Data},
ok
end),
CallPid = spawn(fun call_stub/0),
meck:new(call_manager, [passthrough, no_link]),
meck:expect(call_manager, lookup, fun(_ChannelId) ->
case erlang:get(call_p2p_call_voice_states) of
not_found ->
{error, not_found};
VoiceStates when is_list(VoiceStates) ->
CallPid ! {voice_states, VoiceStates},
{ok, CallPid};
undefined ->
CallPid ! {join_reply, erlang:get(call_p2p_join_reply)},
{ok, CallPid}
end
end),
try
Fun()
after
CallPid ! stop,
meck:unload(call_manager),
meck:unload(presence_manager),
meck:unload(rpc_client),
stop_sink(Sink)
end.
sink(Test) ->
receive
Message ->
Test ! Message,
sink(Test)
after 30000 ->
ok
end.
stop_sink(Sink) ->
exit(Sink, kill),
flush_mailbox().
flush_mailbox() ->
receive
_Message -> flush_mailbox()
after 0 ->
ok
end.
call_stub() ->
call_stub([], ok).
call_stub(VoiceStates, JoinReply) ->
receive
{voice_states, NewVoiceStates} ->
call_stub(NewVoiceStates, JoinReply);
{join_reply, undefined} ->
call_stub(VoiceStates, JoinReply);
{join_reply, NewJoinReply} ->
call_stub(VoiceStates, NewJoinReply);
{'$gen_call', From, {get_state}} ->
gen_server:reply(From, {ok, #{voice_states => VoiceStates, region => undefined}}),
call_stub(VoiceStates, JoinReply);
{'$gen_call', From, _Request} ->
gen_server:reply(From, JoinReply),
call_stub(VoiceStates, JoinReply);
stop ->
ok;
_Other ->
call_stub(VoiceStates, JoinReply)
after 30000 ->
ok
end.
rpc_reply(#{<<"type">> := <<"voice_get_token">>} = Request) ->
ConnectionId = maps:get(<<"connection_id">>, Request, <<"conn-new">>),
case {maps:get(<<"p2p">>, Request, false), erlang:get(call_p2p_stub_mode)} of
{true, channel_full} ->
{ok, #{<<"p2p">> => false, <<"declineReason">> => <<"channel_full">>}};
{true, Mode} when Mode =/= decline ->
{ok, #{
<<"p2p">> => true,
<<"connectionId">> => ConnectionId,
<<"iceServers">> => [#{<<"urls">> => [<<"stun:stun.example:3478">>]}],
<<"maxParticipants">> => 3
}};
_ ->
{ok, #{
<<"token">> => <<"tok">>,
<<"endpoint">> => <<"wss://voice.example">>,
<<"connectionId">> => ConnectionId
}}
end;
rpc_reply(_Request) ->
{ok, #{}}.