Adding guardrails in case the data channel is never established.

This commit is contained in:
filipi87
2026-03-09 10:04:33 -03:00
parent 25165d6e2b
commit 322e317a00

View File

@@ -41,6 +41,11 @@ AUDIO_TRANSCEIVER_INDEX = 0
VIDEO_TRANSCEIVER_INDEX = 1 VIDEO_TRANSCEIVER_INDEX = 1
SCREEN_VIDEO_TRANSCEIVER_INDEX = 2 SCREEN_VIDEO_TRANSCEIVER_INDEX = 2
# Maximum number of messages to queue while the data channel is not yet open.
MAX_MESSAGE_QUEUE_SIZE = 50
# Seconds to wait for the data channel to open after the peer connection is established.
DATA_CHANNEL_TIMEOUT_SECS = 10
class TrackStatusMessage(BaseModel): class TrackStatusMessage(BaseModel):
"""Message for updating track enabled/disabled status. """Message for updating track enabled/disabled status.
@@ -283,9 +288,11 @@ class SmallWebRTCConnection(BaseObject):
self._data_channel = None self._data_channel = None
self._renegotiation_in_progress = False self._renegotiation_in_progress = False
self._last_received_time = None self._last_received_time = None
self._message_queue = [] self._outgoing_messages_queue = []
self._data_channel_enabled = True
self._pending_app_messages = [] self._pending_app_messages = []
self._connecting_timeout_task = None self._connecting_timeout_task = None
self._data_channel_timeout_task = None
def _setup_listeners(self): def _setup_listeners(self):
"""Set up event listeners for the peer connection.""" """Set up event listeners for the peer connection."""
@@ -501,10 +508,12 @@ class SmallWebRTCConnection(BaseObject):
self._track_map.clear() self._track_map.clear()
if self._pc: if self._pc:
await self._pc.close() await self._pc.close()
self._message_queue.clear() self._outgoing_messages_queue.clear()
self._data_channel_enabled = True
self._pending_app_messages.clear() self._pending_app_messages.clear()
self._track_map = {} self._track_map = {}
self._cancel_monitoring_connecting_state() self._cancel_monitoring_connecting_state()
self._cancel_data_channel_timeout()
def get_answer(self): def get_answer(self):
"""Get the SDP answer for the current connection. """Get the SDP answer for the current connection.
@@ -553,6 +562,44 @@ class SmallWebRTCConnection(BaseObject):
self._connecting_timeout_task.cancel() self._connecting_timeout_task.cancel()
self._connecting_timeout_task = None self._connecting_timeout_task = None
def _start_data_channel_timeout(self) -> None:
"""Start a timeout to detect if the data channel fails to open after connection.
Schedules a background task that fires ``DATA_CHANNEL_TIMEOUT_SECS`` seconds after
the peer connection reaches the *connected* state. If the data channel has not
opened by then, the queued messages are discarded, a warning is logged, and future
calls to :meth:`send_app_message` will silently drop messages instead of queuing
them (fall-back to "discard" mode).
The task is automatically cancelled when the data channel opens successfully (see
:meth:`_flush_message_queue`) or when the connection is closed (see
:meth:`_close`).
"""
async def timeout_handler():
await asyncio.sleep(DATA_CHANNEL_TIMEOUT_SECS)
if not self._data_channel or self._data_channel.readyState != "open":
logger.warning(
f"Data channel not established within {DATA_CHANNEL_TIMEOUT_SECS}s after "
"connection. Clearing message queue and disabling future queueing."
)
self._outgoing_messages_queue.clear()
self._data_channel_enabled = False
self._data_channel_timeout_task = asyncio.create_task(timeout_handler())
def _cancel_data_channel_timeout(self) -> None:
"""Cancel the data-channel open timeout task, if any.
Should be called when the data channel opens successfully (the timeout is no longer
needed) or when the connection is being torn down. If the task is still pending it
will be cancelled and the reference cleared.
"""
if self._data_channel_timeout_task and not self._data_channel_timeout_task.done():
logger.debug("Cancelling the data channel timeout task")
self._data_channel_timeout_task.cancel()
self._data_channel_timeout_task = None
async def _handle_new_connection_state(self): async def _handle_new_connection_state(self):
"""Handle changes in the peer connection state.""" """Handle changes in the peer connection state."""
state = self._pc.connectionState state = self._pc.connectionState
@@ -561,6 +608,9 @@ class SmallWebRTCConnection(BaseObject):
else: else:
self._cancel_monitoring_connecting_state() self._cancel_monitoring_connecting_state()
if state == "connected" and not self._data_channel_timeout_task:
self._start_data_channel_timeout()
if state == "connected" and not self._connect_invoked: if state == "connected" and not self._connect_invoked:
# We are going to wait until the pipeline is ready before triggering the event # We are going to wait until the pipeline is ready before triggering the event
return return
@@ -660,20 +710,45 @@ class SmallWebRTCConnection(BaseObject):
def send_app_message(self, message: Any): def send_app_message(self, message: Any):
"""Send an application message through the data channel. """Send an application message through the data channel.
If the data channel is open the message is sent immediately. Otherwise,
the message is placed in an in-memory queue so it can be flushed once the
channel opens, subject to the following constraints:
* Queueing is only attempted when ``_data_channel_enabled`` is ``True``. It is
set to ``False`` when the data-channel open timeout fires (see
:meth:`_start_data_channel_timeout`), after which messages are silently
discarded.
* The queue will not grow beyond ``MAX_MESSAGE_QUEUE_SIZE`` entries.
Messages that arrive when the queue is full are discarded with a warning.
Args: Args:
message: The message to send (will be JSON serialized). message: The message to send (will be JSON serialized).
""" """
json_message = json.dumps(message) json_message = json.dumps(message)
if self._data_channel and self._data_channel.readyState == "open": if self._data_channel and self._data_channel.readyState == "open":
self._data_channel.send(json_message) self._data_channel.send(json_message)
elif self._data_channel_enabled:
if len(self._outgoing_messages_queue) < MAX_MESSAGE_QUEUE_SIZE:
logger.debug("Data channel not ready, queuing message")
self._outgoing_messages_queue.append(json_message)
else:
logger.warning(
f"Message queue is full ({MAX_MESSAGE_QUEUE_SIZE} messages). Discarding message."
)
else: else:
logger.debug("Data channel not ready, queuing message") logger.trace("Data channel unavailable and queueing disabled. Discarding message.")
self._message_queue.append(json_message)
def _flush_message_queue(self): def _flush_message_queue(self):
"""Flush all queued messages through the now-open data channel.
Called when the data channel transitions to the *open* state. Cancels
the data-channel open timeout (it is no longer needed) and sends every
message that was buffered while the channel was unavailable.
"""
self._cancel_data_channel_timeout()
logger.debug("Data channel is open, flushing queued messages") logger.debug("Data channel is open, flushing queued messages")
while self._message_queue: while self._outgoing_messages_queue:
message = self._message_queue.pop(0) message = self._outgoing_messages_queue.pop(0)
self._data_channel.send(message) self._data_channel.send(message)
def ask_to_renegotiate(self): def ask_to_renegotiate(self):