How scrcpy Manages Frame Rate and Bitrate for Optimal Screen Mirroring Performance
Scrcpy controls frame rate and bitrate through Android's MediaCodec surface encoder, using the --max-fps and --bit-rate command-line flags to configure the MediaFormat with KEY_BIT_RATE and the private max-fps-to-encoder key, preventing encoder backlog and maintaining low-latency streaming.
Scrcpy, the popular open-source screen mirroring tool from Genymobile, delivers smooth, responsive Android streaming by precisely managing encoder parameters at the native level. Understanding how scrcpy handles frame rate and bitrate configuration reveals the technical mechanisms that keep latency minimal even on constrained networks or lower-end hardware.
The Core Mechanism: MediaCodec and User Controls
Scrcpy’s video pipeline centers on Android’s MediaCodec surface encoder. Two primary user-configurable parameters—--max-fps and --bit-rate—propagate from command-line arguments through the server’s option parser into the encoder’s MediaFormat configuration.
Bitrate Configuration via --bit-rate
The video_bit_rate parameter, exposed as --bit-rate in kilobits per second, defaults to 8,000,000 bits per second (8 Mbps). In server/src/main/java/com/genymobile/scrcpy/Options.java (lines 35–38), the parser stores this value:
// Options.java - Default bitrate initialization
private int videoBitRate = 8000000; // 8 Mbps
This value passes to SurfaceEncoder.java (lines 59–60), where it configures MediaFormat.KEY_BIT_RATE:
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
Lower values reduce bandwidth consumption and CPU load, while higher values improve visual quality at the cost of network usage.
Frame Rate Control via --max-fps
The max_fps parameter defaults to 0 (no explicit limit) and is parsed in Options.java (lines 70–73):
case "max_fps":
options.maxFps = parseFloat("max_fps", value);
break;
When maxFps exceeds 0, SurfaceEncoder.createFormat() (lines 69–74) injects the private key max-fps-to-encoder into the MediaFormat. This proprietary flag instructs the encoder to cap its output frame rate, allowing the device to drop frames when it cannot sustain the target rate. This mechanism prevents the encoder from queuing an ever-growing backlog of frames, which is critical for maintaining low latency.
Notably, SurfaceEncoder hard-codes KEY_FRAME_RATE to 60 fps (lines 60–62) to initialize the encoder, but the actual output remains variable and constrained by the max-fps-to-encoder ceiling.
Step-by-Step Data Flow
The configuration propagates through the server architecture in four distinct stages:
-
Command-Line Parsing – The
Optionsclass reads arguments and storesmaxFpsandvideoBitRatein instance fields. -
Encoder Construction –
SurfaceEncoderreceives theOptionsinstance and copies values into its own fields (lines 58–60):this.videoBitRate = options.getVideoBitRate(); this.maxFps = options.getMaxFps(); -
MediaFormat Preparation – The
createFormat()method builds the encoder configuration:- Sets
KEY_BIT_RATEto the user-defined bitrate - Forces
KEY_FRAME_RATEto 60 fps (required for encoder initialization) - Conditionally adds
max-fps-to-encoderifmaxFps > 0
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_FRAME_RATE, 60); if (maxFps > 0) { format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps); } - Sets
-
Runtime Behavior – The capture thread continuously feeds the encoder via a
Surface. If the device cannot keep pace, the encoder honors themax-fps-to-encoderlimit and utilizesKEY_REPEAT_PREVIOUS_FRAME_AFTERto manage frame repetition, ensuring the stream remains responsive rather than accumulating latency through buffering.
Practical Configuration Examples
Optimizing for Slow Network Conditions
Limit the stream to 30 fps and 2 Mbps to reduce bandwidth usage on congested Wi-Fi:
scrcpy --max-fps 30 --bit-rate 2000
Programmatic Configuration in Java
The following snippet mirrors the server’s internal initialization pattern:
// Build Options manually (as the server does)
Options opts = new Options();
opts.videoBitRate = 2_000_000; // 2 Mbps
opts.maxFps = 30f; // 30 fps cap
// Initialise encoder with those options
SurfaceEncoder encoder = new SurfaceEncoder(capture, streamer, opts);
encoder.start(() -> System.out.println("Encoder stopped"));
This follows the same flow as SurfaceEncoder’s constructor (lines 55–63) and the createFormat method implementation in the Genymobile/scrcpy repository.
Summary
- Default bitrate is set to 8 Mbps in
Options.javaand applied viaMediaFormat.KEY_BIT_RATEinSurfaceEncoder.java. - Frame rate limiting uses the private
max-fps-to-encoderkey to cap encoder output without affecting the hard-coded 60 fps initialization value. - Latency prevention is achieved by allowing frame drops when the device cannot sustain the target rate, preventing backlog accumulation in the encoder queue.
- Configuration occurs through
--bit-rate(kbps) and--max-fpscommand-line arguments parsed by the server’sOptionsclass.
Frequently Asked Questions
What is the default bitrate in scrcpy?
The default video bitrate is 8 Mbps (8,000,000 bits per second), defined as a constant in server/src/main/java/com/genymobile/scrcpy/Options.java at line 36. This provides a balance between quality and bandwidth for most modern networks.
How does --max-fps differ from the 60 fps hard-coded value?
The hard-coded KEY_FRAME_RATE of 60 fps in SurfaceEncoder.java (line 61) is required to initialize the Android MediaCodec encoder. However, the --max-fps value configures the private max-fps-to-encoder key, which actually constrains the encoder’s output frame rate. If the source cannot sustain 60 fps, the encoder operates in variable-fps mode, but it will never exceed the user-defined --max-fps ceiling.
Can I change the frame rate and bitrate during an active session?
No, scrcpy applies these parameters during encoder initialization in SurfaceEncoder.createFormat(). To modify settings, you must terminate the current session and restart scrcpy with new --max-fps or --bit-rate arguments. The encoder configuration is immutable once the MediaCodec instance starts.
Why does scrcpy use a private key max-fps-to-encoder instead of standard Android APIs?
The max-fps-to-encoder key is a private vendor-specific extension to Android’s MediaCodec that allows direct frame rate capping at the encoder level. Standard APIs like KEY_FRAME_RATE only suggest a target rate rather than enforcing a hard limit. Using this private key ensures the encoder drops excess frames immediately rather than buffering them, which is essential for maintaining the low-latency characteristics that distinguish scrcpy from other screen mirroring solutions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →