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:

  1. Command-Line Parsing – The Options class reads arguments and stores maxFps and videoBitRate in instance fields.

  2. Encoder ConstructionSurfaceEncoder receives the Options instance and copies values into its own fields (lines 58–60):

    this.videoBitRate = options.getVideoBitRate();
    this.maxFps       = options.getMaxFps();
  3. MediaFormat Preparation – The createFormat() method builds the encoder configuration:

    • Sets KEY_BIT_RATE to the user-defined bitrate
    • Forces KEY_FRAME_RATE to 60 fps (required for encoder initialization)
    • Conditionally adds max-fps-to-encoder if maxFps > 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);
    }
  4. Runtime Behavior – The capture thread continuously feeds the encoder via a Surface. If the device cannot keep pace, the encoder honors the max-fps-to-encoder limit and utilizes KEY_REPEAT_PREVIOUS_FRAME_AFTER to 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.java and applied via MediaFormat.KEY_BIT_RATE in SurfaceEncoder.java.
  • Frame rate limiting uses the private max-fps-to-encoder key 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-fps command-line arguments parsed by the server’s Options class.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →