Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions getstream/video/rtc/audio_track.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ async def write(self, pcm: PcmData):
)

# Drop from the beginning of the buffer to keep latest data
self._buffer = self._buffer[bytes_to_drop:]
del self._buffer[:bytes_to_drop]

buffer_duration_ms = (
len(self._buffer)
Expand Down Expand Up @@ -192,7 +192,7 @@ async def recv(self) -> Frame:
if len(self._buffer) >= self._bytes_per_frame:
# We have enough data
audio_bytes = bytes(self._buffer[: self._bytes_per_frame])
self._buffer = self._buffer[self._bytes_per_frame :]
del self._buffer[: self._bytes_per_frame]
elif len(self._buffer) > 0:
# We have some data but not enough - pad with silence
audio_bytes = bytes(self._buffer)
Expand Down
54 changes: 54 additions & 0 deletions tests/test_audio_stream_track.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,60 @@ async def _continuous_reader(self, track):
frames_received += 1
assert frame.samples == 960

@pytest.mark.asyncio
async def test_recv_does_not_reallocate_buffer(self):
"""Test that recv consumes data in-place without creating a new buffer object."""
track = AudioStreamTrack(sample_rate=48000, channels=1, format="s16")

# Write 40ms of data (enough for 2 frames)
samples = np.zeros(1920, dtype=np.int16)
pcm = PcmData(
samples=samples,
sample_rate=48000,
format=AudioFormat.S16,
channels=1,
)
await track.write(pcm)

# Store reference to the buffer object
buffer_id = id(track._buffer)

# Receive a frame (consumes 20ms from buffer)
await track.recv()

assert id(track._buffer) == buffer_id, "recv should modify buffer in-place, not create a new one"
assert len(track._buffer) == 960 * 2, "should have 20ms of data remaining (960 samples * 2 bytes)"

@pytest.mark.asyncio
async def test_buffer_overflow_does_not_reallocate(self):
"""Test that buffer overflow trims in-place without creating a new buffer object."""
track = AudioStreamTrack(
sample_rate=48000, channels=1, format="s16", audio_buffer_size_ms=100
)

# Write 50ms of data first to get a buffer reference
samples_50ms = np.zeros(2400, dtype=np.int16)
pcm = PcmData(
samples=samples_50ms,
sample_rate=48000,
format=AudioFormat.S16,
channels=1,
)
await track.write(pcm)
buffer_id = id(track._buffer)

# Write 200ms of data (exceeds 100ms limit, triggers overflow trim)
samples_200ms = np.zeros(9600, dtype=np.int16)
pcm_large = PcmData(
samples=samples_200ms,
sample_rate=48000,
format=AudioFormat.S16,
channels=1,
)
await track.write(pcm_large)

assert id(track._buffer) == buffer_id, "overflow trim should modify buffer in-place, not create a new one"

Comment on lines +330 to +358
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assert trimmed length to prove overflow path executed.

At Line 357, identity-only validation can pass even if overflow trimming fails but _buffer is still the same object. Add a length assertion for the 100ms cap.

Suggested test hardening
         await track.write(pcm_large)

         assert id(track._buffer) == buffer_id, "overflow trim should modify buffer in-place, not create a new one"
+        expected_max_bytes = int(0.1 * 48000) * 2  # 100ms * sample_rate * bytes_per_sample
+        assert len(track._buffer) == expected_max_bytes, "buffer should be trimmed to configured max size"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_audio_stream_track.py` around lines 330 - 358, The test
test_buffer_overflow_does_not_reallocate only checks identity of track._buffer,
which can pass even if trimming didn't occur; after writing pcm_large, also
assert the buffer length equals the 100ms cap (compute as sample_rate * channels
* audio_buffer_size_ms / 1000) to prove the overflow path executed — e.g., for
sample_rate=48000, channels=1, audio_buffer_size_ms=100 the expected length is
4800 samples; add this length assertion immediately after the second await
track.write(pcm_large) alongside the existing id(track._buffer) check.

@pytest.mark.asyncio
async def test_media_stream_error(self):
"""Test that MediaStreamError is raised when track is not live."""
Expand Down
Loading