feat(voice): harden Android audio and channel joins
This commit is contained in:
@@ -983,6 +983,18 @@ pub enum BridgeEvent {
|
||||
mute: bool,
|
||||
/// Current release-tail in milliseconds (0..=500).
|
||||
release_tail_ms: u32,
|
||||
/// Last confirmed authoritative channel id.
|
||||
current_channel_id: Option<u64>,
|
||||
/// Non-authoritative pending join target channel id.
|
||||
pending_target_channel_id: Option<u64>,
|
||||
/// Whether a new join intent is currently allowed.
|
||||
can_join: bool,
|
||||
/// Whether leave intent is currently allowed.
|
||||
can_leave: bool,
|
||||
/// Join projection sync state.
|
||||
join_sync_state: BridgeVoiceJoinSyncState,
|
||||
/// Last stable join error code, if any.
|
||||
join_error_code: Option<BridgeVoiceJoinErrorCode>,
|
||||
},
|
||||
/// iOS audio interruption state (SDD-101).
|
||||
InterruptionState {
|
||||
@@ -1013,6 +1025,44 @@ pub enum BridgeEvent {
|
||||
},
|
||||
}
|
||||
|
||||
/// Bridge mirror of core join projection sync state.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BridgeVoiceJoinSyncState {
|
||||
/// Reducer is ready to accept channel actions.
|
||||
Ready,
|
||||
/// Reducer is waiting on initial snapshot reconciliation.
|
||||
SynchronizingInitialSnapshot,
|
||||
/// Reducer is waiting on reconnect snapshot reconciliation.
|
||||
SynchronizingReconnect,
|
||||
}
|
||||
|
||||
/// Bridge mirror of stable join error/status codes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BridgeVoiceJoinErrorCode {
|
||||
/// Duplicate same-target join intent was coalesced.
|
||||
DuplicateSameTargetCoalesced,
|
||||
/// A different target was requested while one is already pending.
|
||||
JoinAlreadyPendingDifferentTarget,
|
||||
/// Join denied by server policy/permission.
|
||||
JoinDenied,
|
||||
/// Join failed due to protocol-level error.
|
||||
JoinProtocolFailure,
|
||||
/// Join failed due to transport/network error.
|
||||
JoinNetworkFailure,
|
||||
/// Join timed out awaiting confirmation.
|
||||
JoinTimeout,
|
||||
/// Pending join was superseded by user leave.
|
||||
JoinSupersededByLeave,
|
||||
/// Stale join outcome was ignored.
|
||||
JoinStaleOutcomeIgnored,
|
||||
/// Authoritative membership reconciled to different channel.
|
||||
JoinReconciledDifferentChannel,
|
||||
/// Join command was rejected before send acceptance.
|
||||
JoinCommandRejectedBeforeSend,
|
||||
/// Join intent rejected while reducer synchronizing.
|
||||
JoinCannotStartWhileSynchronizing,
|
||||
}
|
||||
|
||||
/// Schema-controlled mirror of the Kotlin
|
||||
/// `AndroidPermissionRequester.PermissionState` sealed class
|
||||
/// (SDD-106 §5). Crosses the bridge as an enum so the Dart side can
|
||||
@@ -1094,11 +1144,23 @@ impl From<chanora_core::SessionEvent> for BridgeEvent {
|
||||
transmit_mode,
|
||||
mute,
|
||||
release_tail_ms,
|
||||
current_channel_id,
|
||||
pending_target_channel_id,
|
||||
can_join,
|
||||
can_leave,
|
||||
join_sync_state,
|
||||
join_error_code,
|
||||
} => BridgeEvent::VoiceState {
|
||||
in_channel,
|
||||
transmit_mode: transmit_mode_from_u8(transmit_mode),
|
||||
mute,
|
||||
release_tail_ms,
|
||||
current_channel_id,
|
||||
pending_target_channel_id,
|
||||
can_join,
|
||||
can_leave,
|
||||
join_sync_state: map_join_sync_state(join_sync_state),
|
||||
join_error_code: join_error_code.map(map_join_error_code),
|
||||
},
|
||||
chanora_core::SessionEvent::InterruptionState {
|
||||
began,
|
||||
@@ -1111,6 +1173,52 @@ impl From<chanora_core::SessionEvent> for BridgeEvent {
|
||||
}
|
||||
}
|
||||
|
||||
fn map_join_sync_state(state: chanora_core::VoiceJoinSyncState) -> BridgeVoiceJoinSyncState {
|
||||
match state {
|
||||
chanora_core::VoiceJoinSyncState::Ready => BridgeVoiceJoinSyncState::Ready,
|
||||
chanora_core::VoiceJoinSyncState::SynchronizingInitialSnapshot => {
|
||||
BridgeVoiceJoinSyncState::SynchronizingInitialSnapshot
|
||||
}
|
||||
chanora_core::VoiceJoinSyncState::SynchronizingReconnect => {
|
||||
BridgeVoiceJoinSyncState::SynchronizingReconnect
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_join_error_code(code: chanora_core::VoiceJoinErrorCode) -> BridgeVoiceJoinErrorCode {
|
||||
match code {
|
||||
chanora_core::VoiceJoinErrorCode::DuplicateSameTargetCoalesced => {
|
||||
BridgeVoiceJoinErrorCode::DuplicateSameTargetCoalesced
|
||||
}
|
||||
chanora_core::VoiceJoinErrorCode::JoinAlreadyPendingDifferentTarget => {
|
||||
BridgeVoiceJoinErrorCode::JoinAlreadyPendingDifferentTarget
|
||||
}
|
||||
chanora_core::VoiceJoinErrorCode::JoinDenied => BridgeVoiceJoinErrorCode::JoinDenied,
|
||||
chanora_core::VoiceJoinErrorCode::JoinProtocolFailure => {
|
||||
BridgeVoiceJoinErrorCode::JoinProtocolFailure
|
||||
}
|
||||
chanora_core::VoiceJoinErrorCode::JoinNetworkFailure => {
|
||||
BridgeVoiceJoinErrorCode::JoinNetworkFailure
|
||||
}
|
||||
chanora_core::VoiceJoinErrorCode::JoinTimeout => BridgeVoiceJoinErrorCode::JoinTimeout,
|
||||
chanora_core::VoiceJoinErrorCode::JoinSupersededByLeave => {
|
||||
BridgeVoiceJoinErrorCode::JoinSupersededByLeave
|
||||
}
|
||||
chanora_core::VoiceJoinErrorCode::JoinStaleOutcomeIgnored => {
|
||||
BridgeVoiceJoinErrorCode::JoinStaleOutcomeIgnored
|
||||
}
|
||||
chanora_core::VoiceJoinErrorCode::JoinReconciledDifferentChannel => {
|
||||
BridgeVoiceJoinErrorCode::JoinReconciledDifferentChannel
|
||||
}
|
||||
chanora_core::VoiceJoinErrorCode::JoinCommandRejectedBeforeSend => {
|
||||
BridgeVoiceJoinErrorCode::JoinCommandRejectedBeforeSend
|
||||
}
|
||||
chanora_core::VoiceJoinErrorCode::JoinCannotStartWhileSynchronizing => {
|
||||
BridgeVoiceJoinErrorCode::JoinCannotStartWhileSynchronizing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to lifecycle events. Each call yields a fresh
|
||||
/// subscription; multiple subscribers are supported. On slow
|
||||
/// consumers, events are dropped rather than blocking the supervisor
|
||||
|
||||
@@ -1403,11 +1403,25 @@ impl SseDecode for crate::api::BridgeEvent {
|
||||
<crate::api::BridgeTransmitMode>::sse_decode(deserializer);
|
||||
let mut var_mute = <bool>::sse_decode(deserializer);
|
||||
let mut var_releaseTailMs = <u32>::sse_decode(deserializer);
|
||||
let mut var_currentChannelId = <Option<u64>>::sse_decode(deserializer);
|
||||
let mut var_pendingTargetChannelId = <Option<u64>>::sse_decode(deserializer);
|
||||
let mut var_canJoin = <bool>::sse_decode(deserializer);
|
||||
let mut var_canLeave = <bool>::sse_decode(deserializer);
|
||||
let mut var_joinSyncState =
|
||||
<crate::api::BridgeVoiceJoinSyncState>::sse_decode(deserializer);
|
||||
let mut var_joinErrorCode =
|
||||
<Option<crate::api::BridgeVoiceJoinErrorCode>>::sse_decode(deserializer);
|
||||
return crate::api::BridgeEvent::VoiceState {
|
||||
in_channel: var_inChannel,
|
||||
transmit_mode: var_transmitMode,
|
||||
mute: var_mute,
|
||||
release_tail_ms: var_releaseTailMs,
|
||||
current_channel_id: var_currentChannelId,
|
||||
pending_target_channel_id: var_pendingTargetChannelId,
|
||||
can_join: var_canJoin,
|
||||
can_leave: var_canLeave,
|
||||
join_sync_state: var_joinSyncState,
|
||||
join_error_code: var_joinErrorCode,
|
||||
};
|
||||
}
|
||||
9 => {
|
||||
@@ -1494,6 +1508,40 @@ impl SseDecode for crate::api::BridgeTransmitMode {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::api::BridgeVoiceJoinErrorCode {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut inner = <i32>::sse_decode(deserializer);
|
||||
return match inner {
|
||||
0 => crate::api::BridgeVoiceJoinErrorCode::DuplicateSameTargetCoalesced,
|
||||
1 => crate::api::BridgeVoiceJoinErrorCode::JoinAlreadyPendingDifferentTarget,
|
||||
2 => crate::api::BridgeVoiceJoinErrorCode::JoinDenied,
|
||||
3 => crate::api::BridgeVoiceJoinErrorCode::JoinProtocolFailure,
|
||||
4 => crate::api::BridgeVoiceJoinErrorCode::JoinNetworkFailure,
|
||||
5 => crate::api::BridgeVoiceJoinErrorCode::JoinTimeout,
|
||||
6 => crate::api::BridgeVoiceJoinErrorCode::JoinSupersededByLeave,
|
||||
7 => crate::api::BridgeVoiceJoinErrorCode::JoinStaleOutcomeIgnored,
|
||||
8 => crate::api::BridgeVoiceJoinErrorCode::JoinReconciledDifferentChannel,
|
||||
9 => crate::api::BridgeVoiceJoinErrorCode::JoinCommandRejectedBeforeSend,
|
||||
10 => crate::api::BridgeVoiceJoinErrorCode::JoinCannotStartWhileSynchronizing,
|
||||
_ => unreachable!("Invalid variant for BridgeVoiceJoinErrorCode: {}", inner),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::api::BridgeVoiceJoinSyncState {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut inner = <i32>::sse_decode(deserializer);
|
||||
return match inner {
|
||||
0 => crate::api::BridgeVoiceJoinSyncState::Ready,
|
||||
1 => crate::api::BridgeVoiceJoinSyncState::SynchronizingInitialSnapshot,
|
||||
2 => crate::api::BridgeVoiceJoinSyncState::SynchronizingReconnect,
|
||||
_ => unreachable!("Invalid variant for BridgeVoiceJoinSyncState: {}", inner),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for f32 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@@ -1563,6 +1611,30 @@ impl SseDecode for Vec<u8> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for Option<crate::api::BridgeVoiceJoinErrorCode> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
if (<bool>::sse_decode(deserializer)) {
|
||||
return Some(<crate::api::BridgeVoiceJoinErrorCode>::sse_decode(
|
||||
deserializer,
|
||||
));
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for Option<u64> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
if (<bool>::sse_decode(deserializer)) {
|
||||
return Some(<u64>::sse_decode(deserializer));
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::api::PermissionStateKind {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@@ -1841,12 +1913,24 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeEvent {
|
||||
transmit_mode,
|
||||
mute,
|
||||
release_tail_ms,
|
||||
current_channel_id,
|
||||
pending_target_channel_id,
|
||||
can_join,
|
||||
can_leave,
|
||||
join_sync_state,
|
||||
join_error_code,
|
||||
} => [
|
||||
8.into_dart(),
|
||||
in_channel.into_into_dart().into_dart(),
|
||||
transmit_mode.into_into_dart().into_dart(),
|
||||
mute.into_into_dart().into_dart(),
|
||||
release_tail_ms.into_into_dart().into_dart(),
|
||||
current_channel_id.into_into_dart().into_dart(),
|
||||
pending_target_channel_id.into_into_dart().into_dart(),
|
||||
can_join.into_into_dart().into_dart(),
|
||||
can_leave.into_into_dart().into_dart(),
|
||||
join_sync_state.into_into_dart().into_dart(),
|
||||
join_error_code.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart(),
|
||||
crate::api::BridgeEvent::InterruptionState {
|
||||
@@ -1964,6 +2048,58 @@ impl flutter_rust_bridge::IntoIntoDart<crate::api::BridgeTransmitMode>
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::api::BridgeVoiceJoinErrorCode {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
match self {
|
||||
Self::DuplicateSameTargetCoalesced => 0.into_dart(),
|
||||
Self::JoinAlreadyPendingDifferentTarget => 1.into_dart(),
|
||||
Self::JoinDenied => 2.into_dart(),
|
||||
Self::JoinProtocolFailure => 3.into_dart(),
|
||||
Self::JoinNetworkFailure => 4.into_dart(),
|
||||
Self::JoinTimeout => 5.into_dart(),
|
||||
Self::JoinSupersededByLeave => 6.into_dart(),
|
||||
Self::JoinStaleOutcomeIgnored => 7.into_dart(),
|
||||
Self::JoinReconciledDifferentChannel => 8.into_dart(),
|
||||
Self::JoinCommandRejectedBeforeSend => 9.into_dart(),
|
||||
Self::JoinCannotStartWhileSynchronizing => 10.into_dart(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
|
||||
for crate::api::BridgeVoiceJoinErrorCode
|
||||
{
|
||||
}
|
||||
impl flutter_rust_bridge::IntoIntoDart<crate::api::BridgeVoiceJoinErrorCode>
|
||||
for crate::api::BridgeVoiceJoinErrorCode
|
||||
{
|
||||
fn into_into_dart(self) -> crate::api::BridgeVoiceJoinErrorCode {
|
||||
self
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::api::BridgeVoiceJoinSyncState {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
match self {
|
||||
Self::Ready => 0.into_dart(),
|
||||
Self::SynchronizingInitialSnapshot => 1.into_dart(),
|
||||
Self::SynchronizingReconnect => 2.into_dart(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
|
||||
for crate::api::BridgeVoiceJoinSyncState
|
||||
{
|
||||
}
|
||||
impl flutter_rust_bridge::IntoIntoDart<crate::api::BridgeVoiceJoinSyncState>
|
||||
for crate::api::BridgeVoiceJoinSyncState
|
||||
{
|
||||
fn into_into_dart(self) -> crate::api::BridgeVoiceJoinSyncState {
|
||||
self
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::api::PermissionStateKind {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
match self {
|
||||
@@ -2146,12 +2282,27 @@ impl SseEncode for crate::api::BridgeEvent {
|
||||
transmit_mode,
|
||||
mute,
|
||||
release_tail_ms,
|
||||
current_channel_id,
|
||||
pending_target_channel_id,
|
||||
can_join,
|
||||
can_leave,
|
||||
join_sync_state,
|
||||
join_error_code,
|
||||
} => {
|
||||
<i32>::sse_encode(8, serializer);
|
||||
<bool>::sse_encode(in_channel, serializer);
|
||||
<crate::api::BridgeTransmitMode>::sse_encode(transmit_mode, serializer);
|
||||
<bool>::sse_encode(mute, serializer);
|
||||
<u32>::sse_encode(release_tail_ms, serializer);
|
||||
<Option<u64>>::sse_encode(current_channel_id, serializer);
|
||||
<Option<u64>>::sse_encode(pending_target_channel_id, serializer);
|
||||
<bool>::sse_encode(can_join, serializer);
|
||||
<bool>::sse_encode(can_leave, serializer);
|
||||
<crate::api::BridgeVoiceJoinSyncState>::sse_encode(join_sync_state, serializer);
|
||||
<Option<crate::api::BridgeVoiceJoinErrorCode>>::sse_encode(
|
||||
join_error_code,
|
||||
serializer,
|
||||
);
|
||||
}
|
||||
crate::api::BridgeEvent::InterruptionState {
|
||||
began,
|
||||
@@ -2237,6 +2388,48 @@ impl SseEncode for crate::api::BridgeTransmitMode {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::api::BridgeVoiceJoinErrorCode {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<i32>::sse_encode(
|
||||
match self {
|
||||
crate::api::BridgeVoiceJoinErrorCode::DuplicateSameTargetCoalesced => 0,
|
||||
crate::api::BridgeVoiceJoinErrorCode::JoinAlreadyPendingDifferentTarget => 1,
|
||||
crate::api::BridgeVoiceJoinErrorCode::JoinDenied => 2,
|
||||
crate::api::BridgeVoiceJoinErrorCode::JoinProtocolFailure => 3,
|
||||
crate::api::BridgeVoiceJoinErrorCode::JoinNetworkFailure => 4,
|
||||
crate::api::BridgeVoiceJoinErrorCode::JoinTimeout => 5,
|
||||
crate::api::BridgeVoiceJoinErrorCode::JoinSupersededByLeave => 6,
|
||||
crate::api::BridgeVoiceJoinErrorCode::JoinStaleOutcomeIgnored => 7,
|
||||
crate::api::BridgeVoiceJoinErrorCode::JoinReconciledDifferentChannel => 8,
|
||||
crate::api::BridgeVoiceJoinErrorCode::JoinCommandRejectedBeforeSend => 9,
|
||||
crate::api::BridgeVoiceJoinErrorCode::JoinCannotStartWhileSynchronizing => 10,
|
||||
_ => {
|
||||
unimplemented!("");
|
||||
}
|
||||
},
|
||||
serializer,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::api::BridgeVoiceJoinSyncState {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<i32>::sse_encode(
|
||||
match self {
|
||||
crate::api::BridgeVoiceJoinSyncState::Ready => 0,
|
||||
crate::api::BridgeVoiceJoinSyncState::SynchronizingInitialSnapshot => 1,
|
||||
crate::api::BridgeVoiceJoinSyncState::SynchronizingReconnect => 2,
|
||||
_ => {
|
||||
unimplemented!("");
|
||||
}
|
||||
},
|
||||
serializer,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for f32 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
@@ -2298,6 +2491,26 @@ impl SseEncode for Vec<u8> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for Option<crate::api::BridgeVoiceJoinErrorCode> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<bool>::sse_encode(self.is_some(), serializer);
|
||||
if let Some(value) = self {
|
||||
<crate::api::BridgeVoiceJoinErrorCode>::sse_encode(value, serializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for Option<u64> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<bool>::sse_encode(self.is_some(), serializer);
|
||||
if let Some(value) = self {
|
||||
<u64>::sse_encode(value, serializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::api::PermissionStateKind {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
|
||||
Reference in New Issue
Block a user