# How scrcpy Implements the V4L2 Sink for Linux Webcam Mirroring

> Learn how scrcpy implements the V4L2 sink for Linux webcam mirroring by streaming Android screen frames to a loopback device using FFmpeg. Use your Android device as a virtual webcam.

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

---

**Scrcpy implements the V4L2 sink by streaming decoded Android screen frames to a Video4Linux loopback device using FFmpeg's raw video encoder, enabling any v4l2-compatible application to use the device as a virtual webcam.**

The Genymobile/scrcpy project provides a sophisticated **scrcpy V4L2 sink** implementation that bridges Android screen capture with Linux webcam infrastructure. This feature allows users to present their Android device screen as a standard video device node (e.g., `/dev/video2`) that applications like OBS Studio, Chrome, or VLC can consume directly.

## Architecture of the scrcpy V4L2 Sink

### Core Data Structures (`sc_v4l2_sink`)

The implementation centers around the `sc_v4l2_sink` structure defined in [`app/src/v4l2_sink.h`](https://github.com/Genymobile/scrcpy/blob/main/app/src/v4l2_sink.h). This struct encapsulates all V4L2-specific state:

- **Device path**: Target loopback device (e.g., `/dev/video0`)
- **FFmpeg contexts**: `AVFormatContext` for the v4l2 muxer, `AVCodecContext` for the raw video encoder
- **Synchronization primitives**: `sc_mutex` and `sc_cond` for thread-safe frame buffering
- **Frame buffer**: Lock-free queue (`sc_frame_buffer`) between decoder and sink thread

### Frame-Sink Trait Interface

Scrcpy abstracts video outputs through a generic **frame-sink trait** (`sc_frame_sink`). The V4L2 sink registers its operations via `sc_v4l2_frame_sink_ops`, implementing:

- `open`: Initializes FFmpeg muxer and encoder
- `close`: Signals thread shutdown and releases resources
- `push`: Enqueues decoded frames for the worker thread

This design isolates V4L2-specific logic in [`app/src/v4l2_sink.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/v4l2_sink.c) while allowing the main video pipeline in [`app/src/video.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/video.c) to remain agnostic of the output type.

### FFmpeg Integration

The sink leverages FFmpeg's **v4l2 muxer** to treat the loopback device as an output format. Key technical details:

- **Muxer identification**: Uses `av_guess_format("v4l2", NULL, NULL)` to locate the v4l2 output driver
- **Raw video encoding**: Configures `AV_CODEC_ID_RAWVIDEO` with `AV_PIX_FMT_YUV420P` to match scrcpy's decoded frame format
- **Header handling**: Stores the first encoded packet as `extradata` in the video stream to satisfy V4L2 driver header requirements

## Step-by-Step Implementation Flow

### 1. Initialization and Device Setup

When scrcpy launches with `--v4l2-sink=/dev/videoN`, the following occurs:

1. The CLI parser validates the device path
2. `sc_v4l2_sink_init()` allocates the sink structure and copies the device string
3. The sink is registered with the video pipeline via `sc_video_set_sink()`

### 2. Opening the V4L2 Sink

The `sc_v4l2_sink_open()` function in [`app/src/v4l2_sink.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/v4l2_sink.c) (lines 68-120) performs the heavy lifting:

```c
// Locate the v4l2 muxer
const AVOutputFormat *format = av_guess_format("v4l2", NULL, NULL);

// Allocate output context
AVFormatContext *format_ctx;
avformat_alloc_output_context2(&format_ctx, format, NULL, device_path);

// Open the device for writing
avio_open(&format_ctx->pb, device_path, AVIO_FLAG_WRITE);

```

It then initializes the raw video encoder with parameters matching the source:

```c
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_RAWVIDEO);
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
codec_ctx->width = width;
codec_ctx->height = height;
avcodec_open2(codec_ctx, codec, NULL);

```

### 3. Feeding Frames to the Buffer

When the decoder produces a new frame, `sc_v4l2_sink_push()` enqueues it into the lock-free `sc_frame_buffer`. If the buffer transitions from empty to non-empty, it signals the worker thread via `sc_cond_signal()`.

### 4. Worker Thread Processing

The `run_v4l2_sink()` thread (lines 14-46 in [`v4l2_sink.c`](https://github.com/Genymobile/scrcpy/blob/main/v4l2_sink.c)) operates asynchronously:

1. **Wait for frames**: Blocks on `sc_cond_wait()` until signaled
2. **Retrieve frame**: Pops from `sc_frame_buffer` using `sc_frame_buffer_take()`
3. **Encode**: Calls `avcodec_send_frame()` followed by `avcodec_receive_packet()`
4. **Header handling** (first packet only): In `write_header()` (lines 34-57), copies packet data to `stream->codecpar->extradata` and writes the format header via `avformat_write_header()`
5. **Write packet**: Sends encoded data to the v4l2 device with `av_write_frame()`
6. **Cleanup**: Releases packet and frame resources

### 5. Shutdown and Cleanup

When the session ends:

1. `sc_v4l2_sink_close()` sets the `stopped` atomic flag
2. Signals the condition variable to wake the worker
3. Joins the thread with `sc_thread_join()`
4. Flushes the encoder with `avcodec_send_frame(NULL)` to retrieve remaining packets
5. Writes the trailer with `av_write_trailer()`
6. Closes the AVIO context with `avio_close()`
7. Frees all FFmpeg contexts and synchronization objects

## Practical Usage Examples

### Command-Line Setup with v4l2loopback

To use the scrcpy V4L2 sink, you must first install the kernel module:

```bash

# Install the v4l2loopback driver

sudo apt install v4l2loopback-dkms
sudo modprobe v4l2loopback

# Verify the device was created

ls /dev/video*

```

Then start scrcpy with the sink enabled:

```bash

# Stream to /dev/video2 without displaying locally

scrcpy --v4l2-sink=/dev/video2 --no-video-playback

# Or use a specific device name for clarity

scrcpy --v4l2-sink=/dev/video0

```

### Configuring Buffer Latency

Scrcpy provides a configurable buffering delay to balance latency against frame drops:

```bash

# Set 300ms buffer (default may vary by version)

scrcpy --v4l2-sink=/dev/video0 --v4l2-buffer=300

```

The underlying implementation adjusts the `sc_frame_buffer` capacity in [`app/src/v4l2_sink.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/v4l2_sink.c) based on this parameter.

