Merge pull request #241 from pipecat-ai/aleix/transports-async
transports: fully use asyncio in all read/write operations
This commit is contained in:
@@ -20,6 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- Added new `AzureSTTService`. This allows you to use Azure Speech-To-Text.
|
- Added new `AzureSTTService`. This allows you to use Azure Speech-To-Text.
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- Convert `BaseOutputTransport` and `BaseOutputTransport` to fully use asyncio
|
||||||
|
and remove the use of threads.
|
||||||
|
|
||||||
### Other
|
### Other
|
||||||
|
|
||||||
- Added `twilio-chatbot`. This is an example that shows how to integrate Twilio
|
- Added `twilio-chatbot`. This is an example that shows how to integrate Twilio
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ autopep8~=2.1.0
|
|||||||
build~=1.2.1
|
build~=1.2.1
|
||||||
grpcio-tools~=1.62.2
|
grpcio-tools~=1.62.2
|
||||||
pip-tools~=7.4.1
|
pip-tools~=7.4.1
|
||||||
|
pyright~=1.1.367
|
||||||
pytest~=8.2.0
|
pytest~=8.2.0
|
||||||
setuptools~=69.5.1
|
setuptools~=69.5.1
|
||||||
setuptools_scm~=8.1.0
|
setuptools_scm~=8.1.0
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ class PipelineTask:
|
|||||||
await self._source.process_frame(CancelFrame(), FrameDirection.DOWNSTREAM)
|
await self._source.process_frame(CancelFrame(), FrameDirection.DOWNSTREAM)
|
||||||
self._process_down_task.cancel()
|
self._process_down_task.cancel()
|
||||||
self._process_up_task.cancel()
|
self._process_up_task.cancel()
|
||||||
|
await self._process_down_task
|
||||||
|
await self._process_up_task
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
self._process_up_task = asyncio.create_task(self._process_up_queue())
|
self._process_up_task = asyncio.create_task(self._process_up_queue())
|
||||||
@@ -122,6 +124,7 @@ class PipelineTask:
|
|||||||
await self._pipeline.cleanup()
|
await self._pipeline.cleanup()
|
||||||
# We just enqueue None to terminate the task gracefully.
|
# We just enqueue None to terminate the task gracefully.
|
||||||
self._process_up_task.cancel()
|
self._process_up_task.cancel()
|
||||||
|
await self._process_up_task
|
||||||
|
|
||||||
async def _process_up_queue(self):
|
async def _process_up_queue(self):
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -146,10 +146,11 @@ class AzureSTTService(AIService):
|
|||||||
async def cancel(self, frame: CancelFrame):
|
async def cancel(self, frame: CancelFrame):
|
||||||
self._speech_recognizer.stop_continuous_recognition_async()
|
self._speech_recognizer.stop_continuous_recognition_async()
|
||||||
self._push_frame_task.cancel()
|
self._push_frame_task.cancel()
|
||||||
|
await self._push_frame_task
|
||||||
|
|
||||||
def _create_push_task(self):
|
def _create_push_task(self):
|
||||||
self._push_frame_task = self.get_event_loop().create_task(self._push_frame_task_handler())
|
|
||||||
self._push_queue = asyncio.Queue()
|
self._push_queue = asyncio.Queue()
|
||||||
|
self._push_frame_task = self.get_event_loop().create_task(self._push_frame_task_handler())
|
||||||
|
|
||||||
async def _push_frame_task_handler(self):
|
async def _push_frame_task_handler(self):
|
||||||
running = True
|
running = True
|
||||||
|
|||||||
@@ -134,10 +134,11 @@ class DeepgramSTTService(AIService):
|
|||||||
async def cancel(self, frame: CancelFrame):
|
async def cancel(self, frame: CancelFrame):
|
||||||
await self._connection.finish()
|
await self._connection.finish()
|
||||||
self._push_frame_task.cancel()
|
self._push_frame_task.cancel()
|
||||||
|
await self._push_frame_task
|
||||||
|
|
||||||
def _create_push_task(self):
|
def _create_push_task(self):
|
||||||
self._push_frame_task = self.get_event_loop().create_task(self._push_frame_task_handler())
|
|
||||||
self._push_queue = asyncio.Queue()
|
self._push_queue = asyncio.Queue()
|
||||||
|
self._push_frame_task = self.get_event_loop().create_task(self._push_frame_task_handler())
|
||||||
|
|
||||||
async def _push_frame_task_handler(self):
|
async def _push_frame_task_handler(self):
|
||||||
running = True
|
running = True
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import queue
|
|
||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
@@ -33,8 +32,6 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
|
|
||||||
self._params = params
|
self._params = params
|
||||||
|
|
||||||
self._running = False
|
|
||||||
|
|
||||||
self._executor = ThreadPoolExecutor(max_workers=5)
|
self._executor = ThreadPoolExecutor(max_workers=5)
|
||||||
|
|
||||||
# Create push frame task. This is the task that will push frames in
|
# Create push frame task. This is the task that will push frames in
|
||||||
@@ -42,34 +39,21 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
self._create_push_task()
|
self._create_push_task()
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
if self._running:
|
# Create audio input queue and task if needed.
|
||||||
return
|
|
||||||
|
|
||||||
self._running = True
|
|
||||||
|
|
||||||
# Create audio input queue and thread if needed.
|
|
||||||
if self._params.audio_in_enabled or self._params.vad_enabled:
|
if self._params.audio_in_enabled or self._params.vad_enabled:
|
||||||
self._audio_in_queue = queue.Queue()
|
self._audio_in_queue = asyncio.Queue()
|
||||||
self._audio_thread = self._loop.run_in_executor(
|
self._audio_task = self.get_event_loop().create_task(self._audio_task_handler())
|
||||||
self._executor, self._audio_thread_handler)
|
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
if not self._running:
|
# Wait for the task to finish.
|
||||||
return
|
|
||||||
|
|
||||||
# This will exit all threads.
|
|
||||||
self._running = False
|
|
||||||
|
|
||||||
# Wait for the threads to finish.
|
|
||||||
if self._params.audio_in_enabled or self._params.vad_enabled:
|
if self._params.audio_in_enabled or self._params.vad_enabled:
|
||||||
await self._audio_thread
|
self._audio_task.cancel()
|
||||||
|
await self._audio_task
|
||||||
self._push_frame_task.cancel()
|
|
||||||
|
|
||||||
def vad_analyzer(self) -> VADAnalyzer | None:
|
def vad_analyzer(self) -> VADAnalyzer | None:
|
||||||
return self._params.vad_analyzer
|
return self._params.vad_analyzer
|
||||||
|
|
||||||
def push_audio_frame(self, frame: AudioRawFrame):
|
async def push_audio_frame(self, frame: AudioRawFrame):
|
||||||
if self._params.audio_in_enabled or self._params.vad_enabled:
|
if self._params.audio_in_enabled or self._params.vad_enabled:
|
||||||
self._audio_in_queue.put_nowait(frame)
|
self._audio_in_queue.put_nowait(frame)
|
||||||
|
|
||||||
@@ -78,7 +62,8 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
#
|
#
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
pass
|
self._push_frame_task.cancel()
|
||||||
|
await self._push_frame_task
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
@@ -102,8 +87,8 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
|
|
||||||
def _create_push_task(self):
|
def _create_push_task(self):
|
||||||
loop = self.get_event_loop()
|
loop = self.get_event_loop()
|
||||||
self._push_frame_task = loop.create_task(self._push_frame_task_handler())
|
|
||||||
self._push_queue = asyncio.Queue()
|
self._push_queue = asyncio.Queue()
|
||||||
|
self._push_frame_task = loop.create_task(self._push_frame_task_handler())
|
||||||
|
|
||||||
async def _internal_push_frame(
|
async def _internal_push_frame(
|
||||||
self,
|
self,
|
||||||
@@ -129,6 +114,7 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
if isinstance(frame, UserStartedSpeakingFrame):
|
if isinstance(frame, UserStartedSpeakingFrame):
|
||||||
logger.debug("User started speaking")
|
logger.debug("User started speaking")
|
||||||
self._push_frame_task.cancel()
|
self._push_frame_task.cancel()
|
||||||
|
await self._push_frame_task
|
||||||
self._create_push_task()
|
self._create_push_task()
|
||||||
await self.push_frame(StartInterruptionFrame())
|
await self.push_frame(StartInterruptionFrame())
|
||||||
elif isinstance(frame, UserStoppedSpeakingFrame):
|
elif isinstance(frame, UserStoppedSpeakingFrame):
|
||||||
@@ -140,15 +126,16 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
# Audio input
|
# Audio input
|
||||||
#
|
#
|
||||||
|
|
||||||
def _vad_analyze(self, audio_frames: bytes) -> VADState:
|
async def _vad_analyze(self, audio_frames: bytes) -> VADState:
|
||||||
state = VADState.QUIET
|
state = VADState.QUIET
|
||||||
vad_analyzer = self.vad_analyzer()
|
vad_analyzer = self.vad_analyzer()
|
||||||
if vad_analyzer:
|
if vad_analyzer:
|
||||||
state = vad_analyzer.analyze_audio(audio_frames)
|
state = await self.get_event_loop().run_in_executor(
|
||||||
|
self._executor, vad_analyzer.analyze_audio, audio_frames)
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def _handle_vad(self, audio_frames: bytes, vad_state: VADState):
|
async def _handle_vad(self, audio_frames: bytes, vad_state: VADState):
|
||||||
new_vad_state = self._vad_analyze(audio_frames)
|
new_vad_state = await self._vad_analyze(audio_frames)
|
||||||
if new_vad_state != vad_state and new_vad_state != VADState.STARTING and new_vad_state != VADState.STOPPING:
|
if new_vad_state != vad_state and new_vad_state != VADState.STARTING and new_vad_state != VADState.STOPPING:
|
||||||
frame = None
|
frame = None
|
||||||
if new_vad_state == VADState.SPEAKING:
|
if new_vad_state == VADState.SPEAKING:
|
||||||
@@ -157,33 +144,29 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
frame = UserStoppedSpeakingFrame()
|
frame = UserStoppedSpeakingFrame()
|
||||||
|
|
||||||
if frame:
|
if frame:
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
await self._handle_interruptions(frame)
|
||||||
self._handle_interruptions(frame), self.get_event_loop())
|
|
||||||
future.result()
|
|
||||||
|
|
||||||
vad_state = new_vad_state
|
vad_state = new_vad_state
|
||||||
return vad_state
|
return vad_state
|
||||||
|
|
||||||
def _audio_thread_handler(self):
|
async def _audio_task_handler(self):
|
||||||
vad_state: VADState = VADState.QUIET
|
vad_state: VADState = VADState.QUIET
|
||||||
while self._running:
|
while True:
|
||||||
try:
|
try:
|
||||||
frame: AudioRawFrame = self._audio_in_queue.get(timeout=1)
|
frame: AudioRawFrame = await self._audio_in_queue.get()
|
||||||
|
|
||||||
audio_passthrough = True
|
audio_passthrough = True
|
||||||
|
|
||||||
# Check VAD and push event if necessary. We just care about
|
# Check VAD and push event if necessary. We just care about
|
||||||
# changes from QUIET to SPEAKING and vice versa.
|
# changes from QUIET to SPEAKING and vice versa.
|
||||||
if self._params.vad_enabled:
|
if self._params.vad_enabled:
|
||||||
vad_state = self._handle_vad(frame.audio, vad_state)
|
vad_state = await self._handle_vad(frame.audio, vad_state)
|
||||||
audio_passthrough = self._params.vad_audio_passthrough
|
audio_passthrough = self._params.vad_audio_passthrough
|
||||||
|
|
||||||
# Push audio downstream if passthrough.
|
# Push audio downstream if passthrough.
|
||||||
if audio_passthrough:
|
if audio_passthrough:
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
await self._internal_push_frame(frame)
|
||||||
self._internal_push_frame(frame), self._loop)
|
except asyncio.CancelledError:
|
||||||
future.result()
|
break
|
||||||
except queue.Empty:
|
|
||||||
pass
|
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
logger.error(f"{self} error reading audio frames: {e}")
|
logger.error(f"{self} error reading audio frames: {e}")
|
||||||
|
|||||||
@@ -7,11 +7,6 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import itertools
|
import itertools
|
||||||
import queue
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from typing import List
|
from typing import List
|
||||||
@@ -42,67 +37,51 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
|
|
||||||
self._params = params
|
self._params = params
|
||||||
|
|
||||||
self._running = False
|
|
||||||
|
|
||||||
self._executor = ThreadPoolExecutor(max_workers=5)
|
|
||||||
|
|
||||||
# These are the images that we should send to the camera at our desired
|
# These are the images that we should send to the camera at our desired
|
||||||
# framerate.
|
# framerate.
|
||||||
self._camera_images = None
|
self._camera_images = None
|
||||||
|
|
||||||
# Create media threads queues.
|
|
||||||
if self._params.camera_out_enabled:
|
|
||||||
self._camera_out_queue = queue.Queue()
|
|
||||||
self._sink_queue = queue.Queue()
|
|
||||||
self._sink_thread = None
|
|
||||||
|
|
||||||
self._stopped_event = asyncio.Event()
|
|
||||||
self._is_interrupted = threading.Event()
|
|
||||||
|
|
||||||
# We will write 20ms audio at a time. If we receive long audio frames we
|
# We will write 20ms audio at a time. If we receive long audio frames we
|
||||||
# will chunk them. This will help with interruption handling.
|
# will chunk them. This will help with interruption handling.
|
||||||
audio_bytes_10ms = int(self._params.audio_out_sample_rate / 100) * \
|
audio_bytes_10ms = int(self._params.audio_out_sample_rate / 100) * \
|
||||||
self._params.audio_out_channels * 2
|
self._params.audio_out_channels * 2
|
||||||
self._audio_chunk_size = audio_bytes_10ms * 2
|
self._audio_chunk_size = audio_bytes_10ms * 2
|
||||||
|
|
||||||
|
self._stopped_event = asyncio.Event()
|
||||||
|
|
||||||
|
# Create sink frame task. This is the task that will actually write
|
||||||
|
# audio or video frames. We write audio/video in a task so we can keep
|
||||||
|
# generating frames upstream while, for example, the audio is playing.
|
||||||
|
self._create_sink_task()
|
||||||
|
|
||||||
# Create push frame task. This is the task that will push frames in
|
# 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.
|
# order. We also guarantee that all frames are pushed in the same task.
|
||||||
self._create_push_task()
|
self._create_push_task()
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
if self._running:
|
# Create media threads queues.
|
||||||
return
|
|
||||||
|
|
||||||
self._running = True
|
|
||||||
|
|
||||||
loop = self.get_event_loop()
|
|
||||||
|
|
||||||
# Create queues and threads.
|
|
||||||
if self._params.camera_out_enabled:
|
if self._params.camera_out_enabled:
|
||||||
self._camera_out_thread = loop.run_in_executor(
|
self._camera_out_queue = asyncio.Queue()
|
||||||
self._executor, self._camera_out_thread_handler)
|
self._camera_out_task = self.get_event_loop().create_task(self._camera_out_task_handler())
|
||||||
|
|
||||||
self._sink_thread = loop.run_in_executor(self._executor, self._sink_thread_handler)
|
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
if not self._running:
|
# Wait on the threads to finish.
|
||||||
return
|
if self._params.camera_out_enabled:
|
||||||
|
self._camera_out_task.cancel()
|
||||||
# This will exit all threads.
|
await self._camera_out_task
|
||||||
self._running = False
|
|
||||||
|
|
||||||
self._stopped_event.set()
|
self._stopped_event.set()
|
||||||
|
|
||||||
def send_message(self, frame: TransportMessageFrame):
|
async def send_message(self, frame: TransportMessageFrame):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def send_metrics(self, frame: MetricsFrame):
|
async def send_metrics(self, frame: MetricsFrame):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def write_frame_to_camera(self, frame: ImageRawFrame):
|
async def write_frame_to_camera(self, frame: ImageRawFrame):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def write_raw_audio_frames(self, frames: bytes):
|
async def write_raw_audio_frames(self, frames: bytes):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -110,12 +89,12 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
#
|
#
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
# Wait on the threads to finish.
|
if self._sink_task:
|
||||||
if self._params.camera_out_enabled:
|
self._sink_task.cancel()
|
||||||
await self._camera_out_thread
|
await self._sink_task
|
||||||
|
|
||||||
if self._sink_thread:
|
self._push_frame_task.cancel()
|
||||||
await self._sink_thread
|
await self._push_frame_task
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
@@ -128,7 +107,7 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
if isinstance(frame, StartFrame):
|
if isinstance(frame, StartFrame):
|
||||||
await self.start(frame)
|
await self.start(frame)
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
# EndFrame is managed in the queue handler.
|
# EndFrame is managed in the sink queue handler.
|
||||||
elif isinstance(frame, CancelFrame):
|
elif isinstance(frame, CancelFrame):
|
||||||
await self.stop()
|
await self.stop()
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
@@ -136,14 +115,14 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
await self._handle_interruptions(frame)
|
await self._handle_interruptions(frame)
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
elif isinstance(frame, MetricsFrame):
|
elif isinstance(frame, MetricsFrame):
|
||||||
self.send_metrics(frame)
|
await self.send_metrics(frame)
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
elif isinstance(frame, SystemFrame):
|
elif isinstance(frame, SystemFrame):
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
elif isinstance(frame, AudioRawFrame):
|
elif isinstance(frame, AudioRawFrame):
|
||||||
await self._handle_audio(frame)
|
await self._handle_audio(frame)
|
||||||
else:
|
else:
|
||||||
self._sink_queue.put_nowait(frame)
|
await self._sink_queue.put(frame)
|
||||||
|
|
||||||
# If we are finishing, wait here until we have stopped, otherwise we might
|
# If we are finishing, wait here until we have stopped, otherwise we might
|
||||||
# close things too early upstream. We need this event because we don't
|
# close things too early upstream. We need this event because we don't
|
||||||
@@ -156,50 +135,51 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(frame, StartInterruptionFrame):
|
if isinstance(frame, StartInterruptionFrame):
|
||||||
self._is_interrupted.set()
|
# Stop sink task.
|
||||||
|
self._sink_task.cancel()
|
||||||
|
await self._sink_task
|
||||||
|
self._create_sink_task()
|
||||||
|
# Stop push task.
|
||||||
self._push_frame_task.cancel()
|
self._push_frame_task.cancel()
|
||||||
|
await self._push_frame_task
|
||||||
self._create_push_task()
|
self._create_push_task()
|
||||||
elif isinstance(frame, StopInterruptionFrame):
|
|
||||||
self._is_interrupted.clear()
|
|
||||||
|
|
||||||
async def _handle_audio(self, frame: AudioRawFrame):
|
async def _handle_audio(self, frame: AudioRawFrame):
|
||||||
audio = frame.audio
|
audio = frame.audio
|
||||||
for i in range(0, len(audio), self._audio_chunk_size):
|
for i in range(0, len(audio), self._audio_chunk_size):
|
||||||
chunk = AudioRawFrame(audio[i: i + self._audio_chunk_size],
|
chunk = AudioRawFrame(audio[i: i + self._audio_chunk_size],
|
||||||
sample_rate=frame.sample_rate, num_channels=frame.num_channels)
|
sample_rate=frame.sample_rate, num_channels=frame.num_channels)
|
||||||
self._sink_queue.put_nowait(chunk)
|
await self._sink_queue.put(chunk)
|
||||||
|
|
||||||
def _sink_thread_handler(self):
|
def _create_sink_task(self):
|
||||||
|
loop = self.get_event_loop()
|
||||||
|
self._sink_queue = asyncio.Queue()
|
||||||
|
self._sink_task = loop.create_task(self._sink_task_handler())
|
||||||
|
|
||||||
|
async def _sink_task_handler(self):
|
||||||
# Audio accumlation buffer
|
# Audio accumlation buffer
|
||||||
buffer = bytearray()
|
buffer = bytearray()
|
||||||
while self._running:
|
while True:
|
||||||
try:
|
try:
|
||||||
frame = self._sink_queue.get(timeout=1)
|
frame = await self._sink_queue.get()
|
||||||
if not self._is_interrupted.is_set():
|
if isinstance(frame, AudioRawFrame) and self._params.audio_out_enabled:
|
||||||
if isinstance(frame, AudioRawFrame) and self._params.audio_out_enabled:
|
buffer.extend(frame.audio)
|
||||||
buffer.extend(frame.audio)
|
buffer = await self._maybe_send_audio(buffer)
|
||||||
buffer = self._maybe_send_audio(buffer)
|
elif isinstance(frame, ImageRawFrame) and self._params.camera_out_enabled:
|
||||||
elif isinstance(frame, ImageRawFrame) and self._params.camera_out_enabled:
|
await self._set_camera_image(frame)
|
||||||
self._set_camera_image(frame)
|
elif isinstance(frame, SpriteFrame) and self._params.camera_out_enabled:
|
||||||
elif isinstance(frame, SpriteFrame) and self._params.camera_out_enabled:
|
await self._set_camera_images(frame.images)
|
||||||
self._set_camera_images(frame.images)
|
elif isinstance(frame, TransportMessageFrame):
|
||||||
elif isinstance(frame, TransportMessageFrame):
|
await self.send_message(frame)
|
||||||
self.send_message(frame)
|
|
||||||
else:
|
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
|
||||||
self._internal_push_frame(frame), self.get_event_loop())
|
|
||||||
future.result()
|
|
||||||
else:
|
else:
|
||||||
# If we get interrupted just clear the output buffer.
|
await self._internal_push_frame(frame)
|
||||||
buffer = bytearray()
|
|
||||||
|
|
||||||
if isinstance(frame, EndFrame):
|
if isinstance(frame, EndFrame):
|
||||||
future = asyncio.run_coroutine_threadsafe(self.stop(), self.get_event_loop())
|
await self.stop()
|
||||||
future.result()
|
|
||||||
|
|
||||||
self._sink_queue.task_done()
|
self._sink_queue.task_done()
|
||||||
except queue.Empty:
|
except asyncio.CancelledError:
|
||||||
pass
|
break
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
logger.error(f"{self} error processing sink queue: {e}")
|
logger.error(f"{self} error processing sink queue: {e}")
|
||||||
|
|
||||||
@@ -209,8 +189,8 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
|
|
||||||
def _create_push_task(self):
|
def _create_push_task(self):
|
||||||
loop = self.get_event_loop()
|
loop = self.get_event_loop()
|
||||||
self._push_frame_task = loop.create_task(self._push_frame_task_handler())
|
|
||||||
self._push_queue = asyncio.Queue()
|
self._push_queue = asyncio.Queue()
|
||||||
|
self._push_frame_task = loop.create_task(self._push_frame_task_handler())
|
||||||
|
|
||||||
async def _internal_push_frame(
|
async def _internal_push_frame(
|
||||||
self,
|
self,
|
||||||
@@ -233,7 +213,7 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
async def send_image(self, frame: ImageRawFrame | SpriteFrame):
|
async def send_image(self, frame: ImageRawFrame | SpriteFrame):
|
||||||
await self.process_frame(frame, FrameDirection.DOWNSTREAM)
|
await self.process_frame(frame, FrameDirection.DOWNSTREAM)
|
||||||
|
|
||||||
def _draw_image(self, frame: ImageRawFrame):
|
async def _draw_image(self, frame: ImageRawFrame):
|
||||||
desired_size = (self._params.camera_out_width, self._params.camera_out_height)
|
desired_size = (self._params.camera_out_width, self._params.camera_out_height)
|
||||||
|
|
||||||
if frame.size != desired_size:
|
if frame.size != desired_size:
|
||||||
@@ -243,32 +223,32 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
f"{frame} does not have the expected size {desired_size}, resizing")
|
f"{frame} does not have the expected size {desired_size}, resizing")
|
||||||
frame = ImageRawFrame(resized_image.tobytes(), resized_image.size, resized_image.format)
|
frame = ImageRawFrame(resized_image.tobytes(), resized_image.size, resized_image.format)
|
||||||
|
|
||||||
self.write_frame_to_camera(frame)
|
await self.write_frame_to_camera(frame)
|
||||||
|
|
||||||
def _set_camera_image(self, image: ImageRawFrame):
|
async def _set_camera_image(self, image: ImageRawFrame):
|
||||||
if self._params.camera_out_is_live:
|
if self._params.camera_out_is_live:
|
||||||
self._camera_out_queue.put_nowait(image)
|
await self._camera_out_queue.put(image)
|
||||||
else:
|
else:
|
||||||
self._camera_images = itertools.cycle([image])
|
self._camera_images = itertools.cycle([image])
|
||||||
|
|
||||||
def _set_camera_images(self, images: List[ImageRawFrame]):
|
async def _set_camera_images(self, images: List[ImageRawFrame]):
|
||||||
self._camera_images = itertools.cycle(images)
|
self._camera_images = itertools.cycle(images)
|
||||||
|
|
||||||
def _camera_out_thread_handler(self):
|
async def _camera_out_task_handler(self):
|
||||||
while self._running:
|
while True:
|
||||||
try:
|
try:
|
||||||
if self._params.camera_out_is_live:
|
if self._params.camera_out_is_live:
|
||||||
image = self._camera_out_queue.get(timeout=1)
|
image = await self._camera_out_queue.get()
|
||||||
self._draw_image(image)
|
await self._draw_image(image)
|
||||||
self._camera_out_queue.task_done()
|
self._camera_out_queue.task_done()
|
||||||
elif self._camera_images:
|
elif self._camera_images:
|
||||||
image = next(self._camera_images)
|
image = next(self._camera_images)
|
||||||
self._draw_image(image)
|
await self._draw_image(image)
|
||||||
time.sleep(1.0 / self._params.camera_out_framerate)
|
await asyncio.sleep(1.0 / self._params.camera_out_framerate)
|
||||||
else:
|
else:
|
||||||
time.sleep(1.0 / self._params.camera_out_framerate)
|
await asyncio.sleep(1.0 / self._params.camera_out_framerate)
|
||||||
except queue.Empty:
|
except asyncio.CancelledError:
|
||||||
pass
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"{self} error writing to camera: {e}")
|
logger.error(f"{self} error writing to camera: {e}")
|
||||||
|
|
||||||
@@ -279,12 +259,8 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
async def send_audio(self, frame: AudioRawFrame):
|
async def send_audio(self, frame: AudioRawFrame):
|
||||||
await self.process_frame(frame, FrameDirection.DOWNSTREAM)
|
await self.process_frame(frame, FrameDirection.DOWNSTREAM)
|
||||||
|
|
||||||
def _maybe_send_audio(self, buffer: bytearray) -> bytearray:
|
async def _maybe_send_audio(self, buffer: bytearray) -> bytearray:
|
||||||
try:
|
if len(buffer) >= self._audio_chunk_size:
|
||||||
if len(buffer) >= self._audio_chunk_size:
|
await self.write_raw_audio_frames(bytes(buffer[:self._audio_chunk_size]))
|
||||||
self.write_raw_audio_frames(bytes(buffer[:self._audio_chunk_size]))
|
buffer = buffer[self._audio_chunk_size:]
|
||||||
buffer = buffer[self._audio_chunk_size:]
|
return buffer
|
||||||
return buffer
|
|
||||||
except BaseException as e:
|
|
||||||
logger.error(f"{self} error writing audio frames: {e}")
|
|
||||||
return buffer
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
from pipecat.frames.frames import AudioRawFrame, StartFrame
|
from pipecat.frames.frames import AudioRawFrame, StartFrame
|
||||||
from pipecat.processors.frame_processor import FrameProcessor
|
from pipecat.processors.frame_processor import FrameProcessor
|
||||||
from pipecat.transports.base_input import BaseInputTransport
|
from pipecat.transports.base_input import BaseInputTransport
|
||||||
@@ -43,26 +45,20 @@ class LocalAudioInputTransport(BaseInputTransport):
|
|||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
self._in_stream.start_stream()
|
self._in_stream.start_stream()
|
||||||
|
|
||||||
async def stop(self):
|
|
||||||
await super().stop()
|
|
||||||
self._in_stream.stop_stream()
|
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
|
await super().cleanup()
|
||||||
|
self._in_stream.stop_stream()
|
||||||
# This is not very pretty (taken from PyAudio docs).
|
# This is not very pretty (taken from PyAudio docs).
|
||||||
while self._in_stream.is_active():
|
while self._in_stream.is_active():
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
self._in_stream.close()
|
self._in_stream.close()
|
||||||
|
|
||||||
await super().cleanup()
|
|
||||||
|
|
||||||
def _audio_in_callback(self, in_data, frame_count, time_info, status):
|
def _audio_in_callback(self, in_data, frame_count, time_info, status):
|
||||||
if not self._running:
|
|
||||||
return (None, pyaudio.paAbort)
|
|
||||||
|
|
||||||
frame = AudioRawFrame(audio=in_data,
|
frame = AudioRawFrame(audio=in_data,
|
||||||
sample_rate=self._params.audio_in_sample_rate,
|
sample_rate=self._params.audio_in_sample_rate,
|
||||||
num_channels=self._params.audio_in_channels)
|
num_channels=self._params.audio_in_channels)
|
||||||
self.push_audio_frame(frame)
|
|
||||||
|
asyncio.run_coroutine_threadsafe(self.push_audio_frame(frame), self.get_event_loop())
|
||||||
|
|
||||||
return (None, pyaudio.paContinue)
|
return (None, pyaudio.paContinue)
|
||||||
|
|
||||||
@@ -72,19 +68,29 @@ class LocalAudioOutputTransport(BaseOutputTransport):
|
|||||||
def __init__(self, py_audio: pyaudio.PyAudio, params: TransportParams):
|
def __init__(self, py_audio: pyaudio.PyAudio, params: TransportParams):
|
||||||
super().__init__(params)
|
super().__init__(params)
|
||||||
|
|
||||||
|
self._executor = ThreadPoolExecutor(max_workers=5)
|
||||||
|
|
||||||
self._out_stream = py_audio.open(
|
self._out_stream = py_audio.open(
|
||||||
format=py_audio.get_format_from_width(2),
|
format=py_audio.get_format_from_width(2),
|
||||||
channels=params.audio_out_channels,
|
channels=params.audio_out_channels,
|
||||||
rate=params.audio_out_sample_rate,
|
rate=params.audio_out_sample_rate,
|
||||||
output=True)
|
output=True)
|
||||||
|
|
||||||
def write_raw_audio_frames(self, frames: bytes):
|
async def start(self, frame: StartFrame):
|
||||||
self._out_stream.write(frames)
|
await super().start(frame)
|
||||||
|
self._out_stream.start_stream()
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
await super().cleanup()
|
await super().cleanup()
|
||||||
|
self._out_stream.stop_stream()
|
||||||
|
# This is not very pretty (taken from PyAudio docs).
|
||||||
|
while self._out_stream.is_active():
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
self._out_stream.close()
|
self._out_stream.close()
|
||||||
|
|
||||||
|
async def write_raw_audio_frames(self, frames: bytes):
|
||||||
|
await self.get_event_loop().run_in_executor(self._executor, self._out_stream.write, frames)
|
||||||
|
|
||||||
|
|
||||||
class LocalAudioTransport(BaseTransport):
|
class LocalAudioTransport(BaseTransport):
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
|
||||||
@@ -53,25 +55,20 @@ class TkInputTransport(BaseInputTransport):
|
|||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
self._in_stream.start_stream()
|
self._in_stream.start_stream()
|
||||||
|
|
||||||
async def stop(self):
|
|
||||||
await super().stop()
|
|
||||||
self._in_stream.stop_stream()
|
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
await super().cleanup()
|
await super().cleanup()
|
||||||
|
self._in_stream.stop_stream()
|
||||||
# This is not very pretty (taken from PyAudio docs).
|
# This is not very pretty (taken from PyAudio docs).
|
||||||
while self._in_stream.is_active():
|
while self._in_stream.is_active():
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
self._in_stream.close()
|
self._in_stream.close()
|
||||||
|
|
||||||
def _audio_in_callback(self, in_data, frame_count, time_info, status):
|
def _audio_in_callback(self, in_data, frame_count, time_info, status):
|
||||||
if not self._running:
|
|
||||||
return (None, pyaudio.paAbort)
|
|
||||||
|
|
||||||
frame = AudioRawFrame(audio=in_data,
|
frame = AudioRawFrame(audio=in_data,
|
||||||
sample_rate=self._params.audio_in_sample_rate,
|
sample_rate=self._params.audio_in_sample_rate,
|
||||||
num_channels=self._params.audio_in_channels)
|
num_channels=self._params.audio_in_channels)
|
||||||
self.push_audio_frame(frame)
|
|
||||||
|
asyncio.run_coroutine_threadsafe(self.push_audio_frame(frame), self.get_event_loop())
|
||||||
|
|
||||||
return (None, pyaudio.paContinue)
|
return (None, pyaudio.paContinue)
|
||||||
|
|
||||||
@@ -81,6 +78,8 @@ class TkOutputTransport(BaseOutputTransport):
|
|||||||
def __init__(self, tk_root: tk.Tk, py_audio: pyaudio.PyAudio, params: TransportParams):
|
def __init__(self, tk_root: tk.Tk, py_audio: pyaudio.PyAudio, params: TransportParams):
|
||||||
super().__init__(params)
|
super().__init__(params)
|
||||||
|
|
||||||
|
self._executor = ThreadPoolExecutor(max_workers=5)
|
||||||
|
|
||||||
self._out_stream = py_audio.open(
|
self._out_stream = py_audio.open(
|
||||||
format=py_audio.get_format_from_width(2),
|
format=py_audio.get_format_from_width(2),
|
||||||
channels=params.audio_out_channels,
|
channels=params.audio_out_channels,
|
||||||
@@ -94,16 +93,24 @@ class TkOutputTransport(BaseOutputTransport):
|
|||||||
self._image_label = tk.Label(tk_root, image=photo)
|
self._image_label = tk.Label(tk_root, image=photo)
|
||||||
self._image_label.pack()
|
self._image_label.pack()
|
||||||
|
|
||||||
def write_raw_audio_frames(self, frames: bytes):
|
async def start(self, frame: StartFrame):
|
||||||
self._out_stream.write(frames)
|
await super().start(frame)
|
||||||
|
self._out_stream.start_stream()
|
||||||
def write_frame_to_camera(self, frame: ImageRawFrame):
|
|
||||||
self.get_event_loop().call_soon(self._write_frame_to_tk, frame)
|
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
await super().cleanup()
|
await super().cleanup()
|
||||||
|
self._out_stream.stop_stream()
|
||||||
|
# This is not very pretty (taken from PyAudio docs).
|
||||||
|
while self._out_stream.is_active():
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
self._out_stream.close()
|
self._out_stream.close()
|
||||||
|
|
||||||
|
async def write_raw_audio_frames(self, frames: bytes):
|
||||||
|
await self.get_event_loop().run_in_executor(self._executor, self._out_stream.write, frames)
|
||||||
|
|
||||||
|
async def write_frame_to_camera(self, frame: ImageRawFrame):
|
||||||
|
self.get_event_loop().call_soon(self._write_frame_to_tk, frame)
|
||||||
|
|
||||||
def _write_frame_to_tk(self, frame: ImageRawFrame):
|
def _write_frame_to_tk(self, frame: ImageRawFrame):
|
||||||
width = frame.size[0]
|
width = frame.size[0]
|
||||||
height = frame.size[1]
|
height = frame.size[1]
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class FastAPIWebsocketInputTransport(BaseInputTransport):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(frame, AudioRawFrame):
|
if isinstance(frame, AudioRawFrame):
|
||||||
self.push_audio_frame(frame)
|
await self.push_audio_frame(frame)
|
||||||
|
|
||||||
await self._callbacks.on_client_disconnected(self._websocket)
|
await self._callbacks.on_client_disconnected(self._websocket)
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ class FastAPIWebsocketOutputTransport(BaseOutputTransport):
|
|||||||
self._params = params
|
self._params = params
|
||||||
self._audio_buffer = bytes()
|
self._audio_buffer = bytes()
|
||||||
|
|
||||||
def write_raw_audio_frames(self, frames: bytes):
|
async def write_raw_audio_frames(self, frames: bytes):
|
||||||
self._audio_buffer += frames
|
self._audio_buffer += frames
|
||||||
while len(self._audio_buffer) >= self._params.audio_frame_size:
|
while len(self._audio_buffer) >= self._params.audio_frame_size:
|
||||||
frame = AudioRawFrame(
|
frame = AudioRawFrame(
|
||||||
@@ -115,9 +115,7 @@ class FastAPIWebsocketOutputTransport(BaseOutputTransport):
|
|||||||
|
|
||||||
payload = self._params.serializer.serialize(frame)
|
payload = self._params.serializer.serialize(frame)
|
||||||
if payload:
|
if payload:
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
await self._websocket.send_text(payload)
|
||||||
self._websocket.send_text(payload), self.get_event_loop())
|
|
||||||
future.result()
|
|
||||||
|
|
||||||
self._audio_buffer = self._audio_buffer[self._params.audio_frame_size:]
|
self._audio_buffer = self._audio_buffer[self._params.audio_frame_size:]
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class WebsocketServerInputTransport(BaseInputTransport):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(frame, AudioRawFrame):
|
if isinstance(frame, AudioRawFrame):
|
||||||
self.push_audio_frame(frame)
|
await self.push_audio_frame(frame)
|
||||||
else:
|
else:
|
||||||
await self._internal_push_frame(frame)
|
await self._internal_push_frame(frame)
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ class WebsocketServerOutputTransport(BaseOutputTransport):
|
|||||||
logger.warning("Only one client allowed, using new connection")
|
logger.warning("Only one client allowed, using new connection")
|
||||||
self._websocket = websocket
|
self._websocket = websocket
|
||||||
|
|
||||||
def write_raw_audio_frames(self, frames: bytes):
|
async def write_raw_audio_frames(self, frames: bytes):
|
||||||
self._audio_buffer += frames
|
self._audio_buffer += frames
|
||||||
while len(self._audio_buffer) >= self._params.audio_frame_size:
|
while len(self._audio_buffer) >= self._params.audio_frame_size:
|
||||||
frame = AudioRawFrame(
|
frame = AudioRawFrame(
|
||||||
@@ -149,9 +149,7 @@ class WebsocketServerOutputTransport(BaseOutputTransport):
|
|||||||
|
|
||||||
proto = self._params.serializer.serialize(frame)
|
proto = self._params.serializer.serialize(frame)
|
||||||
|
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
await self._websocket.send(proto)
|
||||||
self._websocket.send(proto), self.get_event_loop())
|
|
||||||
future.result()
|
|
||||||
|
|
||||||
self._audio_buffer = self._audio_buffer[self._params.audio_frame_size:]
|
self._audio_buffer = self._audio_buffer[self._params.audio_frame_size:]
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,10 @@
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
import asyncio
|
||||||
import queue
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Callable, Mapping
|
from typing import Any, Awaitable, Callable, Mapping
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
from daily import (
|
from daily import (
|
||||||
@@ -108,20 +107,27 @@ class DailyParams(TransportParams):
|
|||||||
|
|
||||||
|
|
||||||
class DailyCallbacks(BaseModel):
|
class DailyCallbacks(BaseModel):
|
||||||
on_joined: Callable[[Mapping[str, Any]], None]
|
on_joined: Callable[[Mapping[str, Any]], Awaitable[None]]
|
||||||
on_left: Callable[[], None]
|
on_left: Callable[[], Awaitable[None]]
|
||||||
on_error: Callable[[str], None]
|
on_error: Callable[[str], Awaitable[None]]
|
||||||
on_app_message: Callable[[Any, str], None]
|
on_app_message: Callable[[Any, str], Awaitable[None]]
|
||||||
on_call_state_updated: Callable[[str], None]
|
on_call_state_updated: Callable[[str], Awaitable[None]]
|
||||||
on_dialin_ready: Callable[[str], None]
|
on_dialin_ready: Callable[[str], Awaitable[None]]
|
||||||
on_dialout_answered: Callable[[Any], None]
|
on_dialout_answered: Callable[[Any], Awaitable[None]]
|
||||||
on_dialout_connected: Callable[[Any], None]
|
on_dialout_connected: Callable[[Any], Awaitable[None]]
|
||||||
on_dialout_stopped: Callable[[Any], None]
|
on_dialout_stopped: Callable[[Any], Awaitable[None]]
|
||||||
on_dialout_error: Callable[[Any], None]
|
on_dialout_error: Callable[[Any], Awaitable[None]]
|
||||||
on_dialout_warning: Callable[[Any], None]
|
on_dialout_warning: Callable[[Any], Awaitable[None]]
|
||||||
on_first_participant_joined: Callable[[Mapping[str, Any]], None]
|
on_first_participant_joined: Callable[[Mapping[str, Any]], Awaitable[None]]
|
||||||
on_participant_joined: Callable[[Mapping[str, Any]], None]
|
on_participant_joined: Callable[[Mapping[str, Any]], Awaitable[None]]
|
||||||
on_participant_left: Callable[[Mapping[str, Any], str], None]
|
on_participant_left: Callable[[Mapping[str, Any], str], Awaitable[None]]
|
||||||
|
|
||||||
|
|
||||||
|
def completion_callback(future):
|
||||||
|
def _callback(*args):
|
||||||
|
if not future.cancelled():
|
||||||
|
future.get_loop().call_soon_threadsafe(future.set_result, *args)
|
||||||
|
return _callback
|
||||||
|
|
||||||
|
|
||||||
class DailyTransportClient(EventHandler):
|
class DailyTransportClient(EventHandler):
|
||||||
@@ -161,7 +167,6 @@ class DailyTransportClient(EventHandler):
|
|||||||
self._joined = False
|
self._joined = False
|
||||||
self._joining = False
|
self._joining = False
|
||||||
self._leaving = False
|
self._leaving = False
|
||||||
self._sync_response = {k: queue.Queue() for k in ["join", "leave"]}
|
|
||||||
|
|
||||||
self._executor = ThreadPoolExecutor(max_workers=5)
|
self._executor = ThreadPoolExecutor(max_workers=5)
|
||||||
|
|
||||||
@@ -174,10 +179,16 @@ class DailyTransportClient(EventHandler):
|
|||||||
color_format=self._params.camera_out_color_format)
|
color_format=self._params.camera_out_color_format)
|
||||||
|
|
||||||
self._mic: VirtualMicrophoneDevice = Daily.create_microphone_device(
|
self._mic: VirtualMicrophoneDevice = Daily.create_microphone_device(
|
||||||
"mic", sample_rate=self._params.audio_out_sample_rate, channels=self._params.audio_out_channels)
|
"mic",
|
||||||
|
sample_rate=self._params.audio_out_sample_rate,
|
||||||
|
channels=self._params.audio_out_channels,
|
||||||
|
non_blocking=True)
|
||||||
|
|
||||||
self._speaker: VirtualSpeakerDevice = Daily.create_speaker_device(
|
self._speaker: VirtualSpeakerDevice = Daily.create_speaker_device(
|
||||||
"speaker", sample_rate=self._params.audio_in_sample_rate, channels=self._params.audio_in_channels)
|
"speaker",
|
||||||
|
sample_rate=self._params.audio_in_sample_rate,
|
||||||
|
channels=self._params.audio_in_channels,
|
||||||
|
non_blocking=True)
|
||||||
Daily.select_speaker_device("speaker")
|
Daily.select_speaker_device("speaker")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -187,30 +198,39 @@ class DailyTransportClient(EventHandler):
|
|||||||
def set_callbacks(self, callbacks: DailyCallbacks):
|
def set_callbacks(self, callbacks: DailyCallbacks):
|
||||||
self._callbacks = callbacks
|
self._callbacks = callbacks
|
||||||
|
|
||||||
def send_message(self, frame: DailyTransportMessageFrame):
|
async def send_message(self, frame: DailyTransportMessageFrame):
|
||||||
self._client.send_app_message(frame.message, frame.participant_id)
|
future = self._loop.create_future()
|
||||||
|
self._client.send_app_message(
|
||||||
|
frame.message,
|
||||||
|
frame.participant_id,
|
||||||
|
completion=completion_callback(future))
|
||||||
|
await future
|
||||||
|
|
||||||
def read_next_audio_frame(self) -> AudioRawFrame | None:
|
async def read_next_audio_frame(self) -> AudioRawFrame | None:
|
||||||
sample_rate = self._params.audio_in_sample_rate
|
sample_rate = self._params.audio_in_sample_rate
|
||||||
num_channels = self._params.audio_in_channels
|
num_channels = self._params.audio_in_channels
|
||||||
|
|
||||||
if self._other_participant_has_joined:
|
if self._other_participant_has_joined:
|
||||||
num_frames = int(sample_rate / 100) * 2 # 20ms of audio
|
num_frames = int(sample_rate / 100) * 2 # 20ms of audio
|
||||||
|
|
||||||
audio = self._speaker.read_frames(num_frames)
|
future = self._loop.create_future()
|
||||||
|
self._speaker.read_frames(num_frames, completion=completion_callback(future))
|
||||||
|
audio = await future
|
||||||
|
|
||||||
return AudioRawFrame(audio=audio, sample_rate=sample_rate, num_channels=num_channels)
|
return AudioRawFrame(audio=audio, sample_rate=sample_rate, num_channels=num_channels)
|
||||||
else:
|
else:
|
||||||
# If no one has ever joined the meeting `read_frames()` would block,
|
# If no one has ever joined the meeting `read_frames()` would block,
|
||||||
# instead we just wait a bit. daily-python should probably return
|
# instead we just wait a bit. daily-python should probably return
|
||||||
# silence instead.
|
# silence instead.
|
||||||
time.sleep(0.01)
|
await asyncio.sleep(0.01)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def write_raw_audio_frames(self, frames: bytes):
|
async def write_raw_audio_frames(self, frames: bytes):
|
||||||
self._mic.write_frames(frames)
|
future = self._loop.create_future()
|
||||||
|
self._mic.write_frames(frames, completion=completion_callback(future))
|
||||||
|
await future
|
||||||
|
|
||||||
def write_frame_to_camera(self, frame: ImageRawFrame):
|
async def write_frame_to_camera(self, frame: ImageRawFrame):
|
||||||
self._camera.write_frame(frame.image)
|
self._camera.write_frame(frame.image)
|
||||||
|
|
||||||
async def join(self):
|
async def join(self):
|
||||||
@@ -218,13 +238,10 @@ class DailyTransportClient(EventHandler):
|
|||||||
if self._joined or self._joining:
|
if self._joined or self._joining:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._joining = True
|
|
||||||
|
|
||||||
await self._loop.run_in_executor(self._executor, self._join)
|
|
||||||
|
|
||||||
def _join(self):
|
|
||||||
logger.info(f"Joining {self._room_url}")
|
logger.info(f"Joining {self._room_url}")
|
||||||
|
|
||||||
|
self._joining = True
|
||||||
|
|
||||||
# For performance reasons, never subscribe to video streams (unless a
|
# For performance reasons, never subscribe to video streams (unless a
|
||||||
# video renderer is registered).
|
# video renderer is registered).
|
||||||
self._client.update_subscription_profiles({
|
self._client.update_subscription_profiles({
|
||||||
@@ -236,10 +253,42 @@ class DailyTransportClient(EventHandler):
|
|||||||
|
|
||||||
self._client.set_user_name(self._bot_name)
|
self._client.set_user_name(self._bot_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
(data, error) = await self._join()
|
||||||
|
|
||||||
|
if not error:
|
||||||
|
self._joined = True
|
||||||
|
self._joining = False
|
||||||
|
|
||||||
|
logger.info(f"Joined {self._room_url}")
|
||||||
|
|
||||||
|
if self._token and self._params.transcription_enabled:
|
||||||
|
logger.info(
|
||||||
|
f"Enabling transcription with settings {self._params.transcription_settings}")
|
||||||
|
self._client.start_transcription(
|
||||||
|
self._params.transcription_settings.model_dump())
|
||||||
|
|
||||||
|
await self._callbacks.on_joined(data["participants"]["local"])
|
||||||
|
else:
|
||||||
|
error_msg = f"Error joining {self._room_url}: {error}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
await self._callbacks.on_error(error_msg)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
error_msg = f"Time out joining {self._room_url}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
await self._callbacks.on_error(error_msg)
|
||||||
|
|
||||||
|
async def _join(self):
|
||||||
|
future = self._loop.create_future()
|
||||||
|
|
||||||
|
def handle_join_response(data, error):
|
||||||
|
if not future.cancelled():
|
||||||
|
future.get_loop().call_soon_threadsafe(future.set_result, (data, error))
|
||||||
|
|
||||||
self._client.join(
|
self._client.join(
|
||||||
self._room_url,
|
self._room_url,
|
||||||
self._token,
|
self._token,
|
||||||
completion=self._call_joined,
|
completion=handle_join_response,
|
||||||
client_settings={
|
client_settings={
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"camera": {
|
"camera": {
|
||||||
@@ -275,33 +324,7 @@ class DailyTransportClient(EventHandler):
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
self._handle_join_response()
|
return await asyncio.wait_for(future, timeout=10)
|
||||||
|
|
||||||
def _handle_join_response(self):
|
|
||||||
try:
|
|
||||||
(data, error) = self._sync_response["join"].get(timeout=10)
|
|
||||||
if not error:
|
|
||||||
self._joined = True
|
|
||||||
self._joining = False
|
|
||||||
|
|
||||||
logger.info(f"Joined {self._room_url}")
|
|
||||||
|
|
||||||
if self._token and self._params.transcription_enabled:
|
|
||||||
logger.info(
|
|
||||||
f"Enabling transcription with settings {self._params.transcription_settings}")
|
|
||||||
self._client.start_transcription(
|
|
||||||
self._params.transcription_settings.model_dump())
|
|
||||||
|
|
||||||
self._callbacks.on_joined(data["participants"]["local"])
|
|
||||||
else:
|
|
||||||
error_msg = f"Error joining {self._room_url}: {error}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
self._callbacks.on_error(error_msg)
|
|
||||||
self._sync_response["join"].task_done()
|
|
||||||
except queue.Empty:
|
|
||||||
error_msg = f"Time out joining {self._room_url}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
self._callbacks.on_error(error_msg)
|
|
||||||
|
|
||||||
async def leave(self):
|
async def leave(self):
|
||||||
# Transport not joined, ignore.
|
# Transport not joined, ignore.
|
||||||
@@ -311,34 +334,36 @@ class DailyTransportClient(EventHandler):
|
|||||||
self._joined = False
|
self._joined = False
|
||||||
self._leaving = True
|
self._leaving = True
|
||||||
|
|
||||||
await self._loop.run_in_executor(self._executor, self._leave)
|
|
||||||
|
|
||||||
def _leave(self):
|
|
||||||
logger.info(f"Leaving {self._room_url}")
|
logger.info(f"Leaving {self._room_url}")
|
||||||
|
|
||||||
if self._params.transcription_enabled:
|
if self._params.transcription_enabled:
|
||||||
self._client.stop_transcription()
|
self._client.stop_transcription()
|
||||||
|
|
||||||
self._client.leave(completion=self._call_left)
|
|
||||||
|
|
||||||
self._handle_leave_response()
|
|
||||||
|
|
||||||
def _handle_leave_response(self):
|
|
||||||
try:
|
try:
|
||||||
error = self._sync_response["leave"].get(timeout=10)
|
error = await self._leave()
|
||||||
if not error:
|
if not error:
|
||||||
self._leaving = False
|
self._leaving = False
|
||||||
logger.info(f"Left {self._room_url}")
|
logger.info(f"Left {self._room_url}")
|
||||||
self._callbacks.on_left()
|
await self._callbacks.on_left()
|
||||||
else:
|
else:
|
||||||
error_msg = f"Error leaving {self._room_url}: {error}"
|
error_msg = f"Error leaving {self._room_url}: {error}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
self._callbacks.on_error(error_msg)
|
await self._callbacks.on_error(error_msg)
|
||||||
self._sync_response["leave"].task_done()
|
except asyncio.TimeoutError:
|
||||||
except queue.Empty:
|
|
||||||
error_msg = f"Time out leaving {self._room_url}"
|
error_msg = f"Time out leaving {self._room_url}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
self._callbacks.on_error(error_msg)
|
await self._callbacks.on_error(error_msg)
|
||||||
|
|
||||||
|
async def _leave(self):
|
||||||
|
future = self._loop.create_future()
|
||||||
|
|
||||||
|
def handle_leave_response(error):
|
||||||
|
if not future.cancelled():
|
||||||
|
future.get_loop().call_soon_threadsafe(future.set_result, error)
|
||||||
|
|
||||||
|
self._client.leave(completion=handle_leave_response)
|
||||||
|
|
||||||
|
return await asyncio.wait_for(future, timeout=10)
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
await self._loop.run_in_executor(self._executor, self._cleanup)
|
await self._loop.run_in_executor(self._executor, self._cleanup)
|
||||||
@@ -400,28 +425,28 @@ class DailyTransportClient(EventHandler):
|
|||||||
#
|
#
|
||||||
|
|
||||||
def on_app_message(self, message: Any, sender: str):
|
def on_app_message(self, message: Any, sender: str):
|
||||||
self._callbacks.on_app_message(message, sender)
|
self._call_async_callback(self._callbacks.on_app_message, message, sender)
|
||||||
|
|
||||||
def on_call_state_updated(self, state: str):
|
def on_call_state_updated(self, state: str):
|
||||||
self._callbacks.on_call_state_updated(state)
|
self._call_async_callback(self._callbacks.on_call_state_updated, state)
|
||||||
|
|
||||||
def on_dialin_ready(self, sip_endpoint: str):
|
def on_dialin_ready(self, sip_endpoint: str):
|
||||||
self._callbacks.on_dialin_ready(sip_endpoint)
|
self._call_async_callback(self._callbacks.on_dialin_ready, sip_endpoint)
|
||||||
|
|
||||||
def on_dialout_answered(self, data: Any):
|
def on_dialout_answered(self, data: Any):
|
||||||
self._callbacks.on_dialout_answered(data)
|
self._call_async_callback(self._callbacks.on_dialout_answered, data)
|
||||||
|
|
||||||
def on_dialout_connected(self, data: Any):
|
def on_dialout_connected(self, data: Any):
|
||||||
self._callbacks.on_dialout_connected(data)
|
self._call_async_callback(self._callbacks.on_dialout_connected, data)
|
||||||
|
|
||||||
def on_dialout_stopped(self, data: Any):
|
def on_dialout_stopped(self, data: Any):
|
||||||
self._callbacks.on_dialout_stopped(data)
|
self._call_async_callback(self._callbacks.on_dialout_stopped, data)
|
||||||
|
|
||||||
def on_dialout_error(self, data: Any):
|
def on_dialout_error(self, data: Any):
|
||||||
self._callbacks.on_dialout_error(data)
|
self._call_async_callback(self._callbacks.on_dialout_error, data)
|
||||||
|
|
||||||
def on_dialout_warning(self, data: Any):
|
def on_dialout_warning(self, data: Any):
|
||||||
self._callbacks.on_dialout_warning(data)
|
self._call_async_callback(self._callbacks.on_dialout_warning, data)
|
||||||
|
|
||||||
def on_participant_joined(self, participant):
|
def on_participant_joined(self, participant):
|
||||||
id = participant["id"]
|
id = participant["id"]
|
||||||
@@ -429,15 +454,15 @@ class DailyTransportClient(EventHandler):
|
|||||||
|
|
||||||
if not self._other_participant_has_joined:
|
if not self._other_participant_has_joined:
|
||||||
self._other_participant_has_joined = True
|
self._other_participant_has_joined = True
|
||||||
self._callbacks.on_first_participant_joined(participant)
|
self._call_async_callback(self._callbacks.on_first_participant_joined, participant)
|
||||||
|
|
||||||
self._callbacks.on_participant_joined(participant)
|
self._call_async_callback(self._callbacks.on_participant_joined, participant)
|
||||||
|
|
||||||
def on_participant_left(self, participant, reason):
|
def on_participant_left(self, participant, reason):
|
||||||
id = participant["id"]
|
id = participant["id"]
|
||||||
logger.info(f"Participant left {id}")
|
logger.info(f"Participant left {id}")
|
||||||
|
|
||||||
self._callbacks.on_participant_left(participant, reason)
|
self._call_async_callback(self._callbacks.on_participant_left, participant, reason)
|
||||||
|
|
||||||
def on_transcription_message(self, message: Mapping[str, Any]):
|
def on_transcription_message(self, message: Mapping[str, Any]):
|
||||||
participant_id = ""
|
participant_id = ""
|
||||||
@@ -446,7 +471,7 @@ class DailyTransportClient(EventHandler):
|
|||||||
|
|
||||||
if participant_id in self._transcription_renderers:
|
if participant_id in self._transcription_renderers:
|
||||||
callback = self._transcription_renderers[participant_id]
|
callback = self._transcription_renderers[participant_id]
|
||||||
callback(participant_id, message)
|
self._call_async_callback(callback, participant_id, message)
|
||||||
|
|
||||||
def on_transcription_error(self, message):
|
def on_transcription_error(self, message):
|
||||||
logger.error(f"Transcription error: {message}")
|
logger.error(f"Transcription error: {message}")
|
||||||
@@ -461,18 +486,19 @@ class DailyTransportClient(EventHandler):
|
|||||||
# Daily (CallClient callbacks)
|
# Daily (CallClient callbacks)
|
||||||
#
|
#
|
||||||
|
|
||||||
def _call_joined(self, data, error):
|
|
||||||
self._sync_response["join"].put((data, error))
|
|
||||||
|
|
||||||
def _call_left(self, error):
|
|
||||||
self._sync_response["leave"].put(error)
|
|
||||||
|
|
||||||
def _video_frame_received(self, participant_id, video_frame):
|
def _video_frame_received(self, participant_id, video_frame):
|
||||||
callback = self._video_renderers[participant_id]
|
callback = self._video_renderers[participant_id]
|
||||||
callback(participant_id,
|
self._call_async_callback(
|
||||||
video_frame.buffer,
|
callback,
|
||||||
(video_frame.width, video_frame.height),
|
participant_id,
|
||||||
video_frame.color_format)
|
video_frame.buffer,
|
||||||
|
(video_frame.width,
|
||||||
|
video_frame.height),
|
||||||
|
video_frame.color_format)
|
||||||
|
|
||||||
|
def _call_async_callback(self, callback, *args):
|
||||||
|
future = asyncio.run_coroutine_threadsafe(callback(*args), self._loop)
|
||||||
|
future.result()
|
||||||
|
|
||||||
|
|
||||||
class DailyInputTransport(BaseInputTransport):
|
class DailyInputTransport(BaseInputTransport):
|
||||||
@@ -491,8 +517,6 @@ class DailyInputTransport(BaseInputTransport):
|
|||||||
num_channels=self._params.audio_in_channels)
|
num_channels=self._params.audio_in_channels)
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
if self._running:
|
|
||||||
return
|
|
||||||
# Parent start.
|
# Parent start.
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
# Join the room.
|
# Join the room.
|
||||||
@@ -500,19 +524,17 @@ class DailyInputTransport(BaseInputTransport):
|
|||||||
# Create audio task. It reads audio frames from Daily and push them
|
# Create audio task. It reads audio frames from Daily and push them
|
||||||
# internally for VAD processing.
|
# internally for VAD processing.
|
||||||
if self._params.audio_in_enabled or self._params.vad_enabled:
|
if self._params.audio_in_enabled or self._params.vad_enabled:
|
||||||
self._audio_in_thread = self._loop.run_in_executor(
|
self._audio_in_task = self.get_event_loop().create_task(self._audio_in_task_handler())
|
||||||
self._executor, self._audio_in_thread_handler)
|
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
if not self._running:
|
# Parent stop.
|
||||||
return
|
|
||||||
# Parent stop. This will set _running to False.
|
|
||||||
await super().stop()
|
await super().stop()
|
||||||
# Leave the room.
|
# Leave the room.
|
||||||
await self._client.leave()
|
await self._client.leave()
|
||||||
# Stop audio thread.
|
# Stop audio thread.
|
||||||
if self._params.audio_in_enabled or self._params.vad_enabled:
|
if self._params.audio_in_enabled or self._params.vad_enabled:
|
||||||
await self._audio_in_thread
|
self._audio_in_task.cancel()
|
||||||
|
await self._audio_in_task
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
await super().cleanup()
|
await super().cleanup()
|
||||||
@@ -535,26 +557,25 @@ class DailyInputTransport(BaseInputTransport):
|
|||||||
# Frames
|
# Frames
|
||||||
#
|
#
|
||||||
|
|
||||||
def push_transcription_frame(self, frame: TranscriptionFrame | InterimTranscriptionFrame):
|
async def push_transcription_frame(self, frame: TranscriptionFrame | InterimTranscriptionFrame):
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
await self._internal_push_frame(frame)
|
||||||
self._internal_push_frame(frame), self.get_event_loop())
|
|
||||||
future.result()
|
|
||||||
|
|
||||||
def push_app_message(self, message: Any, sender: str):
|
async def push_app_message(self, message: Any, sender: str):
|
||||||
frame = DailyTransportMessageFrame(message=message, participant_id=sender)
|
frame = DailyTransportMessageFrame(message=message, participant_id=sender)
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
await self._internal_push_frame(frame)
|
||||||
self._internal_push_frame(frame), self.get_event_loop())
|
|
||||||
future.result()
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Audio in
|
# Audio in
|
||||||
#
|
#
|
||||||
|
|
||||||
def _audio_in_thread_handler(self):
|
async def _audio_in_task_handler(self):
|
||||||
while self._running:
|
while True:
|
||||||
frame = self._client.read_next_audio_frame()
|
try:
|
||||||
if frame:
|
frame = await self._client.read_next_audio_frame()
|
||||||
self.push_audio_frame(frame)
|
if frame:
|
||||||
|
await self.push_audio_frame(frame)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
|
|
||||||
#
|
#
|
||||||
# Camera in
|
# Camera in
|
||||||
@@ -584,7 +605,7 @@ class DailyInputTransport(BaseInputTransport):
|
|||||||
if participant_id in self._video_renderers:
|
if participant_id in self._video_renderers:
|
||||||
self._video_renderers[participant_id]["render_next_frame"] = True
|
self._video_renderers[participant_id]["render_next_frame"] = True
|
||||||
|
|
||||||
def _on_participant_video_frame(self, participant_id: str, buffer, size, format):
|
async def _on_participant_video_frame(self, participant_id: str, buffer, size, format):
|
||||||
render_frame = False
|
render_frame = False
|
||||||
|
|
||||||
curr_time = time.time()
|
curr_time = time.time()
|
||||||
@@ -604,9 +625,7 @@ class DailyInputTransport(BaseInputTransport):
|
|||||||
image=buffer,
|
image=buffer,
|
||||||
size=size,
|
size=size,
|
||||||
format=format)
|
format=format)
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
await self._internal_push_frame(frame)
|
||||||
self._internal_push_frame(frame), self.get_event_loop())
|
|
||||||
future.result()
|
|
||||||
|
|
||||||
self._video_renderers[participant_id]["timestamp"] = curr_time
|
self._video_renderers[participant_id]["timestamp"] = curr_time
|
||||||
|
|
||||||
@@ -619,17 +638,13 @@ class DailyOutputTransport(BaseOutputTransport):
|
|||||||
self._client = client
|
self._client = client
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
if self._running:
|
|
||||||
return
|
|
||||||
# Parent start.
|
# Parent start.
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
# Join the room.
|
# Join the room.
|
||||||
await self._client.join()
|
await self._client.join()
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
if not self._running:
|
# Parent stop.
|
||||||
return
|
|
||||||
# Parent stop. This will set _running to False.
|
|
||||||
await super().stop()
|
await super().stop()
|
||||||
# Leave the room.
|
# Leave the room.
|
||||||
await self._client.leave()
|
await self._client.leave()
|
||||||
@@ -638,10 +653,10 @@ class DailyOutputTransport(BaseOutputTransport):
|
|||||||
await super().cleanup()
|
await super().cleanup()
|
||||||
await self._client.cleanup()
|
await self._client.cleanup()
|
||||||
|
|
||||||
def send_message(self, frame: DailyTransportMessageFrame):
|
async def send_message(self, frame: DailyTransportMessageFrame):
|
||||||
self._client.send_message(frame)
|
await self._client.send_message(frame)
|
||||||
|
|
||||||
def send_metrics(self, frame: MetricsFrame):
|
async def send_metrics(self, frame: MetricsFrame):
|
||||||
ttfb = [{"name": n, "time": t} for n, t in frame.ttfb.items()]
|
ttfb = [{"name": n, "time": t} for n, t in frame.ttfb.items()]
|
||||||
message = DailyTransportMessageFrame(message={
|
message = DailyTransportMessageFrame(message={
|
||||||
"type": "pipecat-metrics",
|
"type": "pipecat-metrics",
|
||||||
@@ -649,13 +664,13 @@ class DailyOutputTransport(BaseOutputTransport):
|
|||||||
"ttfb": ttfb
|
"ttfb": ttfb
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
self._client.send_message(message)
|
await self._client.send_message(message)
|
||||||
|
|
||||||
def write_raw_audio_frames(self, frames: bytes):
|
async def write_raw_audio_frames(self, frames: bytes):
|
||||||
self._client.write_raw_audio_frames(frames)
|
await self._client.write_raw_audio_frames(frames)
|
||||||
|
|
||||||
def write_frame_to_camera(self, frame: ImageRawFrame):
|
async def write_frame_to_camera(self, frame: ImageRawFrame):
|
||||||
self._client.write_frame_to_camera(frame)
|
await self._client.write_frame_to_camera(frame)
|
||||||
|
|
||||||
|
|
||||||
class DailyTransport(BaseTransport):
|
class DailyTransport(BaseTransport):
|
||||||
@@ -774,24 +789,24 @@ class DailyTransport(BaseTransport):
|
|||||||
self._input.capture_participant_video(
|
self._input.capture_participant_video(
|
||||||
participant_id, framerate, video_source, color_format)
|
participant_id, framerate, video_source, color_format)
|
||||||
|
|
||||||
def _on_joined(self, participant):
|
async def _on_joined(self, participant):
|
||||||
self._call_async_event_handler("on_joined", participant)
|
await self._call_event_handler("on_joined", participant)
|
||||||
|
|
||||||
def _on_left(self):
|
async def _on_left(self):
|
||||||
self._call_async_event_handler("on_left")
|
await self._call_event_handler("on_left")
|
||||||
|
|
||||||
def _on_error(self, error):
|
async def _on_error(self, error):
|
||||||
# TODO(aleix): Report error to input/output transports. The one managing
|
# TODO(aleix): Report error to input/output transports. The one managing
|
||||||
# the client should report the error.
|
# the client should report the error.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _on_app_message(self, message: Any, sender: str):
|
async def _on_app_message(self, message: Any, sender: str):
|
||||||
if self._input:
|
if self._input:
|
||||||
self._input.push_app_message(message, sender)
|
await self._input.push_app_message(message, sender)
|
||||||
self._call_async_event_handler("on_app_message", message, sender)
|
await self._call_event_handler("on_app_message", message, sender)
|
||||||
|
|
||||||
def _on_call_state_updated(self, state: str):
|
async def _on_call_state_updated(self, state: str):
|
||||||
self._call_async_event_handler("on_call_state_updated", state)
|
await self._call_event_handler("on_call_state_updated", state)
|
||||||
|
|
||||||
async def _handle_dialin_ready(self, sip_endpoint: str):
|
async def _handle_dialin_ready(self, sip_endpoint: str):
|
||||||
if not self._params.dialin_settings:
|
if not self._params.dialin_settings:
|
||||||
@@ -824,36 +839,36 @@ class DailyTransport(BaseTransport):
|
|||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
logger.error(f"Error handling dialin-ready event ({url}): {e}")
|
logger.error(f"Error handling dialin-ready event ({url}): {e}")
|
||||||
|
|
||||||
def _on_dialin_ready(self, sip_endpoint):
|
async def _on_dialin_ready(self, sip_endpoint):
|
||||||
if self._params.dialin_settings:
|
if self._params.dialin_settings:
|
||||||
asyncio.run_coroutine_threadsafe(self._handle_dialin_ready(sip_endpoint), self._loop)
|
await self._handle_dialin_ready(sip_endpoint)
|
||||||
self._call_async_event_handler("on_dialin_ready", sip_endpoint)
|
await self._call_event_handler("on_dialin_ready", sip_endpoint)
|
||||||
|
|
||||||
def _on_dialout_answered(self, data):
|
async def _on_dialout_answered(self, data):
|
||||||
self._call_async_event_handler("on_dialout_answered", data)
|
await self._call_event_handler("on_dialout_answered", data)
|
||||||
|
|
||||||
def _on_dialout_connected(self, data):
|
async def _on_dialout_connected(self, data):
|
||||||
self._call_async_event_handler("on_dialout_connected", data)
|
await self._call_event_handler("on_dialout_connected", data)
|
||||||
|
|
||||||
def _on_dialout_stopped(self, data):
|
async def _on_dialout_stopped(self, data):
|
||||||
self._call_async_event_handler("on_dialout_stopped", data)
|
await self._call_event_handler("on_dialout_stopped", data)
|
||||||
|
|
||||||
def _on_dialout_error(self, data):
|
async def _on_dialout_error(self, data):
|
||||||
self._call_async_event_handler("on_dialout_error", data)
|
await self._call_event_handler("on_dialout_error", data)
|
||||||
|
|
||||||
def _on_dialout_warning(self, data):
|
async def _on_dialout_warning(self, data):
|
||||||
self._call_async_event_handler("on_dialout_warning", data)
|
await self._call_event_handler("on_dialout_warning", data)
|
||||||
|
|
||||||
def _on_participant_joined(self, participant):
|
async def _on_participant_joined(self, participant):
|
||||||
self._call_async_event_handler("on_participant_joined", participant)
|
await self._call_event_handler("on_participant_joined", participant)
|
||||||
|
|
||||||
def _on_participant_left(self, participant, reason):
|
async def _on_participant_left(self, participant, reason):
|
||||||
self._call_async_event_handler("on_participant_left", participant, reason)
|
await self._call_event_handler("on_participant_left", participant, reason)
|
||||||
|
|
||||||
def _on_first_participant_joined(self, participant):
|
async def _on_first_participant_joined(self, participant):
|
||||||
self._call_async_event_handler("on_first_participant_joined", participant)
|
await self._call_event_handler("on_first_participant_joined", participant)
|
||||||
|
|
||||||
def _on_transcription_message(self, participant_id, message):
|
async def _on_transcription_message(self, participant_id, message):
|
||||||
text = message["text"]
|
text = message["text"]
|
||||||
timestamp = message["timestamp"]
|
timestamp = message["timestamp"]
|
||||||
is_final = message["rawResponse"]["is_final"]
|
is_final = message["rawResponse"]["is_final"]
|
||||||
@@ -864,9 +879,4 @@ class DailyTransport(BaseTransport):
|
|||||||
frame = InterimTranscriptionFrame(text, participant_id, timestamp)
|
frame = InterimTranscriptionFrame(text, participant_id, timestamp)
|
||||||
|
|
||||||
if self._input:
|
if self._input:
|
||||||
self._input.push_transcription_frame(frame)
|
await self._input.push_transcription_frame(frame)
|
||||||
|
|
||||||
def _call_async_event_handler(self, event_name: str, *args, **kwargs):
|
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
|
||||||
self._call_event_handler(event_name, *args, **kwargs), self._loop)
|
|
||||||
future.result()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user