> This page is for version 5.0 (Latest) (default).
> For other versions, use one of these documentation indexes:
> - 5.0 (Latest) (default): https://docs.nvidia.com/holoscan/sdk-user-guide/5-0-latest/llms.txt
> - ltsb-2.0: https://docs.nvidia.com/holoscan/sdk-user-guide/ltsb-2-0/llms.txt
> - 4.6: https://docs.nvidia.com/holoscan/sdk-user-guide/4-6/llms.txt
> - 4.5: https://docs.nvidia.com/holoscan/sdk-user-guide/4-5/llms.txt
> - 4.4: https://docs.nvidia.com/holoscan/sdk-user-guide/4-4/llms.txt
> - 4.3: https://docs.nvidia.com/holoscan/sdk-user-guide/4-3/llms.txt
> - Archives: https://docs.nvidia.com/holoscan/sdk-user-guide/archives/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/holoscan/sdk-user-guide/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/holoscan/sdk-user-guide/_mcp/server.

V4L2 camera capture for Holoscan SDK 5.x on Linux. See the
[module README](/holoscan/sdk-user-guide/modules/camera/overview) for SDK setup and build instructions.
For Python setup, see the [Camera Python guide](/holoscan/sdk-user-guide/modules/camera/python-support).
For the Python operator interface, see the [V4L2 binding section](/holoscan/sdk-user-guide/modules/camera/python-support#v4l2).

## C++ reference application

See the [V4L2 application guide](/holoscan/sdk-user-guide/modules/camera/v4l2-application) for graph validation,
live capture, memory placement options, and CTest commands.

## Python reference application

See the [V4L2 Python application guide](/holoscan/sdk-user-guide/modules/camera/v4l2-python-application)
for `validate`, `capture`, and Holoviz `preview` modes, environment setup,
and application tests.

## Interface

| Operator        | Implementation                  | Ports                                                                                                               | Parameters                                                    |
| --------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `V4l2CaptureOp` | C++, memory-mapped V4L2 capture | `frame`: `holoscan::schema::ImageT`, rank-2 `uint8` tensor of shape `[height, 2 * width]`, placed per `memory_kind` | `device`, `width`, `height`, `fps`, `frame_id`, `memory_kind` |

`V4l2CaptureOp` opens `/dev/video*`, negotiates YUYV, streams via `mmap`, and publishes
frames from a reader thread that wakes the operator through a notification source.

The published frame is the **packed YUYV 4:2:2 frame**, chroma included, under
`ImageEncoding_YUYV`. The tensor is rank 2 and one byte per element, shaped `[height, 2 * width]`:
a row is its own byte extent, and the pixel geometry is read from the descriptor's `width`,
`height`, and `encoding` rather than inferred from the shape. A downstream consumer must declare
that carriage, including the placement the operator was constructed with:

```cpp
spec.input(frame_in, "frame_in")
    .queue_depth(4U)
    .expects_tensor(holoscan::TensorInputSpec{
        .representation = {.memory_kind = holoscan::MemoryKind::kHost,
                           .dtype = holoscan::camera::kFrameElementDtype,
                           .rank = 2U},
    });
```

Capture width must be even. A packed 4:2:2 group spans two columns, and an even width is what
makes the driver's `2 * width` row equal the `4 * ceil(width / 2)` the schema derives, so the
tensor addresses exactly the frame the descriptor describes.

A consumer whose declared rank, dtype, or placement differs from what the operator publishes will
fail the port contract at graph authoring time. The rank and dtype are fixed by the payload above;
the placement is the operator's `memory_kind`, so the two must be chosen together.

## Frame placement

The `memory_kind` constructor parameter selects where the frame lands. It is frozen into the port
contract at `setup()`, so it is an authoring-time choice rather than something a running graph
renegotiates, and a consumer's `expects_tensor` must name the same placement.

| `memory_kind`     | Frame lands in          | Use when                                                                                                                            | Needs a device binding |
| ----------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| `kHost` (default) | Pageable host memory    | Consumers read on the CPU, and nothing downstream needs the GPU                                                                     | No — must be absent    |
| `kPinnedHost`     | Page-locked host memory | Consumers read on the CPU *and* something downstream transfers to the GPU, which then becomes a direct DMA instead of a staged copy | No — must be absent    |
| `kCudaDevice`     | CUDA device memory      | Every consumer is device-resident, so paying the transfer here avoids a round trip                                                  | **Yes**                |

The driver always delivers into a pageable `mmap` buffer, so every placement copies once out of it;
the parameter chooses what that copy targets, not whether it happens. Use it to state where this
operator's own consumers want the data. Repairing a placement mismatch *between* two operators is
an edge concern — see `ConnectionOptions::memory_conversion` — not a reason to change the producer.

`kUnknown` and `kCudaManaged` are rejected at construction.

### `kCudaDevice` requires the application to name a GPU

Compilation will not choose a device for a device-resident pool; it rejects the plan with
`DEVICE_UNRESOLVED`, because an implicitly chosen GPU is an implicitly chosen cross-device transfer
later. The application supplies it, since which GPU to use is a deployment fact the operator does
not own:

```cpp
holoscan::CompileOptions options{};
options.deployment.bind_tensor_output_device(holoscan::TensorOutputDevicePlacement{
    .operator_path = "camera", .output_port = "frame", .device = holoscan::DeviceId{0}});
const holoscan::ExecutionPlan plan = holoscan::compile(graph, std::move(options));
```

The binding must be *absent* for `kHost` and process-local `kPinnedHost`, where supplying one is
rejected as `TENSOR_DEVICE_BINDING_UNEXPECTED`. It is therefore not safe to set unconditionally —
see `--memory-kind` handling in the reference application for the conditional form.

## C++ Usage

```cpp
#include <holoscan/core/compile.hpp>
#include <holoscan/core/graph.hpp>
#include <v4l2_capture_op/v4l2_capture_op.hpp>

namespace mm = holoscan::camera;

holoscan::Graph graph{"example"};
auto camera = graph.op<mm::V4l2CaptureOp>("camera", "/dev/video0", 640, 480, 30);
```

## Capture counters

A C++ reference capture run ends with one summary line:

```text
frames: received=N with-capture-time=N degraded=N corrupt=N unnamed-clock=N
```

Use these counters to check completion, timestamp coverage, and sample quality:

| Counter             | Meaning                                                                                                                                                                                                                                                                                        |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `received`          | Number of frames received by the reference sink. Compare with the requested `--frames`.                                                                                                                                                                                                        |
| `with-capture-time` | Number of received frames carrying a projected capture timestamp.                                                                                                                                                                                                                              |
| `degraded`          | `SampleFlags::kDegraded`. Set when the device sequence skipped forward (the driver produced frames that were never collected), when the device sequence went backwards, or when the frame arrived shorter than its declared byte extent. The rows that did arrive are still real measurements. |
| `corrupt`           | `SampleFlags::kInvalid`. Set only when the driver returned the buffer with `V4L2_BUF_FLAG_ERROR` — the driver reporting its own contents as bad. The pixels are not measurements and must not be treated as such.                                                                              |
| `unnamed-clock`     | Number of frames with a valid `source_clock_id` whose name is absent from the compiled plan's clock catalog. Frames without a capture timestamp do not increment this counter.                                                                                                                 |

Reject samples carrying `SampleFlags::kInvalid`; inspect `SampleFlags::kDegraded`
when deciding whether a partial frame or sequence discontinuity is acceptable.
Check `received` against the requested frame count and `with-capture-time`
against `received` to assess completion and timestamp coverage.

See the [V4L2 benchmarks and qualification results](/holoscan/sdk-user-guide/modules/camera/v4l2-benchmarks)
for recorded platform behavior and the [release notes](https://github.com/nvidia-holoscan/holoscan-camera/blob/v1.0.0/RELEASE_NOTES.md)
for known issues.