[v3,1/2] libcamera: Add SensorSequence metadata control
diff mbox series

Message ID 20260805151819.16382-2-ballen4705@googlemail.com
State New
Headers show
Series
  • libcamera: Add SensorSequence request metadata control
Related show

Commit Message

Bruce Allen Aug. 5, 2026, 3:18 p.m. UTC
Add controls::SensorSequence (int64_t, out) to report the monotonic
sequence number from the sensor-facing capture device in request
metadata, and populate it in pipeline handlers that already report
SensorTimestamp.

Signed-off-by: Bruce Allen <ballen4705@googlemail.com>
---
 src/libcamera/control_ids_core.yaml           | 29 +++++++++++++++++++
 src/libcamera/pipeline/imx8-isi/imx8-isi.cpp  |  3 ++
 src/libcamera/pipeline/ipu3/ipu3.cpp          |  2 ++
 src/libcamera/pipeline/mali-c55/mali-c55.cpp  |  2 ++
 src/libcamera/pipeline/rkisp1/rkisp1.cpp      |  2 ++
 .../pipeline/rpi/common/pipeline_base.cpp     |  2 ++
 src/libcamera/pipeline/rpi/pisp/pisp.cpp      |  2 ++
 src/libcamera/pipeline/rpi/vc4/vc4.cpp        |  2 ++
 src/libcamera/pipeline/simple/simple.cpp      |  5 +++-
 src/libcamera/pipeline/uvcvideo/uvcvideo.cpp  |  2 ++
 src/libcamera/pipeline/vimc/vimc.cpp          |  2 ++
 11 files changed, 52 insertions(+), 1 deletion(-)

Comments

Barnabás Pőcze Aug. 5, 2026, 3:25 p.m. UTC | #1
CC Naushir Patuck

Please see below

2026. 08. 05. 17:18 keltezéssel, Bruce Allen írta:
> Add controls::SensorSequence (int64_t, out) to report the monotonic
> sequence number from the sensor-facing capture device in request
> metadata, and populate it in pipeline handlers that already report
> SensorTimestamp.
> 
> Signed-off-by: Bruce Allen <ballen4705@googlemail.com>
> ---
>   src/libcamera/control_ids_core.yaml           | 29 +++++++++++++++++++
>   src/libcamera/pipeline/imx8-isi/imx8-isi.cpp  |  3 ++
>   src/libcamera/pipeline/ipu3/ipu3.cpp          |  2 ++
>   src/libcamera/pipeline/mali-c55/mali-c55.cpp  |  2 ++
>   src/libcamera/pipeline/rkisp1/rkisp1.cpp      |  2 ++
>   .../pipeline/rpi/common/pipeline_base.cpp     |  2 ++
>   src/libcamera/pipeline/rpi/pisp/pisp.cpp      |  2 ++
>   src/libcamera/pipeline/rpi/vc4/vc4.cpp        |  2 ++
>   src/libcamera/pipeline/simple/simple.cpp      |  5 +++-
>   src/libcamera/pipeline/uvcvideo/uvcvideo.cpp  |  2 ++
>   src/libcamera/pipeline/vimc/vimc.cpp          |  2 ++
>   11 files changed, 52 insertions(+), 1 deletion(-)
> 
> diff --git a/src/libcamera/control_ids_core.yaml b/src/libcamera/control_ids_core.yaml
> index 89991d0..bebfb49 100644
> --- a/src/libcamera/control_ids_core.yaml
> +++ b/src/libcamera/control_ids_core.yaml
> @@ -792,6 +792,35 @@ controls:
>           \todo Define how the sensor timestamp has to be used in the reprocessing
>           use case.
>   
> +  - SensorSequence:
> +      type: int64_t
> +      direction: out
> +      description: |
> +        The monotonic sequence number from the sensor capture device.
> +
> +        Pipeline handlers populate this metadata control with the sequence
> +        number reported by the sensor-facing capture device (for example the
> +        CSI-2 receiver or V4L2 video node) for the frame associated with the
> +        request.
> +
> +        While per-buffer FrameMetadata::sequence identifies frames in each
> +        completed stream, SensorSequence reflects the sensor-side capture path.
> +        If the value increases by more than one between consecutive completed
> +        requests, frames were not captured before the earlier request finished.
> +        This complements sequence numbers derived from request delivery and is
> +        intended for diagnosing frame continuity.
> +
> +        After Camera::stop() followed by Camera::start(), SensorSequence restarts
> +        from 0 for the new streaming session. The first returned frame has
> +        SensorSequence == 0 when the first capture buffer of the session is
> +        completed to that request. Applications must not assume continuity of
> +        SensorSequence values across camera session stop/start boundaries.

In https://patchwork.libcamera.org/patch/15057/#21643 it was stated

   Should we add that there are no guarantees about this sequence number being reset on start/stop?

I'm guessing this still applies to the raspberry pi pipeline handlers?


Regards,
Barnabás Pőcze


