[libcamera-devel,v4,07/11] libcamera: timeline: Add a basic timeline implementation

Message ID 20191003174941.1296988-8-niklas.soderlund@ragnatech.se
State Superseded
Headers show
Series
  • libcamera: ipa: Add basic IPA support
Related show

Commit Message

Niklas Söderlund Oct. 3, 2019, 5:49 p.m. UTC
The timeline is a helper for pipeline handlers to ease interacting with
an IPA. The idea is that the pipeline handler runs a timeline which
schedules and executes actions on the hardware. The pipeline listens to
signals from the IPA which it translates into actions which are schedule
on the timeline.

Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
---
 src/libcamera/include/meson.build |   1 +
 src/libcamera/include/timeline.h  |  71 ++++++++
 src/libcamera/meson.build         |   1 +
 src/libcamera/timeline.cpp        | 267 ++++++++++++++++++++++++++++++
 4 files changed, 340 insertions(+)
 create mode 100644 src/libcamera/include/timeline.h
 create mode 100644 src/libcamera/timeline.cpp

Comments

Laurent Pinchart Oct. 4, 2019, 6:57 a.m. UTC | #1
Hi Niklas,

Thank you for the patch.

On Thu, Oct 03, 2019 at 07:49:37PM +0200, Niklas Söderlund wrote:
> The timeline is a helper for pipeline handlers to ease interacting with
> an IPA.

It's not just a helper. Given that the IPA API is based around queuing
frame actions, the timeline is a central concept in the design.

