diff --git a/applications/reconstruct/src/main.cpp b/applications/reconstruct/src/main.cpp
index 2ea9408133893ead019b7f769d4c2446c2ac0a4f..2b20142fa9198efce95368ad4c0bbbf8dba5874b 100644
--- a/applications/reconstruct/src/main.cpp
+++ b/applications/reconstruct/src/main.cpp
@@ -15,6 +15,7 @@
 #include <ftl/slave.hpp>
 #include <ftl/rgbd/group.hpp>
 #include <ftl/threads.hpp>
+#include <ftl/codecs/writer.hpp>
 
 #include "ilw/ilw.hpp"
 #include <ftl/render/splat_render.hpp>
@@ -159,6 +160,33 @@ static void run(ftl::Configurable *root) {
 		group.addSource(in);
 	}
 
+	// ---- Recording code -----------------------------------------------------
+
+	std::ofstream fileout;
+	ftl::codecs::Writer writer(fileout);
+	auto recorder = [&writer](ftl::rgbd::Source *src, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
+		writer.write(spkt, pkt);
+	};
+
+	// Allow stream recording
+	root->on("record", [&group,&fileout,&writer,&recorder](const ftl::config::Event &e) {
+		if (e.entity->value("record", false)) {
+			char timestamp[18];
+			std::time_t t=std::time(NULL);
+			std::strftime(timestamp, sizeof(timestamp), "%F-%H%M%S", std::localtime(&t));
+			fileout.open(std::string(timestamp) + ".ftl");
+
+			writer.begin();
+			group.addRawCallback(std::function(recorder));
+		} else {
+			group.removeRawCallback(recorder);
+			writer.end();
+			fileout.close();
+		}
+	});
+
+	// -------------------------------------------------------------------------
+
 	stream->setLatency(5);  // FIXME: This depends on source!?
 	stream->add(&group);
 	stream->run();
diff --git a/components/codecs/CMakeLists.txt b/components/codecs/CMakeLists.txt
index f951f94a6c10127c989862d154dbe6cee896812a..db41431d49b2ec8672b70dafb7f14ccfb7497d28 100644
--- a/components/codecs/CMakeLists.txt
+++ b/components/codecs/CMakeLists.txt
@@ -5,6 +5,8 @@ set(CODECSRC
 	src/opencv_encoder.cpp
 	src/opencv_decoder.cpp
 	src/generate.cpp
+	src/writer.cpp
+	src/reader.cpp
 )
 
 if (HAVE_NVPIPE)
diff --git a/components/rgbd-sources/include/ftl/rgbd/group.hpp b/components/rgbd-sources/include/ftl/rgbd/group.hpp
index 3c7b26e171a3f119f2ca515696ffdd03e1ead54a..f32ada4f70df19e026358b02337ca3a6caea9785 100644
--- a/components/rgbd-sources/include/ftl/rgbd/group.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/group.hpp
@@ -73,12 +73,12 @@ class Group {
 	 * There is no guarantee about order or timing and the callback itself will
 	 * need to ensure synchronisation of timestamps.
 	 */
-	void addRawCallback(std::function<void(ftl::rgbd::Source*, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> &);
+	void addRawCallback(const std::function<void(ftl::rgbd::Source*, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> &);
 
 	/**
 	 * Removes a raw data callback from all sources in the group.
 	 */
-	void removeRawCallback(std::function<void(ftl::rgbd::Source*, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> &);
+	void removeRawCallback(const std::function<void(ftl::rgbd::Source*, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> &);
 
 	inline std::vector<Source*> sources() const { return sources_; }
 
diff --git a/components/rgbd-sources/src/group.cpp b/components/rgbd-sources/src/group.cpp
index 8dec0292a40f7d8224afe2d8e26ca22e0526ea4c..6b684940d101892a9b7ecd8312878e05b2c2230e 100644
--- a/components/rgbd-sources/src/group.cpp
+++ b/components/rgbd-sources/src/group.cpp
@@ -226,13 +226,13 @@ void Group::sync(std::function<bool(ftl::rgbd::FrameSet &)> cb) {
 	ftl::timer::start(true);
 }
 
-void Group::addRawCallback(std::function<void(ftl::rgbd::Source*, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> &f) {
+void Group::addRawCallback(const std::function<void(ftl::rgbd::Source*, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> &f) {
 	for (auto s : sources_) {
 		s->addRawCallback(f);
 	}
 }
 
-void Group::removeRawCallback(std::function<void(ftl::rgbd::Source*, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> &f) {
+void Group::removeRawCallback(const std::function<void(ftl::rgbd::Source*, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> &f) {
 	for (auto s : sources_) {
 		s->removeRawCallback(f);
 	}