diff --git a/include/libcamera/internal/v4l2_device.h b/include/libcamera/internal/v4l2_device.h
index cc8116874e9eeecac76f5c25e8b5719c07976781..0c3bbcf2df39f581ceffb9afa6b008368b302156 100644
--- a/include/libcamera/internal/v4l2_device.h
+++ b/include/libcamera/internal/v4l2_device.h
@@ -49,7 +49,7 @@ public:
 
 	bool supportsEvents(V4L2EventSubscription &sub);
 	int setFrameStartEnabled(bool enable);
-	Signal<uint32_t> frameStart;
+	Signal<std::shared_ptr<V4L2Event>> eventReady;
 
 	void updateControlInfo();
 
diff --git a/src/libcamera/pipeline/ipu3/cio2.h b/src/libcamera/pipeline/ipu3/cio2.h
index 91651a16404e2c524b13829b4909474e579ad217..1d29f19e01ae16b7d5500d0584ee226ee4e6453e 100644
--- a/src/libcamera/pipeline/ipu3/cio2.h
+++ b/src/libcamera/pipeline/ipu3/cio2.h
@@ -13,6 +13,7 @@
 
 #include <libcamera/base/signal.h>
 
+#include "libcamera/internal/v4l2_event.h"
 #include "libcamera/internal/v4l2_subdevice.h"
 #include "libcamera/internal/v4l2_videodevice.h"
 
@@ -58,7 +59,7 @@ public:
 	FrameBuffer *queueBuffer(FrameBuffer *rawBuffer);
 	void tryReturnBuffer(FrameBuffer *buffer);
 	Signal<FrameBuffer *> &bufferReady() { return output_->bufferReady; }
-	Signal<uint32_t> &frameStart() { return csi2_->frameStart; }
+	Signal<std::shared_ptr<V4L2Event>> &eventReady() { return csi2_->eventReady; }
 
 	Signal<> bufferAvailable;
 
diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/pipeline/ipu3/ipu3.cpp
index 14cab9e5559b809328e1a408368c906b72e6c027..63aa2aab20d3917b4acdaf508123a46e7fc1bec0 100644
--- a/src/libcamera/pipeline/ipu3/ipu3.cpp
+++ b/src/libcamera/pipeline/ipu3/ipu3.cpp
@@ -63,7 +63,7 @@ public:
 	void statBufferReady(FrameBuffer *buffer);
 	void queuePendingRequests();
 	void cancelPendingRequests();
-	void frameStart(uint32_t sequence);
+	void handleEvent(std::shared_ptr<V4L2Event> event);
 
 	CIO2Device cio2_;
 	ImgUDevice *imgu_;
@@ -1089,8 +1089,8 @@ int PipelineHandlerIPU3::registerCameras()
 		data->delayedCtrls_ =
 			std::make_unique<DelayedControls>(cio2->sensor()->device(),
 							  params);
-		data->cio2_.frameStart().connect(data.get(),
-						 &IPU3CameraData::frameStart);
+		data->cio2_.eventReady().connect(data.get(),
+						 &IPU3CameraData::handleEvent);
 
 		/* Convert the sensor rotation to a transformation */
 		const auto &rotation = data->properties_.get(properties::Rotation);
@@ -1313,7 +1313,7 @@ void IPU3CameraData::cio2BufferReady(FrameBuffer *buffer)
 	 * Record the sensor's timestamp in the request metadata.
 	 *
 	 * \todo The sensor timestamp should be better estimated by connecting
-	 * to the V4L2Device::frameStart signal.
+	 * to the V4L2Device::eventReady signal for FrameSync events.
 	 */
 	request->_d()->metadata().set(controls::SensorTimestamp,
 				      buffer->metadata().timestamp);
@@ -1382,9 +1382,14 @@ void IPU3CameraData::statBufferReady(FrameBuffer *buffer)
  * TestPatternMode one. Other controls are handled through the delayed
  * controls class.
  */
