Skip to content

Commit ebcd026

Browse files
authored
Merge pull request #228 from GetStream/fix/audio-track-buffer-leak
Fix audio buffer memory leak: use in-place del instead of slice-copy
2 parents 22d53fb + 823c419 commit ebcd026

File tree

2 files changed

+67
-2
lines changed

2 files changed

+67
-2
lines changed

getstream/video/rtc/audio_track.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ async def write(self, pcm: PcmData):
130130
)
131131

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

135135
buffer_duration_ms = (
136136
len(self._buffer)
@@ -192,7 +192,7 @@ async def recv(self) -> Frame:
192192
if len(self._buffer) >= self._bytes_per_frame:
193193
# We have enough data
194194
audio_bytes = bytes(self._buffer[: self._bytes_per_frame])
195-
self._buffer = self._buffer[self._bytes_per_frame :]
195+
del self._buffer[: self._bytes_per_frame]
196196
elif len(self._buffer) > 0:
197197
# We have some data but not enough - pad with silence
198198
audio_bytes = bytes(self._buffer)

tests/test_audio_stream_track.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,71 @@ async def _continuous_reader(self, track):
302302
frames_received += 1
303303
assert frame.samples == 960
304304

305+
@pytest.mark.asyncio
306+
async def test_recv_does_not_reallocate_buffer(self):
307+
"""Test that recv consumes data in-place without creating a new buffer object."""
308+
track = AudioStreamTrack(sample_rate=48000, channels=1, format="s16")
309+
310+
# Write 40ms of data (enough for 2 frames)
311+
samples = np.zeros(1920, dtype=np.int16)
312+
pcm = PcmData(
313+
samples=samples,
314+
sample_rate=48000,
315+
format=AudioFormat.S16,
316+
channels=1,
317+
)
318+
await track.write(pcm)
319+
320+
buffer_ref = track._buffer # save reference before recv
321+
322+
# Receive a frame (consumes 20ms from buffer)
323+
await track.recv()
324+
325+
assert track._buffer is buffer_ref, (
326+
"recv should modify buffer in-place, not create a new one"
327+
)
328+
assert len(track._buffer) == 960 * 2, (
329+
"should have 20ms of data remaining (960 samples * 2 bytes)"
330+
)
331+
332+
@pytest.mark.asyncio
333+
async def test_buffer_overflow_does_not_reallocate(self):
334+
"""Test that buffer overflow trims in-place without creating a new buffer object."""
335+
track = AudioStreamTrack(
336+
sample_rate=48000, channels=1, format="s16", audio_buffer_size_ms=100
337+
)
338+
339+
# Write 50ms of data first to get a buffer reference
340+
samples_50ms = np.zeros(2400, dtype=np.int16)
341+
pcm = PcmData(
342+
samples=samples_50ms,
343+
sample_rate=48000,
344+
format=AudioFormat.S16,
345+
channels=1,
346+
)
347+
await track.write(pcm)
348+
buffer_ref = track._buffer # save reference before overflow
349+
350+
# Write 200ms of data (exceeds 100ms limit, triggers overflow trim)
351+
samples_200ms = np.zeros(9600, dtype=np.int16)
352+
pcm_large = PcmData(
353+
samples=samples_200ms,
354+
sample_rate=48000,
355+
format=AudioFormat.S16,
356+
channels=1,
357+
)
358+
await track.write(pcm_large)
359+
360+
assert track._buffer is buffer_ref, (
361+
"overflow trim should modify buffer in-place, not create a new one"
362+
)
363+
max_buffer_seconds = 100 / 1000
364+
bytes_per_sample = 2
365+
expected_max_bytes = int(max_buffer_seconds * 48000) * bytes_per_sample
366+
assert len(track._buffer) == expected_max_bytes, (
367+
"buffer should be trimmed to configured max size"
368+
)
369+
305370
@pytest.mark.asyncio
306371
async def test_media_stream_error(self):
307372
"""Test that MediaStreamError is raised when track is not live."""

0 commit comments

Comments
 (0)