Skip to content

Commit 4fca4ad

Browse files
committed
CPU optimizations for Opus encoder to reduce consumption, especially for idle publishers like agents
- Default channel layout: "stereo" -> "mono" - Default bitrate: 96kbps -> 64kbps - Default Opus compression level: "10" -> "7"
1 parent 1526ccf commit 4fca4ad

3 files changed

Lines changed: 112 additions & 12 deletions

File tree

getstream/video/rtc/encoders_patches.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
from aiortc import RTCRtpCodecParameters
88
from aiortc.codecs.h264 import MAX_FRAME_RATE as H264_MAX_FRAME_RATE
99
from aiortc.codecs.h264 import H264Encoder
10+
from aiortc.codecs.opus import SAMPLE_RATE as OPUS_SAMPLE_RATE
11+
from aiortc.codecs.opus import SAMPLES_PER_FRAME as OPUS_SAMPLES_PER_FRAME
12+
from aiortc.codecs.opus import OpusEncoder
1013
from aiortc.codecs.vpx import Vp8Encoder
1114
from aiortc.rtcrtpsender import RTCEncodedFrame, RTCRtpSender
15+
from av import AudioResampler
1216

1317
logger = logging.getLogger(__name__)
1418

@@ -18,6 +22,13 @@
1822
STREAM_VIDEO_MIN_BITRATE = 1_500_000 # 1.5 Mbps
1923
STREAM_VIDEO_MAX_BITRATE = 3_000_000 # 3 Mbps
2024

25+
# Override hardcoded Opus defaults to reduce CPU usage. Lower than aiortc's 96 kbps stereo default; tuned for
26+
# voice. Both are configurable per StreamOpusEncoder() instance.
27+
STREAM_OPUS_DEFAULT_BITRATE = 64_000 # 96kbps -> 64kbps
28+
STREAM_OPUS_DEFAULT_LAYOUT = "mono" # stereo -> mono
29+
# Compression level <= 7 reduces the CPU work for encoder with minimal quality impact at 64kbps
30+
STREAM_OPUS_DEFAULT_COMPRESSION_LEVEL = 7 # 10 -> 7
31+
2132