> The idea is that the pipeline handler runs a timeline which
> schedules and executes actions on the hardware. The pipeline listens to
> signals from the IPA which it translates into actions which are schedule
> on the timeline.
> 
> Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
> ---
>  src/libcamera/include/meson.build |   1 +
>  src/libcamera/include/timeline.h  |  71 ++++++++
>  src/libcamera/meson.build         |   1 +
>  src/libcamera/timeline.cpp        | 267 ++++++++++++++++++++++++++++++
>  4 files changed, 340 insertions(+)
>  create mode 100644 src/libcamera/include/timeline.h
>  create mode 100644 src/libcamera/timeline.cpp
> 
> diff --git a/src/libcamera/include/meson.build b/src/libcamera/include/meson.build
> index 2c74d29bd925ede2..238ddc2c2cf96a5d 100644
> --- a/src/libcamera/include/meson.build
> +++ b/src/libcamera/include/meson.build
> @@ -18,6 +18,7 @@ libcamera_headers = files([
>      'pipeline_handler.h',
>      'process.h',
>      'thread.h',
> +    'timeline.h',
>      'utils.h',
>      'v4l2_controls.h',
>      'v4l2_device.h',
> diff --git a/src/libcamera/include/timeline.h b/src/libcamera/include/timeline.h
> new file mode 100644
> index 0000000000000000..1ec28a0d9134d35c
> --- /dev/null
> +++ b/src/libcamera/include/timeline.h
> @@ -0,0 +1,71 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2019, Google Inc.
> + *
> + * timeline.h - Timeline for per-frame controls
> + */
> +#ifndef __LIBCAMERA_TIMELINE_H__
> +#define __LIBCAMERA_TIMELINE_H__
> +
> +#include <list>
> +#include <map>
> +
> +#include <libcamera/timer.h>
> +
> +#include "utils.h"
> +
> +namespace libcamera {
> +
> +class FrameAction
> +{
> +public:
> +	FrameAction(unsigned int type, unsigned int frame)
> +		: type_(type), frame_(frame) {}

Let's flip frame and type, here and in the methods and fields below.
frame is the most generic parameter, and within a frame, type further
qualifies the action.

> +
> +	virtual ~FrameAction() {}
> +
> +	unsigned int type() const { return type_; }
> +	unsigned int frame() const { return frame_; }
> +
> +	virtual void run() = 0;
> +
> +private:
> +	unsigned int type_;
> +	unsigned int frame_;
> +};
> +
> +class Timeline
> +{
> +public:
> +	Timeline();
> +	virtual ~Timeline() {}
> +
> +	virtual void reset();
> +	virtual void scheduleAction(FrameAction *action);
> +	virtual void notifyStartOfExposure(unsigned int frame, utils::time_point time);
> +
> +	utils::duration frameInterval() const { return frameInterval_; }

Is this method needed ?

> +
> +protected:
> +	int frameOffset(unsigned int type) const;
> +	utils::duration timeOffset(unsigned int type) const;
> +
> +	void setRawDelay(unsigned int type, int frame, utils::duration time);
> +
> +private:
> +	static constexpr unsigned int HistoryDepth = 10;

CamelCase is used for class names. You should use SNAKE_CASE for
constants.

> +
> +	void timeout(Timer *timer);
> +	void updateDeadline(const utils::time_point &deadline);

If time_point (and duration) are large enough to be passed as a const
reference, you should do the same above in all functions that take or
return such a value. Otherwise you should pass deadline by value here.

> +
> +	std::list<std::pair<unsigned int, utils::time_point>> history_;
> +	std::multimap<utils::time_point, FrameAction *> actions_;
> +	std::map<unsigned int, std::pair<int, utils::duration>> delays_;
> +	utils::duration frameInterval_;
> +
> +	Timer timer_;
> +};
> +
> +} /* namespace libcamera */
> +
> +#endif /* __LIBCAMERA_TIMELINE_H__ */
> diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
> index 3a3b388a6a344961..195368e77e5e6f01 100644
> --- a/src/libcamera/meson.build
> +++ b/src/libcamera/meson.build
> @@ -30,6 +30,7 @@ libcamera_sources = files([
>      'signal.cpp',
>      'stream.cpp',
>      'thread.cpp',
> +    'timeline.cpp',
>      'timer.cpp',
>      'utils.cpp',
>      'v4l2_controls.cpp',
> diff --git a/src/libcamera/timeline.cpp b/src/libcamera/timeline.cpp
> new file mode 100644
> index 0000000000000000..042ead41d2ff54d5
> --- /dev/null
> +++ b/src/libcamera/timeline.cpp
> @@ -0,0 +1,267 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2019, Google Inc.
> + *
> + * timeline.cpp - Timeline for per-frame controls
> + */
> +
> +#include "timeline.h"
> +
> +#include "log.h"

You're missing \file, which prevents the documentation from being
compiled.

> +namespace libcamera {
> +
> +LOG_DEFINE_CATEGORY(Timeline)
> +
> +/**
> + * \class FrameAction
> + * \brief Action which can be schedule within a frame's lifetime
> + *
> + * A frame action is an event which takes place at some point during a frame's
> + * lifetime inside libcamera. Each action has two primal attributes; type and
> + * frame number.
> + *
> + * The type is a numerical ID which identifies the action within the pipeline
> + * handler. The type is pipeline specific and has no meaning outside the
> + * specific implementation. The frame number is the frame the action applies to.
> + */
> +
> +/**
> + * \fn FrameAction::FrameAction

s/$/()/

> + * \brief Create a frame action
> + * \param[in] type Action type
> + * \param[in] frame Frame number
> + */
> +
> +/**
> + * \fn FrameAction::type()
> + * \brief Retrieve the type of action
> + * \return Action type
> + */
> +
> +/**
> + * \fn FrameAction::frame()
> + * \brief Retrieve the frame number
> + * \return Frame number
> + */
> +
> +/**
> + * \fn FrameAction::run()
> + * \brief The action to perform when the action is triggered
> + *
> + * Pipeline handlers shall subclass the FrameAction object and implement run()

s/object/class/

> + * functions which describes the actions they wish to happen when the act is

s/functions/methods/

> + * schedule using the Timeline.
> + *
> + * \sa Timeline
> + */
> +
> +/**
> + * \class Timeline
> + * \brief Scheduler and executor of FrameAction's
> + *
> + * On the timeline the pipeline can schedule FrameAction and expect them to be
> + * executed at the correct time. The correct time to execute them is a sum
> + * of which frame the action should apply to and a list of timing delays for
> + * each action the pipeline handler describes.
> + *
> + * The timing delays can either be set by the pipeline handler or by the IPA.
> + * The exact implementation of how the timing delays are setup is pipeline
> + * specific. It is expected that the IPA will update the timing delays as it
> + * make changes to how the pipeline operates in different situations.
> + *
> + * The pipeline handler is responsible for feeding the timeline as accurate as
> + * possible information of the exposures it observes.
> + */
> +
> +Timeline::Timeline()
> +	: frameInterval_(std::chrono::milliseconds(1))

Why 1 ? I don't think we should have magic values.

> +{
> +	timer_.timeout.connect(this, &Timeline::timeout);
> +}
> +
> +/**
> + * \brief Reset the timeline

I wondered when reading the class definition above what the reset method
would do, am I'm none the wiser now :-S

> + */
> +void Timeline::reset()
> +{
> +	timer_.stop();
> +
> +	actions_.clear();
> +	history_.clear();
> +}
> +
> +/**
> + * \brief Add a action to the timeline
> + * \param[in] action FrameAction to add
> + *
> + * Schedule an action at a specific time taking the action and timing delays
> + * into account. If a action is schedule too late execute it immediately and
> + * try to recover.
> + */
> +void Timeline::scheduleAction(FrameAction *action)

Ownership of action isn't clear. Is it transferred to this function as I
believe it is ? If so you should use a unique pointer to make this
explicit.

> +{
> +	unsigned int lastFrame;
> +	utils::time_point lastTime;
> +
> +	if (history_.empty()) {
> +		/*
> +		 * This only happens for the first number of frames, up to
> +		 * pipeline depth.
> +		 */
> +		lastFrame = 0;
> +		lastTime = std::chrono::steady_clock::now();
> +	} else {
> +		lastFrame = history_.back().first;
> +		lastTime = history_.back().second;
> +	}
> +
> +	/*
> +	 * Calculate action trigger time by first figuring the out the start of
> +	 * exposure (SOE) of the frame the action corresponds to and then adding
> +	 * the frame and timing offsets.
> +	 */
> +	int frame = action->frame() + frameOffset(action->type()) - lastFrame;
> +	utils::time_point deadline = lastTime + frame * frameInterval_
> +		+ timeOffset(action->type());
> +
> +	utils::time_point now = std::chrono::steady_clock::now();
> +	if (deadline < now) {
> +		LOG(Timeline, Warning)
> +			<< "Action scheduled too late "
> +			<< utils::time_point_to_string(deadline)
> +			<< ", run now " << utils::time_point_to_string(now);
> +		action->run();

action is leaked.

> +	} else {
> +		actions_.insert({ deadline, action });
> +		updateDeadline(deadline);
> +	}
> +}
> +
> +/**
> + * \brief Inform timeline of a new exposure
> + * \param[in] frame Frame number of the exposure
> + * \param[in] time The best approximation of when the exposure started
> + */
> +void Timeline::notifyStartOfExposure(unsigned int frame, utils::time_point time)
> +{
> +	history_.push_back(std::make_pair(frame, time));
> +
> +	if (history_.size() <= HistoryDepth / 2)
> +		return;
> +
> +	while (history_.size() > HistoryDepth)
> +		history_.pop_front();
> +
> +	/* Update esitmated time between two start of exposures. */
> +	utils::duration sumExposures = std::chrono::duration_values<std::chrono::nanoseconds>::zero();


	utils::duration sumExposures(0);

compiles, does it do the right thing ?

> +	unsigned int numExposures = 0;
> +
> +	utils::time_point lastTime;
> +	for (auto it = history_.begin(); it != history_.end(); it++) {
> +		if (it != history_.begin()) {
> +			sumExposures += it->second - lastTime;
> +			numExposures++;
> +		}
> +
> +		lastTime = it->second;
> +	}
> +
> +	frameInterval_ = sumExposures;
> +	if (numExposures)
> +		frameInterval_ /= numExposures;

This algorithm is going to produce an awful jitter, and I'm not even
sure it will give a correct average value.

> +}
> +
> +/**
> + * \fn Timeline::frameInterval()
> + * \brief Retrieve the best estimate of the frame interval
> + * \return Frame interval estimate
> + */
> +
> +/**
> + * \brief Retrieve the frame offset for an action type
> + * \param[in] type Action type
> + * \return Frame offset for the action type
> + */
> +int Timeline::frameOffset(unsigned int type) const
> +{
> +	const auto it = delays_.find(type);
> +	if (it == delays_.end()) {
> +		LOG(Timeline, Error)
> +			<< "No frame offset set for action type " << type;
> +		return 0;
> +	}
> +
> +	return it->second.first;
> +}
> +
> +/**
> + * \brief Retrieve the time offset for an action type
> + * \param[in] type Action type
> + * \return Time offset for the action type
> + */
> +utils::duration Timeline::timeOffset(unsigned int type) const
> +{
> +	const auto it = delays_.find(type);
> +	if (it == delays_.end()) {
> +		LOG(Timeline, Error)
> +			<< "No time offset set for action type " << type;
> +		return utils::duration::zero();
> +	}
> +
> +	return it->second.second;
> +}
> +
> +/**
> + * \brief Set the frame and time offset delays for an action type
> + * \param[in] type Action type
> + * \param[in] frame Frame offset
> + * \param[in] time Time offset
> + */
> +void Timeline::setRawDelay(unsigned int type, int frame, utils::duration time)
> +{
> +	delays_[type] = std::make_pair(frame, time);
> +}
> +
> +void Timeline::updateDeadline(const utils::time_point &deadline)
> +{
> +	if (timer_.isRunning() && deadline >= timer_.deadline())
> +		return;
> +
> +	utils::time_point now = std::chrono::steady_clock::now();
> +	utils::duration duration = deadline - now;
> +
> +	/*
> +	 * Translate nanoseconds resolution the timeline to milliseconds using
> +	 * a ceiling approach to not trigger an action before it's scheduled.

	/*
	 * Convert the duration to milliseconds. Round up to avoid
	 * trigerring the action before it should be scheduled.
	 */

> +	 *
> +	 * \todo: Should the Timer operate using nanoseconds?

I think Timer should have an overloaded start() method that takes a time
point, as the first thing start() does is converting the duration to a
time point.

> +	 */
> +	unsigned long nsecs = std::chrono::duration_cast<std::chrono::nanoseconds>(duration).count();
> +	unsigned int msecs = nsecs / 1000000 + (nsecs % 1000000 ? 1 : 0);

Should we add an implementation of
https://en.cppreference.com/w/cpp/chrono/duration/ceil to utils ?

> +
> +	timer_.stop();
> +	timer_.start(msecs);
> +}
> +
> +void Timeline::timeout(Timer *timer)
> +{
> +	for (auto it = actions_.begin(); it != actions_.end();) {
> +		utils::time_point now = std::chrono::steady_clock::now();

Can this be moved outside of the loop ?

> +
> +		utils::time_point sched = it->first;

const reference ?

> +		FrameAction *action = it->second;

This can be moved after the if ().

> +
> +		if (sched >= now) {
> +			updateDeadline(sched);

I would move this after the for loop.

> +			break;
> +		}
> +
> +		action->run();
> +
> +		it = actions_.erase(it);
> +		delete action;

The delete won't be needed anymore if you store unique pointers.

> +	}

	updateDeadline();

with no argument here, and use actions_.front() to retrieve the next
action to schedule in updateDeadline() (after a check for empty() of
course).

> +}
> +
> +} /* namespace libcamera */

Patch

diff --git a/src/libcamera/include/meson.build b/src/libcamera/include/meson.build
index 2c74d29bd925ede2..238ddc2c2cf96a5d 100644
--- a/src/libcamera/include/meson.build
+++ b/src/libcamera/include/meson.build
@@ -18,6 +18,7 @@  libcamera_headers = files([
     'pipeline_handler.h',
     'process.h',
     'thread.h',
+    'timeline.h',
     'utils.h',
     'v4l2_controls.h',
     'v4l2_device.h',
diff --git a/src/libcamera/include/timeline.h b/src/libcamera/include/timeline.h
new file mode 100644
index 0000000000000000..1ec28a0d9134d35c
--- /dev/null
+++ b/src/libcamera/include/timeline.h
@@ -0,0 +1,71 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * timeline.h - Timeline for per-frame controls
+ */
+#ifndef __LIBCAMERA_TIMELINE_H__
+#define __LIBCAMERA_TIMELINE_H__
+
+#include <list>
+#include <map>
+
+#include <libcamera/timer.h>
+
+#include "utils.h"
+
+namespace libcamera {
+
+class FrameAction
+{
+public:
+	FrameAction(unsigned int type, unsigned int frame)
+		: type_(type), frame_(frame) {}
+
+	virtual ~FrameAction() {}
+
+	unsigned int type() const { return type_; }
+	unsigned int frame() const { return frame_; }
+
+	virtual void run() = 0;
+
+private:
+	unsigned int type_;
+	unsigned int frame_;
+};
+
+class Timeline
+{
+public:
+	Timeline();
+	virtual ~Timeline() {}
+
+	virtual void reset();
+	virtual void scheduleAction(FrameAction *action);
+	virtual void notifyStartOfExposure(unsigned int frame, utils::time_point time);
+
+	utils::duration frameInterval() const { return frameInterval_; }
+
+protected:
+	int frameOffset(unsigned int type) const;
+	utils::duration timeOffset(unsigned int type) const;
+
+	void setRawDelay(unsigned int type, int frame, utils::duration time);
+
+private:
+	static constexpr unsigned int HistoryDepth = 10;
+
+	void timeout(Timer *timer);
+	void updateDeadline(const utils::time_point &deadline);
+
+	std::list<std::pair<unsigned int, utils::time_point>> history_;
+	std::multimap<utils::time_point, FrameAction *> actions_;
+	std::map<unsigned int, std::pair<int, utils::duration>> delays_;
+	utils::duration frameInterval_;
+
+	Timer timer_;
+};
+
+} /* namespace libcamera */
+
+#endif /* __LIBCAMERA_TIMELINE_H__ */
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index 3a3b388a6a344961..195368e77e5e6f01 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -30,6 +30,7 @@  libcamera_sources = files([
     'signal.cpp',
     'stream.cpp',
     'thread.cpp',
+    'timeline.cpp',
     'timer.cpp',
     'utils.cpp',
     'v4l2_controls.cpp',
diff --git a/src/libcamera/timeline.cpp b/src/libcamera/timeline.cpp
new file mode 100644
index 0000000000000000..042ead41d2ff54d5
--- /dev/null
+++ b/src/libcamera/timeline.cpp
@@ -0,0 +1,267 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * timeline.cpp - Timeline for per-frame controls
+ */
+
+#include "timeline.h"
+
+#include "log.h"
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(Timeline)
+
+/**
+ * \class FrameAction
+ * \brief Action which can be schedule within a frame's lifetime
+ *
+ * A frame action is an event which takes place at some point during a frame's
+ * lifetime inside libcamera. Each action has two primal attributes; type and
+ * frame number.
+ *
+ * The type is a numerical ID which identifies the action within the pipeline
+ * handler. The type is pipeline specific and has no meaning outside the
+ * specific implementation. The frame number is the frame the action applies to.
+ */
+
+/**
+ * \fn FrameAction::FrameAction
+ * \brief Create a frame action
+ * \param[in] type Action type
+ * \param[in] frame Frame number
+ */
+
+/**
+ * \fn FrameAction::type()
+ * \brief Retrieve the type of action
+ * \return Action type
+ */
+
+/**
+ * \fn FrameAction::frame()
+ * \brief Retrieve the frame number
+ * \return Frame number
+ */
+
+/**
+ * \fn FrameAction::run()
+ * \brief The action to perform when the action is triggered
+ *
+ * Pipeline handlers shall subclass the FrameAction object and implement run()
+ * functions which describes the actions they wish to happen when the act is
+ * schedule using the Timeline.
+ *
+ * \sa Timeline
+ */
+
+/**
+ * \class Timeline
+ * \brief Scheduler and executor of FrameAction's
+ *
+ * On the timeline the pipeline can schedule FrameAction and expect them to be
+ * executed at the correct time. The correct time to execute them is a sum
+ * of which frame the action should apply to and a list of timing delays for
+ * each action the pipeline handler describes.
+ *
+ * The timing delays can either be set by the pipeline handler or by the IPA.
+ * The exact implementation of how the timing delays are setup is pipeline
+ * specific. It is expected that the IPA will update the timing delays as it
+ * make changes to how the pipeline operates in different situations.
+ *
+ * The pipeline handler is responsible for feeding the timeline as accurate as
+ * possible information of the exposures it observes.
+ */
+
+Timeline::Timeline()
+	: frameInterval_(std::chrono::milliseconds(1))
+{
+	timer_.timeout.connect(this, &Timeline::timeout);
+}
+
+/**
+ * \brief Reset the timeline
+ */
+void Timeline::reset()
+{
+	timer_.stop();
+
+	actions_.clear();
+	history_.clear();
+}
+
+/**
+ * \brief Add a action to the timeline
+ * \param[in] action FrameAction to add
+ *
+ * Schedule an action at a specific time taking the action and timing delays
+ * into account. If a action is schedule too late execute it immediately and
+ * try to recover.
+ */
+void Timeline::scheduleAction(FrameAction *action)
+{
+	unsigned int lastFrame;
+	utils::time_point lastTime;
+
+	if (history_.empty()) {
+		/*
+		 * This only happens for the first number of frames, up to
+		 * pipeline depth.
+		 */
+		lastFrame = 0;
+		lastTime = std::chrono::steady_clock::now();
+	} else {
+		lastFrame = history_.back().first;
+		lastTime = history_.back().second;
+	}
+
+	/*
+	 * Calculate action trigger time by first figuring the out the start of
+	 * exposure (SOE) of the frame the action corresponds to and then adding
+	 * the frame and timing offsets.
+	 */
+	int frame = action->frame() + frameOffset(action->type()) - lastFrame;
+	utils::time_point deadline = lastTime + frame * frameInterval_
+		+ timeOffset(action->type());
+
+	utils::time_point now = std::chrono::steady_clock::now();
+	if (deadline < now) {
+		LOG(Timeline, Warning)
+			<< "Action scheduled too late "
+			<< utils::time_point_to_string(deadline)
+			<< ", run now " << utils::time_point_to_string(now);
+		action->run();
+	} else {
+		actions_.insert({ deadline, action });
+		updateDeadline(deadline);
+	}
+}
+
+/**
+ * \brief Inform timeline of a new exposure
+ * \param[in] frame Frame number of the exposure
+ * \param[in] time The best approximation of when the exposure started
+ */
+void Timeline::notifyStartOfExposure(unsigned int frame, utils::time_point time)
+{
+	history_.push_back(std::make_pair(frame, time));
+
+	if (history_.size() <= HistoryDepth / 2)
+		return;
+
+	while (history_.size() > HistoryDepth)
+		history_.pop_front();
+
+	/* Update esitmated time between two start of exposures. */
+	utils::duration sumExposures = std::chrono::duration_values<std::chrono::nanoseconds>::zero();
+	unsigned int numExposures = 0;
+
+	utils::time_point lastTime;
+	for (auto it = history_.begin(); it != history_.end(); it++) {
+		if (it != history_.begin()) {
+			sumExposures += it->second - lastTime;
+			numExposures++;
+		}
+
+		lastTime = it->second;
+	}
+
+	frameInterval_ = sumExposures;
+	if (numExposures)
+		frameInterval_ /= numExposures;
+}
+
+/**
+ * \fn Timeline::frameInterval()
+ * \brief Retrieve the best estimate of the frame interval
+ * \return Frame interval estimate
+ */
+
+/**
+ * \brief Retrieve the frame offset for an action type
+ * \param[in] type Action type
+ * \return Frame offset for the action type
+ */
+int Timeline::frameOffset(unsigned int type) const
+{
+	const auto it = delays_.find(type);
+	if (it == delays_.end()) {
+		LOG(Timeline, Error)
+			<< "No frame offset set for action type " << type;
+		return 0;
+	}
+
+	return it->second.first;
+}
+
+/**
+ * \brief Retrieve the time offset for an action type
+ * \param[in] type Action type
+ * \return Time offset for the action type
+ */
+utils::duration Timeline::timeOffset(unsigned int type) const
+{
+	const auto it = delays_.find(type);
+	if (it == delays_.end()) {
+		LOG(Timeline, Error)
+			<< "No time offset set for action type " << type;
+		return utils::duration::zero();
+	}
+
+	return it->second.second;
+}
+
+/**
+ * \brief Set the frame and time offset delays for an action type
+ * \param[in] type Action type
+ * \param[in] frame Frame offset
+ * \param[in] time Time offset
+ */
+void Timeline::setRawDelay(unsigned int type, int frame, utils::duration time)
+{
+	delays_[type] = std::make_pair(frame, time);
+}
+
+void Timeline::updateDeadline(const utils::time_point &deadline)
+{
+	if (timer_.isRunning() && deadline >= timer_.deadline())
+		return;
+
+	utils::time_point now = std::chrono::steady_clock::now();
+	utils::duration duration = deadline - now;
+
+	/*
+	 * Translate nanoseconds resolution the timeline to milliseconds using
+	 * a ceiling approach to not trigger an action before it's scheduled.
+	 *
+	 * \todo: Should the Timer operate using nanoseconds?
+	 */
+	unsigned long nsecs = std::chrono::duration_cast<std::chrono::nanoseconds>(duration).count();
+	unsigned int msecs = nsecs / 1000000 + (nsecs % 1000000 ? 1 : 0);
+
+	timer_.stop();
+	timer_.start(msecs);
+}
+
+void Timeline::timeout(Timer *timer)
+{
+	for (auto it = actions_.begin(); it != actions_.end();) {
+		utils::time_point now = std::chrono::steady_clock::now();
+
+		utils::time_point sched = it->first;
+		FrameAction *action = it->second;
+
+		if (sched >= now) {
+			updateDeadline(sched);
+			break;
+		}
+
+		action->run();
+
+		it = actions_.erase(it);
+		delete action;
+	}
+}
+
+} /* namespace libcamera */