-void IPU3CameraData::frameStart(uint32_t sequence)
+
+void IPU3CameraData::handleEvent(std::shared_ptr<V4L2Event> event)
 {
-	delayedCtrls_->applyControls(sequence);
+	if (event->type() != V4L2Event::Type::FrameSync)
+		return;
+
+	auto frameSyncEvent = static_cast<V4L2FrameSyncEvent *>(event.get());
+	delayedCtrls_->applyControls(frameSyncEvent->sequence());
 
 	if (processingRequests_.empty())
 		return;
diff --git a/src/libcamera/pipeline/mali-c55/mali-c55.cpp b/src/libcamera/pipeline/mali-c55/mali-c55.cpp
index 73a03373c8334ea41478d18047c90c97aeb52c2e..627882f194447912cbed7d97e20bd8baec0e982a 100644
--- a/src/libcamera/pipeline/mali-c55/mali-c55.cpp
+++ b/src/libcamera/pipeline/mali-c55/mali-c55.cpp
@@ -42,6 +42,7 @@
 #include "libcamera/internal/media_device.h"
 #include "libcamera/internal/pipeline_handler.h"
 #include "libcamera/internal/request.h"
+#include "libcamera/internal/v4l2_event.h"
 #include "libcamera/internal/v4l2_subdevice.h"
 #include "libcamera/internal/v4l2_videodevice.h"
 
@@ -198,6 +199,8 @@ public:
 	PixelFormat adjustRawFormat(const PixelFormat &pixFmt) const;
 	Size adjustRawSizes(const PixelFormat &pixFmt, const Size &rawSize) const;
 
+	void handleEvent(std::shared_ptr<V4L2Event> event);
+
 	Stream frStream_;
 	Stream dsStream_;
 
@@ -451,6 +454,15 @@ Size MaliC55CameraData::adjustRawSizes(const PixelFormat &rawFmt, const Size &si
 	return bestSize;
 }
 
+void MaliC55CameraData::handleEvent(std::shared_ptr<V4L2Event> event)
+{
+	if (event->type() != V4L2Event::Type::FrameSync)
+		return;
+
+	auto frameSyncEvent = static_cast<V4L2FrameSyncEvent *>(event.get());
+	delayedCtrls_->applyControls(frameSyncEvent->sequence());
+}
+
 int MaliC55CameraData::loadIPA()
 {
 	int ret;
@@ -1849,8 +1861,7 @@ bool PipelineHandlerMaliC55::registerSensorCamera(MediaLink *ispLink)
 		V4L2Subdevice *sensorSubdev = in->sensor_->device();
 		data->delayedCtrls_ = std::make_unique<DelayedControls>(sensorSubdev,
 									params);
-		isp_->frameStart.connect(data->delayedCtrls_.get(),
-					 &DelayedControls::applyControls);
+		isp_->eventReady.connect(data.get(), &MaliC55CameraData::handleEvent);
 
 		/* \todo Init properties. */
 
@@ -1913,8 +1924,7 @@ bool PipelineHandlerMaliC55::registerMemoryInputCamera(MediaLink *link)
 
 	data->delayedCtrls_ =
 		std::make_unique<DelayedControls>(sensor->device(), params);
-	isp_->frameStart.connect(data->delayedCtrls_.get(),
-				 &DelayedControls::applyControls);
+	isp_->eventReady.connect(data.get(), &MaliC55CameraData::handleEvent);
 
 	ivc_->bufferReady.connect(mem->cru_.get(), &RZG2LCRU::returnBuffer);
 
diff --git a/src/libcamera/pipeline/rkisp1/rkisp1.cpp b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
index 96382c93a4273297e439891d9cea0287fd84fdad..53ec0007cc95c5ab2da0a5d8a86b5a3330d55f99 100644
--- a/src/libcamera/pipeline/rkisp1/rkisp1.cpp
+++ b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
@@ -44,6 +44,7 @@
 #include "libcamera/internal/media_pipeline.h"
 #include "libcamera/internal/pipeline_handler.h"
 #include "libcamera/internal/request.h"
+#include "libcamera/internal/v4l2_event.h"
 #include "libcamera/internal/v4l2_subdevice.h"
 #include "libcamera/internal/v4l2_videodevice.h"
 #include "libcamera/internal/yaml_parser.h"
@@ -106,6 +107,8 @@ public:
 	const PipelineHandlerRkISP1 *pipe() const;
 	int loadIPA(unsigned int hwRevision, uint32_t supportedBlocks);
 
+	void handleEvent(std::shared_ptr<V4L2Event> event);
+
 	Stream mainPathStream_;
 	Stream selfPathStream_;
 	std::unique_ptr<CameraSensor> sensor_;
@@ -430,6 +433,15 @@ int RkISP1CameraData::loadIPA(unsigned int hwRevision, uint32_t supportedBlocks)
 	return 0;
 }
 
+void RkISP1CameraData::handleEvent(std::shared_ptr<V4L2Event> event)
+{
+	if (event->type() != V4L2Event::Type::FrameSync)
+		return;
+
+	auto frameSyncEvent = static_cast<V4L2FrameSyncEvent *>(event.get());
+	delayedCtrls_->applyControls(frameSyncEvent->sequence());
+}
+
 int RkISP1CameraData::loadTuningFile(const std::string &path)
 {
 	int ret;
@@ -1472,8 +1484,7 @@ int PipelineHandlerRkISP1::createCamera(MediaEntity *sensor)
 	data->delayedCtrls_ =
 		std::make_unique<DelayedControls>(data->sensor_->device(),
 						  params);
-	isp_->frameStart.connect(data->delayedCtrls_.get(),
-				 &DelayedControls::applyControls);
+	isp_->eventReady.connect(data.get(), &RkISP1CameraData::handleEvent);
 
 	uint32_t supportedBlocks = kDefaultExtParamsBlocks;
 
diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
index 71f3f26a4ffeb933176665677231549269782369..c6b51e8c335635788cdc99b7a33e37df7de8a1cb 100644
--- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
@@ -20,6 +20,7 @@
 #include <libcamera/property_ids.h>
 
 #include "libcamera/internal/camera_lens.h"
+#include "libcamera/internal/v4l2_event.h"
 #include "libcamera/internal/v4l2_subdevice.h"
 #include "libcamera/internal/yaml_parser.h"
 
@@ -873,7 +874,7 @@ int PipelineHandlerBase::registerCamera(std::unique_ptr<RPi::CameraData> &camera
 
 	/* Setup the general IPA signal handlers. */
 	data->frontendDevice()->dequeueTimeout.connect(data, &RPi::CameraData::cameraTimeout);
-	data->frontendDevice()->frameStart.connect(data, &RPi::CameraData::frameStarted);
+	data->frontendDevice()->eventReady.connect(data, &RPi::CameraData::handleEvent);
 	data->ipa_->setDelayedControls.connect(data, &CameraData::setDelayedControls);
 	data->ipa_->setLensControls.connect(data, &CameraData::setLensControls);
 	data->ipa_->metadataReady.connect(data, &CameraData::metadataReady);
@@ -1392,12 +1393,17 @@ void CameraData::cameraTimeout()
 	clearIncompleteRequests();
 }
 
-void CameraData::frameStarted(uint32_t sequence)
+void CameraData::handleEvent(std::shared_ptr<V4L2Event> event)
 {
-	LOG(RPI, Debug) << "Frame start " << sequence;
+	if (event->type() != V4L2Event::Type::FrameSync)
+		return;
+
+	auto frameSyncEvent = static_cast<V4L2FrameSyncEvent *>(event.get());
+
+	LOG(RPI, Debug) << "Frame start " << frameSyncEvent->sequence();
 
 	/* Write any controls for the next frame as soon as we can. */
-	delayedCtrls_->applyControls(sequence);
+	delayedCtrls_->applyControls(frameSyncEvent->sequence());
 }
 
 void CameraData::clearIncompleteRequests()
diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.h b/src/libcamera/pipeline/rpi/common/pipeline_base.h
index ca12a4875faf4f1b8b8ad1d637a20414857fe2ff..eeee0d4414b819effb97c1d3d12771e50012be6a 100644
--- a/src/libcamera/pipeline/rpi/common/pipeline_base.h
+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.h
@@ -25,6 +25,7 @@
 #include "libcamera/internal/media_object.h"
 #include "libcamera/internal/pipeline_handler.h"
 #include "libcamera/internal/request.h"
+#include "libcamera/internal/v4l2_event.h"
 #include "libcamera/internal/v4l2_videodevice.h"
 #include "libcamera/internal/value_node.h"
 
@@ -86,7 +87,7 @@ public:
 	virtual void platformSetIspCrop(unsigned int index, const Rectangle &ispCrop) = 0;
 
 	void cameraTimeout();
-	void frameStarted(uint32_t sequence);
+	void handleEvent(std::shared_ptr<V4L2Event> event);
 
 	void clearIncompleteRequests();
 	void handleStreamBuffer(FrameBuffer *buffer, Stream *stream);
diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
index 4d70d50ce50bb1b94e4c30c3fecfb78af678e592..af3caf74ff56d4e6cc45ad098a93e9fde45449c2 100644
--- a/src/libcamera/pipeline/simple/simple.cpp
+++ b/src/libcamera/pipeline/simple/simple.cpp
@@ -299,6 +299,8 @@ public:
 		return stream - &streams_.front();
 	}
 
+	void handleEvent(std::shared_ptr<V4L2Event> event);
+
 	struct Entity {
 		/* The media entity, always valid. */
 		MediaEntity *entity;
@@ -985,6 +987,15 @@ void SimpleCameraData::clearIncompleteRequests()
 	}
 }
 
