How scrcpy Implements the V4L2 Sink for Linux Webcam Mirroring

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. 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 while allowing the main video pipeline in 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 (lines 68-120) performs the heavy lifting:

// 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:

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) 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:


# 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:


# 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:


# 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 based on this parameter.

C API Integration Example

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

#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.

Key Source Files

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

  • 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 — 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 and 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 — Orchestrates the video pipeline and connects the decoder output to the selected frame sink via sc_video_set_sink().

  • 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 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 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, 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 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.

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 →