diff --git a/applications/gui/CMakeLists.txt b/applications/gui/CMakeLists.txt
index fe553becc5ef883873f0385383b06ede5f3cc6ab..9764984a7b430c4bc256918aac2b39e356a1ee16 100644
--- a/applications/gui/CMakeLists.txt
+++ b/applications/gui/CMakeLists.txt
@@ -13,6 +13,7 @@ set(GUISRC
 	src/camera.cpp
 	src/media_panel.cpp
 	src/thumbview.cpp
+	src/record_window.cpp
 )
 
 if (HAVE_OPENVR)
diff --git a/applications/gui/src/camera.cpp b/applications/gui/src/camera.cpp
index 64a100a597dd6848b54edc67750e2bb5be957338..fb5777740397e855f291258826655125eb620ca3 100644
--- a/applications/gui/src/camera.cpp
+++ b/applications/gui/src/camera.cpp
@@ -139,7 +139,6 @@ ftl::gui::Camera::Camera(ftl::gui::Screen *screen, ftl::rgbd::Source *src) : scr
 	sdepth_ = false;
 	ftime_ = (float)glfwGetTime();
 	pause_ = false;
-	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) {
@@ -358,6 +357,27 @@ static void drawEdges(	const cv::Mat &in, cv::Mat &out,
 	cv::addWeighted(edges, weight, out, 1.0, 0.0, out, CV_8UC3);
 }
 
+cv::Mat ftl::gui::Camera::visualizeActiveChannel() {
+	cv::Mat result;
+	switch(channel_) {
+		case Channel::Smoothing:
+		case Channel::Confidence:
+			visualizeEnergy(im2_, result, 1.0);
+			break;
+		case Channel::Density:
+		case Channel::Energy:
+			visualizeEnergy(im2_, result, 10.0);
+			break;
+		case Channel::Depth:
+			visualizeDepthMap(im2_, result, 7.0);
+			if (screen_->root()->value("showEdgesInDepth", false)) drawEdges(im1_, result);
+			break;
+		case Channel::Right:
+			result = im2_;
+	}
+	return result;
+}
+
 bool ftl::gui::Camera::thumbnail(cv::Mat &thumb) {
 	UNIQUE_LOCK(mutex_, lk);
 	src_->grab(1,9);
@@ -474,12 +494,12 @@ const GLTexture &ftl::gui::Camera::captureFrame() {
 				texture2_.update(tmp);*/
 				break;
 
-		//case Channel::Flow:
-		case Channel::ColourNormals:
-		case Channel::Right:
-				if (im2_.rows == 0 || im2_.type() != CV_8UC3) { break; }
-				texture2_.update(im2_);
-				break;
+			//case Channel::Flow:
+			case Channel::ColourNormals:
+			case Channel::Right:
+					if (im2_.rows == 0 || im2_.type() != CV_8UC3) { break; }
+					texture2_.update(im2_);
+					break;
 
 			default:
 				break;
@@ -504,35 +524,35 @@ const GLTexture &ftl::gui::Camera::captureFrame() {
 	return texture1_;
 }
 
-void ftl::gui::Camera::snapshot() {
+void ftl::gui::Camera::snapshot(const std::string &filename) {
 	UNIQUE_LOCK(mutex_, lk);
-	char timestamp[18];
-	std::time_t t = std::time(NULL);
-	std::strftime(timestamp, sizeof(timestamp), "%F-%H%M%S", std::localtime(&t));
-	cv::Mat image;
-	cv::flip(im1_, image, 0);
-	cv::imwrite(std::string(timestamp) + ".png", image);
+	cv::Mat blended;
+	cv::Mat visualized = visualizeActiveChannel();
+	if (!visualized.empty()) {
+		double alpha = screen_->root()->value("blending", 0.5);
+		cv::addWeighted(im1_, alpha, visualized, 1.0-alpha, 0, blended);
+	} else {
+		blended = im1_;
+	}
+	cv::Mat flipped;
+	cv::flip(blended, flipped, 0);
+	cv::imwrite(filename, flipped);
 }
 
-void ftl::gui::Camera::toggleVideoRecording() {
-	if (recording_) {
-		src_->removeRawCallback(recorder_);
-		writer_->end();
-		fileout_->close();
-		recording_ = false;
-	} else {
-		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");
+void ftl::gui::Camera::startVideoRecording(const std::string &filename) {
+	fileout_->open(filename);
 
-		writer_->begin();
-		src_->addRawCallback(recorder_);
+	writer_->begin();
+	src_->addRawCallback(recorder_);
 
-		src_->inject(Channel::Calibration, src_->parameters(), Channel::Left, src_->getCapabilities());
-		src_->inject(src_->getPose());
-		recording_ = true;
-	}
+	src_->inject(Channel::Calibration, src_->parameters(), Channel::Left, src_->getCapabilities());
+	src_->inject(src_->getPose());
+}
+
+void ftl::gui::Camera::stopVideoRecording() {
+	src_->removeRawCallback(recorder_);
+	writer_->end();
+	fileout_->close();
 }
 
 nlohmann::json ftl::gui::Camera::getMetaData() {
diff --git a/applications/gui/src/camera.hpp b/applications/gui/src/camera.hpp
index 43cf9c783b1e4b5e3ae003c428b8d6126273ef40..90914cf599942107f8d7bbba8b8f5a9505f815e9 100644
--- a/applications/gui/src/camera.hpp
+++ b/applications/gui/src/camera.hpp
@@ -52,9 +52,11 @@ class Camera {
 
 	bool thumbnail(cv::Mat &thumb);
 
-	void snapshot();
+	void snapshot(const std::string &filename);
 
-	void toggleVideoRecording();
+	void startVideoRecording(const std::string &filename);
+
+	void stopVideoRecording();
 
 	nlohmann::json getMetaData();
 
@@ -69,6 +71,8 @@ class Camera {
 #endif
 
 	private:
+	cv::Mat visualizeActiveChannel();
+
 	Screen *screen_;
 	ftl::rgbd::Source *src_;
 	GLTexture thumb_;
diff --git a/applications/gui/src/media_panel.cpp b/applications/gui/src/media_panel.cpp
index 693024bd3c6a89ccb0ac6e893b01641dbd77c574..c2fa4285ebd433a8ac67a09eb0475e80f860100b 100644
--- a/applications/gui/src/media_panel.cpp
+++ b/applications/gui/src/media_panel.cpp
@@ -1,5 +1,6 @@
 #include "media_panel.hpp"
 #include "screen.hpp"
+#include "record_window.hpp"
 
 #include <nanogui/layout.h>
 #include <nanogui/button.h>
@@ -13,7 +14,7 @@
 using ftl::gui::MediaPanel;
 using ftl::codecs::Channel;
 
-MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""), screen_(screen) {
+MediaPanel::MediaPanel(ftl::gui::Screen *screen, ftl::gui::SourceWindow *sourceWindow) : nanogui::Window(screen, ""), screen_(screen) {
 	using namespace nanogui;
 
 	paused_ = false;
@@ -36,57 +37,70 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
 	virtualCameraRecording_ = std::optional<ftl::gui::Camera*>();
 	sceneRecording_ = std::optional<ftl::Configurable*>();
 
-	auto recordbutton = new PopupButton(this, "", ENTYPO_ICON_CONTROLLER_RECORD);
-	recordbutton->setTooltip("Record");
-	recordbutton->setSide(Popup::Side::Right);
-	recordbutton->setChevronIcon(0);
-	auto recordpopup = recordbutton->popup();
+	recordbutton_ = new PopupButton(this, "", ENTYPO_ICON_CONTROLLER_RECORD);
+	recordbutton_->setTooltip("Record");
+	recordbutton_->setSide(Popup::Side::Right);
+	recordbutton_->setChevronIcon(0);
+	auto recordpopup = recordbutton_->popup();
 	recordpopup->setLayout(new GroupLayout());
 	recordpopup->setTheme(screen->toolbuttheme);
 	recordpopup->setAnchorHeight(150);
 	auto itembutton = new Button(recordpopup, "2D snapshot (.png)");
-	itembutton->setCallback([this,recordbutton]() {
-		screen_->activeCamera()->snapshot();
-		recordbutton->setPushed(false);
+	itembutton->setCallback([this]() {
+		char timestamp[18];
+		std::time_t t=std::time(NULL);
+		std::strftime(timestamp, sizeof(timestamp), "%F-%H%M%S", std::localtime(&t));
+		screen_->activeCamera()->snapshot(std::string(timestamp) + ".png");
+		recordbutton_->setPushed(false);
 	});
 	itembutton = new Button(recordpopup, "Virtual camera recording (.ftl)");
-	itembutton->setCallback([this,recordbutton]() {
-		auto activeCamera = screen_->activeCamera();
-		activeCamera->toggleVideoRecording();
-		recordbutton->setTextColor(nanogui::Color(1.0f,0.1f,0.1f,1.0f));
-		recordbutton->setPushed(false);
-		virtualCameraRecording_ = std::optional<ftl::gui::Camera*>(activeCamera);
+	itembutton->setCallback([this]() {
+		char timestamp[18];
+		std::time_t t=std::time(NULL);
+		std::strftime(timestamp, sizeof(timestamp), "%F-%H%M%S", std::localtime(&t));
+		auto filename = std::string(timestamp) + ".ftl";
+		startRecording2D(screen_->activeCamera(), filename);
+		recordbutton_->setTextColor(nanogui::Color(1.0f,0.1f,0.1f,1.0f));
+		recordbutton_->setPushed(false);
+	});
+	itembutton = new Button(recordpopup, "3D scene snapshot (.ftl)");
+	itembutton->setCallback([this]() {
+		char timestamp[18];
+		std::time_t t=std::time(NULL);
+		std::strftime(timestamp, sizeof(timestamp), "%F-%H%M%S", std::localtime(&t));
+		snapshot3D(screen_->activeCamera(), std::string(timestamp) + ".ftl");
+		recordbutton_->setPushed(false);
 	});
 	itembutton = new Button(recordpopup, "3D scene recording (.ftl)");
-	itembutton->setCallback([this,recordbutton]() {
-		auto tag = screen_->activeCamera()->source()->get<std::string>("uri");
-		if (tag) {
-			auto tagvalue = tag.value();
-			auto configurables = ftl::config::findByTag(tagvalue);
-			if (configurables.size() > 0) {
-				ftl::Configurable *configurable = configurables[0];
-				configurable->set("record", true);
-				recordbutton->setTextColor(nanogui::Color(1.0f,0.1f,0.1f,1.0f));
-				sceneRecording_ = std::optional<ftl::Configurable*>(configurable);
-			}
-		}
-		recordbutton->setPushed(false);
+	itembutton->setCallback([this]() {
+		char timestamp[18];
+		std::time_t t=std::time(NULL);
+		std::strftime(timestamp, sizeof(timestamp), "%F-%H%M%S", std::localtime(&t));
+		startRecording3D(screen_->activeCamera(), std::string(timestamp) + ".ftl");
+		recordbutton_->setTextColor(nanogui::Color(1.0f,0.1f,0.1f,1.0f));
+		recordbutton_->setPushed(false);
 	});
 	itembutton = new Button(recordpopup, "Detailed recording options");
+	itembutton->setCallback([this,sourceWindow] {
+		auto record_window = new RecordWindow(screen_, screen_, sourceWindow->getCameras(), this);
+		record_window->setTheme(screen_->windowtheme);
+		recordbutton_->setPushed(false);
+		recordbutton_->setEnabled(false);
+	});
 
-	recordbutton->setCallback([this,recordbutton](){
+	recordbutton_->setCallback([this](){
 		if (virtualCameraRecording_) {
-			virtualCameraRecording_.value()->toggleVideoRecording();
-			recordbutton->setTextColor(nanogui::Color(1.0f,1.0f,1.0f,1.0f));
+			virtualCameraRecording_.value()->stopVideoRecording();
+			recordbutton_->setTextColor(nanogui::Color(1.0f,1.0f,1.0f,1.0f));
 
 			// Prevents the popup from being opened, though it is shown while the button
 			// is being pressed.
-			recordbutton->setPushed(false);
+			recordbutton_->setPushed(false);
 			virtualCameraRecording_ = std::nullopt;
 		} else if (sceneRecording_) {
 			sceneRecording_.value()->set("record", false);
-			recordbutton->setTextColor(nanogui::Color(1.0f,1.0f,1.0f,1.0f));
-			recordbutton->setPushed(false);
+			recordbutton_->setTextColor(nanogui::Color(1.0f,1.0f,1.0f,1.0f));
+			recordbutton_->setPushed(false);
 			sceneRecording_ = std::nullopt;
 		}
 	});
@@ -281,3 +295,44 @@ void MediaPanel::cameraChanged() {
 		}
 	}
 }
+
+void MediaPanel::startRecording2D(ftl::gui::Camera *camera, const std::string &filename) {
+	camera->startVideoRecording(filename);
+	virtualCameraRecording_ = std::optional<ftl::gui::Camera*>(camera);
+	recordbutton_->setTextColor(nanogui::Color(1.0f,0.1f,0.1f,1.0f));
+}
+
+void MediaPanel::snapshot3D(ftl::gui::Camera *camera, const std::string &filename) {
+	auto tag = camera->source()->get<std::string>("uri");
+	if (tag) {
+		auto tagvalue = tag.value();
+		auto configurables = ftl::config::findByTag(tagvalue);
+		if (configurables.size() > 0) {
+			ftl::Configurable *configurable = ftl::config::find(configurables[0]->getID() + "/controls");
+			if (configurable) {
+				configurable->set("3D-snapshot", filename);
+			}
+		}
+	}
+}
+
+void MediaPanel::startRecording3D(ftl::gui::Camera *camera, const std::string &filename) {
+	auto tag = camera->source()->get<std::string>("uri");
+	if (tag) {
+		auto tagvalue = tag.value();
+		auto configurables = ftl::config::findByTag(tagvalue);
+		if (configurables.size() > 0) {
+			ftl::Configurable *configurable = ftl::config::find(configurables[0]->getID() + "/controls");
+			if (configurable) {
+				configurable->set("record-name", filename);
+				configurable->set("record", true);
+				sceneRecording_ = std::optional<ftl::Configurable*>(configurable);
+				recordbutton_->setTextColor(nanogui::Color(1.0f,0.1f,0.1f,1.0f));
+			}
+		}
+	}
+}
+
+void MediaPanel::recordWindowClosed() {
+	recordbutton_->setEnabled(true);
+}
\ No newline at end of file
diff --git a/applications/gui/src/media_panel.hpp b/applications/gui/src/media_panel.hpp
index df0b0802294cbe64800c850faa60fadf4f3de55b..211b84c6e0beecdcfd5fd51cb3f413e628d31d87 100644
--- a/applications/gui/src/media_panel.hpp
+++ b/applications/gui/src/media_panel.hpp
@@ -5,6 +5,8 @@
 
 #include <nanogui/window.h>
 
+#include "src_window.hpp"
+
 namespace ftl {
 
 namespace rgbd {
@@ -17,11 +19,19 @@ class Screen;
 
 class MediaPanel : public nanogui::Window {
 	public:
-	explicit MediaPanel(ftl::gui::Screen *);
+	explicit MediaPanel(ftl::gui::Screen *, ftl::gui::SourceWindow *);
 	~MediaPanel();
 
 	void cameraChanged();
 
+	void startRecording2D(ftl::gui::Camera *camera, const std::string &filename);
+
+	void snapshot3D(ftl::gui::Camera *camera, const std::string &filename);
+
+	void startRecording3D(ftl::gui::Camera *camera, const std::string &filename);
+
+	void recordWindowClosed();
+
 	private:
 	ftl::gui::Screen *screen_;
 
@@ -32,6 +42,7 @@ class MediaPanel : public nanogui::Window {
 	nanogui::PopupButton *button_channels_;
 	nanogui::Button *right_button_;
 	nanogui::Button *depth_button_;
+	nanogui::PopupButton *recordbutton_;
 
 	/**
 	 * These members indicate which type of recording is active, if any.
diff --git a/applications/gui/src/record_window.cpp b/applications/gui/src/record_window.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..87fbb9dc0e50562abe10b932b05f728f4e24208e
--- /dev/null
+++ b/applications/gui/src/record_window.cpp
@@ -0,0 +1,124 @@
+#include "record_window.hpp"
+
+#include "screen.hpp"
+
+#include <ftl/codecs/channels.hpp>
+
+#include <nanogui/layout.h>
+#include <nanogui/button.h>
+#include <nanogui/combobox.h>
+#include <nanogui/label.h>
+#include <nanogui/textbox.h>
+#include <nanogui/tabwidget.h>
+
+using ftl::gui::RecordWindow;
+
+RecordWindow::RecordWindow(nanogui::Widget *parent, ftl::gui::Screen *screen, const std::vector<ftl::gui::Camera *> &streams, ftl::gui::MediaPanel *media_panel)
+        : nanogui::Window(parent, "Recording options") {
+    using namespace nanogui;
+
+    setLayout(new GroupLayout());
+
+    new Label(this, "File name", "sans-bold");
+    char timestamp[18];
+	std::time_t t = std::time(NULL);
+	std::strftime(timestamp, sizeof(timestamp), "%F-%H%M%S", std::localtime(&t));
+    Widget *fileNameBox = new Widget(this);
+    fileNameBox->setLayout(new BoxLayout(Orientation::Horizontal, Alignment::Middle, 0, 6));
+    auto fileName = new TextBox(fileNameBox, std::string(timestamp));
+    fileName->setFixedWidth(350);
+    fileName->setEditable(true);
+    auto extension = new Label(fileNameBox, ".png", "sans-bold");
+    new Label(this, "Select stream", "sans-bold");
+    auto streamNames = std::vector<std::string>();
+    streamNames.reserve(streams.size());
+    std::optional<int> ix;
+    for (const auto s : streams) {
+        if (s == screen->activeCamera()) {
+            ix = std::optional<int>(streamNames.size());
+        }
+        streamNames.push_back(s->source()->getURI());
+    }
+    auto streamSelect = new ComboBox(this, streamNames);
+    // TODO: The function availableChannels() only finds those channels that
+    // have been set in camera.cpp. The only channels that are set in
+    // camera.cpp currently are Colour and Depth. This means that currently,
+    // the list of channels returned by availableChannels() is not accurate
+    // and should be fixed.
+    TabWidget *tabWidget = add<TabWidget>();
+    tabWidget->setFixedWidth(400);
+    auto snapshot2D = tabWidget->createTab("2D snapshot");
+    auto recording2D = tabWidget->createTab("2D recording");
+    auto snapshot3D = tabWidget->createTab("3D snapshot");
+    auto recording3D = tabWidget->createTab("3D recording");
+
+    snapshot2D->setLayout(new GroupLayout());
+    recording2D->setLayout(new GroupLayout());
+    snapshot3D->setLayout(new GroupLayout());
+    recording3D->setLayout(new GroupLayout());
+
+    // Set the file name extension based on the type of recording chosen.
+    tabWidget->setCallback([tabWidget,snapshot2D,extension](int ix) {
+        if (tabWidget->tab(ix) == snapshot2D) {
+            extension->setCaption(".png");
+        } else {
+            extension->setCaption(".ftl");
+        }
+    });
+
+    tabWidget->setActiveTab(0);
+
+    new Label(recording2D, "Select channel (in addition to Left)", "sans-bold");
+    auto recordingChannel = recording2D->add<ComboBox>();
+    auto streamCallback = [this,streams,recordingChannel](int ix) {
+        channels_ = std::vector<ftl::codecs::Channel>();
+        channel_names_ = std::vector<std::string>();
+        ftl::codecs::Channels availableChannels = streams[ix]->availableChannels();
+        for (auto c : availableChannels) {
+            channels_.push_back(c);
+            channel_names_.push_back(ftl::codecs::name(c));
+        }
+        recordingChannel->setItems(channel_names_);
+    };
+    streamSelect->setCallback(streamCallback);
+
+    // Set the selection to the active stream and set the channel list
+    // to be the channels available in that stream. The callback must
+    // be called explicitly, since setSelectedIndex() does not trigger it.
+    if (ix) {
+        streamSelect->setSelectedIndex(ix.value());
+        streamCallback(ix.value());
+    }
+
+    Widget *actionButtons = new Widget(this);
+    actionButtons->setLayout(new BoxLayout(Orientation::Horizontal));
+    auto button = new Button(actionButtons, "Start");
+    button->setCallback([this,streams,streamSelect,screen,media_panel,fileName,extension,tabWidget,snapshot2D,recording2D,snapshot3D,recording3D,recordingChannel]() {
+        // Check the chosen stream type and channels, then record them.
+        std::string name = fileName->value() + extension->caption();
+        auto stream = streams[streamSelect->selectedIndex()];
+        auto tab = tabWidget->tab(tabWidget->activeTab());
+        if (tab == snapshot2D) {
+            stream->snapshot(name);
+        } else if (tab == recording2D) {
+            stream->setChannel(channels_[recordingChannel->selectedIndex()]);
+            screen->setActiveCamera(stream);
+            media_panel->startRecording2D(stream, name);
+        } else if (tab == snapshot3D) {
+            media_panel->snapshot3D(stream, name);
+        } else if (tab == recording3D) {
+            media_panel->startRecording3D(stream, name);
+        }
+        dispose();
+        media_panel->recordWindowClosed();
+    });
+    button = new Button(actionButtons, "Cancel");
+    button->setCallback([this,media_panel]() {
+        dispose();
+        media_panel->recordWindowClosed();
+    });
+}
+
+RecordWindow::~RecordWindow() {
+    
+}
diff --git a/applications/gui/src/record_window.hpp b/applications/gui/src/record_window.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..5a9b28fef85b5ef2832b7a43b2db1be2b09aa202
--- /dev/null
+++ b/applications/gui/src/record_window.hpp
@@ -0,0 +1,20 @@
+#include <nanogui/window.h>
+
+#include "camera.hpp"
+#include "media_panel.hpp"
+
+namespace ftl {
+namespace gui {
+
+class RecordWindow : public nanogui::Window {
+    public:
+    explicit RecordWindow(nanogui::Widget *parent, ftl::gui::Screen *screen, const std::vector<ftl::gui::Camera *> &streams, ftl::gui::MediaPanel *media_panel);
+    ~RecordWindow();
+
+    private:
+    std::vector<ftl::codecs::Channel> channels_;
+    std::vector<std::string> channel_names_;
+};
+
+}
+}
\ No newline at end of file
diff --git a/applications/gui/src/screen.cpp b/applications/gui/src/screen.cpp
index 76665281fc1b9fcbb1b0b45609c509d02f88a69d..d0d9944214343699bd04230b07da47799f1f415f 100644
--- a/applications/gui/src/screen.cpp
+++ b/applications/gui/src/screen.cpp
@@ -268,7 +268,7 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 	//configwindow_ = new ConfigWindow(parent, ctrl_);
 	cwindow_ = new ftl::gui::ControlWindow(this, controller);
 	swindow_ = new ftl::gui::SourceWindow(this);
-	mwindow_ = new ftl::gui::MediaPanel(this);
+	mwindow_ = new ftl::gui::MediaPanel(this, swindow_);
 	mwindow_->setVisible(false);
 	mwindow_->setTheme(mediatheme);
 
diff --git a/applications/gui/src/src_window.cpp b/applications/gui/src/src_window.cpp
index 0126810a511b85a1168fe06c638651e533f11a0d..6b930c110bd21280eb1df1395cb11149d3980fe8 100644
--- a/applications/gui/src/src_window.cpp
+++ b/applications/gui/src/src_window.cpp
@@ -75,7 +75,8 @@ SourceWindow::SourceWindow(ftl::gui::Screen *screen)
 }
 
 std::vector<ftl::gui::Camera*> SourceWindow::getCameras() {
-	auto cameras = std::vector<ftl::gui::Camera*>(cameras_.size());
+	auto cameras = std::vector<ftl::gui::Camera*>();
+	cameras.reserve(cameras_.size());
 	for (const auto &kv : cameras_) {
 		cameras.push_back(kv.second);
 	}
diff --git a/applications/reconstruct/src/main.cpp b/applications/reconstruct/src/main.cpp
index 4b802d49f85ebe618fd27ecbfe48b46b2f40b5b1..33db96ae046a1ee5c29898a503d0c483446e7f91 100644
--- a/applications/reconstruct/src/main.cpp
+++ b/applications/reconstruct/src/main.cpp
@@ -45,6 +45,9 @@
 #include <ftl/cuda/normals.hpp>
 #include <ftl/registration.hpp>
 
+#include <ftl/codecs/h264.hpp>
+#include <ftl/codecs/hevc.hpp>
+
 #include <cuda_profiler_api.h>
 
 #ifdef WIN32
@@ -134,10 +137,10 @@ static void run(ftl::Configurable *root) {
 
 	// Controls
 	auto *controls = ftl::create<ftl::Configurable>(root, "controls");
-	
+
 	net->start();
 	net->waitConnections();
-	
+
 	std::vector<int> sourcecounts;
 
 	// Add sources from the configuration file as a single group.
@@ -233,7 +236,9 @@ static void run(ftl::Configurable *root) {
 	//ftl::voxhash::SceneRep *scene = ftl::create<ftl::voxhash::SceneRep>(root, "voxelhash");
 	ftl::rgbd::Streamer *stream = ftl::create<ftl::rgbd::Streamer>(root, "stream", net);
 	ftl::rgbd::VirtualSource *vs = ftl::create<ftl::rgbd::VirtualSource>(root, "virtual");
-	//root->set("tags", nlohmann::json::array({ root->getID()+"/virtual" }));
+	auto tags = root->value<std::vector<std::string>>("tags", nlohmann::json::array({}));
+	tags.push_back(root->getID()+"/virtual");
+	root->set("tags", tags);
 
 	int o = root->value("origin_pose", 0) % sources.size();
 	vs->setPose(sources[o]->getPose());
@@ -267,26 +272,70 @@ static void run(ftl::Configurable *root) {
 	std::ofstream fileout;
 	ftl::codecs::Writer writer(fileout);
 
-	root->set("record", false);
+	std::ofstream snapshotout;
+	ftl::codecs::Writer snapshotwriter(snapshotout);
+
+	controls->set("record", false);
 
+	int64_t timestamp = -1;
+	bool writingSnapshot = false;
+	std::unordered_set<int64_t> precedingFrames, followingFrames;
 	// Add a recording callback to all reconstruction scenes
 	for (size_t i=0; i<sources.size(); ++i) {
-		sources[i]->addRawCallback([&writer,&groups,i](ftl::rgbd::Source *src, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
+		sources[i]->addRawCallback([&writer,&groups,&snapshotout,&snapshotwriter,&timestamp,&writingSnapshot,&precedingFrames,&followingFrames,i](ftl::rgbd::Source *src, const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
 			ftl::codecs::StreamPacket s = spkt;
 
 			// Patch stream ID to match order in group
 			s.streamID = i;
 			writer.write(s, pkt);
+
+			if (snapshotwriter.active()) {
+				// The frame that is captured is the next IFrame, unless that
+				// IFrame is one of the first two frames seen. In this case a
+				// part of the frame might already have been missed, so the
+				// IFrame after that one is captured instead.
+
+				// Write all pose and calibration packets.
+				if ((int)spkt.channel >= 64) {
+					snapshotwriter.write(s, pkt);
+				} else if (precedingFrames.size() >= 2) {
+					bool isIFrame = true;
+					switch (pkt.codec) {
+						case ftl::codecs::codec_t::H264:
+							isIFrame = ftl::codecs::h264::isIFrame(pkt.data);
+							break;
+						case ftl::codecs::codec_t::HEVC:
+							isIFrame = ftl::codecs::hevc::isIFrame(pkt.data);
+					}
+
+					if (isIFrame && precedingFrames.count(s.timestamp) == 0) {
+						timestamp = s.timestamp;
+						writingSnapshot = true;
+						snapshotwriter.write(s, pkt);
+					} else if (writingSnapshot && s.timestamp > timestamp) {
+						followingFrames.insert(s.timestamp);
+					}
+
+					// Keep looking for packets of the captured frame until
+					// packets from two following frames have been seen.
+					if (followingFrames.size() >= 2) {
+						snapshotwriter.end();
+						snapshotout.close();
+					}
+				} else {
+					precedingFrames.insert(s.timestamp);
+				}
+			}
 		});
 	}
 
 	// Allow stream recording
-	root->on("record", [&groups,&fileout,&writer,&sources](const ftl::config::Event &e) {
+	controls->on("record", [&fileout,&writer,&sources](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");
+			fileout.open(e.entity->value<std::string>("record-name", std::string(timestamp) + ".ftl"));
 
 			writer.begin();
 
@@ -295,7 +344,7 @@ static void run(ftl::Configurable *root) {
 			for (size_t i=0; i<sources.size(); ++i) {
 				//writeSourceProperties(writer, i, sources[i]);
 				sources[i]->inject(Channel::Calibration, sources[i]->parameters(), Channel::Left, sources[i]->getCapabilities());
-				sources[i]->inject(sources[i]->getPose()); 
+				sources[i]->inject(sources[i]->getPose());
 			}
 		} else {
 			writer.end();
@@ -303,6 +352,25 @@ static void run(ftl::Configurable *root) {
 		}
 	});
 
+	controls->on("3D-snapshot", [&snapshotout,&snapshotwriter,&writingSnapshot,&precedingFrames,&followingFrames,&sources](const ftl::config::Event &e) {
+		if (!snapshotwriter.active()) {
+			char timestamp[18];
+			std::time_t t=std::time(NULL);
+			std::strftime(timestamp, sizeof(timestamp), "%F-%H%M%S", std::localtime(&t));
+			snapshotout.open(e.entity->value<std::string>("3D-snapshot", std::string(timestamp) + ".ftl"));
+			writingSnapshot = false;
+			precedingFrames.clear();
+			followingFrames.clear();
+			snapshotwriter.begin();
+
+			for (size_t i=0; i<sources.size(); ++i) {
+				//writeSourceProperties(writer, i, sources[i]);
+				sources[i]->inject(Channel::Calibration, sources[i]->parameters(), Channel::Left, sources[i]->getCapabilities());
+				sources[i]->inject(sources[i]->getPose());
+			}
+		}
+	});
+
 	// -------------------------------------------------------------------------
 
 	stream->setLatency(6);  // FIXME: This depends on source!?
diff --git a/components/codecs/include/ftl/codecs/writer.hpp b/components/codecs/include/ftl/codecs/writer.hpp
index 94d82f1ecbfb697bdbad35acc5a4cf29dad22410..c1a0ba6c37c03cf73040fb13adc494420d0c4ca2 100644
--- a/components/codecs/include/ftl/codecs/writer.hpp
+++ b/components/codecs/include/ftl/codecs/writer.hpp
@@ -19,6 +19,7 @@ class Writer {
 	bool begin();
 	bool write(const ftl::codecs::StreamPacket &, const ftl::codecs::Packet &);
 	bool end();
+	bool active();
 
 	private:
 	std::ostream *stream_;
diff --git a/components/codecs/src/writer.cpp b/components/codecs/src/writer.cpp
index 1e3841a8e05f840f808aaddf127833392212af05..dba5853ac985fe71028db6152d4c74028e2d91b6 100644
--- a/components/codecs/src/writer.cpp
+++ b/components/codecs/src/writer.cpp
@@ -46,3 +46,7 @@ bool Writer::write(const ftl::codecs::StreamPacket &s, const ftl::codecs::Packet
 	(*stream_).write(buffer.data(), buffer.size());
 	return true;
 }
+
+bool Writer::active() {
+	return active_;
+}
\ No newline at end of file