### C API Integration Example

For developers integrating scrcpy as a library, the V4L2 sink follows the standard frame-sink pattern:

```c
#include "v4l2_sink.h"

// Initialize the sink structure
struct sc_v4l2_sink v4l2_sink;
if (!sc_v4l2_sink_init(&v4l2_sink, "/dev/video2")) {
    fprintf(stderr, "Failed to initialize V4L2 sink\n");
    return -1;
}

// Open with the decoder context (AVCodecContext*)
if (!sc_v4l2_sink_open(&v4l2_sink, decoder_ctx)) {
    fprintf(stderr, "Failed to open V4L2 device\n");
    return -1;
}

// Push decoded frames (AVFrame*)
sc_v4l2_sink_push(&v4l2_sink, frame);

// Cleanup when done
sc_v4l2_sink_close(&v4l2_sink);
sc_v4l2_sink_destroy(&v4l2_sink);

```

This mirrors the internal flow used by scrcpy's video pipeline in [`app/src/video.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/video.c).

## Key Source Files

The scrcpy V4L2 sink implementation spans these critical files in the Genymobile/scrcpy repository:

- **[`app/src/v4l2_sink.h`](https://github.com/Genymobile/scrcpy/blob/main/app/src/v4l2_sink.h)** — Defines the `sc_v4l2_sink` structure and the public API including `sc_v4l2_sink_init()`, `sc_v4l2_sink_open()`, and `sc_v4l2_sink_push()`.

- **[`app/src/v4l2_sink.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/v4l2_sink.c)** — Contains the full implementation including the `run_v4l2_sink()` worker thread, FFmpeg muxer setup, raw video encoding, and header handling logic.

- **[`app/src/frame_sink.h`](https://github.com/Genymobile/scrcpy/blob/main/app/src/frame_sink.h)** and **[`app/src/frame_sink.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/frame_sink.c)** — Define the generic `sc_frame_sink` trait that the V4L2 sink implements, providing abstraction for the video pipeline.

- **[`app/src/video.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/video.c)** — Orchestrates the video pipeline and connects the decoder output to the selected frame sink via `sc_video_set_sink()`.

- **[`doc/v4l2.md`](https://github.com/Genymobile/scrcpy/blob/main/doc/v4l2.md)** — User-facing documentation explaining how to enable and configure the V4L2 sink feature.

## Summary

- **Scrcpy's V4L2 sink** turns an Android screen stream into a standard Linux video device using FFmpeg's v4l2 muxer and raw video encoding.
- The implementation resides primarily in [`app/src/v4l2_sink.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/v4l2_sink.c) and uses a dedicated worker thread (`run_v4l2_sink`) to asynchronously encode and write frames.
- **Frame-sink abstraction** allows the V4L2 output to plug into scrcpy's generic video pipeline alongside other sinks like SDL or file recording.
- The system requires the **v4l2loopback** kernel module to create the virtual video device node that applications recognize as a webcam.
- Users control latency through the `--v4l2-buffer` option, which adjusts the internal `sc_frame_buffer` capacity.

## Frequently Asked Questions

### What kernel module is required for scrcpy V4L2 sink functionality?

The **v4l2loopback** kernel module is required to create virtual video device nodes. Install it via your distribution's package manager (e.g., `sudo apt install v4l2loopback-dkms` on Debian/Ubuntu), then load it with `sudo modprobe v4l2loopback`. This creates `/dev/videoN` devices that scrcpy can write to using the `--v4l2-sink` option.

### Does the V4L2 sink support audio streaming?

No, the V4L2 sink implementation in [`app/src/v4l2_sink.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/v4l2_sink.c) handles **video frames only**. It uses FFmpeg's raw video encoder (`AV_CODEC_ID_RAWVIDEO`) to stream YUV420P frames to the video device node. Audio is handled separately through scrcpy's audio forwarding system (via `--audio-dup` or `--audio-output`), which does not route through the V4L2 sink.

### What video format does scrcpy use for the V4L2 output?

Scrcpy outputs **raw YUV420P** format to the V4L2 device. In `sc_v4l2_sink_open()` within [`app/src/v4l2_sink.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/v4l2_sink.c), the code configures the FFmpeg encoder with `codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P` and `codec_ctx->codec_id = AV_CODEC_ID_RAWVIDEO`. This uncompressed format ensures compatibility with most v4l2 applications while minimizing CPU overhead from format conversion.

### Can I use the V4L2 sink on macOS or Windows?

No, the V4L2 sink is **Linux-specific**. It relies on the Video4Linux2 API and the v4l2loopback kernel module, which are exclusive to Linux. The implementation in [`app/src/v4l2_sink.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/v4l2_sink.c) uses Linux-specific device paths (`/dev/videoN`) and FFmpeg's v4l2 muxer, which requires V4L2 kernel interfaces. macOS and Windows users seeking similar functionality should use scrcpy's standard SDL display or third-party screen capture tools instead.