diff --git a/applications/gui/src/camera.cpp b/applications/gui/src/camera.cpp
index 3413d23d97f5d03eb5efea85dfc8ae9150946fba..b8a22a2a036e9fb89af7a0457a0676b75aa2a7cb 100644
--- a/applications/gui/src/camera.cpp
+++ b/applications/gui/src/camera.cpp
@@ -137,7 +137,13 @@ ftl::gui::Camera::Camera(ftl::gui::Screen *screen, ftl::rgbd::Source *src) : scr
 	sdepth_ = false;
 	ftime_ = (float)glfwGetTime();
 	pause_ = false;
-	videowriter_ = std::nullopt;
+	recording_ = false;
+	fileout_ = new std::ofstream();
+	writer_ = new ftl::codecs::Writer(*fileout_);
+	recorder_ = std::function([this](ftl::rgbd::Source *src, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
+		ftl::codecs::StreamPacket s = spkt;
+		writer_->write(s, pkt);
+	});
 
 	channel_ = Channel::Left;
 
@@ -167,7 +173,8 @@ ftl::gui::Camera::Camera(ftl::gui::Screen *screen, ftl::rgbd::Source *src) : scr
 }
 
 ftl::gui::Camera::~Camera() {
-
+	delete writer_;
+	delete fileout_;
 }
 
 ftl::rgbd::Source *ftl::gui::Camera::source() {
@@ -489,12 +496,6 @@ const GLTexture &ftl::gui::Camera::captureFrame() {
 			//imageSize = Vector2f(rgb.cols,rgb.rows);
 			texture1_.update(im1_);
 		}
-
-		if (videowriter_) {
-			cv::Mat image;
-			cv::flip(im1_, image, 0);
-			videowriter_.value().write(image);
-		}
 	}
 
 	return texture1_;
@@ -511,17 +512,21 @@ void ftl::gui::Camera::snapshot() {
 }
 
 void ftl::gui::Camera::toggleVideoRecording() {
-	if (videowriter_) {
-		videowriter_.value().release();
-		videowriter_ = std::nullopt;
+	if (recording_) {
+		src_->removeRawCallback(recorder_);
+		writer_->end();
+		fileout_->close();
 	} else {
 		char timestamp[18];
-		std::time_t t = std::time(NULL);
+		std::time_t t=std::time(NULL);
 		std::strftime(timestamp, sizeof(timestamp), "%F-%H%M%S", std::localtime(&t));
-		videowriter_ = std::optional<cv::VideoWriter>(cv::VideoWriter(std::string(timestamp) + ".avi",
-													cv::VideoWriter::fourcc('M','J','P','G'),
-													screen_->root()->value("fps", 20),
-													cv::Size(width(), height())));
+		fileout_->open(std::string(timestamp) + ".ftl");
+
+		writer_->begin();
+		src_->addRawCallback(recorder_);
+
+		src_->inject(Channel::Calibration, src_->parameters(), Channel::Left, src_->getCapabilities());
+		src_->inject(src_->getPose());
 	}
 }
 
diff --git a/applications/gui/src/camera.hpp b/applications/gui/src/camera.hpp
index 156d8bb35b3b071388e551114a5afa884a57b08d..89cfba056a97ceb4a5b200ba77a5d585573a6ea6 100644
--- a/applications/gui/src/camera.hpp
+++ b/applications/gui/src/camera.hpp
@@ -2,6 +2,7 @@
 #define _FTL_GUI_CAMERA_HPP_
 
 #include <ftl/rgbd/source.hpp>
+#include <ftl/codecs/writer.hpp>
 #include "gltexture.hpp"
 
 #include <string>
@@ -89,7 +90,10 @@ class Camera {
 	ftl::codecs::Channels channels_;
 	cv::Mat im1_; // first channel (left)
 	cv::Mat im2_; // second channel ("right")
-	std::optional<cv::VideoWriter> videowriter_;
+	bool recording_;
+	std::ofstream *fileout_;
+	ftl::codecs::Writer *writer_;
+	ftl::rgbd::RawCallback recorder_;
 
 	MUTEX mutex_;
 
diff --git a/applications/gui/src/media_panel.cpp b/applications/gui/src/media_panel.cpp
index fa2fa18bd3c335d576ea9cead9f555ca0e921fd2..9a6eb7259e75d7056d1961bbbe245ec3c8337979 100644
--- a/applications/gui/src/media_panel.cpp
+++ b/applications/gui/src/media_panel.cpp
@@ -76,6 +76,14 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
 		}
 	});
 	itembutton = new Button(recordpopup, "3D scene recording");
+	itembutton->setFlags(Button::ToggleButton);
+	itembutton->setChangeCallback([this,recordbutton](bool state) {
+		if (state) {
+			std::cout << "Starting 3D scene recording." << '\n';
+		} else {
+			std::cout << "Finishing 3D scene recording." << '\n';
+		}
+	});
 	itembutton = new Button(recordpopup, "Detailed recording options");
 
 	button = new Button(this, "", ENTYPO_ICON_CONTROLLER_STOP);
diff --git a/components/rgbd-sources/include/ftl/rgbd/source.hpp b/components/rgbd-sources/include/ftl/rgbd/source.hpp
index 7484788796dba398c525a208ad44708deadf3b70..8958173597a89c00748712a3e1fae69fbc630af0 100644
--- a/components/rgbd-sources/include/ftl/rgbd/source.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/source.hpp
@@ -31,6 +31,8 @@ class SnapshotReader;
 class VirtualSource;
 class Player;
 
+typedef std::function<void(ftl::rgbd::Source*, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> RawCallback;
+
 /**
  * RGBD Generic data source configurable entity. This class hides the
  * internal implementation of an RGBD source by providing accessor functions
@@ -189,12 +191,12 @@ class Source : public ftl::Configurable {
 	 * Currently this only works for a net source since other sources don't
 	 * produce raw encoded data.
 	 */
-	void addRawCallback(const std::function<void(ftl::rgbd::Source*, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> &);
+	void addRawCallback(const RawCallback &);
 
 	/**
 	 * THIS DOES NOT WORK CURRENTLY.
 	 */
-	void removeRawCallback(const std::function<void(ftl::rgbd::Source*, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt)> &);
+	void removeRawCallback(const RawCallback &);
 
 	/**
 	 * INTERNAL. Used to send raw data to callbacks.