This document describes the V4L2 (Video for Linux 2) integration in scrcpy, which enables the Android device screen to be exposed as a virtual webcam device on Linux systems. This allows other applications (such as video conferencing software, streaming tools, or video editors) to use the mirrored Android screen as a video input source.
V4L2 integration is an optional output sink in the media processing pipeline. For information about the complete media pipeline architecture, see Media Processing Pipeline. For details on recording video to files, see Recording System.
Platform Limitation: V4L2 support is Linux-specific and is not available on Windows or macOS.
The V4L2 sink is implemented as a frame sink component that receives decoded video frames from the decoder and writes them to a V4L2 virtual camera device. It operates in parallel with other sinks like the screen display and recorder.
Diagram: V4L2 Position in Media Pipeline
Sources: app/src/v4l2_sink.h14-32 app/src/decoder.h11-19
The sc_v4l2_sink implements the sc_frame_sink interface, allowing it to receive frames from the decoder's frame source. It then re-encodes these frames as raw YUV420P video and writes them to a V4L2 device node.
The V4L2 sink is represented by the sc_v4l2_sink structure, which contains FFmpeg contexts, synchronization primitives, and a dedicated thread for processing.
Diagram: sc_v4l2_sink struct and its relationships
Sources: app/src/v4l2_sink.h14-32 app/src/v4l2_sink.c354-361
| Member | Type | Purpose |
|---|---|---|
frame_sink | struct sc_frame_sink | Interface trait for receiving frames |
fb | struct sc_frame_buffer | Buffer for frame exchange between decoder thread and V4L2 thread |
format_ctx | AVFormatContext* | FFmpeg format context for V4L2 muxer |
encoder_ctx | AVCodecContext* | FFmpeg encoder context for raw video encoding |
device_name | char* | Path to V4L2 device (e.g., /dev/video2) |
thread | sc_thread | Dedicated thread for V4L2 processing |
mutex | sc_mutex | Protects synchronization state |
cond | sc_cond | Condition variable for frame availability |
has_frame | bool | Signals when a new frame is ready |
stopped | bool | Signals thread termination |
header_written | bool | Tracks whether format header has been written |
Sources: app/src/v4l2_sink.h14-32
V4L2 support is conditionally compiled based on build configuration. It requires the libavdevice FFmpeg library and is only available on Linux.
The build system checks for V4L2 support at configuration time:
v4l2_support = get_option('v4l2') and host_machine.system() == 'linux'
Sources: app/meson.build93
When V4L2 support is enabled:
src/v4l2_sink.c is added to the source list (app/meson.build95)libavdevice dependency is added (app/meson.build124)HAVE_V4L2 macro is defined (app/meson.build174)The V4L2 feature can be controlled via the v4l2 meson option:
option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported')
Sources: meson_options.txt7
To disable V4L2 support during build:
The V4L2 sink is initialized through a multi-stage process involving device name configuration, FFmpeg context setup, and thread creation.
Diagram: V4L2 Initialization Sequence
Sources: app/src/v4l2_sink.c346-363 app/src/v4l2_sink.c149-289
sc_v4l2_sink_init() performs basic initialization:
Sources: app/src/v4l2_sink.c346-363
The sc_v4l2_sink_open() function performs complex initialization:
sc_frame_buffer for thread-safe frame exchangeAV_CODEC_ID_RAWVIDEO encoder for YUV420P outputrun_v4l2_sink threadSources: app/src/v4l2_sink.c149-289
The find_muxer() helper function iterates available FFmpeg output formats using either av_muxer_iterate() (new API) or av_oformat_next() (legacy API), returning the first format whose name list contains the requested string. It is called twice — first with "v4l2", then with "video4linux2" as a fallback — to handle variation across FFmpeg builds.
Sources: app/src/v4l2_sink.c17-32
Frames flow from the decoder through the frame buffer to the V4L2 thread for encoding and output.
Diagram: Frame Flow Through V4L2 Sink
Sources: app/src/v4l2_sink.c310-326 app/src/v4l2_sink.c115-146 app/src/v4l2_sink.c88-112
When the decoder produces a frame, it calls sc_v4l2_frame_sink_push():
sc_frame_bufferThe frame buffer mechanism allows the decoder thread to continue without blocking, while the V4L2 thread processes frames at its own pace. If frames arrive faster than they can be encoded, older frames are discarded.
Sources: app/src/v4l2_sink.c310-326
The V4L2 sink uses a dedicated thread (run_v4l2_sink) to avoid blocking the decoder thread during encoding and I/O operations.
Diagram: Thread Synchronization
Sources: app/src/v4l2_sink.c115-146
run_v4l2_sink() implements the V4L2 processing loop app/src/v4l2_sink.c115-146 It:
mutex and calls sc_cond_wait() until either has_frame or stopped is set.stopped, releases the mutex and exits.has_frame, releases the mutex, then calls sc_frame_buffer_consume() and encode_and_write_frame() outside the critical section.av_frame_unref() after encoding and loops back to wait.Sources: app/src/v4l2_sink.c115-146
| State Variable | Protected By | Purpose |
|---|---|---|
has_frame | mutex | Signals frame availability to V4L2 thread |
stopped | mutex | Signals thread termination request |
fb | Lock-free | Actual frame data exchange |
The design minimizes critical sections:
Sources: app/src/v4l2_sink.c115-146 app/src/v4l2_sink.h24-28
The V4L2 sink re-encodes decoded frames as raw YUV420P video before writing them to the V4L2 device. This is necessary because V4L2 devices typically expect uncompressed video.
Diagram: Encoding Pipeline
Sources: app/src/v4l2_sink.c88-112 app/src/v4l2_sink.c35-57 app/src/v4l2_sink.c60-63
The encode_and_write_frame() function handles frame encoding:
write_packet() for outputSources: app/src/v4l2_sink.c88-112
The first packet triggers write_header() app/src/v4l2_sink.c35-57 which:
extradata on the stream's codecpar.avformat_write_header() to initialize the V4L2 muxer.Importantly, the first packet's data is consumed entirely as codec extradata and is not forwarded as a video frame. write_packet() returns early after writing the header app/src/v4l2_sink.c66-74 The header_written flag ensures this path executes exactly once.
Sources: app/src/v4l2_sink.c35-57 app/src/v4l2_sink.c66-85
Before writing each packet, rescale_packet() calls av_packet_rescale_ts() to convert timestamps from SCRCPY_TIME_BASE ({1, 1000000}, i.e., microseconds) to the output stream's time_base app/src/v4l2_sink.c60-63 This normalization step ensures the V4L2 device receives properly-timed frames regardless of the source frame rate.
Sources: app/src/v4l2_sink.c15 app/src/v4l2_sink.c60-63
The rescaled packet is delivered to the device via av_write_frame() app/src/v4l2_sink.c78 FFmpeg's V4L2 muxer handles the low-level interaction with the V4L2 kernel API. Write failures are logged but treated as non-fatal for any individual frame, since subsequent frames do not depend on prior ones app/src/v4l2_sink.c79-83
Sources: app/src/v4l2_sink.c66-85
The V4L2 sink configures a raw video encoder with specific parameters to match V4L2 device requirements.
The encoder context is configured as follows (app/src/v4l2_sink.c233-237):
| Parameter | Value | Purpose |
|---|---|---|
codec_id | AV_CODEC_ID_RAWVIDEO | Uncompressed video format |
width | From input context | Frame width in pixels |
height | From input context | Frame height in pixels |
pix_fmt | AV_PIX_FMT_YUV420P | YUV 4:2:0 planar format |
time_base.num | 1 | Timebase numerator |
time_base.den | 1 | Timebase denominator |
The pixel format is asserted to be YUV420P at open time (app/src/v4l2_sink.c150).
Sources: app/src/v4l2_sink.c233-237 app/src/v4l2_sink.c149-151
The V4L2 sink performs orderly shutdown through a multi-stage cleanup process.
Diagram: Shutdown Sequence
Sources: app/src/v4l2_sink.c292-308
The sc_v4l2_sink_close() function performs cleanup in the correct order:
stopped flag and signal condition variableSources: app/src/v4l2_sink.c292-308
After the frame sink is closed, sc_v4l2_sink_destroy() frees the device_name string that was duplicated during sc_v4l2_sink_init() app/src/v4l2_sink.c365-368
Sources: app/src/v4l2_sink.c365-368
The V4L2 sink implements the sc_frame_sink trait, making it interchangeable with other frame sinks in the system.
Diagram: sc_frame_sink interface — how sc_decoder connects to sc_v4l2_sink
Sources: app/src/v4l2_sink.c328-344 app/src/decoder.h11-19 app/src/decoder.c64-66
sc_v4l2_sink_init() populates a static sc_frame_sink_ops struct with three wrapper functions and assigns it to frame_sink.ops app/src/v4l2_sink.c354-360:
sc_frame_sink_ops field | Wrapper | Internal function |
|---|---|---|
.open | sc_v4l2_frame_sink_open | sc_v4l2_sink_open |
.close | sc_v4l2_frame_sink_close | sc_v4l2_sink_close |
.push | sc_v4l2_frame_sink_push | sc_v4l2_sink_push |
Each wrapper uses the DOWNCAST macro (container_of(SINK, struct sc_v4l2_sink, frame_sink)) to recover the enclosing sc_v4l2_sink pointer from the sc_frame_sink trait pointer app/src/v4l2_sink.c12-13 This pattern is consistent across all frame sink implementations, allowing the decoder to treat all sinks uniformly through the sc_frame_source abstraction.
Sources: app/src/v4l2_sink.c328-344 app/src/v4l2_sink.c354-361
The V4L2 integration provides a Linux-specific feature that exposes the Android device screen as a virtual webcam. Key characteristics:
v4l2_support (app/meson.build93)sc_frame_sink interface for uniform integrationrun_v4l2_sink() to avoid blocking decoderAV_CODEC_ID_RAWVIDEOsc_frame_buffer for efficient frame exchangeThe implementation is self-contained in app/src/v4l2_sink.c and app/src/v4l2_sink.h with build integration in app/meson.build93-125
Refresh this wiki
This wiki was recently refreshed. Please wait 3 days to refresh again.