transports: added new WebsocketServerTransport
This commit is contained in:
228
src/pipecat/transports/network/websocket_server.py
Normal file
228
src/pipecat/transports/network/websocket_server.py
Normal file
@@ -0,0 +1,228 @@
|
||||
#
|
||||
# Copyright (c) 2024, Daily
|
||||
#
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
|
||||
|
||||
import asyncio
|
||||
import websockets
|
||||
|
||||
from typing import Awaitable, Callable
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
from pipecat.frames.frames import (
|
||||
AudioRawFrame,
|
||||
CancelFrame,
|
||||
EndFrame,
|
||||
Frame,
|
||||
StartFrame,
|
||||
TTSStartedFrame,
|
||||
TTSStoppedFrame)
|
||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||
from pipecat.serializers.base_serializer import FrameSerializer
|
||||
from pipecat.serializers.protobuf import ProtobufFrameSerializer
|
||||
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class WebsocketServerParams(TransportParams):
|
||||
audio_frame_size: int = 16000
|
||||
serializer: FrameSerializer = ProtobufFrameSerializer()
|
||||
|
||||
|
||||
class WebsocketServerCallbacks(BaseModel):
|
||||
on_connection: Callable[[websockets.WebSocketServerProtocol], Awaitable[None]]
|
||||
|
||||
|
||||
class WebsocketServerInputTransport(FrameProcessor):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int,
|
||||
params: WebsocketServerParams,
|
||||
callbacks: WebsocketServerCallbacks):
|
||||
super().__init__()
|
||||
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._params = params
|
||||
self._callbacks = callbacks
|
||||
|
||||
self._websocket: websockets.WebSocketServerProtocol | None = None
|
||||
|
||||
self._stop_server_event = asyncio.Event()
|
||||
|
||||
# Create push frame task. This is the task that will push frames in
|
||||
# order. We also guarantee that all frames are pushed in the same task.
|
||||
self._create_push_task()
|
||||
|
||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||
if isinstance(frame, CancelFrame):
|
||||
await self._stop()
|
||||
# We don't queue a CancelFrame since we want to stop ASAP.
|
||||
await self.push_frame(frame, direction)
|
||||
elif isinstance(frame, StartFrame):
|
||||
await self._start()
|
||||
await self._internal_push_frame(frame, direction)
|
||||
elif isinstance(frame, EndFrame):
|
||||
await self._stop()
|
||||
await self._internal_push_frame(frame, direction)
|
||||
else:
|
||||
await self._internal_push_frame(frame, direction)
|
||||
|
||||
async def _server_task_handler(self):
|
||||
logger.info(f"Starting websocket server on {self._host}:{self._port}")
|
||||
async with websockets.serve(self._client_handler, self._host, self._port) as server:
|
||||
await self._stop_server_event.wait()
|
||||
|
||||
async def _client_handler(self, websocket: websockets.WebSocketServerProtocol, path):
|
||||
logger.info(f"New client connection from {websocket.remote_address}")
|
||||
if self._websocket:
|
||||
await self._websocket.close()
|
||||
logger.warning("Only one client connected, using new connection")
|
||||
|
||||
self._websocket = websocket
|
||||
|
||||
# Notify
|
||||
await self._callbacks.on_connection(websocket)
|
||||
|
||||
# Handle incoming messages
|
||||
async for message in websocket:
|
||||
frame = self._params.serializer.deserialize(message)
|
||||
await self._internal_push_frame(frame)
|
||||
|
||||
async def _start(self):
|
||||
loop = self.get_event_loop()
|
||||
self._server_task = loop.create_task(self._server_task_handler())
|
||||
|
||||
async def _stop(self):
|
||||
self._stop_server_event.set()
|
||||
self._push_frame_task.cancel()
|
||||
await self._server_task
|
||||
|
||||
#
|
||||
# Push frames task
|
||||
#
|
||||
|
||||
def _create_push_task(self):
|
||||
loop = self.get_event_loop()
|
||||
self._push_frame_task = loop.create_task(self._push_frame_task_handler())
|
||||
self._push_queue = asyncio.Queue()
|
||||
|
||||
async def _internal_push_frame(
|
||||
self,
|
||||
frame: Frame | None,
|
||||
direction: FrameDirection | None = FrameDirection.DOWNSTREAM):
|
||||
await self._push_queue.put((frame, direction))
|
||||
|
||||
async def _push_frame_task_handler(self):
|
||||
running = True
|
||||
while running:
|
||||
try:
|
||||
(frame, direction) = await self._push_queue.get()
|
||||
await self.push_frame(frame, direction)
|
||||
running = not isinstance(frame, EndFrame)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
|
||||
|
||||
class WebsocketServerOutputTransport(FrameProcessor):
|
||||
|
||||
def __init__(self, params: WebsocketServerParams):
|
||||
super().__init__()
|
||||
|
||||
self._params = params
|
||||
|
||||
self._websocket = None
|
||||
self._audio_buffer = bytes()
|
||||
|
||||
self._websocket: websockets.WebSocketServerProtocol | None = None
|
||||
|
||||
loop = self.get_event_loop()
|
||||
self._send_queue_task = loop.create_task(self._send_queue_task_handler())
|
||||
self._send_queue = asyncio.Queue()
|
||||
|
||||
self._audio_buffer = bytes()
|
||||
self._in_tts_audio = False
|
||||
|
||||
async def set_client_connection(self, websocket: websockets.WebSocketServerProtocol):
|
||||
if self._websocket:
|
||||
await self._websocket.close()
|
||||
logger.warning("Only one client allowed, using new connection")
|
||||
self._websocket = websocket
|
||||
|
||||
async def _send_queue_task_handler(self):
|
||||
running = True
|
||||
while running:
|
||||
frame = await self._send_queue.get()
|
||||
if self._websocket and frame:
|
||||
proto = self._params.serializer.serialize(frame)
|
||||
await self._websocket.send(proto)
|
||||
|
||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||
if isinstance(frame, CancelFrame):
|
||||
# await self.stop()
|
||||
# We don't queue a CancelFrame since we want to stop ASAP.
|
||||
await self.push_frame(frame, direction)
|
||||
elif isinstance(frame, TTSStartedFrame):
|
||||
self._in_tts_audio = True
|
||||
elif isinstance(frame, AudioRawFrame):
|
||||
if self._in_tts_audio:
|
||||
self._audio_buffer += frame.audio
|
||||
while len(self._audio_buffer) >= self._params.audio_frame_size:
|
||||
frame = AudioRawFrame(
|
||||
audio=self._audio_buffer[:self._params.audio_frame_size],
|
||||
sample_rate=self._params.audio_out_sample_rate,
|
||||
num_channels=self._params.audio_out_channels
|
||||
)
|
||||
await self._send_queue.put(frame)
|
||||
self._audio_buffer = self._audio_buffer[self._params.audio_frame_size:]
|
||||
elif isinstance(frame, TTSStoppedFrame):
|
||||
self._in_tts_audio = False
|
||||
if self._audio_buffer:
|
||||
frame = AudioRawFrame(
|
||||
audio=self._audio_buffer,
|
||||
sample_rate=self._params.audio_out_sample_rate,
|
||||
num_channels=self._params.audio_out_channels
|
||||
)
|
||||
await self._send_queue.put(frame)
|
||||
self._audio_buffer = bytes()
|
||||
else:
|
||||
await self.push_frame(frame, direction)
|
||||
|
||||
|
||||
class WebsocketServerTransport(BaseTransport):
|
||||
|
||||
def __init__(self, host: str = "localhost", port: int = 8765,
|
||||
params: WebsocketServerParams = WebsocketServerParams()):
|
||||
super().__init__()
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._params = params
|
||||
|
||||
self._callbacks = WebsocketServerCallbacks(
|
||||
on_connection=self._on_connection
|
||||
)
|
||||
self._input: WebsocketServerInputTransport | None = None
|
||||
self._output: WebsocketServerOutputTransport | None = None
|
||||
self._websocket: websockets.WebSocketServerProtocol | None = None
|
||||
|
||||
def input(self) -> FrameProcessor:
|
||||
if not self._input:
|
||||
self._input = WebsocketServerInputTransport(
|
||||
self._host, self._port, self._params, self._callbacks)
|
||||
return self._input
|
||||
|
||||
def output(self) -> FrameProcessor:
|
||||
if not self._output:
|
||||
self._output = WebsocketServerOutputTransport(self._params)
|
||||
return self._output
|
||||
|
||||
async def _on_connection(self, websocket):
|
||||
if self._output:
|
||||
await self._output.set_client_connection(websocket)
|
||||
else:
|
||||
logger.error("A WebsocketServerTransport output is missing in the pipeline")
|
||||
Reference in New Issue
Block a user