1+ import fractions
12import logging
23import os
34from typing import Optional
45
6+ import av
57from aiortc import RTCRtpCodecParameters
8+ from aiortc .codecs .h264 import MAX_FRAME_RATE as H264_MAX_FRAME_RATE
69from aiortc .codecs .h264 import H264Encoder
710from aiortc .codecs .vpx import Vp8Encoder
811from aiortc .rtcrtpsender import RTCEncodedFrame , RTCRtpSender
2629 "off" ,
2730)
2831
32+ # libx264 preset for the StreamH264Encoder. Defaults to "ultrafast" for
33+ # real-time CPU; can be raised (e.g. "veryfast", "medium") for better
34+ # quality-per-bit at the cost of more CPU per frame.
35+ STREAM_H264_PRESET = os .getenv ("STREAM_PATCH_AIORTC_H264_PRESET" , "ultrafast" ).strip ()
36+
2937
3038try :
3139 # Verify the name-mangled attributes we depend on still exist.
@@ -51,11 +59,13 @@ def target_bitrate(self, bitrate: int) -> None:
5159 self ._Vp8Encoder__target_bitrate = bitrate
5260
5361 class StreamH264Encoder (H264Encoder ):
54- """H264Encoder subclass with higher bitrate bounds for Stream calls."""
62+ """H264Encoder subclass with higher bitrate bounds and a real-time
63+ libx264 preset for Stream calls."""
5564
5665 def __init__ (self ) -> None :
5766 super ().__init__ ()
5867 self ._H264Encoder__target_bitrate = STREAM_VIDEO_DEFAULT_BITRATE
68+ self .preset = STREAM_H264_PRESET
5969
6070 @property
6171 def target_bitrate (self ) -> int :
@@ -68,10 +78,45 @@ def target_bitrate(self, bitrate: int) -> None:
6878 )
6979 self ._H264Encoder__target_bitrate = bitrate
7080
81+ def _encode_frame (self , frame , force_keyframe ):
82+ # Mirror parent's invalidation policy so we own codec creation in
83+ # both first-init AND recreation (resolution/bitrate change) paths;
84+ # otherwise parent's `if self.codec is None` branch silently
85+ # reverts our preset to libx264's default `medium`.
86+ codec = self .codec
87+ if (
88+ codec is not None
89+ and codec .bit_rate
90+ and (
91+ frame .width != codec .width
92+ or frame .height != codec .height
93+ or abs (self .target_bitrate - codec .bit_rate ) / codec .bit_rate > 0.1
94+ )
95+ ):
96+ self .buffer_data = b""
97+ self .buffer_pts = None
98+ self .codec = None
99+
100+ if self .codec is None :
101+ self .codec = av .CodecContext .create ("libx264" , "w" )
102+ self .codec .width = frame .width
103+ self .codec .height = frame .height
104+ self .codec .bit_rate = self .target_bitrate
105+ self .codec .pix_fmt = "yuv420p"
106+ self .codec .framerate = fractions .Fraction (H264_MAX_FRAME_RATE , 1 )
107+ self .codec .time_base = fractions .Fraction (1 , H264_MAX_FRAME_RATE )
108+ self .codec .options = {
109+ "level" : "31" ,
110+ "tune" : "zerolatency" ,
111+ "preset" : self .preset ,
112+ }
113+ self .codec .profile = "Baseline"
114+ yield from super ()._encode_frame (frame , force_keyframe )
115+
71116except Exception :
72117 logger .warning (
73- "Failed to patch aiortc video encoder subclasses with Stream bitrate values (aiortc internals may have changed), "
74- "falling back to default aiortc bitrates . \n "
118+ "Failed to patch aiortc encoder subclasses with Stream values (aiortc internals may have changed), "
119+ "falling back to default aiortc encoders . \n "
75120 "Set STREAM_PATCH_AIORTC_BITRATES=0 to disable patching." ,
76121 exc_info = True ,
77122 )
@@ -80,10 +125,11 @@ def target_bitrate(self, bitrate: int) -> None:
80125
81126
82127def patch_sender_encoder (sender : RTCRtpSender ) -> None :
83- """Patch a video sender to use Stream's higher-bitrate encoders.
128+ """Patch a sender to use Stream's tuned encoders for the negotiated codec .
84129
85- If anything goes wrong (e.g. aiortc internals changed), the sender
86- is left untouched and will use the stock encoder via get_encoder().
130+ Works for video (VP8/H264) senders. If anything
131+ goes wrong (e.g. aiortc internals changed), the sender is left untouched
132+ and will use the stock encoder via get_encoder().
87133 """
88134 if StreamVp8Encoder is None or StreamH264Encoder is None :
89135 return
@@ -105,8 +151,8 @@ async def _next_with_stream_encoder(
105151 sender ._next_encoded_frame = _next_with_stream_encoder # type: ignore[method-assign]
106152 except Exception :
107153 logger .warning (
108- "Failed to patch aiortc video encoder subclasses with Stream bitrate values (aiortc internals may have changed), "
109- "falling back to default aiortc bitrates . \n "
154+ "Failed to patch aiortc encoder subclasses with Stream values (aiortc internals may have changed), "
155+ "falling back to default aiortc encoders . \n "
110156 "Set STREAM_PATCH_AIORTC_BITRATES=0 to disable patching." ,
111157 exc_info = True ,
112158 )
0 commit comments