Driver: Automate (re)connection logic (#81)
This PR adds several enhancements to Driver connection logic: * Driver (re)connection attempts now have a default timeout of around 10s. * The driver will now attempt to retry full connection attempts using a user-provided strategy: currently, this defaults to 5 attempts under an exponential backoff strategy. * The driver will now fire `DriverDisconnect` events at the end of any session -- this unifies (re)connection failure events with session expiry as seen in #76, which should provide users with enough detail to know *which* voice channel to reconnect to. Users still need to be careful to read the session/channel IDs to ensure that they aren't overwriting another join. This has been tested using `cargo make ready`, and by setting low timeouts to force failures in the voice receive example (with some additional error handlers). Closes #68.
This commit is contained in:
@@ -1,7 +1,18 @@
|
||||
use crate::id::*;
|
||||
|
||||
/// Voice connection details gathered at setup/reinstantiation.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub struct ConnectData<'a> {
|
||||
/// ID of the voice channel being joined, if it is known.
|
||||
///
|
||||
/// If this is available, then this can be used to reconnect/renew
|
||||
/// a voice session via thew gateway.
|
||||
pub channel_id: Option<ChannelId>,
|
||||
/// ID of the target voice channel's parent guild.
|
||||
pub guild_id: GuildId,
|
||||
/// Unique string describing this session for validation/authentication purposes.
|
||||
pub session_id: &'a str,
|
||||
/// The domain name of Discord's voice/TURN server.
|
||||
///
|
||||
/// With the introduction of Discord's automatic voice server selection,
|
||||
|
||||
119
src/events/context/data/disconnect.rs
Normal file
119
src/events/context/data/disconnect.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use crate::{
|
||||
error::ConnectionError,
|
||||
id::*,
|
||||
model::{CloseCode as VoiceCloseCode, FromPrimitive},
|
||||
ws::Error as WsError,
|
||||
};
|
||||
#[cfg(not(feature = "tokio-02-marker"))]
|
||||
use async_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
|
||||
#[cfg(feature = "tokio-02-marker")]
|
||||
use async_tungstenite_compat::tungstenite::protocol::frame::coding::CloseCode;
|
||||
|
||||
/// Voice connection details gathered at termination or failure.
|
||||
///
|
||||
/// In the event of a failure, this event data is gathered after
|
||||
/// a reconnection strategy has exhausted all of its attempts.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct DisconnectData<'a> {
|
||||
/// The location that a voice connection was terminated.
|
||||
pub kind: DisconnectKind,
|
||||
/// The cause of any connection failure.
|
||||
///
|
||||
/// If `None`, then this disconnect was requested by the user in some way
|
||||
/// (i.e., leaving or changing voice channels).
|
||||
pub reason: Option<DisconnectReason>,
|
||||
/// ID of the voice channel being joined, if it is known.
|
||||
///
|
||||
/// If this is available, then this can be used to reconnect/renew
|
||||
/// a voice session via thew gateway.
|
||||
pub channel_id: Option<ChannelId>,
|
||||
/// ID of the target voice channel's parent guild.
|
||||
pub guild_id: GuildId,
|
||||
/// Unique string describing this session for validation/authentication purposes.
|
||||
pub session_id: &'a str,
|
||||
}
|
||||
|
||||
/// The location that a voice connection was terminated.
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum DisconnectKind {
|
||||
/// The voice driver failed to connect to the server.
|
||||
///
|
||||
/// This requires explicit handling at the gateway level
|
||||
/// to either reconnect or fully disconnect.
|
||||
Connect,
|
||||
/// The voice driver failed to reconnect to the server.
|
||||
///
|
||||
/// This requires explicit handling at the gateway level
|
||||
/// to either reconnect or fully disconnect.
|
||||
Reconnect,
|
||||
/// The voice connection was terminated mid-session by either
|
||||
/// the user or Discord.
|
||||
///
|
||||
/// If `reason == None`, then this disconnection is either
|
||||
/// a full disconnect or a user-requested channel change.
|
||||
/// Otherwise, this is likely a session expiry (requiring user
|
||||
/// handling to fully disconnect/reconnect).
|
||||
Runtime,
|
||||
}
|
||||
|
||||
/// The reason that a voice connection failed.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum DisconnectReason {
|
||||
/// This (re)connection attempt was dropped due to another request.
|
||||
AttemptDiscarded,
|
||||
/// Songbird had an internal error.
|
||||
///
|
||||
/// This should never happen; if this is ever seen, raise an issue with logs.
|
||||
Internal,
|
||||
/// A host-specific I/O error caused the fault; this is likely transient, and
|
||||
/// should be retried some time later.
|
||||
Io,
|
||||
/// Songbird and Discord disagreed on the protocol used to establish a
|
||||
/// voice connection.
|
||||
///
|
||||
/// This should never happen; if this is ever seen, raise an issue with logs.
|
||||
ProtocolViolation,
|
||||
/// A voice connection was not established in the specified time.
|
||||
TimedOut,
|
||||
/// The Websocket connection was closed by Discord.
|
||||
///
|
||||
/// This typically indicates that the voice session has expired,
|
||||
/// and a new one needs to be requested via the gateway.
|
||||
WsClosed(Option<VoiceCloseCode>),
|
||||
}
|
||||
|
||||
impl From<&ConnectionError> for DisconnectReason {
|
||||
fn from(e: &ConnectionError) -> Self {
|
||||
use ConnectionError::*;
|
||||
|
||||
match e {
|
||||
AttemptDiscarded => Self::AttemptDiscarded,
|
||||
CryptoModeInvalid
|
||||
| CryptoModeUnavailable
|
||||
| EndpointUrl
|
||||
| ExpectedHandshake
|
||||
| IllegalDiscoveryResponse
|
||||
| IllegalIp
|
||||
| Json(_) => Self::ProtocolViolation,
|
||||
Io(_) => Self::Io,
|
||||
Crypto(_) | InterconnectFailure(_) => Self::Internal,
|
||||
Ws(ws) => ws.into(),
|
||||
TimedOut => Self::TimedOut,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&WsError> for DisconnectReason {
|
||||
fn from(e: &WsError) -> Self {
|
||||
Self::WsClosed(match e {
|
||||
WsError::WsClosed(Some(frame)) => match frame.code {
|
||||
CloseCode::Library(l) => VoiceCloseCode::from_u16(l),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,11 @@
|
||||
//!
|
||||
//! [`EventContext`]: super::EventContext
|
||||
mod connect;
|
||||
mod disconnect;
|
||||
mod rtcp;
|
||||
mod speaking;
|
||||
mod voice;
|
||||
|
||||
use discortp::{rtcp::Rtcp, rtp::Rtp};
|
||||
|
||||
pub use self::{connect::*, rtcp::*, speaking::*, voice::*};
|
||||
pub use self::{connect::*, disconnect::*, rtcp::*, speaking::*, voice::*};
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
use super::context_data::*;
|
||||
use crate::ConnectionInfo;
|
||||
use discortp::{rtcp::Rtcp, rtp::Rtp};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct InternalConnect {
|
||||
pub server: String,
|
||||
pub info: ConnectionInfo,
|
||||
pub ssrc: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InternalDisconnect {
|
||||
pub kind: DisconnectKind,
|
||||
pub reason: Option<DisconnectReason>,
|
||||
pub info: ConnectionInfo,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct InternalSpeakingUpdate {
|
||||
pub ssrc: u32,
|
||||
@@ -31,12 +39,27 @@ pub struct InternalRtcpPacket {
|
||||
impl<'a> From<&'a InternalConnect> for ConnectData<'a> {
|
||||
fn from(val: &'a InternalConnect) -> Self {
|
||||
Self {
|
||||
server: &val.server,
|
||||
channel_id: val.info.channel_id,
|
||||
guild_id: val.info.guild_id,
|
||||
session_id: &val.info.session_id,
|
||||
server: &val.info.endpoint,
|
||||
ssrc: val.ssrc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a InternalDisconnect> for DisconnectData<'a> {
|
||||
fn from(val: &'a InternalDisconnect) -> Self {
|
||||
Self {
|
||||
kind: val.kind,
|
||||
reason: val.reason,
|
||||
channel_id: val.info.channel_id,
|
||||
guild_id: val.info.guild_id,
|
||||
session_id: &val.info.session_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a InternalSpeakingUpdate> for SpeakingUpdateData {
|
||||
fn from(val: &'a InternalSpeakingUpdate) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -17,7 +17,7 @@ use internal_data::*;
|
||||
///
|
||||
/// [`Track`]: crate::tracks::Track
|
||||
/// [`Driver::add_global_event`]: crate::driver::Driver::add_global_event
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum EventContext<'a> {
|
||||
/// Track event context, passed to events created via [`TrackHandle::add_event`],
|
||||
@@ -47,12 +47,32 @@ pub enum EventContext<'a> {
|
||||
DriverConnect(ConnectData<'a>),
|
||||
/// Fires when this driver successfully reconnects after a network error.
|
||||
DriverReconnect(ConnectData<'a>),
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Please use the DriverDisconnect event instead."
|
||||
)]
|
||||
/// Fires when this driver fails to connect to a voice channel.
|
||||
///
|
||||
/// Users will need to manually reconnect on receipt of this error.
|
||||
/// **This event is deprecated in favour of [`DriverDisconnect`].**
|
||||
///
|
||||
/// [`DriverDisconnect`]: Self::DriverDisconnect
|
||||
// TODO: remove in 0.3.x
|
||||
DriverConnectFailed,
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Please use the DriverDisconnect event instead."
|
||||
)]
|
||||
/// Fires when this driver fails to reconnect to a voice channel after a network error.
|
||||
///
|
||||
/// Users will need to manually reconnect on receipt of this error.
|
||||
/// **This event is deprecated in favour of [`DriverDisconnect`].**
|
||||
///
|
||||
/// [`DriverDisconnect`]: Self::DriverDisconnect
|
||||
// TODO: remove in 0.3.x
|
||||
DriverReconnectFailed,
|
||||
/// Fires when this driver fails to connect to, or drops from, a voice channel.
|
||||
DriverDisconnect(DisconnectData<'a>),
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Please use the DriverConnect/Reconnect events instead."
|
||||
@@ -69,7 +89,7 @@ pub enum EventContext<'a> {
|
||||
SsrcKnown(u32),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug)]
|
||||
pub enum CoreContext {
|
||||
SpeakingStateUpdate(Speaking),
|
||||
SpeakingUpdate(InternalSpeakingUpdate),
|
||||
@@ -79,6 +99,7 @@ pub enum CoreContext {
|
||||
ClientDisconnect(ClientDisconnect),
|
||||
DriverConnect(InternalConnect),
|
||||
DriverReconnect(InternalConnect),
|
||||
DriverDisconnect(InternalDisconnect),
|
||||
DriverConnectFailed,
|
||||
DriverReconnectFailed,
|
||||
SsrcKnown(u32),
|
||||
@@ -97,7 +118,10 @@ impl<'a> CoreContext {
|
||||
ClientDisconnect(evt) => EventContext::ClientDisconnect(*evt),
|
||||
DriverConnect(evt) => EventContext::DriverConnect(ConnectData::from(evt)),
|
||||
DriverReconnect(evt) => EventContext::DriverReconnect(ConnectData::from(evt)),
|
||||
DriverDisconnect(evt) => EventContext::DriverDisconnect(DisconnectData::from(evt)),
|
||||
#[allow(deprecated)]
|
||||
DriverConnectFailed => EventContext::DriverConnectFailed,
|
||||
#[allow(deprecated)]
|
||||
DriverReconnectFailed => EventContext::DriverReconnectFailed,
|
||||
#[allow(deprecated)]
|
||||
SsrcKnown(s) => EventContext::SsrcKnown(*s),
|
||||
@@ -112,15 +136,18 @@ impl EventContext<'_> {
|
||||
use EventContext::*;
|
||||
|
||||
match self {
|
||||
SpeakingStateUpdate { .. } => Some(CoreEvent::SpeakingStateUpdate),
|
||||
SpeakingUpdate { .. } => Some(CoreEvent::SpeakingUpdate),
|
||||
VoicePacket { .. } => Some(CoreEvent::VoicePacket),
|
||||
RtcpPacket { .. } => Some(CoreEvent::RtcpPacket),
|
||||
ClientConnect { .. } => Some(CoreEvent::ClientConnect),
|
||||
ClientDisconnect { .. } => Some(CoreEvent::ClientDisconnect),
|
||||
SpeakingStateUpdate(_) => Some(CoreEvent::SpeakingStateUpdate),
|
||||
SpeakingUpdate(_) => Some(CoreEvent::SpeakingUpdate),
|
||||
VoicePacket(_) => Some(CoreEvent::VoicePacket),
|
||||
RtcpPacket(_) => Some(CoreEvent::RtcpPacket),
|
||||
ClientConnect(_) => Some(CoreEvent::ClientConnect),
|
||||
ClientDisconnect(_) => Some(CoreEvent::ClientDisconnect),
|
||||
DriverConnect(_) => Some(CoreEvent::DriverConnect),
|
||||
DriverReconnect(_) => Some(CoreEvent::DriverReconnect),
|
||||
DriverDisconnect(_) => Some(CoreEvent::DriverDisconnect),
|
||||
#[allow(deprecated)]
|
||||
DriverConnectFailed => Some(CoreEvent::DriverConnectFailed),
|
||||
#[allow(deprecated)]
|
||||
DriverReconnectFailed => Some(CoreEvent::DriverReconnectFailed),
|
||||
#[allow(deprecated)]
|
||||
SsrcKnown(_) => Some(CoreEvent::SsrcKnown),
|
||||
|
||||
@@ -33,12 +33,22 @@ pub enum CoreEvent {
|
||||
DriverConnect,
|
||||
/// Fires when this driver successfully reconnects after a network error.
|
||||
DriverReconnect,
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Please use the DriverDisconnect event instead."
|
||||
)]
|
||||
/// Fires when this driver fails to connect to a voice channel.
|
||||
DriverConnectFailed,
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Please use the DriverDisconnect event instead."
|
||||
)]
|
||||
/// Fires when this driver fails to reconnect to a voice channel after a network error.
|
||||
///
|
||||
/// Users will need to manually reconnect on receipt of this error.
|
||||
DriverReconnectFailed,
|
||||
/// Fires when this driver fails to connect to, or drops from, a voice channel.
|
||||
DriverDisconnect,
|
||||
/// Fires whenever the driver is assigned a new [RTP SSRC] by the voice server.
|
||||
///
|
||||
/// This typically fires alongside a [DriverConnect], or a full [DriverReconnect].
|
||||
|
||||
Reference in New Issue
Block a user