# How scrcpy Manages Frame Rate and Bitrate for Optimal Screen Mirroring Performance

> Discover how scrcpy optimizes screen mirroring performance by managing frame rate and bitrate using MediaCodec. Learn about --max-fps and --bit-rate flags for low-latency streaming.

- Repository: [Genymobile/scrcpy](https://github.com/Genymobile/scrcpy)
- Tags: performance
- Published: 2026-02-25

---

**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`](https://github.com/Genymobile/scrcpy/blob/main/server/src/main/java/com/genymobile/scrcpy/Options.java) (lines 35–38), the parser stores this value:

```java
// Options.java - Default bitrate initialization
private int videoBitRate = 8000000;  // 8 Mbps

```

This value passes to [`SurfaceEncoder.java`](https://github.com/Genymobile/scrcpy/blob/main/SurfaceEncoder.java) (lines 59–60), where it configures `MediaFormat.KEY_BIT_RATE`:

```java
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`](https://github.com/Genymobile/scrcpy/blob/main/Options.java) (lines 70–73):

```java
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 Construction** – `SurfaceEncoder` receives the `Options` instance and copies values into its own fields (lines 58–60):

   ```java
   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`

   ```java
   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:

```bash
scrcpy --max-fps 30 --bit-rate 2000

```

### Programmatic Configuration in Java

The following snippet mirrors the server’s internal initialization pattern:

```java
// 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`](https://github.com/Genymobile/scrcpy/blob/main/Options.java) and applied via `MediaFormat.KEY_BIT_RATE` in [`SurfaceEncoder.java`](https://github.com/Genymobile/scrcpy/blob/main/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`](https://github.com/Genymobile/scrcpy/blob/main/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`](https://github.com/Genymobile/scrcpy/blob/main/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.