diff --git a/agent_call/real_cell.py b/agent_call/real_cell.py index 5bc8798..edf13ea 100644 --- a/agent_call/real_cell.py +++ b/agent_call/real_cell.py @@ -340,6 +340,7 @@ class RTPMedia: self.sequence = 0 self.timestamp = 0 self.ssrc = uuid.uuid4().int & 0xFFFFFFFF + self._next_send_at: float | None = None self._send_lock = threading.Lock() @property @@ -399,11 +400,18 @@ class RTPMedia: ) payload = pcm16_to_alaw(raw, input_rate_hz, 8000) sent = 0 + packet_interval_s = 0.02 with self._send_lock: + now = time.monotonic() + if self._next_send_at is None or self._next_send_at < now - packet_interval_s: + self._next_send_at = now for offset in range(0, len(payload), 160): frame = payload[offset : offset + 160] if not frame: continue + delay = self._next_send_at - time.monotonic() + if delay > 0: + time.sleep(delay) header = struct.pack( "!BBHII", 0x80, @@ -421,7 +429,9 @@ class RTPMedia: self.sequence += 1 self.timestamp += 160 sent += len(frame) - time.sleep(0.02) + self._next_send_at = max( + self._next_send_at + packet_interval_s, time.monotonic() + ) return sent @staticmethod diff --git a/tests/test_real_cell.py b/tests/test_real_cell.py index d89e062..994d2f4 100644 --- a/tests/test_real_cell.py +++ b/tests/test_real_cell.py @@ -10,6 +10,7 @@ import wave from pathlib import Path from types import SimpleNamespace from typing import Any, cast +from unittest.mock import patch from agent_call.real_cell import ( CellCallConfig, @@ -154,6 +155,35 @@ class RealCellTests(unittest.TestCase): self.assertEqual(fake_ari.record_requests, 0) self.assertNotIn("opening", order) + def test_rtp_send_keeps_packet_clock_across_frames(self) -> None: + media = RTPMedia("127.0.0.1", 0) + receiver = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + receiver.bind(("127.0.0.1", 0)) + media.peer = ("127.0.0.1", receiver.getsockname()[1]) + clock = [0.0] + sleeps: list[float] = [] + + def monotonic() -> float: + return clock[0] + + def sleep(seconds: float) -> None: + sleeps.append(seconds) + clock[0] += seconds + + try: + with ( + patch("agent_call.real_cell.time.monotonic", monotonic), + patch("agent_call.real_cell.time.sleep", sleep), + ): + media.send_pcm16(bytes([0, 0]) * 160, 8000) + media.send_pcm16(bytes([0, 0]) * 160, 8000) + self.assertEqual(sleeps, [0.02]) + self.assertEqual(len(receiver.recvfrom(512)[0]), 172) + self.assertEqual(len(receiver.recvfrom(512)[0]), 172) + finally: + media.close() + receiver.close() + def test_pcma_decode_and_rtp_payload(self) -> None: media = RTPMedia("127.0.0.1", 0) receiver = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)