This commit is contained in:
+11
-1
@@ -340,6 +340,7 @@ class RTPMedia:
|
|||||||
self.sequence = 0
|
self.sequence = 0
|
||||||
self.timestamp = 0
|
self.timestamp = 0
|
||||||
self.ssrc = uuid.uuid4().int & 0xFFFFFFFF
|
self.ssrc = uuid.uuid4().int & 0xFFFFFFFF
|
||||||
|
self._next_send_at: float | None = None
|
||||||
self._send_lock = threading.Lock()
|
self._send_lock = threading.Lock()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -399,11 +400,18 @@ class RTPMedia:
|
|||||||
)
|
)
|
||||||
payload = pcm16_to_alaw(raw, input_rate_hz, 8000)
|
payload = pcm16_to_alaw(raw, input_rate_hz, 8000)
|
||||||
sent = 0
|
sent = 0
|
||||||
|
packet_interval_s = 0.02
|
||||||
with self._send_lock:
|
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):
|
for offset in range(0, len(payload), 160):
|
||||||
frame = payload[offset : offset + 160]
|
frame = payload[offset : offset + 160]
|
||||||
if not frame:
|
if not frame:
|
||||||
continue
|
continue
|
||||||
|
delay = self._next_send_at - time.monotonic()
|
||||||
|
if delay > 0:
|
||||||
|
time.sleep(delay)
|
||||||
header = struct.pack(
|
header = struct.pack(
|
||||||
"!BBHII",
|
"!BBHII",
|
||||||
0x80,
|
0x80,
|
||||||
@@ -421,7 +429,9 @@ class RTPMedia:
|
|||||||
self.sequence += 1
|
self.sequence += 1
|
||||||
self.timestamp += 160
|
self.timestamp += 160
|
||||||
sent += len(frame)
|
sent += len(frame)
|
||||||
time.sleep(0.02)
|
self._next_send_at = max(
|
||||||
|
self._next_send_at + packet_interval_s, time.monotonic()
|
||||||
|
)
|
||||||
return sent
|
return sent
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import wave
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from agent_call.real_cell import (
|
from agent_call.real_cell import (
|
||||||
CellCallConfig,
|
CellCallConfig,
|
||||||
@@ -154,6 +155,35 @@ class RealCellTests(unittest.TestCase):
|
|||||||
self.assertEqual(fake_ari.record_requests, 0)
|
self.assertEqual(fake_ari.record_requests, 0)
|
||||||
self.assertNotIn("opening", order)
|
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:
|
def test_pcma_decode_and_rtp_payload(self) -> None:
|
||||||
media = RTPMedia("127.0.0.1", 0)
|
media = RTPMedia("127.0.0.1", 0)
|
||||||
receiver = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
receiver = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
|||||||
Reference in New Issue
Block a user