transports(output): initial sink clock synchronization
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import itertools
|
import itertools
|
||||||
import time
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from typing import List
|
from typing import List
|
||||||
@@ -30,11 +31,14 @@ from pipecat.frames.frames import (
|
|||||||
SystemFrame,
|
SystemFrame,
|
||||||
TTSStartedFrame,
|
TTSStartedFrame,
|
||||||
TTSStoppedFrame,
|
TTSStoppedFrame,
|
||||||
|
TextFrame,
|
||||||
TransportMessageFrame)
|
TransportMessageFrame)
|
||||||
from pipecat.transports.base_transport import TransportParams
|
from pipecat.transports.base_transport import TransportParams
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from pipecat.utils.time import nanoseconds_to_seconds
|
||||||
|
|
||||||
|
|
||||||
class BaseOutputTransport(FrameProcessor):
|
class BaseOutputTransport(FrameProcessor):
|
||||||
|
|
||||||
@@ -64,7 +68,7 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
# Create sink frame task. This is the task that will actually write
|
# 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
|
# 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.
|
# generating frames upstream while, for example, the audio is playing.
|
||||||
self._create_sink_task()
|
self._create_sink_tasks()
|
||||||
|
|
||||||
# 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.
|
||||||
@@ -149,6 +153,7 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
await self._sink_queue.put(frame)
|
await self._sink_queue.put(frame)
|
||||||
await self.start(frame)
|
await self.start(frame)
|
||||||
elif isinstance(frame, EndFrame):
|
elif isinstance(frame, EndFrame):
|
||||||
|
await self._sink_clock_queue.put((sys.maxsize, frame.id, frame))
|
||||||
await self._sink_queue.put(frame)
|
await self._sink_queue.put(frame)
|
||||||
await self.stop(frame)
|
await self.stop(frame)
|
||||||
# Other frames.
|
# Other frames.
|
||||||
@@ -158,6 +163,9 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
await self._handle_image(frame)
|
await self._handle_image(frame)
|
||||||
elif isinstance(frame, TransportMessageFrame) and frame.urgent:
|
elif isinstance(frame, TransportMessageFrame) and frame.urgent:
|
||||||
await self.send_message(frame)
|
await self.send_message(frame)
|
||||||
|
# TODO(aleix): Images and audio should support presentation timestamps.
|
||||||
|
elif frame.pts:
|
||||||
|
await self._sink_clock_queue.put((frame.pts, frame.id, frame))
|
||||||
else:
|
else:
|
||||||
await self._sink_queue.put(frame)
|
await self._sink_queue.put(frame)
|
||||||
|
|
||||||
@@ -166,10 +174,14 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(frame, StartInterruptionFrame):
|
if isinstance(frame, StartInterruptionFrame):
|
||||||
# Stop sink task.
|
# Stop sink tasks.
|
||||||
self._sink_task.cancel()
|
self._sink_task.cancel()
|
||||||
await self._sink_task
|
await self._sink_task
|
||||||
self._create_sink_task()
|
# Stop sink clock tasks.
|
||||||
|
self._sink_clock_task.cancel()
|
||||||
|
await self._sink_clock_task
|
||||||
|
# Create sink tasks.
|
||||||
|
self._create_sink_tasks()
|
||||||
# Stop push task.
|
# Stop push task.
|
||||||
self._push_frame_task.cancel()
|
self._push_frame_task.cancel()
|
||||||
await self._push_frame_task
|
await self._push_frame_task
|
||||||
@@ -201,43 +213,83 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
else:
|
else:
|
||||||
await self._sink_queue.put(frame)
|
await self._sink_queue.put(frame)
|
||||||
|
|
||||||
def _create_sink_task(self):
|
#
|
||||||
|
# Sink tasks
|
||||||
|
#
|
||||||
|
|
||||||
|
def _create_sink_tasks(self):
|
||||||
loop = self.get_event_loop()
|
loop = self.get_event_loop()
|
||||||
self._sink_queue = asyncio.Queue()
|
self._sink_queue = asyncio.Queue()
|
||||||
self._sink_task = loop.create_task(self._sink_task_handler())
|
self._sink_task = loop.create_task(self._sink_task_handler())
|
||||||
|
self._sink_clock_queue = asyncio.PriorityQueue()
|
||||||
|
self._sink_clock_task = loop.create_task(self._sink_clock_task_handler())
|
||||||
|
|
||||||
|
async def _sink_frame_handler(self, frame: Frame):
|
||||||
|
if isinstance(frame, AudioRawFrame):
|
||||||
|
await self.write_raw_audio_frames(frame.audio)
|
||||||
|
await self._internal_push_frame(frame)
|
||||||
|
await self.push_frame(BotSpeakingFrame(), FrameDirection.UPSTREAM)
|
||||||
|
elif isinstance(frame, ImageRawFrame):
|
||||||
|
await self._set_camera_image(frame)
|
||||||
|
elif isinstance(frame, SpriteFrame):
|
||||||
|
await self._set_camera_images(frame.images)
|
||||||
|
elif isinstance(frame, TransportMessageFrame):
|
||||||
|
await self.send_message(frame)
|
||||||
|
elif isinstance(frame, TTSStartedFrame):
|
||||||
|
await self._bot_started_speaking()
|
||||||
|
await self._internal_push_frame(frame)
|
||||||
|
elif isinstance(frame, TTSStoppedFrame):
|
||||||
|
await self._bot_stopped_speaking()
|
||||||
|
await self._internal_push_frame(frame)
|
||||||
|
else:
|
||||||
|
await self._internal_push_frame(frame)
|
||||||
|
|
||||||
async def _sink_task_handler(self):
|
async def _sink_task_handler(self):
|
||||||
running = True
|
running = True
|
||||||
while running:
|
while running:
|
||||||
try:
|
try:
|
||||||
frame = await self._sink_queue.get()
|
frame = await self._sink_queue.get()
|
||||||
if isinstance(frame, AudioRawFrame):
|
await self._sink_frame_handler(frame)
|
||||||
await self.write_raw_audio_frames(frame.audio)
|
|
||||||
await self._internal_push_frame(frame)
|
|
||||||
await self.push_frame(BotSpeakingFrame(), FrameDirection.UPSTREAM)
|
|
||||||
elif isinstance(frame, ImageRawFrame):
|
|
||||||
await self._set_camera_image(frame)
|
|
||||||
elif isinstance(frame, SpriteFrame):
|
|
||||||
await self._set_camera_images(frame.images)
|
|
||||||
elif isinstance(frame, TransportMessageFrame):
|
|
||||||
await self.send_message(frame)
|
|
||||||
elif isinstance(frame, TTSStartedFrame):
|
|
||||||
await self._bot_started_speaking()
|
|
||||||
await self._internal_push_frame(frame)
|
|
||||||
elif isinstance(frame, TTSStoppedFrame):
|
|
||||||
await self._bot_stopped_speaking()
|
|
||||||
await self._internal_push_frame(frame)
|
|
||||||
else:
|
|
||||||
await self._internal_push_frame(frame)
|
|
||||||
|
|
||||||
running = not isinstance(frame, EndFrame)
|
running = not isinstance(frame, EndFrame)
|
||||||
|
|
||||||
self._sink_queue.task_done()
|
self._sink_queue.task_done()
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"{self} error processing sink queue: {e}")
|
logger.exception(f"{self} error processing sink queue: {e}")
|
||||||
|
|
||||||
|
async def _sink_clock_frame_handler(self, frame: Frame):
|
||||||
|
# TODO(aleix): For now we just process TextFrame. But we should process
|
||||||
|
# audio and video as well.
|
||||||
|
if isinstance(frame, TextFrame):
|
||||||
|
await self._internal_push_frame(frame)
|
||||||
|
|
||||||
|
async def _sink_clock_task_handler(self):
|
||||||
|
running = True
|
||||||
|
while running:
|
||||||
|
try:
|
||||||
|
timestamp, _, frame = await self._sink_clock_queue.get()
|
||||||
|
|
||||||
|
# If we hit an EndFrame, we cna finish right away.
|
||||||
|
running = not isinstance(frame, EndFrame)
|
||||||
|
|
||||||
|
# If we have a frame we check it's presentation timestamp. If it
|
||||||
|
# has already passed we process it, otherwise we wait until it's
|
||||||
|
# time to process it.
|
||||||
|
if running:
|
||||||
|
current_time = self.get_clock().get_time()
|
||||||
|
if timestamp <= current_time:
|
||||||
|
await self._sink_clock_frame_handler(frame)
|
||||||
|
else:
|
||||||
|
wait_time = nanoseconds_to_seconds(timestamp - current_time)
|
||||||
|
await asyncio.sleep(wait_time)
|
||||||
|
await self._sink_frame_handler(frame)
|
||||||
|
|
||||||
|
self._sink_clock_queue.task_done()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"{self} error processing sink clock queue: {e}")
|
||||||
|
|
||||||
async def _bot_started_speaking(self):
|
async def _bot_started_speaking(self):
|
||||||
logger.debug("Bot started speaking")
|
logger.debug("Bot started speaking")
|
||||||
self._bot_speaking = True
|
self._bot_speaking = True
|
||||||
|
|||||||
Reference in New Issue
Block a user