2233
# Check if the Stream bitrate patching is disabled via environment variable
2334
BITRATE_PATCH_DISABLED = os.getenv(
@@ -36,6 +47,7 @@
3647

3748

3849
try:
50+
# TODO: Implement a way to configure encoders per track.
3951
# Verify the name-mangled attributes we depend on still exist.
4052
assert hasattr(Vp8Encoder(), "_Vp8Encoder__target_bitrate")
4153
assert hasattr(H264Encoder(), "_H264Encoder__target_bitrate")
@@ -124,16 +136,38 @@ def _encode_frame(self, frame, force_keyframe):
124136
StreamH264Encoder = None # type: ignore[assignment, misc]
125137

126138

139+
class StreamOpusEncoder(OpusEncoder):
140+
"""OpusEncoder subclass with configurable bitrate and channel layout."""
141+
142+
def __init__(
143+
self,
144+
bitrate: int = STREAM_OPUS_DEFAULT_BITRATE,
145+
layout: str = STREAM_OPUS_DEFAULT_LAYOUT,
146+
compression_level: int = STREAM_OPUS_DEFAULT_COMPRESSION_LEVEL,
147+
) -> None:
148+
super().__init__()
149+
self.codec.bit_rate = bitrate
150+
self.codec.layout = layout
151+
self.codec.options = {
152+
"application": "voip",
153+
"compression_level": str(compression_level),
154+
}
155+
# Resampler layout must match codec layout; parent hard-codes stereo.
156+
self.resampler = AudioResampler(
157+
format="s16",
158+
layout=layout,
159+
rate=OPUS_SAMPLE_RATE,
160+
frame_size=OPUS_SAMPLES_PER_FRAME,
161+
)
162+
163+
127164
def patch_sender_encoder(sender: RTCRtpSender) -> None:
128165
"""Patch a sender to use Stream's tuned encoders for the negotiated codec.
129166
130-
Works for video (VP8/H264) senders. If anything
167+
Works for video (VP8/H264) and audio (Opus) senders. If anything
131168
goes wrong (e.g. aiortc internals changed), the sender is left untouched
132169
and will use the stock encoder via get_encoder().
133170
"""
134-
if StreamVp8Encoder is None or StreamH264Encoder is None:
135-
return
136-
137171
try:
138172
_orig_next = sender._next_encoded_frame
139173

@@ -142,10 +176,12 @@ async def _next_with_stream_encoder(
142176
) -> Optional[RTCEncodedFrame]:
143177
if sender._RTCRtpSender__encoder is None: # type: ignore[attr-defined]
144178
mime = codec.mimeType.lower()
145-
if mime == "video/vp8":
179+
if mime == "video/vp8" and StreamVp8Encoder is not None:
146180
sender._RTCRtpSender__encoder = StreamVp8Encoder() # type: ignore[attr-defined]
147-
elif mime == "video/h264":
181+
elif mime == "video/h264" and StreamH264Encoder is not None:
148182
sender._RTCRtpSender__encoder = StreamH264Encoder() # type: ignore[attr-defined]
183+
elif mime == "audio/opus":
184+
sender._RTCRtpSender__encoder = StreamOpusEncoder() # type: ignore[attr-defined]
149185
return await _orig_next(codec)
150186

151187
sender._next_encoded_frame = _next_with_stream_encoder # type: ignore[method-assign]

getstream/video/rtc/pc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ def addTrack(self, track: MediaStreamTrack) -> RTCRtpSender:
8080
if transceiver.sender is sender:
8181
transceiver.setCodecPreferences(publish_codec_preferences())
8282
break
83-
if not BITRATE_PATCH_DISABLED:
84-
patch_sender_encoder(sender)
83+
if not BITRATE_PATCH_DISABLED:
84+
patch_sender_encoder(sender)
8585
return sender
8686

8787
async def handle_answer(self, response):

tests/rtc/test_encoders_patches.py

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@
33

44
import pytest
55
from aiortc.codecs.h264 import H264Encoder
6+
from aiortc.codecs.opus import OpusEncoder
67
from aiortc.codecs.vpx import Vp8Encoder
78

89
from getstream.video.rtc.encoders_patches import (
10+
STREAM_OPUS_DEFAULT_BITRATE,
11+
STREAM_OPUS_DEFAULT_COMPRESSION_LEVEL,
12+
STREAM_OPUS_DEFAULT_LAYOUT,
913
STREAM_VIDEO_DEFAULT_BITRATE,
1014
STREAM_VIDEO_MAX_BITRATE,
1115
STREAM_VIDEO_MIN_BITRATE,
1216
StreamH264Encoder,
17+
StreamOpusEncoder,
1318
StreamVp8Encoder,
1419
patch_sender_encoder,
1520
)
@@ -68,6 +73,31 @@ def test_accepts_in_range(self):
6873
assert enc.target_bitrate == 2_000_000
6974

7075

76+
class TestStreamOpusEncoder:
77+
def test_defaults(self):
78+
enc = StreamOpusEncoder()
79+
assert enc.codec.bit_rate == STREAM_OPUS_DEFAULT_BITRATE
80+
assert enc.codec.layout.name == STREAM_OPUS_DEFAULT_LAYOUT
81+
assert enc.codec.options["compression_level"] == str(
82+
STREAM_OPUS_DEFAULT_COMPRESSION_LEVEL
83+
)
84+
assert enc.codec.options["application"] == "voip"
85+
86+
def test_custom_bitrate(self):
87+
enc = StreamOpusEncoder(bitrate=32_000)
88+
assert enc.codec.bit_rate == 32_000
89+
90+
def test_custom_layout_updates_resampler(self):
91+
"""Resampler layout must track codec layout — encode() asserts on it."""
92+
enc = StreamOpusEncoder(layout="stereo")
93+
assert enc.codec.layout.name == "stereo"
94+
assert enc.resampler.layout.name == "stereo"
95+
96+
def test_custom_compression_level(self):
97+
enc = StreamOpusEncoder(compression_level=10)
98+
assert enc.codec.options["compression_level"] == "10"
99+
100+
71101
class TestBitratePatchDisabled:
72102
@pytest.mark.parametrize("env_val", [None, "", "1", "true", "yes", "on"])
73103
def test_enabled_by_default(self, monkeypatch, env_val):
@@ -165,6 +195,25 @@ async def _orig_coro(codec):
165195

166196
assert isinstance(sender._RTCRtpSender__encoder, H264Cls)
167197

198+
@pytest.mark.asyncio
199+
async def test_installs_opus_encoder(self):
200+
sender = MagicMock()
201+
sender._RTCRtpSender__encoder = None
202+
203+
async def _orig_coro(codec):
204+
return None
205+
206+
sender._next_encoded_frame = _orig_coro
207+
patch_sender_encoder(sender)
208+
209+
codec = MagicMock()
210+
codec.mimeType = "audio/opus"
211+
await sender._next_encoded_frame(codec)
212+
213+
from getstream.video.rtc.encoders_patches import StreamOpusEncoder as OpusCls
214+
215+
assert isinstance(sender._RTCRtpSender__encoder, OpusCls)
216+
168217
@pytest.mark.asyncio
169218
async def test_does_not_replace_existing_encoder(self):
170219
"""Already-set encoder is not replaced."""
@@ -183,15 +232,25 @@ async def _orig_coro(codec):
183232
await sender._next_encoded_frame(codec)
184233
assert sender._RTCRtpSender__encoder is existing_encoder
185234

186-
def test_noop_when_encoders_none(self, monkeypatch):
187-
"""patch_sender_encoder is a no-op when encoder classes failed to load."""
235+
@pytest.mark.asyncio
236+
async def test_skips_mime_when_encoder_class_is_none(self, monkeypatch):
237+
"""If a Stream* class failed to load, that mime branch leaves the encoder unset."""
188238
import getstream.video.rtc.encoders_patches as mod
189239

190240
monkeypatch.setattr(mod, "StreamVp8Encoder", None)
191241
sender = MagicMock()
192-
orig = sender._next_encoded_frame
242+
sender._RTCRtpSender__encoder = None
243+
244+
async def _orig_coro(codec):
245+
return None
246+
247+
sender._next_encoded_frame = _orig_coro
193248
mod.patch_sender_encoder(sender)
194-
assert sender._next_encoded_frame is orig
249+
250+
codec = MagicMock()
251+
codec.mimeType = "video/VP8"
252+
await sender._next_encoded_frame(codec)
253+
assert sender._RTCRtpSender__encoder is None
195254

196255

197256
class TestUpstreamAssumptions:
@@ -216,6 +275,11 @@ def test_upstream_assumptions_hold(self):
216275
"H264Encoder name-mangled __target_bitrate changed"
217276
)
218277

278+
# OpusEncoder still exposes the public attrs StreamOpusEncoder overrides.
279+
opus = OpusEncoder()
280+
assert hasattr(opus, "codec"), "OpusEncoder lost codec"
281+
assert hasattr(opus, "resampler"), "OpusEncoder lost resampler"
282+
219283
# RTCRtpSender has _next_encoded_frame and uses __encoder
220284
import inspect
221285

0 commit comments

Comments
 (0)