+void SimpleCameraData::handleEvent(std::shared_ptr<V4L2Event> event)
+{
+	if (event->type() != V4L2Event::Type::FrameSync)
+		return;
+
+	auto frameSyncEvent = static_cast<V4L2FrameSyncEvent *>(event.get());
+	delayedCtrls_->applyControls(frameSyncEvent->sequence());
+}
+
 void SimpleCameraData::tryCompleteRequest(Request *request)
 {
 	if (request->hasPendingBuffers())
@@ -1673,8 +1684,8 @@ int SimplePipelineHandler::start(Camera *camera, [[maybe_unused]] const ControlL
 			stop(camera);
 			return ret;
 		}
-		frameStartEmitter->frameStart.connect(data->delayedCtrls_.get(),
-						      &DelayedControls::applyControls);
+		frameStartEmitter->eventReady.connect(data,
+						      &SimpleCameraData::handleEvent);
 	}
 
 	ret = video->streamOn();
@@ -1713,8 +1724,8 @@ void SimplePipelineHandler::stopDevice(Camera *camera)
 
 	if (frameStartEmitter) {
 		frameStartEmitter->setFrameStartEnabled(false);
-		frameStartEmitter->frameStart.disconnect(data->delayedCtrls_.get(),
-							 &DelayedControls::applyControls);
+		frameStartEmitter->eventReady.connect(data,
+						      &SimpleCameraData::handleEvent);
 	}
 
 	if (data->useConversion_) {
diff --git a/src/libcamera/v4l2_device.cpp b/src/libcamera/v4l2_device.cpp
index 654472286d4645ff96a404a09ff8de382a49678c..f231bbb17d8528aca274388b8dcaccc0dc339ad3 100644
--- a/src/libcamera/v4l2_device.cpp
+++ b/src/libcamera/v4l2_device.cpp
@@ -503,7 +503,7 @@ bool V4L2Device::supportsEvents(V4L2EventSubscription &sub)
  * \param[in] enable True to enable frame start events, false to disable them
  *
  * This function enables or disables generation of frame start events. Once
- * enabled, the events are signalled through the frameStart signal.
+ * enabled, the events are signalled through the eventReady signal.
  *
  * \return 0 on success, a negative error code otherwise
  */
@@ -528,8 +528,8 @@ int V4L2Device::setFrameStartEnabled(bool enable)
 }
 
 /**
- * \var V4L2Device::frameStart
- * \brief A Signal emitted when capture of a frame has started
+ * \var V4L2Device::eventReady
+ * \brief A Signal emitted when a V4L2 event is received
  */
 
 /**
@@ -867,15 +867,15 @@ void V4L2Device::eventAvailable()
 		return;
 	}
 
-	if (event.type != V4L2_EVENT_FRAME_SYNC) {
+	auto v4l2Event = V4L2Event::createEvent(&event);
+	if (!v4l2Event) {
 		LOG(V4L2, Error)
-			<< "Spurious event (" << event.type
-			<< "), disabling event notifier";
+			<< "Invalid V4L2 Event type, disabling event notifier";
 		fdEventNotifier_->setEnabled(false);
 		return;
 	}
 
-	frameStart.emit(event.u.frame_sync.frame_sequence);
+	eventReady.emit(v4l2Event);
 }
 
 static const std::map<uint32_t, ColorSpace> v4l2ToColorSpace = {
