Creating a keep alive connection

This commit is contained in:
Filipi Fuchter
2025-03-13 14:20:25 -03:00
parent d33a4b3a11
commit 30432639b4
3 changed files with 50 additions and 9 deletions

View File

@@ -34,6 +34,7 @@ export class SmallWebRTCTransport {
private reconnectionAttempts = 0; private reconnectionAttempts = 0;
private maxReconnectionAttempts = 3; private maxReconnectionAttempts = 3;
private isReconnecting = false; private isReconnecting = false;
private keepAliveInterval: number | null = null;
constructor(callbacks: SmallWebRTCTransportCallbacks) { constructor(callbacks: SmallWebRTCTransportCallbacks) {
this._callbacks = callbacks this._callbacks = callbacks
@@ -287,12 +288,22 @@ export class SmallWebRTCTransport {
dc.addEventListener('close', () => { dc.addEventListener('close', () => {
this.log("datachannel closed") this.log("datachannel closed")
if (this.keepAliveInterval) {
clearInterval(this.keepAliveInterval)
this.keepAliveInterval = null
}
}); });
dc.addEventListener('open', () => { dc.addEventListener('open', () => {
this.log("datachannel opened") this.log("datachannel opened")
// Sending message that the client is ready, just for testing // Sending message that the client is ready, just for testing
dc.send(JSON.stringify({id: 'clientReady', label: 'rtvi-ai', type:'client-ready'})) dc.send(JSON.stringify({id: 'clientReady', label: 'rtvi-ai', type:'client-ready'}))
// @ts-ignore
this.keepAliveInterval = setInterval(() => {
const message = 'ping: ' + new Date().getTime()
dc.send(message);
}, 1000);
}); });
dc.addEventListener('message', (evt: MessageEvent) => { dc.addEventListener('message', (evt: MessageEvent) => {

View File

@@ -189,7 +189,7 @@ class SmallWebRTCClient:
except asyncio.TimeoutError: except asyncio.TimeoutError:
if self._webrtcConnection.is_connected(): if self._webrtcConnection.is_connected():
logger.warning("Timeout: No video frame received within the specified time.") logger.warning("Timeout: No video frame received within the specified time.")
# self._webrtcConnection.ask_to_renegotiate() self._webrtcConnection.ask_to_renegotiate()
frame = None frame = None
if frame is None or not isinstance(frame, VideoFrame): if frame is None or not isinstance(frame, VideoFrame):

View File

@@ -1,6 +1,7 @@
import asyncio import asyncio
import json import json
import uuid import uuid
import time
from enum import Enum from enum import Enum
from typing import Any, Optional from typing import Any, Optional
@@ -26,6 +27,7 @@ class SmallWebRTCConnection(EventEmitter):
self._tracks = set() self._tracks = set()
self._data_channel = None self._data_channel = None
self._renegotiation_in_progress = False self._renegotiation_in_progress = False
self._last_received_time = None
def _setup_listeners(self): def _setup_listeners(self):
@self.pc.on("datachannel") @self.pc.on("datachannel")
@@ -35,17 +37,29 @@ class SmallWebRTCConnection(EventEmitter):
@channel.on("message") @channel.on("message")
async def on_message(message): async def on_message(message):
try: try:
json_message = json.loads(message) # aiortc does not provide any way so we can be aware when we are disconnected,
await self.emit("appMessage", json_message) # so we are using this keep alive message as a way to implement that
if isinstance(message, str) and message.startswith("ping"):
self._last_received_time = time.time()
else:
json_message = json.loads(message)
await self.emit("appMessage", json_message)
except Exception as e: except Exception as e:
logger.exception(f"Error parsing JSON message {message}, {e}") logger.exception(f"Error parsing JSON message {message}, {e}")
# Despite the fact that aiortc provides this listener, they don't have a status for "disconnected"
# So, in case we loose connection, this event will not be triggered
@self.pc.on("connectionstatechange") @self.pc.on("connectionstatechange")
async def on_connectionstatechange(): async def on_connectionstatechange():
logger.info(f"Connection state is {self.pc.connectionState}") await self._handle_new_connection_state()
await self.emit(self.pc.connectionState, self)
if self.pc.connectionState == "failed": # Despite the fact that aiortc provides this listener, they don't have a status for "disconnected"
await self.close() # So, in case we loose connection, this event will not be triggered
@self.pc.on("iceconnectionstatechange")
async def on_iceconnectionstatechange():
logger.info(
f"Ice connection state is {self.pc.iceConnectionState}, connection is {self.pc.connectionState}"
)
@self.pc.on("track") @self.pc.on("track")
async def on_track(track): async def on_track(track):
@@ -80,7 +94,7 @@ class SmallWebRTCConnection(EventEmitter):
await self._create_answer(sdp, type) await self._create_answer(sdp, type)
await self.pc.setLocalDescription(self.answer) await self.pc.setLocalDescription(self.answer)
# TODO maybe we should refactor to receive a message from the client side when the renegotiation is completed. # Maybe we should refactor to receive a message from the client side when the renegotiation is completed.
# or look at the peer connection listeners # or look at the peer connection listeners
# but this is good enough for now for testing. # but this is good enough for now for testing.
async def delayed_task(): async def delayed_task():
@@ -133,8 +147,24 @@ class SmallWebRTCConnection(EventEmitter):
"pc_id": self.pc_id, "pc_id": self.pc_id,
} }
async def _handle_new_connection_state(self):
state = self.pc.connectionState
logger.info(f"Connection state changed to: {state}")
await self.emit(state, self)
if state == "failed":
logger.warning("Connection failed, closing peer connection.")
await self.close()
# Despite the fact that aiortc provides this listener, they don't have a status for "disconnected"
# So, there is no advantage in looking at self.pc.connectionState
# That is why we are trying to keep our own state
def is_connected(self): def is_connected(self):
return self.pc.connectionState == "connected" if self._last_received_time is None:
# if we have never received a message, it is probably because the client has not created a data channel
# so we are going to trust aiortc in this case
return self.pc.connectionState == "connected"
# Checks if the last received ping was within the last 3 seconds.
return (time.time() - self._last_received_time) < 3
def audio_input_track(self): def audio_input_track(self):
# Transceivers always appear in creation-order for both peers # Transceivers always appear in creation-order for both peers