> +
> +        On successful frames where SensorTimestamp is populated,
> +        SensorSequence will also be populated.
> +
> +        The SensorSequence control can only be returned in metadata.
> +
>     - AfMode:
>         type: int32_t
>         direction: inout
> diff --git a/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp b/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp
> index c3d2695..d2175b9 100644
> --- a/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp
> +++ b/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp
> @@ -1130,6 +1130,9 @@ void PipelineHandlerISI::bufferReady(FrameBuffer *buffer)
>   	if (!metadata.contains(controls::SensorTimestamp.id()))
>   		metadata.set(controls::SensorTimestamp,
>   			     buffer->metadata().timestamp);
> +	if (!metadata.contains(controls::SensorSequence.id()))
> +		metadata.set(controls::SensorSequence,
> +			     static_cast<int64_t>(buffer->metadata().sequence));
>   
>   	if (completeBuffer(request, buffer))
>   		completeRequest(request);
> diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/pipeline/ipu3/ipu3.cpp
> index 0f3e169..6494259 100644
> --- a/src/libcamera/pipeline/ipu3/ipu3.cpp
> +++ b/src/libcamera/pipeline/ipu3/ipu3.cpp
> @@ -1317,6 +1317,8 @@ void IPU3CameraData::cio2BufferReady(FrameBuffer *buffer)
>   	 */
>   	request->_d()->metadata().set(controls::SensorTimestamp,
>   				      buffer->metadata().timestamp);
> +	request->_d()->metadata().set(controls::SensorSequence,
> +				      static_cast<int64_t>(buffer->metadata().sequence));
>   
>   	info->effectiveSensorControls = delayedCtrls_->get(buffer->metadata().sequence);
>   
> diff --git a/src/libcamera/pipeline/mali-c55/mali-c55.cpp b/src/libcamera/pipeline/mali-c55/mali-c55.cpp
> index 599ff88..119b249 100644
> --- a/src/libcamera/pipeline/mali-c55/mali-c55.cpp
> +++ b/src/libcamera/pipeline/mali-c55/mali-c55.cpp
> @@ -1704,6 +1704,8 @@ void PipelineHandlerMaliC55::cruBufferReady(FrameBuffer *buffer)
>   	Request *request = info->request;
>   	request->_d()->metadata().set(controls::SensorTimestamp,
>   				      buffer->metadata().timestamp);
> +	request->_d()->metadata().set(controls::SensorSequence,
> +				      static_cast<int64_t>(buffer->metadata().sequence));
>   
>   	MaliC55CameraData *data = cameraData(request->_d()->camera());
>   	data->ipa_->fillParams(request->sequence(), info->paramBuffer->cookie());
> diff --git a/src/libcamera/pipeline/rkisp1/rkisp1.cpp b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
> index 4f9c0aa..0c73f5a 100644
> --- a/src/libcamera/pipeline/rkisp1/rkisp1.cpp
> +++ b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
> @@ -1647,6 +1647,8 @@ void PipelineHandlerRkISP1::imageBufferReady(FrameBuffer *buffer)
>   		 */
>   		request->_d()->metadata().set(controls::SensorTimestamp,
>   					      metadata.timestamp);
> +		request->_d()->metadata().set(controls::SensorSequence,
> +					      static_cast<int64_t>(metadata.sequence));
>   
>   		if (isRaw_) {
>   			const ControlList &ctrls =
> diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
> index 5a5acf6..dcf8e6a 100644
> --- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
> +++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
> @@ -1514,6 +1514,8 @@ void CameraData::fillRequestMetadata(const ControlList &bufferControls, Request
>   {
>   	if (auto x = bufferControls.get(controls::SensorTimestamp))
>   		request->_d()->metadata().set(controls::SensorTimestamp, *x);
> +	if (auto x = bufferControls.get(controls::SensorSequence))
> +		request->_d()->metadata().set(controls::SensorSequence, *x);
>   	if (auto x = bufferControls.get(controls::FrameWallClock))
>   		request->_d()->metadata().set(controls::FrameWallClock, *x);
>   
> diff --git a/src/libcamera/pipeline/rpi/pisp/pisp.cpp b/src/libcamera/pipeline/rpi/pisp/pisp.cpp
> index 406d683..5cf428c 100644
> --- a/src/libcamera/pipeline/rpi/pisp/pisp.cpp
> +++ b/src/libcamera/pipeline/rpi/pisp/pisp.cpp
> @@ -1783,6 +1783,8 @@ void PiSPCameraData::cfeBufferDequeue(FrameBuffer *buffer)
>   
>   		ctrl.set(controls::SensorTimestamp, sensorTimestamp);
>   		ctrl.set(controls::FrameWallClock, wallClockTimestamp);
> +		ctrl.set(controls::SensorSequence,
> +			 static_cast<int64_t>(buffer->metadata().sequence));
>   		job.sensorControls = std::move(ctrl);
>   		job.delayContext = delayContext;
>   	} else if (stream == &cfe_[Cfe::Config]) {
> diff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
> index 3e9a490..1e7b26a 100644
> --- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp
> +++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
> @@ -787,6 +787,8 @@ void Vc4CameraData::unicamBufferDequeue(FrameBuffer *buffer)
>   
>   		ctrl.set(controls::SensorTimestamp, sensorTimestamp);
>   		ctrl.set(controls::FrameWallClock, wallClockTimestamp);
> +		ctrl.set(controls::SensorSequence,
> +			 static_cast<int64_t>(buffer->metadata().sequence));
>   		bayerQueue_.push({ buffer, std::move(ctrl), delayContext });
>   	} else {
>   		embeddedQueue_.push(buffer);
> diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
> index e26f438..ce34bb2 100644
> --- a/src/libcamera/pipeline/simple/simple.cpp
> +++ b/src/libcamera/pipeline/simple/simple.cpp
> @@ -939,9 +939,12 @@ void SimpleCameraData::imageBufferReady(FrameBuffer *buffer)
>   		}
>   	}
>   
> -	if (request)
> +	if (request) {
>   		request->_d()->metadata().set(controls::SensorTimestamp,
>   					      buffer->metadata().timestamp);
> +		request->_d()->metadata().set(controls::SensorSequence,
> +					      static_cast<int64_t>(buffer->metadata().sequence));
> +	}
>   
>   	/*
>   	 * Queue the captured and the request buffer to the converter or Software
> diff --git a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
> index 3435a76..e0b7dad 100644
> --- a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
> +++ b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
> @@ -897,6 +897,8 @@ void UVCCameraData::imageBufferReady(FrameBuffer *buffer)
>   	/* \todo Use the UVC metadata to calculate a more precise timestamp */
>   	request->_d()->metadata().set(controls::SensorTimestamp,
>   				      buffer->metadata().timestamp);
> +	request->_d()->metadata().set(controls::SensorSequence,
> +				      static_cast<int64_t>(buffer->metadata().sequence));
>   
>   	pipe()->completeBuffer(request, buffer);
>   	pipe()->completeRequest(request);
> diff --git a/src/libcamera/pipeline/vimc/vimc.cpp b/src/libcamera/pipeline/vimc/vimc.cpp
> index 01d8fb2..ff809a1 100644
> --- a/src/libcamera/pipeline/vimc/vimc.cpp
> +++ b/src/libcamera/pipeline/vimc/vimc.cpp
> @@ -617,6 +617,8 @@ void VimcCameraData::imageBufferReady(FrameBuffer *buffer)
>   	/* Record the sensor's timestamp in the request metadata. */
>   	request->_d()->metadata().set(controls::SensorTimestamp,
>   				      buffer->metadata().timestamp);
> +	request->_d()->metadata().set(controls::SensorSequence,
> +				      static_cast<int64_t>(buffer->metadata().sequence));
>   
>   	pipe->completeBuffer(request, buffer);
>   	pipe->completeRequest(request);
Bruce Allen Aug. 5, 2026, 3:39 p.m. UTC | #2
Hi Barnabás,
(CC Naushir)

thanks, that was incredibly fast! Regarding Naushir's earlier comment:

   https://patchwork.libcamera.org/patch/15057/#21643

   "Should we add that there are no guarantees about this sequence number
    being reset on start/stop?"

That caution was about the kernel/driver sequence, which may not restart 
at zero on STREAMON. It still applies in that sense (Raspberry Pi and 
other drivers sometimes start at a non-zero V4L2 sequence).

However, for the application-visible SensorSequence value, libcamera 
already initializes this. V4L2VideoDevice resets its rebase state on 
every streamOn() and subtracts the first dequeued sequence so 
FrameMetadata::sequence starts at 0 for each new streaming session:

  
https://git.libcamera.org/libcamera/libcamera.git/tree/src/libcamera/v4l2_videodevice.cpp

(See V4L2VideoDevice::streamOn() resetting firstFrame_, and 
dequeueBuffer() subtracting firstFrame_ from buf.sequence. Background on 
that behaviour:

https://lists.libcamera.org/pipermail/libcamera-devel/2022-June/031163.html)

The Raspberry Pi pipeline handlers (and the others in this series) 
populate SensorSequence from that same buffer->metadata().sequence, so 
after Camera::stop() followed by Camera::start() the capture-device 
sequence DOES restart from 0 for the new session.

The remaining caveat, which is already in the v3 control docs, is only 
about which request the application sees first: SensorSequence == 0 on 
the first returned frame when the first capture buffer of the session is 
completed to that request. Internal / freewheel buffers could mean the 
first request returned to the application has SensorSequence > 0, even 
though the counter has been reset for the new session.

So I believe the v3 wording is accurate for RPi as well: we do not claim
continuity across stop/start, and we do claim a session restart from 0 
at the V4L2VideoDevice layer, with the first-returned-request hedge above.

Cheers,
      Bruce



On 05.08.26 17:25, Barnabás Pőcze wrote:
> CC Naushir Patuck
> 
> Please see below
> 
> 2026. 08. 05. 17:18 keltezéssel, Bruce Allen írta:
>> Add controls::SensorSequence (int64_t, out) to report the monotonic
>> sequence number from the sensor-facing capture device in request
>> metadata, and populate it in pipeline handlers that already report
>> SensorTimestamp.
>>
>> Signed-off-by: Bruce Allen <ballen4705@googlemail.com>
>> ---
>>   src/libcamera/control_ids_core.yaml           | 29 +++++++++++++++++++
>>   src/libcamera/pipeline/imx8-isi/imx8-isi.cpp  |  3 ++
>>   src/libcamera/pipeline/ipu3/ipu3.cpp          |  2 ++
>>   src/libcamera/pipeline/mali-c55/mali-c55.cpp  |  2 ++
>>   src/libcamera/pipeline/rkisp1/rkisp1.cpp      |  2 ++
>>   .../pipeline/rpi/common/pipeline_base.cpp     |  2 ++
>>   src/libcamera/pipeline/rpi/pisp/pisp.cpp      |  2 ++
>>   src/libcamera/pipeline/rpi/vc4/vc4.cpp        |  2 ++
>>   src/libcamera/pipeline/simple/simple.cpp      |  5 +++-
>>   src/libcamera/pipeline/uvcvideo/uvcvideo.cpp  |  2 ++
>>   src/libcamera/pipeline/vimc/vimc.cpp          |  2 ++
>>   11 files changed, 52 insertions(+), 1 deletion(-)
>>
>> diff --git a/src/libcamera/control_ids_core.yaml b/src/libcamera/ 
>> control_ids_core.yaml
>> index 89991d0..bebfb49 100644
>> --- a/src/libcamera/control_ids_core.yaml
>> +++ b/src/libcamera/control_ids_core.yaml
>> @@ -792,6 +792,35 @@ controls:
>>           \todo Define how the sensor timestamp has to be used in the 
>> reprocessing
>>           use case.
>> +  - SensorSequence:
>> +      type: int64_t
>> +      direction: out
>> +      description: |
>> +        The monotonic sequence number from the sensor capture device.
>> +
>> +        Pipeline handlers populate this metadata control with the 
>> sequence
>> +        number reported by the sensor-facing capture device (for 
>> example the
>> +        CSI-2 receiver or V4L2 video node) for the frame associated 
>> with the
>> +        request.
>> +
>> +        While per-buffer FrameMetadata::sequence identifies frames in 
>> each
>> +        completed stream, SensorSequence reflects the sensor-side 
>> capture path.
>> +        If the value increases by more than one between consecutive 
>> completed
>> +        requests, frames were not captured before the earlier request 
>> finished.
>> +        This complements sequence numbers derived from request 
>> delivery and is
>> +        intended for diagnosing frame continuity.
>> +
>> +        After Camera::stop() followed by Camera::start(), 
>> SensorSequence restarts
>> +        from 0 for the new streaming session. The first returned 
>> frame has
>> +        SensorSequence == 0 when the first capture buffer of the 
>> session is
>> +        completed to that request. Applications must not assume 
>> continuity of
>> +        SensorSequence values across camera session stop/start 
>> boundaries.
> 
> In https://patchwork.libcamera.org/patch/15057/#21643 it was stated
> 
>    Should we add that there are no guarantees about this sequence number 
> being reset on start/stop?
> 
> I'm guessing this still applies to the raspberry pi pipeline handlers?
> 
> 
> Regards,
> Barnabás Pőcze
> 
> 
>> +
>> +        On successful frames where SensorTimestamp is populated,
>> +        SensorSequence will also be populated.
>> +
>> +        The SensorSequence control can only be returned in metadata.
>> +
>>     - AfMode:
>>         type: int32_t
>>         direction: inout
>> diff --git a/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp b/src/ 
>> libcamera/pipeline/imx8-isi/imx8-isi.cpp
>> index c3d2695..d2175b9 100644
>> --- a/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp
>> +++ b/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp
>> @@ -1130,6 +1130,9 @@ void PipelineHandlerISI::bufferReady(FrameBuffer 
>> *buffer)
>>       if (!metadata.contains(controls::SensorTimestamp.id()))
>>           metadata.set(controls::SensorTimestamp,
>>                    buffer->metadata().timestamp);
>> +    if (!metadata.contains(controls::SensorSequence.id()))
>> +        metadata.set(controls::SensorSequence,
>> +                 static_cast<int64_t>(buffer->metadata().sequence));
>>       if (completeBuffer(request, buffer))
>>           completeRequest(request);
>> diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/ 
>> pipeline/ipu3/ipu3.cpp
>> index 0f3e169..6494259 100644
>> --- a/src/libcamera/pipeline/ipu3/ipu3.cpp
>> +++ b/src/libcamera/pipeline/ipu3/ipu3.cpp
>> @@ -1317,6 +1317,8 @@ void IPU3CameraData::cio2BufferReady(FrameBuffer 
>> *buffer)
>>        */
>>       request->_d()->metadata().set(controls::SensorTimestamp,
>>                         buffer->metadata().timestamp);
>> +    request->_d()->metadata().set(controls::SensorSequence,
>> +                      static_cast<int64_t>(buffer- 
>> >metadata().sequence));
>>       info->effectiveSensorControls = delayedCtrls_->get(buffer- 
>> >metadata().sequence);
>> diff --git a/src/libcamera/pipeline/mali-c55/mali-c55.cpp b/src/ 
>> libcamera/pipeline/mali-c55/mali-c55.cpp
>> index 599ff88..119b249 100644
>> --- a/src/libcamera/pipeline/mali-c55/mali-c55.cpp
>> +++ b/src/libcamera/pipeline/mali-c55/mali-c55.cpp
>> @@ -1704,6 +1704,8 @@ void 
>> PipelineHandlerMaliC55::cruBufferReady(FrameBuffer *buffer)
>>       Request *request = info->request;
>>       request->_d()->metadata().set(controls::SensorTimestamp,
>>                         buffer->metadata().timestamp);
>> +    request->_d()->metadata().set(controls::SensorSequence,
>> +                      static_cast<int64_t>(buffer- 
>> >metadata().sequence));
>>       MaliC55CameraData *data = cameraData(request->_d()->camera());
>>       data->ipa_->fillParams(request->sequence(), info->paramBuffer- 
>> >cookie());
>> diff --git a/src/libcamera/pipeline/rkisp1/rkisp1.cpp b/src/libcamera/ 
>> pipeline/rkisp1/rkisp1.cpp
>> index 4f9c0aa..0c73f5a 100644
>> --- a/src/libcamera/pipeline/rkisp1/rkisp1.cpp
>> +++ b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
>> @@ -1647,6 +1647,8 @@ void 
>> PipelineHandlerRkISP1::imageBufferReady(FrameBuffer *buffer)
>>            */
>>           request->_d()->metadata().set(controls::SensorTimestamp,
>>                             metadata.timestamp);
>> +        request->_d()->metadata().set(controls::SensorSequence,
>> +                          static_cast<int64_t>(metadata.sequence));
>>           if (isRaw_) {
>>               const ControlList &ctrls =
>> diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/ 
>> src/libcamera/pipeline/rpi/common/pipeline_base.cpp
>> index 5a5acf6..dcf8e6a 100644
>> --- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
>> +++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
>> @@ -1514,6 +1514,8 @@ void CameraData::fillRequestMetadata(const 
>> ControlList &bufferControls, Request
>>   {
>>       if (auto x = bufferControls.get(controls::SensorTimestamp))
>>           request->_d()->metadata().set(controls::SensorTimestamp, *x);
>> +    if (auto x = bufferControls.get(controls::SensorSequence))
>> +        request->_d()->metadata().set(controls::SensorSequence, *x);
>>       if (auto x = bufferControls.get(controls::FrameWallClock))
>>           request->_d()->metadata().set(controls::FrameWallClock, *x);
>> diff --git a/src/libcamera/pipeline/rpi/pisp/pisp.cpp b/src/libcamera/ 
>> pipeline/rpi/pisp/pisp.cpp
>> index 406d683..5cf428c 100644
>> --- a/src/libcamera/pipeline/rpi/pisp/pisp.cpp
>> +++ b/src/libcamera/pipeline/rpi/pisp/pisp.cpp
>> @@ -1783,6 +1783,8 @@ void 
>> PiSPCameraData::cfeBufferDequeue(FrameBuffer *buffer)
>>           ctrl.set(controls::SensorTimestamp, sensorTimestamp);
>>           ctrl.set(controls::FrameWallClock, wallClockTimestamp);
>> +        ctrl.set(controls::SensorSequence,
>> +             static_cast<int64_t>(buffer->metadata().sequence));
>>           job.sensorControls = std::move(ctrl);
>>           job.delayContext = delayContext;
>>       } else if (stream == &cfe_[Cfe::Config]) {
>> diff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/ 
>> pipeline/rpi/vc4/vc4.cpp
>> index 3e9a490..1e7b26a 100644
>> --- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp
>> +++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
>> @@ -787,6 +787,8 @@ void 
>> Vc4CameraData::unicamBufferDequeue(FrameBuffer *buffer)
>>           ctrl.set(controls::SensorTimestamp, sensorTimestamp);
>>           ctrl.set(controls::FrameWallClock, wallClockTimestamp);
>> +        ctrl.set(controls::SensorSequence,
>> +             static_cast<int64_t>(buffer->metadata().sequence));
>>           bayerQueue_.push({ buffer, std::move(ctrl), delayContext });
>>       } else {
>>           embeddedQueue_.push(buffer);
>> diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/ 
>> pipeline/simple/simple.cpp
>> index e26f438..ce34bb2 100644
>> --- a/src/libcamera/pipeline/simple/simple.cpp
>> +++ b/src/libcamera/pipeline/simple/simple.cpp
>> @@ -939,9 +939,12 @@ void 
>> SimpleCameraData::imageBufferReady(FrameBuffer *buffer)
>>           }
>>       }
>> -    if (request)
>> +    if (request) {
>>           request->_d()->metadata().set(controls::SensorTimestamp,
>>                             buffer->metadata().timestamp);
>> +        request->_d()->metadata().set(controls::SensorSequence,
>> +                          static_cast<int64_t>(buffer- 
>> >metadata().sequence));
>> +    }
>>       /*
>>        * Queue the captured and the request buffer to the converter or 
>> Software
>> diff --git a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp b/src/ 
>> libcamera/pipeline/uvcvideo/uvcvideo.cpp
>> index 3435a76..e0b7dad 100644
>> --- a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
>> +++ b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
>> @@ -897,6 +897,8 @@ void UVCCameraData::imageBufferReady(FrameBuffer 
>> *buffer)
>>       /* \todo Use the UVC metadata to calculate a more precise 
>> timestamp */
>>       request->_d()->metadata().set(controls::SensorTimestamp,
>>                         buffer->metadata().timestamp);
>> +    request->_d()->metadata().set(controls::SensorSequence,
>> +                      static_cast<int64_t>(buffer- 
>> >metadata().sequence));
>>       pipe()->completeBuffer(request, buffer);
>>       pipe()->completeRequest(request);
>> diff --git a/src/libcamera/pipeline/vimc/vimc.cpp b/src/libcamera/ 
>> pipeline/vimc/vimc.cpp
>> index 01d8fb2..ff809a1 100644
>> --- a/src/libcamera/pipeline/vimc/vimc.cpp
>> +++ b/src/libcamera/pipeline/vimc/vimc.cpp
>> @@ -617,6 +617,8 @@ void VimcCameraData::imageBufferReady(FrameBuffer 
>> *buffer)
>>       /* Record the sensor's timestamp in the request metadata. */
>>       request->_d()->metadata().set(controls::SensorTimestamp,
>>                         buffer->metadata().timestamp);
>> +    request->_d()->metadata().set(controls::SensorSequence,
>> +                      static_cast<int64_t>(buffer- 
>> >metadata().sequence));
>>       pipe->completeBuffer(request, buffer);
>>       pipe->completeRequest(request);
>
Barnabás Pőcze Aug. 5, 2026, 4 p.m. UTC | #3
Hi


2026. 08. 05. 17:39 keltezéssel, Bruce Allen írta:
> Hi Barnabás,
> (CC Naushir)
> 
> thanks, that was incredibly fast! Regarding Naushir's earlier comment:
> 
>    https://patchwork.libcamera.org/patch/15057/#21643
> 
>    "Should we add that there are no guarantees about this sequence number
>     being reset on start/stop?"
> 
> That caution was about the kernel/driver sequence, which may not restart at zero on STREAMON. It still applies in that sense (Raspberry Pi and other drivers sometimes start at a non-zero V4L2 sequence).
> 
> However, for the application-visible SensorSequence value, libcamera already initializes this. V4L2VideoDevice resets its rebase state on every streamOn() and subtracts the first dequeued sequence so FrameMetadata::sequence starts at 0 for each new streaming session:
> 
> 
> https://git.libcamera.org/libcamera/libcamera.git/tree/src/libcamera/v4l2_videodevice.cpp
> 
> (See V4L2VideoDevice::streamOn() resetting firstFrame_, and dequeueBuffer() subtracting firstFrame_ from buf.sequence. Background on that behaviour:
> 
> https://lists.libcamera.org/pipermail/libcamera-devel/2022-June/031163.html)

Ahh, I see, this was added later and I completely forgot about it.


> 
> The Raspberry Pi pipeline handlers (and the others in this series) populate SensorSequence from that same buffer->metadata().sequence, so after Camera::stop() followed by Camera::start() the capture-device sequence DOES restart from 0 for the new session.
> 
> The remaining caveat, which is already in the v3 control docs, is only about which request the application sees first: SensorSequence == 0 on the first returned frame when the first capture buffer of the session is completed to that request. Internal / freewheel buffers could mean the first request returned to the application has SensorSequence > 0, even though the counter has been reset for the new session.
> 
> So I believe the v3 wording is accurate for RPi as well: we do not claim
> continuity across stop/start, and we do claim a session restart from 0 at the V4L2VideoDevice layer, with the first-returned-request hedge above.

I feel like the wording is not too useful then. In essence it's saying that
the sequence number starts at 0 except if it doesn't due to some unknowable
internal implementation details. So maybe it would be better to just mention
that the first sequence number after `start()` is implementation defined,
and applications must be prepare to handle that. Not sure...


> 
> Cheers,
>       Bruce
> 
> 
> 
> On 05.08.26 17:25, Barnabás Pőcze wrote:
>> CC Naushir Patuck
>>
>> Please see below
>>
>> 2026. 08. 05. 17:18 keltezéssel, Bruce Allen írta:
>>> Add controls::SensorSequence (int64_t, out) to report the monotonic
>>> sequence number from the sensor-facing capture device in request
>>> metadata, and populate it in pipeline handlers that already report
>>> SensorTimestamp.
>>>
>>> Signed-off-by: Bruce Allen <ballen4705@googlemail.com>
>>> ---
>>>   src/libcamera/control_ids_core.yaml           | 29 +++++++++++++++++++
>>>   src/libcamera/pipeline/imx8-isi/imx8-isi.cpp  |  3 ++
>>>   src/libcamera/pipeline/ipu3/ipu3.cpp          |  2 ++
>>>   src/libcamera/pipeline/mali-c55/mali-c55.cpp  |  2 ++
>>>   src/libcamera/pipeline/rkisp1/rkisp1.cpp      |  2 ++
>>>   .../pipeline/rpi/common/pipeline_base.cpp     |  2 ++
>>>   src/libcamera/pipeline/rpi/pisp/pisp.cpp      |  2 ++
>>>   src/libcamera/pipeline/rpi/vc4/vc4.cpp        |  2 ++
>>>   src/libcamera/pipeline/simple/simple.cpp      |  5 +++-
>>>   src/libcamera/pipeline/uvcvideo/uvcvideo.cpp  |  2 ++
>>>   src/libcamera/pipeline/vimc/vimc.cpp          |  2 ++
>>>   11 files changed, 52 insertions(+), 1 deletion(-)
>>>
>>> diff --git a/src/libcamera/control_ids_core.yaml b/src/libcamera/ control_ids_core.yaml
>>> index 89991d0..bebfb49 100644
>>> --- a/src/libcamera/control_ids_core.yaml
>>> +++ b/src/libcamera/control_ids_core.yaml
>>> @@ -792,6 +792,35 @@ controls:
>>>           \todo Define how the sensor timestamp has to be used in the reprocessing
>>>           use case.
>>> +  - SensorSequence:
>>> +      type: int64_t
>>> +      direction: out
>>> +      description: |
>>> +        The monotonic sequence number from the sensor capture device.
>>> +
>>> +        Pipeline handlers populate this metadata control with the sequence
>>> +        number reported by the sensor-facing capture device (for example the
>>> +        CSI-2 receiver or V4L2 video node) for the frame associated with the
>>> +        request.
>>> +
>>> +        While per-buffer FrameMetadata::sequence identifies frames in each
>>> +        completed stream, SensorSequence reflects the sensor-side capture path.
>>> +        If the value increases by more than one between consecutive completed
>>> +        requests, frames were not captured before the earlier request finished.
>>> +        This complements sequence numbers derived from request delivery and is
>>> +        intended for diagnosing frame continuity.
>>> +
>>> +        After Camera::stop() followed by Camera::start(), SensorSequence restarts
>>> +        from 0 for the new streaming session. The first returned frame has
>>> +        SensorSequence == 0 when the first capture buffer of the session is
>>> +        completed to that request. Applications must not assume continuity of
>>> +        SensorSequence values across camera session stop/start boundaries.
>>
>> In https://patchwork.libcamera.org/patch/15057/#21643 it was stated
>>
>>    Should we add that there are no guarantees about this sequence number being reset on start/stop?
>>
>> I'm guessing this still applies to the raspberry pi pipeline handlers?
>>
>>
>> Regards,
>> Barnabás Pőcze
>>
>>
>>> +
>>> +        On successful frames where SensorTimestamp is populated,
>>> +        SensorSequence will also be populated.
>>> +
>>> +        The SensorSequence control can only be returned in metadata.
>>> +
>>>     - AfMode:
>>>         type: int32_t
>>>         direction: inout
>>> diff --git a/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp b/src/ libcamera/pipeline/imx8-isi/imx8-isi.cpp
>>> index c3d2695..d2175b9 100644
>>> --- a/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp
>>> +++ b/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp
>>> @@ -1130,6 +1130,9 @@ void PipelineHandlerISI::bufferReady(FrameBuffer *buffer)
>>>       if (!metadata.contains(controls::SensorTimestamp.id()))
>>>           metadata.set(controls::SensorTimestamp,
>>>                    buffer->metadata().timestamp);
>>> +    if (!metadata.contains(controls::SensorSequence.id()))
>>> +        metadata.set(controls::SensorSequence,
>>> +                 static_cast<int64_t>(buffer->metadata().sequence));
>>>       if (completeBuffer(request, buffer))
>>>           completeRequest(request);
>>> diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/ pipeline/ipu3/ipu3.cpp
>>> index 0f3e169..6494259 100644
>>> --- a/src/libcamera/pipeline/ipu3/ipu3.cpp
>>> +++ b/src/libcamera/pipeline/ipu3/ipu3.cpp
>>> @@ -1317,6 +1317,8 @@ void IPU3CameraData::cio2BufferReady(FrameBuffer *buffer)
>>>        */
>>>       request->_d()->metadata().set(controls::SensorTimestamp,
>>>                         buffer->metadata().timestamp);
>>> +    request->_d()->metadata().set(controls::SensorSequence,
>>> +                      static_cast<int64_t>(buffer- >metadata().sequence));
>>>       info->effectiveSensorControls = delayedCtrls_->get(buffer- >metadata().sequence);
>>> diff --git a/src/libcamera/pipeline/mali-c55/mali-c55.cpp b/src/ libcamera/pipeline/mali-c55/mali-c55.cpp
>>> index 599ff88..119b249 100644
>>> --- a/src/libcamera/pipeline/mali-c55/mali-c55.cpp
>>> +++ b/src/libcamera/pipeline/mali-c55/mali-c55.cpp
>>> @@ -1704,6 +1704,8 @@ void PipelineHandlerMaliC55::cruBufferReady(FrameBuffer *buffer)
>>>       Request *request = info->request;
>>>       request->_d()->metadata().set(controls::SensorTimestamp,
>>>                         buffer->metadata().timestamp);
>>> +    request->_d()->metadata().set(controls::SensorSequence,
>>> +                      static_cast<int64_t>(buffer- >metadata().sequence));
>>>       MaliC55CameraData *data = cameraData(request->_d()->camera());
>>>       data->ipa_->fillParams(request->sequence(), info->paramBuffer- >cookie());
>>> diff --git a/src/libcamera/pipeline/rkisp1/rkisp1.cpp b/src/libcamera/ pipeline/rkisp1/rkisp1.cpp
>>> index 4f9c0aa..0c73f5a 100644
>>> --- a/src/libcamera/pipeline/rkisp1/rkisp1.cpp
>>> +++ b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
>>> @@ -1647,6 +1647,8 @@ void PipelineHandlerRkISP1::imageBufferReady(FrameBuffer *buffer)
>>>            */
>>>           request->_d()->metadata().set(controls::SensorTimestamp,
>>>                             metadata.timestamp);
>>> +        request->_d()->metadata().set(controls::SensorSequence,
>>> +                          static_cast<int64_t>(metadata.sequence));
>>>           if (isRaw_) {
>>>               const ControlList &ctrls =
>>> diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/ src/libcamera/pipeline/rpi/common/pipeline_base.cpp
>>> index 5a5acf6..dcf8e6a 100644
>>> --- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
>>> +++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
>>> @@ -1514,6 +1514,8 @@ void CameraData::fillRequestMetadata(const ControlList &bufferControls, Request
>>>   {
>>>       if (auto x = bufferControls.get(controls::SensorTimestamp))
>>>           request->_d()->metadata().set(controls::SensorTimestamp, *x);
>>> +    if (auto x = bufferControls.get(controls::SensorSequence))
>>> +        request->_d()->metadata().set(controls::SensorSequence, *x);
>>>       if (auto x = bufferControls.get(controls::FrameWallClock))
>>>           request->_d()->metadata().set(controls::FrameWallClock, *x);
>>> diff --git a/src/libcamera/pipeline/rpi/pisp/pisp.cpp b/src/libcamera/ pipeline/rpi/pisp/pisp.cpp
>>> index 406d683..5cf428c 100644
>>> --- a/src/libcamera/pipeline/rpi/pisp/pisp.cpp
>>> +++ b/src/libcamera/pipeline/rpi/pisp/pisp.cpp
>>> @@ -1783,6 +1783,8 @@ void PiSPCameraData::cfeBufferDequeue(FrameBuffer *buffer)
>>>           ctrl.set(controls::SensorTimestamp, sensorTimestamp);
>>>           ctrl.set(controls::FrameWallClock, wallClockTimestamp);
>>> +        ctrl.set(controls::SensorSequence,
>>> +             static_cast<int64_t>(buffer->metadata().sequence));
>>>           job.sensorControls = std::move(ctrl);
>>>           job.delayContext = delayContext;
>>>       } else if (stream == &cfe_[Cfe::Config]) {
>>> diff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/ pipeline/rpi/vc4/vc4.cpp
>>> index 3e9a490..1e7b26a 100644
>>> --- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp
>>> +++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
>>> @@ -787,6 +787,8 @@ void Vc4CameraData::unicamBufferDequeue(FrameBuffer *buffer)
>>>           ctrl.set(controls::SensorTimestamp, sensorTimestamp);
>>>           ctrl.set(controls::FrameWallClock, wallClockTimestamp);
>>> +        ctrl.set(controls::SensorSequence,
>>> +             static_cast<int64_t>(buffer->metadata().sequence));
>>>           bayerQueue_.push({ buffer, std::move(ctrl), delayContext });
>>>       } else {
>>>           embeddedQueue_.push(buffer);
>>> diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/ pipeline/simple/simple.cpp
>>> index e26f438..ce34bb2 100644
>>> --- a/src/libcamera/pipeline/simple/simple.cpp
>>> +++ b/src/libcamera/pipeline/simple/simple.cpp
>>> @@ -939,9 +939,12 @@ void SimpleCameraData::imageBufferReady(FrameBuffer *buffer)
>>>           }
>>>       }
>>> -    if (request)
>>> +    if (request) {
>>>           request->_d()->metadata().set(controls::SensorTimestamp,
>>>                             buffer->metadata().timestamp);
>>> +        request->_d()->metadata().set(controls::SensorSequence,
>>> +                          static_cast<int64_t>(buffer- >metadata().sequence));
>>> +    }
>>>       /*
>>>        * Queue the captured and the request buffer to the converter or Software
>>> diff --git a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp b/src/ libcamera/pipeline/uvcvideo/uvcvideo.cpp
>>> index 3435a76..e0b7dad 100644
>>> --- a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
>>> +++ b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
>>> @@ -897,6 +897,8 @@ void UVCCameraData::imageBufferReady(FrameBuffer *buffer)
>>>       /* \todo Use the UVC metadata to calculate a more precise timestamp */
>>>       request->_d()->metadata().set(controls::SensorTimestamp,
>>>                         buffer->metadata().timestamp);
>>> +    request->_d()->metadata().set(controls::SensorSequence,
>>> +                      static_cast<int64_t>(buffer- >metadata().sequence));
>>>       pipe()->completeBuffer(request, buffer);
>>>       pipe()->completeRequest(request);
>>> diff --git a/src/libcamera/pipeline/vimc/vimc.cpp b/src/libcamera/ pipeline/vimc/vimc.cpp
>>> index 01d8fb2..ff809a1 100644
>>> --- a/src/libcamera/pipeline/vimc/vimc.cpp
>>> +++ b/src/libcamera/pipeline/vimc/vimc.cpp
>>> @@ -617,6 +617,8 @@ void VimcCameraData::imageBufferReady(FrameBuffer *buffer)
>>>       /* Record the sensor's timestamp in the request metadata. */
>>>       request->_d()->metadata().set(controls::SensorTimestamp,
>>>                         buffer->metadata().timestamp);
>>> +    request->_d()->metadata().set(controls::SensorSequence,
>>> +                      static_cast<int64_t>(buffer- >metadata().sequence));
>>>       pipe->completeBuffer(request, buffer);
>>>       pipe->completeRequest(request);
>>
>
Bruce Allen Aug. 5, 2026, 4:10 p.m. UTC | #4
Hi Barnabás,

>> However, for the application-visible SensorSequence value, libcamera 
>> already initializes this. V4L2VideoDevice resets its rebase state on 
>> every streamOn() and subtracts the first dequeued sequence so 
>> FrameMetadata::sequence starts at 0 for each new streaming session:
>>
>>
>> https://git.libcamera.org/libcamera/libcamera.git/tree/src/libcamera/ 
>> v4l2_videodevice.cpp
>>
>> (See V4L2VideoDevice::streamOn() resetting firstFrame_, and 
>> dequeueBuffer() subtracting firstFrame_ from buf.sequence. Background 
>> on that behaviour:
>>
>> https://lists.libcamera.org/pipermail/libcamera-devel/2022- 
>> June/031163.html)
> 
> Ahh, I see, this was added later and I completely forgot about it.

(:-)
>> So I believe the v3 wording is accurate for RPi as well: we do not claim
>> continuity across stop/start, and we do claim a session restart from 0 
>> at the V4L2VideoDevice layer, with the first-returned-request hedge 
>> above.

> I feel like the wording is not too useful then. In essence it's saying that
> the sequence number starts at 0 except if it doesn't due to some unknowable
> internal implementation details. So maybe it would be better to just 
> mention
> that the first sequence number after `start()` is implementation defined,
> and applications must be prepare to handle that. Not sure...

Let me argue that this wording is useful.

In almost all cases, SensorSequence will start at zero. In the rare 
cases where it does not, it will likely start at one.  So I think it is 
worth describing the usual case.  If SensorSequence starts at a *large* 
value, that is an important clue that s*omething* is going on which 
needs investigation.  We need to describe the expected/usual behavior in 
the docs, so that the user can look out for unusual behavior.

(Having said this, I don't care much. I just don't want to have to keep 
patching libcamera to get SensorSequence.  So if you ask me to trim the 
docs as you describe above, or just do it yourself, fine with me.)

Cheers,
	Bruce

Patch
diff mbox series

diff --git a/src/libcamera/control_ids_core.yaml b/src/libcamera/control_ids_core.yaml
index 89991d0..bebfb49 100644
--- a/src/libcamera/control_ids_core.yaml
+++ b/src/libcamera/control_ids_core.yaml
@@ -792,6 +792,35 @@  controls:
         \todo Define how the sensor timestamp has to be used in the reprocessing
         use case.
 
+  - SensorSequence:
+      type: int64_t
+      direction: out
+      description: |
+        The monotonic sequence number from the sensor capture device.
+
+        Pipeline handlers populate this metadata control with the sequence
+        number reported by the sensor-facing capture device (for example the
+        CSI-2 receiver or V4L2 video node) for the frame associated with the
+        request.
+
+        While per-buffer FrameMetadata::sequence identifies frames in each
+        completed stream, SensorSequence reflects the sensor-side capture path.
+        If the value increases by more than one between consecutive completed
+        requests, frames were not captured before the earlier request finished.
+        This complements sequence numbers derived from request delivery and is
+        intended for diagnosing frame continuity.
+
+        After Camera::stop() followed by Camera::start(), SensorSequence restarts
+        from 0 for the new streaming session. The first returned frame has
+        SensorSequence == 0 when the first capture buffer of the session is
+        completed to that request. Applications must not assume continuity of
+        SensorSequence values across camera session stop/start boundaries.
+
+        On successful frames where SensorTimestamp is populated,
+        SensorSequence will also be populated.
+
+        The SensorSequence control can only be returned in metadata.
+
   - AfMode:
       type: int32_t
       direction: inout
diff --git a/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp b/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp
index c3d2695..d2175b9 100644
--- a/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp
+++ b/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp
@@ -1130,6 +1130,9 @@  void PipelineHandlerISI::bufferReady(FrameBuffer *buffer)
 	if (!metadata.contains(controls::SensorTimestamp.id()))
 		metadata.set(controls::SensorTimestamp,
 			     buffer->metadata().timestamp);
+	if (!metadata.contains(controls::SensorSequence.id()))
+		metadata.set(controls::SensorSequence,
+			     static_cast<int64_t>(buffer->metadata().sequence));
 
 	if (completeBuffer(request, buffer))
 		completeRequest(request);
diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/pipeline/ipu3/ipu3.cpp
index 0f3e169..6494259 100644
--- a/src/libcamera/pipeline/ipu3/ipu3.cpp
+++ b/src/libcamera/pipeline/ipu3/ipu3.cpp
@@ -1317,6 +1317,8 @@  void IPU3CameraData::cio2BufferReady(FrameBuffer *buffer)
 	 */
 	request->_d()->metadata().set(controls::SensorTimestamp,
 				      buffer->metadata().timestamp);
+	request->_d()->metadata().set(controls::SensorSequence,
+				      static_cast<int64_t>(buffer->metadata().sequence));
 
 	info->effectiveSensorControls = delayedCtrls_->get(buffer->metadata().sequence);
 
diff --git a/src/libcamera/pipeline/mali-c55/mali-c55.cpp b/src/libcamera/pipeline/mali-c55/mali-c55.cpp
index 599ff88..119b249 100644
--- a/src/libcamera/pipeline/mali-c55/mali-c55.cpp
+++ b/src/libcamera/pipeline/mali-c55/mali-c55.cpp
@@ -1704,6 +1704,8 @@  void PipelineHandlerMaliC55::cruBufferReady(FrameBuffer *buffer)
 	Request *request = info->request;
 	request->_d()->metadata().set(controls::SensorTimestamp,
 				      buffer->metadata().timestamp);
+	request->_d()->metadata().set(controls::SensorSequence,
+				      static_cast<int64_t>(buffer->metadata().sequence));
 
 	MaliC55CameraData *data = cameraData(request->_d()->camera());
 	data->ipa_->fillParams(request->sequence(), info->paramBuffer->cookie());
diff --git a/src/libcamera/pipeline/rkisp1/rkisp1.cpp b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
index 4f9c0aa..0c73f5a 100644
--- a/src/libcamera/pipeline/rkisp1/rkisp1.cpp
+++ b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
@@ -1647,6 +1647,8 @@  void PipelineHandlerRkISP1::imageBufferReady(FrameBuffer *buffer)
 		 */
 		request->_d()->metadata().set(controls::SensorTimestamp,
 					      metadata.timestamp);
+		request->_d()->metadata().set(controls::SensorSequence,
+					      static_cast<int64_t>(metadata.sequence));
 
 		if (isRaw_) {
 			const ControlList &ctrls =
diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
index 5a5acf6..dcf8e6a 100644
--- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
@@ -1514,6 +1514,8 @@  void CameraData::fillRequestMetadata(const ControlList &bufferControls, Request
 {
 	if (auto x = bufferControls.get(controls::SensorTimestamp))
 		request->_d()->metadata().set(controls::SensorTimestamp, *x);
+	if (auto x = bufferControls.get(controls::SensorSequence))
+		request->_d()->metadata().set(controls::SensorSequence, *x);
 	if (auto x = bufferControls.get(controls::FrameWallClock))
 		request->_d()->metadata().set(controls::FrameWallClock, *x);
 
diff --git a/src/libcamera/pipeline/rpi/pisp/pisp.cpp b/src/libcamera/pipeline/rpi/pisp/pisp.cpp
index 406d683..5cf428c 100644
--- a/src/libcamera/pipeline/rpi/pisp/pisp.cpp
+++ b/src/libcamera/pipeline/rpi/pisp/pisp.cpp
@@ -1783,6 +1783,8 @@  void PiSPCameraData::cfeBufferDequeue(FrameBuffer *buffer)
 
 		ctrl.set(controls::SensorTimestamp, sensorTimestamp);
 		ctrl.set(controls::FrameWallClock, wallClockTimestamp);
+		ctrl.set(controls::SensorSequence,
+			 static_cast<int64_t>(buffer->metadata().sequence));
 		job.sensorControls = std::move(ctrl);
 		job.delayContext = delayContext;
 	} else if (stream == &cfe_[Cfe::Config]) {
diff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
index 3e9a490..1e7b26a 100644
--- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp
+++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
@@ -787,6 +787,8 @@  void Vc4CameraData::unicamBufferDequeue(FrameBuffer *buffer)
 
 		ctrl.set(controls::SensorTimestamp, sensorTimestamp);
 		ctrl.set(controls::FrameWallClock, wallClockTimestamp);
+		ctrl.set(controls::SensorSequence,
+			 static_cast<int64_t>(buffer->metadata().sequence));
 		bayerQueue_.push({ buffer, std::move(ctrl), delayContext });
 	} else {
 		embeddedQueue_.push(buffer);
diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
index e26f438..ce34bb2 100644
--- a/src/libcamera/pipeline/simple/simple.cpp
+++ b/src/libcamera/pipeline/simple/simple.cpp
@@ -939,9 +939,12 @@  void SimpleCameraData::imageBufferReady(FrameBuffer *buffer)
 		}
 	}
 
-	if (request)
+	if (request) {
 		request->_d()->metadata().set(controls::SensorTimestamp,
 					      buffer->metadata().timestamp);
+		request->_d()->metadata().set(controls::SensorSequence,
+					      static_cast<int64_t>(buffer->metadata().sequence));
+	}
 
 	/*
 	 * Queue the captured and the request buffer to the converter or Software
diff --git a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
index 3435a76..e0b7dad 100644
--- a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
+++ b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
@@ -897,6 +897,8 @@  void UVCCameraData::imageBufferReady(FrameBuffer *buffer)
 	/* \todo Use the UVC metadata to calculate a more precise timestamp */
 	request->_d()->metadata().set(controls::SensorTimestamp,
 				      buffer->metadata().timestamp);
+	request->_d()->metadata().set(controls::SensorSequence,
+				      static_cast<int64_t>(buffer->metadata().sequence));
 
 	pipe()->completeBuffer(request, buffer);
 	pipe()->completeRequest(request);
diff --git a/src/libcamera/pipeline/vimc/vimc.cpp b/src/libcamera/pipeline/vimc/vimc.cpp
index 01d8fb2..ff809a1 100644
--- a/src/libcamera/pipeline/vimc/vimc.cpp
+++ b/src/libcamera/pipeline/vimc/vimc.cpp
@@ -617,6 +617,8 @@  void VimcCameraData::imageBufferReady(FrameBuffer *buffer)
 	/* Record the sensor's timestamp in the request metadata. */
 	request->_d()->metadata().set(controls::SensorTimestamp,
 				      buffer->metadata().timestamp);
+	request->_d()->metadata().set(controls::SensorSequence,
+				      static_cast<int64_t>(buffer->metadata().sequence));
 
 	pipe->completeBuffer(request, buffer);
 	pipe->completeRequest(request);