Improving the reconnection logic to be able to recreate the peer connection in some cases.
This commit is contained in:
@@ -35,9 +35,14 @@ export class SmallWebRTCTransport {
|
|||||||
private maxReconnectionAttempts = 3;
|
private maxReconnectionAttempts = 3;
|
||||||
private isReconnecting = false;
|
private isReconnecting = false;
|
||||||
private keepAliveInterval: number | null = null;
|
private keepAliveInterval: number | null = null;
|
||||||
|
private audioDevice: string | undefined;
|
||||||
|
private videoDevice: string | undefined;
|
||||||
|
|
||||||
constructor(callbacks: SmallWebRTCTransportCallbacks) {
|
constructor(callbacks: SmallWebRTCTransportCallbacks) {
|
||||||
this._callbacks = callbacks
|
this._callbacks = callbacks
|
||||||
|
// for testing reconnections
|
||||||
|
// @ts-ignore
|
||||||
|
window.attemptReconnection = this.attemptReconnection.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private log(message: string): void {
|
private log(message: string): void {
|
||||||
@@ -65,6 +70,9 @@ export class SmallWebRTCTransport {
|
|||||||
|
|
||||||
pc.addEventListener('signalingstatechange', () => {
|
pc.addEventListener('signalingstatechange', () => {
|
||||||
this.log(`signalingState: ${this.pc!.signalingState}`)
|
this.log(`signalingState: ${this.pc!.signalingState}`)
|
||||||
|
if (this.pc!.signalingState == 'stable') {
|
||||||
|
this.handleReconnectionCompleted()
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.log(`signalingState: ${pc.signalingState}`)
|
this.log(`signalingState: ${pc.signalingState}`)
|
||||||
|
|
||||||
@@ -94,20 +102,24 @@ export class SmallWebRTCTransport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleReconnectionCompleted() {
|
||||||
|
this.reconnectionAttempts = 0;
|
||||||
|
this.isReconnecting = false;
|
||||||
|
}
|
||||||
|
|
||||||
private handleConnectionStateChange(): void {
|
private handleConnectionStateChange(): void {
|
||||||
if (!this.pc) return;
|
if (!this.pc) return;
|
||||||
this.log(`Connection State: ${this.pc.connectionState}`);
|
this.log(`Connection State: ${this.pc.connectionState}`);
|
||||||
|
|
||||||
if (this.pc.connectionState === "connected") {
|
if (this.pc.connectionState === "connected") {
|
||||||
this.reconnectionAttempts = 0;
|
this.handleReconnectionCompleted()
|
||||||
this.isReconnecting = false;
|
|
||||||
this._callbacks.onConnected();
|
this._callbacks.onConnected();
|
||||||
} else if (this.pc.connectionState === "failed") {
|
} else if (this.pc.connectionState === "failed") {
|
||||||
void this.attemptReconnection(true);
|
void this.attemptReconnection(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async attemptReconnection(iceRestart: boolean = false): Promise<void> {
|
private async attemptReconnection(recreatePeerConnection: boolean = false): Promise<void> {
|
||||||
if (this.isReconnecting) {
|
if (this.isReconnecting) {
|
||||||
this.log("Reconnection already in progress, skipping.");
|
this.log("Reconnection already in progress, skipping.");
|
||||||
return;
|
return;
|
||||||
@@ -120,17 +132,24 @@ export class SmallWebRTCTransport {
|
|||||||
this.isReconnecting = true;
|
this.isReconnecting = true;
|
||||||
this.reconnectionAttempts++;
|
this.reconnectionAttempts++;
|
||||||
this.log(`Reconnection attempt ${this.reconnectionAttempts}...`);
|
this.log(`Reconnection attempt ${this.reconnectionAttempts}...`);
|
||||||
await this.negotiate(iceRestart);
|
// aiortc it is not working fine when just trying to restart the ice
|
||||||
|
// so in this case we are creating a new peer connection on both sides
|
||||||
|
if (recreatePeerConnection) {
|
||||||
|
//this.pc?.close()
|
||||||
|
await this.startNewPeerConnection(recreatePeerConnection)
|
||||||
|
} else {
|
||||||
|
await this.negotiate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async negotiate(iceRestart: boolean = false): Promise<void> {
|
private async negotiate(recreatePeerConnection: boolean = false): Promise<void> {
|
||||||
if (!this.pc) {
|
if (!this.pc) {
|
||||||
return Promise.reject('Peer connection is not initialized');
|
return Promise.reject('Peer connection is not initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create offer
|
// Create offer
|
||||||
const offer = await this.pc.createOffer({iceRestart});
|
const offer = await this.pc.createOffer();
|
||||||
await this.pc.setLocalDescription(offer);
|
await this.pc.setLocalDescription(offer);
|
||||||
|
|
||||||
// Wait for ICE gathering to complete
|
// Wait for ICE gathering to complete
|
||||||
@@ -170,7 +189,8 @@ export class SmallWebRTCTransport {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
sdp: offerSdp.sdp,
|
sdp: offerSdp.sdp,
|
||||||
type: offerSdp.type,
|
type: offerSdp.type,
|
||||||
pc_id: this.pc_id
|
pc_id: this.pc_id,
|
||||||
|
restart_pc: recreatePeerConnection
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -212,18 +232,24 @@ export class SmallWebRTCTransport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async start(audioDevice: string | undefined, audioCodec: string, videoCodec: string, videoDevice: string | undefined): Promise<void> {
|
async start(audioDevice: string | undefined, audioCodec: string, videoCodec: string, videoDevice: string | undefined): Promise<void> {
|
||||||
|
this.audioDevice = audioDevice
|
||||||
|
this.videoDevice = videoDevice
|
||||||
|
this.audioCodec = audioCodec
|
||||||
|
this.videoCodec = videoCodec
|
||||||
|
await this.startNewPeerConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startNewPeerConnection(recreatePeerConnection: boolean = false) {
|
||||||
this.pc = this.createPeerConnection();
|
this.pc = this.createPeerConnection();
|
||||||
this.addInitialTransceivers();
|
this.addInitialTransceivers();
|
||||||
this.dc = this.createDataChannel('chat', { ordered: true });
|
this.dc = this.createDataChannel('chat', { ordered: true });
|
||||||
await this.addUserMedias(audioDevice, videoDevice);
|
await this.addUserMedias();
|
||||||
this.audioCodec = audioCodec
|
await this.negotiate(recreatePeerConnection);
|
||||||
this.videoCodec = videoCodec
|
|
||||||
await this.negotiate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async addUserMedias(audioDevice: string|undefined, videoDevice:string|undefined): Promise<void> {
|
private async addUserMedias(): Promise<void> {
|
||||||
this.log("Will send the audio and video");
|
this.log("Will send the audio and video");
|
||||||
const constraints = this.createMediaConstraints(audioDevice, videoDevice);
|
const constraints = this.createMediaConstraints();
|
||||||
|
|
||||||
if (constraints.audio || constraints.video) {
|
if (constraints.audio || constraints.video) {
|
||||||
try {
|
try {
|
||||||
@@ -314,16 +340,16 @@ export class SmallWebRTCTransport {
|
|||||||
return dc;
|
return dc;
|
||||||
}
|
}
|
||||||
|
|
||||||
private createMediaConstraints(audioDevice: string|undefined, videoDevice:string|undefined): MediaStreamConstraints {
|
private createMediaConstraints(): MediaStreamConstraints {
|
||||||
const constraints: MediaStreamConstraints = { audio: false, video: false };
|
const constraints: MediaStreamConstraints = { audio: false, video: false };
|
||||||
|
|
||||||
const audioConstraints: MediaTrackConstraints = {};
|
const audioConstraints: MediaTrackConstraints = {};
|
||||||
if (audioDevice) audioConstraints.deviceId = { exact: audioDevice };
|
if (this.audioDevice) audioConstraints.deviceId = { exact: this.audioDevice };
|
||||||
|
|
||||||
constraints.audio = Object.keys(audioConstraints).length ? audioConstraints : true;
|
constraints.audio = Object.keys(audioConstraints).length ? audioConstraints : true;
|
||||||
|
|
||||||
const videoConstraints: MediaTrackConstraints = {};
|
const videoConstraints: MediaTrackConstraints = {};
|
||||||
if (videoDevice) videoConstraints.deviceId = { exact: videoDevice };
|
if (this.videoDevice) videoConstraints.deviceId = { exact: this.videoDevice };
|
||||||
|
|
||||||
constraints.video = Object.keys(videoConstraints).length ? videoConstraints : true;
|
constraints.video = Object.keys(videoConstraints).length ? videoConstraints : true;
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ async def offer(request: dict, background_tasks: BackgroundTasks):
|
|||||||
if pc_id and pc_id in pcs_map:
|
if pc_id and pc_id in pcs_map:
|
||||||
pipecat_connection = pcs_map[pc_id]
|
pipecat_connection = pcs_map[pc_id]
|
||||||
logger.info(f"Reusing existing connection for pc_id: {pc_id}")
|
logger.info(f"Reusing existing connection for pc_id: {pc_id}")
|
||||||
await pipecat_connection.renegotiate(sdp=request["sdp"], type=request["type"])
|
await pipecat_connection.renegotiate(
|
||||||
|
sdp=request["sdp"], type=request["type"], restart_pc=request.get("restart_pc", False)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
pipecat_connection = SmallWebRTCConnection()
|
pipecat_connection = SmallWebRTCConnection()
|
||||||
await pipecat_connection.initialize(sdp=request["sdp"], type=request["type"])
|
await pipecat_connection.initialize(sdp=request["sdp"], type=request["type"])
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from typing import Any, Awaitable, Callable, Optional
|
|||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from aiortc import VideoStreamTrack
|
from aiortc import VideoStreamTrack
|
||||||
from aiortc.mediastreams import AudioStreamTrack, VideoFrame
|
from aiortc.mediastreams import AudioStreamTrack, MediaStreamError, VideoFrame
|
||||||
from av import AudioFrame, AudioResampler
|
from av import AudioFrame, AudioResampler
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -191,6 +191,9 @@ class SmallWebRTCClient:
|
|||||||
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
|
||||||
|
except MediaStreamError:
|
||||||
|
logger.warning("Received an unexpected media stream error while reading the audio.")
|
||||||
|
frame = None
|
||||||
|
|
||||||
if frame is None or not isinstance(frame, VideoFrame):
|
if frame is None or not isinstance(frame, VideoFrame):
|
||||||
# If no valid frame, sleep for a bit
|
# If no valid frame, sleep for a bit
|
||||||
@@ -237,6 +240,9 @@ class SmallWebRTCClient:
|
|||||||
if self._webrtcConnection.is_connected():
|
if self._webrtcConnection.is_connected():
|
||||||
logger.warning("Timeout: No audio frame received within the specified time.")
|
logger.warning("Timeout: No audio frame received within the specified time.")
|
||||||
frame = None
|
frame = None
|
||||||
|
except MediaStreamError:
|
||||||
|
logger.warning("Received an unexpected media stream error while reading the audio.")
|
||||||
|
frame = None
|
||||||
|
|
||||||
if frame is None or not isinstance(frame, AudioFrame):
|
if frame is None or not isinstance(frame, AudioFrame):
|
||||||
# If we don't read any audio let's sleep for a little bit (i.e. busy wait).
|
# If we don't read any audio let's sleep for a little bit (i.e. busy wait).
|
||||||
@@ -265,11 +271,11 @@ class SmallWebRTCClient:
|
|||||||
yield audio_frame
|
yield audio_frame
|
||||||
|
|
||||||
async def write_raw_audio_frames(self, data: bytes):
|
async def write_raw_audio_frames(self, data: bytes):
|
||||||
if self._can_send():
|
if self._can_send() and self._audio_output_track:
|
||||||
await self._audio_output_track.add_audio_bytes(data)
|
await self._audio_output_track.add_audio_bytes(data)
|
||||||
|
|
||||||
async def write_frame_to_camera(self, frame: OutputImageRawFrame):
|
async def write_frame_to_camera(self, frame: OutputImageRawFrame):
|
||||||
if self._can_send():
|
if self._can_send() and self._video_output_track:
|
||||||
self._video_output_track.add_video_frame(frame)
|
self._video_output_track.add_video_frame(frame)
|
||||||
|
|
||||||
async def setup(self, _params: TransportParams, frame):
|
async def setup(self, _params: TransportParams, frame):
|
||||||
@@ -279,23 +285,13 @@ class SmallWebRTCClient:
|
|||||||
self._params = _params
|
self._params = _params
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
if self._audio_output_track or self._video_output_track:
|
if self._webrtcConnection.is_connected() or self._webrtcConnection.is_connecting():
|
||||||
# already initialized
|
# already initialized
|
||||||
return
|
return
|
||||||
|
|
||||||
|
logger.info(f"Connecting to Small WebRTC")
|
||||||
await self._webrtcConnection.connect()
|
await self._webrtcConnection.connect()
|
||||||
|
|
||||||
logger.info(f"Connecting to Small WebRTC")
|
|
||||||
|
|
||||||
if self._params.audio_out_enabled:
|
|
||||||
self._audio_output_track = RawAudioTrack(sample_rate=self._out_sample_rate)
|
|
||||||
self._webrtcConnection.replace_audio_track(self._audio_output_track)
|
|
||||||
|
|
||||||
if self._params.camera_out_enabled:
|
|
||||||
self._video_output_track = RawVideoTrack(
|
|
||||||
width=self._params.camera_out_width, height=self._params.camera_out_height
|
|
||||||
)
|
|
||||||
self._webrtcConnection.replace_video_track(self._video_output_track)
|
|
||||||
|
|
||||||
async def disconnect(self):
|
async def disconnect(self):
|
||||||
if self.is_connected and not self.is_closing:
|
if self.is_connected and not self.is_closing:
|
||||||
@@ -311,12 +307,30 @@ class SmallWebRTCClient:
|
|||||||
async def _handle_client_connected(self):
|
async def _handle_client_connected(self):
|
||||||
self._audio_input_track = self._webrtcConnection.audio_input_track()
|
self._audio_input_track = self._webrtcConnection.audio_input_track()
|
||||||
self._video_input_track = self._webrtcConnection.video_input_track()
|
self._video_input_track = self._webrtcConnection.video_input_track()
|
||||||
|
if self._params.audio_out_enabled:
|
||||||
|
self._audio_output_track = RawAudioTrack(sample_rate=self._out_sample_rate)
|
||||||
|
self._webrtcConnection.replace_audio_track(self._audio_output_track)
|
||||||
|
|
||||||
|
if self._params.camera_out_enabled:
|
||||||
|
self._video_output_track = RawVideoTrack(
|
||||||
|
width=self._params.camera_out_width, height=self._params.camera_out_height
|
||||||
|
)
|
||||||
|
self._webrtcConnection.replace_video_track(self._video_output_track)
|
||||||
|
|
||||||
await self._callbacks.on_client_connected(self._webrtcConnection)
|
await self._callbacks.on_client_connected(self._webrtcConnection)
|
||||||
|
|
||||||
async def _handle_client_disconnected(self):
|
async def _handle_client_disconnected(self):
|
||||||
|
self._audio_input_track = None
|
||||||
|
self._video_input_track = None
|
||||||
|
self._audio_output_track = None
|
||||||
|
self._video_output_track = None
|
||||||
await self._callbacks.on_client_disconnected(self._webrtcConnection)
|
await self._callbacks.on_client_disconnected(self._webrtcConnection)
|
||||||
|
|
||||||
async def _handle_client_closed(self):
|
async def _handle_client_closed(self):
|
||||||
|
self._audio_input_track = None
|
||||||
|
self._video_input_track = None
|
||||||
|
self._audio_output_track = None
|
||||||
|
self._video_output_track = None
|
||||||
await self._callbacks.on_client_closed(self._webrtcConnection)
|
await self._callbacks.on_client_closed(self._webrtcConnection)
|
||||||
|
|
||||||
async def _handle_app_message(self, message: Any):
|
async def _handle_app_message(self, message: Any):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import uuid
|
|
||||||
import time
|
import time
|
||||||
|
import uuid
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
@@ -20,6 +20,11 @@ class SignallingMessage(Enum):
|
|||||||
class SmallWebRTCConnection(EventEmitter):
|
class SmallWebRTCConnection(EventEmitter):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self._is_connecting = False
|
||||||
|
self._initialize()
|
||||||
|
|
||||||
|
def _initialize(self):
|
||||||
|
logger.info("Initializing new peer connection")
|
||||||
self.answer: Optional[RTCSessionDescription] = None
|
self.answer: Optional[RTCSessionDescription] = None
|
||||||
self.pc = RTCPeerConnection()
|
self.pc = RTCPeerConnection()
|
||||||
self.pc_id = "PeerConnection(%s)" % uuid.uuid4()
|
self.pc_id = "PeerConnection(%s)" % uuid.uuid4()
|
||||||
@@ -87,10 +92,21 @@ class SmallWebRTCConnection(EventEmitter):
|
|||||||
await self._create_answer(sdp, type)
|
await self._create_answer(sdp, type)
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
|
self._is_connecting = True
|
||||||
await self.pc.setLocalDescription(self.answer)
|
await self.pc.setLocalDescription(self.answer)
|
||||||
|
|
||||||
async def renegotiate(self, sdp: str, type: str):
|
async def renegotiate(self, sdp: str, type: str, restart_pc: bool = False):
|
||||||
logger.info(f"Renegotiating {self.pc_id}")
|
logger.info(f"Renegotiating {self.pc_id}")
|
||||||
|
|
||||||
|
if restart_pc:
|
||||||
|
await self.emit("disconnected", self)
|
||||||
|
logger.info("Closing old peer connection")
|
||||||
|
# removing the listeners to prevent the bot from closing
|
||||||
|
self.pc.remove_all_listeners()
|
||||||
|
await self.close()
|
||||||
|
# we are initializing a new peer connection in this case.
|
||||||
|
self._initialize()
|
||||||
|
|
||||||
await self._create_answer(sdp, type)
|
await self._create_answer(sdp, type)
|
||||||
await self.pc.setLocalDescription(self.answer)
|
await self.pc.setLocalDescription(self.answer)
|
||||||
|
|
||||||
@@ -136,6 +152,7 @@ class SmallWebRTCConnection(EventEmitter):
|
|||||||
async def close(self):
|
async def close(self):
|
||||||
if self.pc:
|
if self.pc:
|
||||||
await self.pc.close()
|
await self.pc.close()
|
||||||
|
self._is_connecting = False
|
||||||
|
|
||||||
def get_answer(self):
|
def get_answer(self):
|
||||||
if not self.answer:
|
if not self.answer:
|
||||||
@@ -166,6 +183,9 @@ class SmallWebRTCConnection(EventEmitter):
|
|||||||
# Checks if the last received ping was within the last 3 seconds.
|
# Checks if the last received ping was within the last 3 seconds.
|
||||||
return (time.time() - self._last_received_time) < 3
|
return (time.time() - self._last_received_time) < 3
|
||||||
|
|
||||||
|
def is_connecting(self):
|
||||||
|
return self._is_connecting
|
||||||
|
|
||||||
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
|
||||||
# For now we are only considering that we are going to have 02 transceivers,
|
# For now we are only considering that we are going to have 02 transceivers,
|
||||||
|
|||||||
Reference in New Issue
Block a user