o_sfu_protocol/core/
connection_lifecycle.rs1use super::{
13 Command, Commands, ConnectContext, ConnectionState, INITIAL_RECOVERY_DELAY_MS, ProtocolCore,
14 RECOVERY_TIMER_ID, empty_features, next_recovery_delay,
15};
16use crate::{shared::RecordingState, signaling::WebSocketCloseCode};
17
18#[derive(Clone, Copy)]
23enum LifecycleCloseCause {
24 AuthFailed,
26 Kicked,
28 RoomFull,
30}
31
32fn terminal_close_cause(close_code: WebSocketCloseCode) -> Option<LifecycleCloseCause> {
34 match close_code {
35 WebSocketCloseCode::AuthFailed => Some(LifecycleCloseCause::AuthFailed),
36 WebSocketCloseCode::Kicked => Some(LifecycleCloseCause::Kicked),
37 WebSocketCloseCode::RoomFull => Some(LifecycleCloseCause::RoomFull),
38 _ => None,
39 }
40}
41
42fn lifecycle_close_cause_label(cause: LifecycleCloseCause) -> &'static str {
44 match cause {
45 LifecycleCloseCause::AuthFailed => "auth_failed",
46 LifecycleCloseCause::Kicked => "kicked",
47 LifecycleCloseCause::RoomFull => "full",
48 }
49}
50
51fn reset_public_state(commands: &mut Commands) {
52 commands.extend([
53 Command::SetAvailableFeatures {
54 features: empty_features(),
55 },
56 Command::SetRecordingState {
57 state: RecordingState::default(),
58 },
59 ]);
60}
61
62fn state_change(state: ConnectionState, cause: Option<LifecycleCloseCause>) -> Command {
63 Command::EmitStateChange {
64 state,
65 cause: cause.map(lifecycle_close_cause_label).map(str::to_owned),
66 }
67}
68
69pub(super) fn connect(
87 core: &mut ProtocolCore,
88 url: String,
89 jwt: String,
90 room: Option<String>,
91) -> Commands {
92 let mut commands = match core.state() {
93 ConnectionState::Disconnected | ConnectionState::Closed => Vec::new(),
94 ConnectionState::Recovering => vec![Command::CancelTimer {
95 id: RECOVERY_TIMER_ID,
96 }],
97 _ => return Vec::new(),
98 };
99 let connect_url = url.clone();
100 core.connect_context = Some(ConnectContext { url, jwt, room });
101 core.recovery_delay_ms = INITIAL_RECOVERY_DELAY_MS;
102 core.phase
103 .apply_lifecycle_state(ConnectionState::Connecting);
104 core.clear_runtime_state();
105 core.clear_sticky_state();
106 reset_public_state(&mut commands);
107 commands.push(state_change(core.state(), None));
108 commands.push(Command::Connect { url: connect_url });
109 commands
110}
111
112pub(super) fn disconnect(core: &mut ProtocolCore) -> Commands {
120 if matches!(
121 core.state(),
122 ConnectionState::Disconnected | ConnectionState::Closed
123 ) {
124 return Vec::new();
125 }
126 core.phase
127 .apply_lifecycle_state(ConnectionState::Disconnected);
128 core.connect_context = None;
129 core.recovery_delay_ms = INITIAL_RECOVERY_DELAY_MS;
130 let mut commands = vec![Command::CancelTimer {
131 id: RECOVERY_TIMER_ID,
132 }];
133 commands.extend(core.teardown_runtime_state());
134 core.clear_sticky_state();
135 commands.push(Command::CloseWebSocket {
136 code: u16::from(WebSocketCloseCode::Clean),
137 });
138 commands.push(Command::ClosePeerConnection);
139 reset_public_state(&mut commands);
140 commands.push(state_change(core.state(), None));
141 commands
142}
143
144pub(super) fn on_ws_close(core: &mut ProtocolCore, close_code: u16) -> Commands {
163 if matches!(
164 core.state(),
165 ConnectionState::Disconnected | ConnectionState::Closed
166 ) {
167 return Vec::new();
168 }
169
170 if let Some(
171 terminal_code @ (WebSocketCloseCode::ProtocolError
172 | WebSocketCloseCode::AuthFailed
173 | WebSocketCloseCode::Kicked
174 | WebSocketCloseCode::RoomFull),
175 ) = WebSocketCloseCode::from_u16(close_code)
176 {
177 core.phase.apply_lifecycle_state(ConnectionState::Closed);
178 core.connect_context = None;
179 core.recovery_delay_ms = INITIAL_RECOVERY_DELAY_MS;
180 let mut commands = core.teardown_runtime_state();
181 commands.push(Command::CancelTimer {
182 id: RECOVERY_TIMER_ID,
183 });
184 commands.push(Command::ClosePeerConnection);
185 reset_public_state(&mut commands);
186 commands.push(state_change(
187 core.state(),
188 terminal_close_cause(terminal_code),
189 ));
190 return commands;
191 }
192
193 if core.connect_context.is_none() {
194 core.phase
195 .apply_lifecycle_state(ConnectionState::Disconnected);
196 let mut commands = core.teardown_runtime_state();
197 reset_public_state(&mut commands);
198 commands.push(state_change(core.state(), None));
199 return commands;
200 }
201
202 let scheduled_delay_ms = core.recovery_delay_ms;
203 core.recovery_delay_ms = next_recovery_delay(scheduled_delay_ms);
204 core.phase
205 .apply_lifecycle_state(ConnectionState::Recovering);
206 let mut commands = core.teardown_runtime_state();
207 commands.push(Command::ClosePeerConnection);
208 commands.push(state_change(core.state(), None));
209 commands.push(Command::ScheduleTimer {
210 id: RECOVERY_TIMER_ID,
211 ms: scheduled_delay_ms,
212 });
213 commands
214}
215
216pub(super) fn handle_recovery_timer(core: &mut ProtocolCore) -> Commands {
232 if core.state() != ConnectionState::Recovering {
233 return Vec::new();
234 }
235 let Some(connect_context) = core.connect_context.as_ref() else {
236 return Vec::new();
237 };
238 let connect_url = connect_context.url.clone();
239 core.phase
240 .apply_lifecycle_state(ConnectionState::Connecting);
241 let mut commands = vec![state_change(core.state(), None)];
242 commands.push(Command::Connect { url: connect_url });
243 commands
244}