diff --git a/applications/ftl2mkv/src/main.cpp b/applications/ftl2mkv/src/main.cpp
index b555dcbf7f3494dbd66f87415f51cbb0cec3c49e..a6a7a2448869168e7634ecd40ba724ca964bdda5 100644
--- a/applications/ftl2mkv/src/main.cpp
+++ b/applications/ftl2mkv/src/main.cpp
@@ -99,7 +99,7 @@ int main(int argc, char **argv) {
 
 	AVOutputFormat *fmt;
 	AVFormatContext *oc;
-	AVStream *video_st[10] = {nullptr};
+	AVStream *video_st[10][2] = {nullptr};
 
 	av_register_all();
 
@@ -130,28 +130,24 @@ int main(int argc, char **argv) {
     LOG(INFO) << "Converting...";
 
     int current_stream = root->value("stream", 0);
-    int current_channel = 0;
+    int current_channel = root->value("channel", -1);
 
 	//bool stream_added[10] = {false};
 
 	// TODO: In future, find a better way to discover number of streams...
 	// Read entire file to find all streams before reading again to write data
 	bool res = r.read(90000000000000, [&current_stream,&current_channel,&r,&video_st,oc](const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
-        if (spkt.channel != static_cast<ftl::codecs::Channel>(current_channel)) return;
+        if (spkt.channel != static_cast<ftl::codecs::Channel>(current_channel) && current_channel != -1) return;
         if (spkt.streamID == current_stream || current_stream == 255) {
 
-            if (pkt.codec == codec_t::POSE) {
-                return;
-            }
-
-            if (pkt.codec == codec_t::CALIBRATION) {
+            if (pkt.codec != codec_t::HEVC) {
                 return;
             }
 
 			if (spkt.streamID >= 10) return;  // TODO: Allow for more than 10
 
-			if (video_st[spkt.streamID] == nullptr) {
-				video_st[spkt.streamID] = add_video_stream(oc, pkt);
+			if (video_st[spkt.streamID][(spkt.channel == Channel::Left) ? 0 : 1] == nullptr) {
+				video_st[spkt.streamID][(spkt.channel == Channel::Left) ? 0 : 1] = add_video_stream(oc, pkt);
 			}
 		}
 	});
@@ -172,14 +168,10 @@ int main(int argc, char **argv) {
 	bool seen_key[10] = {false};
 
     res = r.read(90000000000000, [&current_stream,&current_channel,&r,&video_st,oc,&seen_key](const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
-        if (spkt.channel != static_cast<ftl::codecs::Channel>(current_channel)) return;
+        if (spkt.channel != static_cast<ftl::codecs::Channel>(current_channel) && current_channel != -1) return;
         if (spkt.streamID == current_stream || current_stream == 255) {
 
-            if (pkt.codec == codec_t::POSE) {
-                return;
-            }
-
-            if (pkt.codec == codec_t::CALIBRATION) {
+            if (pkt.codec != codec_t::HEVC) {
                 return;
             }
 
@@ -201,7 +193,7 @@ int main(int argc, char **argv) {
 			if (keyframe) avpkt.flags |= AV_PKT_FLAG_KEY;
 			avpkt.pts = spkt.timestamp - r.getStartTime();
 			avpkt.dts = avpkt.pts;
-			avpkt.stream_index= video_st[spkt.streamID]->index;
+			avpkt.stream_index= video_st[spkt.streamID][(spkt.channel == Channel::Left) ? 0 : 1]->index;
 			avpkt.data= const_cast<uint8_t*>(pkt.data.data());
 			avpkt.size= pkt.data.size();
 			avpkt.duration = 1;
@@ -220,7 +212,8 @@ int main(int argc, char **argv) {
 	//avcodec_close(video_st->codec);
 
 	for (int i=0; i<10; ++i) {
-		if (video_st[i]) av_free(video_st[i]);
+		if (video_st[i][0]) av_free(video_st[i][0]);
+		if (video_st[i][1]) av_free(video_st[i][1]);
 	}
 
 	if (!(fmt->flags & AVFMT_NOFILE)) {
diff --git a/applications/gui/src/camera.cpp b/applications/gui/src/camera.cpp
index ae0d5627a94a84afb819a9b1daf62b6bddacccae..ff13ce9281ff7c85b0352abea6a9868ee34fcfb0 100644
--- a/applications/gui/src/camera.cpp
+++ b/applications/gui/src/camera.cpp
@@ -137,6 +137,13 @@ 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) {
+		ftl::codecs::StreamPacket s = spkt;
+		writer_->write(s, pkt);
+	});
 
 	channel_ = Channel::Left;
 
@@ -166,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() {
@@ -494,6 +502,37 @@ const GLTexture &ftl::gui::Camera::captureFrame() {
 	return texture1_;
 }
 
+void ftl::gui::Camera::snapshot() {
+	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);
+}
+
+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");
+
+		writer_->begin();
+		src_->addRawCallback(recorder_);
+
+		src_->inject(Channel::Calibration, src_->parameters(), Channel::Left, src_->getCapabilities());
+		src_->inject(src_->getPose());
+		recording_ = true;
+	}
+}
+
 nlohmann::json ftl::gui::Camera::getMetaData() {
 	return nlohmann::json();
 }
diff --git a/applications/gui/src/camera.hpp b/applications/gui/src/camera.hpp
index 24dcbacfdc51d20166ffc49175f268140e4dcdf3..43cf9c783b1e4b5e3ae003c428b8d6126273ef40 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>
@@ -43,7 +44,7 @@ class Camera {
 	
 	void togglePause();
 	void isPaused();
-	const ftl::codecs::Channels &availableChannels();
+	const ftl::codecs::Channels &availableChannels() { return channels_; }
 
 	const GLTexture &captureFrame();
 	const GLTexture &getLeft() const { return texture1_; }
@@ -51,6 +52,10 @@ class Camera {
 
 	bool thumbnail(cv::Mat &thumb);
 
+	void snapshot();
+
+	void toggleVideoRecording();
+
 	nlohmann::json getMetaData();
 
 	StatisticsImage *stats_ = nullptr;
@@ -85,6 +90,10 @@ class Camera {
 	ftl::codecs::Channels channels_;
 	cv::Mat im1_; // first channel (left)
 	cv::Mat im2_; // second channel ("right")
+	bool recording_;
+	std::ofstream *fileout_;
+	ftl::codecs::Writer *writer_;
+	ftl::rgbd::RawCallback recorder_;
 
 	MUTEX mutex_;
 
diff --git a/applications/gui/src/config_window.cpp b/applications/gui/src/config_window.cpp
index fcba8ff7ad2e033ab790790b03aed4a3af25af15..4832454ad3020f4337def5809bd2f05e2ed83d7e 100644
--- a/applications/gui/src/config_window.cpp
+++ b/applications/gui/src/config_window.cpp
@@ -2,10 +2,10 @@
 
 #include <nanogui/layout.h>
 #include <nanogui/label.h>
-#include <nanogui/combobox.h>
 #include <nanogui/button.h>
 #include <nanogui/entypo.h>
 #include <nanogui/formhelper.h>
+#include <nanogui/vscrollpanel.h>
 
 #include <vector>
 #include <string>
@@ -15,28 +15,25 @@ using std::string;
 using std::vector;
 using ftl::config::json_t;
 
-ConfigWindow::ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl, const ftl::UUID &peer) : ConfigWindow(parent, ctrl, std::optional<ftl::UUID>(peer)) {
-
-}
-
-ConfigWindow::ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl, const std::optional<ftl::UUID> &peer)
-		: nanogui::Window(parent, "Settings"), ctrl_(ctrl), peer_(peer) {
+ConfigWindow::ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl)
+		: nanogui::Window(parent, "Settings"), ctrl_(ctrl) {
 	using namespace nanogui;
 
 	setLayout(new GroupLayout());
 	setPosition(Vector2i(parent->width()/2.0f - 100.0f, parent->height()/2.0f - 100.0f));
 	//setModal(true);
 
-	if (peer) {
-		configurables_ = ctrl->getConfigurables(peer.value());
-	} else {
-		configurables_ = ftl::config::list();
-	}
+	configurables_ = ftl::config::list();
 
 	new Label(this, "Select Configurable","sans-bold");
 
+	auto vscroll = new VScrollPanel(this);
+	vscroll->setFixedHeight(300);
+	Widget *buttons = new Widget(vscroll);
+	buttons->setLayout(new BoxLayout(Orientation::Vertical, Alignment::Fill));
+
 	for (auto c : configurables_) {
-		auto itembutton = new Button(this, c);
+		auto itembutton = new Button(buttons, c);
 		itembutton->setCallback([this,c]() {
 			LOG(INFO) << "Change configurable: " << c;
 			_buildForm(c);
@@ -52,67 +49,50 @@ ConfigWindow::~ConfigWindow() {
 
 }
 
-class ConfigWindow::References {
-	public:
-	References(ftl::NetConfigurable* nc, ftl::config::json_t* config, const std::string* suri) : nc(nc), config(config), suri(suri) {
-	}
-
-	~References() {
-		delete nc;
-		delete config;
-		delete suri;
-	}
-
-	private:
-	ftl::NetConfigurable* nc;
-	ftl::config::json_t* config;
-	const std::string* suri;
-};
-
-std::vector<ftl::gui::ConfigWindow::References *> ConfigWindow::_addElements(nanogui::FormHelper *form, ftl::Configurable &nc, const std::string &suri, std::function<ftl::Configurable*(const std::string*, std::vector<References *>&)> construct) {
+void ConfigWindow::_addElements(nanogui::FormHelper *form, const std::string &suri) {
 	using namespace nanogui;
 
-	std::vector<References *> references;
-
-	auto data = nc.getConfig();
+	Configurable *configurable = ftl::config::find(suri);
+	ftl::config::json_t data;
+	if (configurable) {
+		data = configurable->getConfig();
+	}
 
 	for (auto i=data.begin(); i!=data.end(); ++i) {
 		if (i.key() == "$id") continue;
 
 		if (i.key() == "$ref" && i.value().is_string()) {
 			LOG(INFO) << "Follow $ref: " << i.value();
-			const std::string* suri = new std::string(i.value().get<string>());
-			ftl::Configurable* rc = construct(suri, references);
-			auto new_references = _addElements(form, *rc, *suri, construct);
-			references.insert(references.end(), new_references.begin(), new_references.end());
+			const std::string suri = std::string(i.value().get<string>());
+			_addElements(form, suri);
 			continue;
 		}
 
 		if (i.value().is_boolean()) {
 			string key = i.key();
-			form->addVariable<bool>(i.key(), [this,data,key,&nc](const bool &b){
-				nc.set(key, b);
+			form->addVariable<bool>(i.key(), [this,data,key,suri](const bool &b){
+				ftl::config::update(suri+"/"+key, b);
 			}, [data,key]() -> bool {
 				return data[key].get<bool>();
 			});
 		} else if (i.value().is_number_integer()) {
 			string key = i.key();
-			form->addVariable<int>(i.key(), [this,data,key,&nc](const int &f){
-				nc.set(key, f);
+			form->addVariable<int>(i.key(), [this,data,key,suri](const int &f){
+				ftl::config::update(suri+"/"+key, f);
 			}, [data,key]() -> int {
 				return data[key].get<int>();
 			});
 		} else if (i.value().is_number_float()) {
 			string key = i.key();
-			form->addVariable<float>(i.key(), [this,data,key,&nc](const float &f){
-				nc.set(key, f);
+			form->addVariable<float>(i.key(), [this,data,key,suri](const float &f){
+				ftl::config::update(suri+"/"+key, f);
 			}, [data,key]() -> float {
 				return data[key].get<float>();
 			});
 		} else if (i.value().is_string()) {
 			string key = i.key();
-			form->addVariable<string>(i.key(), [this,data,key,&nc](const string &f){
-				nc.set(key, f);
+			form->addVariable<string>(i.key(), [this,data,key,suri](const string &f){
+				ftl::config::update(suri+"/"+key, f);
 			}, [data,key]() -> string {
 				return data[key].get<string>();
 			});
@@ -131,8 +111,6 @@ std::vector<ftl::gui::ConfigWindow::References *> ConfigWindow::_addElements(nan
 			}
 		}
 	}
-
-	return references;
 }
 
 void ConfigWindow::_buildForm(const std::string &suri) {
@@ -145,50 +123,15 @@ void ConfigWindow::_buildForm(const std::string &suri) {
 	form->addWindow(Vector2i(100,50), uri.getFragment());
 	form->window()->setTheme(theme());
 
-	ftl::config::json_t* config;
-	config = new ftl::config::json_t;
-	const std::string* allocated_suri = new std::string(suri);
-	std::vector<ftl::gui::ConfigWindow::References *> references;
-
-	ftl::Configurable* nc;
-
-	if (peer_) {
-		*config = ctrl_->get(peer_.value(), suri);
-		nc = new ftl::NetConfigurable(peer_.value(), *allocated_suri, *ctrl_, *config);
+	_addElements(form, suri);
 
-		references = _addElements(form, *nc, *allocated_suri, [this](auto suri, auto &references) {
-			ftl::config::json_t* config = new ftl::config::json_t;
-			*config = ctrl_->get(peer_.value(), *suri);
-			auto nc = new ftl::NetConfigurable(peer_.value(), *suri, *ctrl_, *config);
-			auto r = new References(nc, config, suri);
-			references.push_back(r);
-			return nc;
-		});
-	} else {
-		nc = ftl::config::find(suri);
-		if (nc) {
-			references = _addElements(form, *nc, *allocated_suri, [this](auto suri, auto &references) {
-				return ftl::config::find(*suri);
-			});
-		}
-	}
-
-	auto closebutton = form->addButton("Close", [this,form,config,allocated_suri,nc,references]() {
+	auto closebutton = form->addButton("Close", [this,form]() {
 		form->window()->setVisible(false);
-		for(auto r : references) {
-			delete r;
-		}
-		if (peer_) {
-			delete nc;
-		}
-		delete config;
-		delete allocated_suri;
 		delete form;
 	});
 	closebutton->setIcon(ENTYPO_ICON_CROSS);
 }
 
 bool ConfigWindow::exists(const std::string &uri) {
-	// If the Configurable is a NetConfigurable, the URI is not checked.
-	return peer_ || ftl::config::find(uri);
-}
\ No newline at end of file
+	return ftl::config::find(uri) != nullptr;
+}
diff --git a/applications/gui/src/config_window.hpp b/applications/gui/src/config_window.hpp
index 23279d93cee1d601e53e0147b4edee8d5309a483..a0fe74155fe5320f983871d8c9237c7571625670 100644
--- a/applications/gui/src/config_window.hpp
+++ b/applications/gui/src/config_window.hpp
@@ -16,23 +16,15 @@ namespace gui {
  */
 class ConfigWindow : public nanogui::Window {
 	public:
-	ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl, const ftl::UUID &peer);
-	ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl, const std::optional<ftl::UUID> &peer = std::nullopt);
+	ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl);
 	~ConfigWindow();
 
 	private:
-	/*
-	References holds the pointers to a NetConfigurable and all its members so that
-	they can all be returned from _addElements() and then simultaneously deleted
-	as the form is closed.
-	*/
-	class References;
 	ftl::ctrl::Master *ctrl_;
-	std::optional<ftl::UUID> peer_;
 	std::vector<std::string> configurables_;
 	
 	void _buildForm(const std::string &uri);
-	std::vector<References *> _addElements(nanogui::FormHelper *form, ftl::Configurable &nc, const std::string &suri, std::function<ftl::Configurable*(const std::string*, std::vector<References *>&)> construct);
+	void _addElements(nanogui::FormHelper *form, const std::string &suri);
 	bool exists(const std::string &uri);
 };
 
diff --git a/applications/gui/src/ctrl_window.cpp b/applications/gui/src/ctrl_window.cpp
index 67a3682bd2fcfcb55a68fb9c71b6dd55aad36491..fc82c45838a568efc8b4e8d456cdb3fbbef2c8b4 100644
--- a/applications/gui/src/ctrl_window.cpp
+++ b/applications/gui/src/ctrl_window.cpp
@@ -69,7 +69,7 @@ ControlWindow::ControlWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl)
 
 	button = new Button(tools, "", ENTYPO_ICON_COG);
 	button->setCallback([this,parent] {
-		auto cfgwin = new ConfigWindow(parent, ctrl_, _getActiveID());
+		auto cfgwin = new ConfigWindow(parent, ctrl_);
 		cfgwin->setTheme(theme());
 	});
 	button->setTooltip("Edit node configuration");
diff --git a/applications/gui/src/main.cpp b/applications/gui/src/main.cpp
index 50bc3638c2e26197fbcf5b74dadacd4b6a49d0ad..7875e52bb04afc59953ac1a728dafe64008b6d6c 100644
--- a/applications/gui/src/main.cpp
+++ b/applications/gui/src/main.cpp
@@ -2,6 +2,7 @@
 #include <ftl/net/universe.hpp>
 #include <ftl/rgbd.hpp>
 #include <ftl/master.hpp>
+#include <ftl/net_configurable.hpp>
 
 #include <loguru.hpp>
 
@@ -12,9 +13,6 @@ int main(int argc, char **argv) {
 	auto root = ftl::configure(argc, argv, "gui_default");
 	ftl::net::Universe *net = ftl::create<ftl::net::Universe>(root, "net");
 
-	net->start();
-	net->waitConnections();
-
 	ftl::ctrl::Master *controller = new ftl::ctrl::Master(root, net);
 	controller->onLog([](const ftl::ctrl::LogEvent &e){
 		const int v = e.verbosity;
@@ -25,6 +23,33 @@ int main(int argc, char **argv) {
 		}
 	});
 
+	std::map<ftl::UUID, std::vector<ftl::NetConfigurable*>> peerConfigurables;
+
+	net->onConnect([&controller, &peerConfigurables](ftl::net::Peer *p) {
+		ftl::UUID peer = p->id();
+		auto cs = controller->getConfigurables(peer);
+		for (auto c : cs) {
+			ftl::config::json_t *configuration = new ftl::config::json_t;
+			*configuration = controller->get(peer, c);
+			if (!configuration->empty()) {
+				ftl::NetConfigurable *nc = new ftl::NetConfigurable(peer, c, *controller, *configuration);
+				peerConfigurables[peer].push_back(nc);
+			}
+		}
+	});
+
+	net->onDisconnect([&peerConfigurables](ftl::net::Peer *p) {
+		ftl::UUID peer = p->id();
+		for (ftl::NetConfigurable *nc : peerConfigurables[peer]) {
+			ftl::config::json_t *configuration = &(nc->getConfig());
+			delete nc;
+			delete configuration;
+		}
+	});
+
+	net->start();
+	net->waitConnections();
+
 	/*auto available = net.findAll<string>("list_streams");
 	for (auto &a : available) {
 		std::cout << " -- " << a << std::endl;
@@ -57,4 +82,4 @@ int main(int argc, char **argv) {
 	delete root;
 
 	return 0;
-}
\ No newline at end of file
+}
diff --git a/applications/gui/src/media_panel.cpp b/applications/gui/src/media_panel.cpp
index 800940199e2488ca1b11bd723498059efd63d374..abfff4a034c93e7be25d919982ed67f98f2d7df8 100644
--- a/applications/gui/src/media_panel.cpp
+++ b/applications/gui/src/media_panel.cpp
@@ -18,7 +18,6 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
 	using namespace nanogui;
 
 	paused_ = false;
-	writer_ = nullptr;
 	disable_switch_channels_ = false;
 
 	setLayout(new BoxLayout(Orientation::Horizontal,
@@ -35,30 +34,57 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
 		if (cam) cam->showPoseWindow();
 	});
 
-	button = new Button(this, "", ENTYPO_ICON_CONTROLLER_RECORD);
-	button->setFlags(Button::ToggleButton);
-	button->setChangeCallback([this,button](bool state) {
-		if (state){
-			auto *cam = screen_->activeCamera();
-
-			button->setTextColor(nanogui::Color(1.0f,0.1f,0.1f,1.0f));
-			char timestamp[18];
-			std::time_t t=std::time(NULL);
-			std::strftime(timestamp, sizeof(timestamp), "%F-%H%M%S", std::localtime(&t));
-			writer_ = new ftl::rgbd::SnapshotStreamWriter(std::string(timestamp) + ".tar.gz", 1000 / 25);
-			writer_->addSource(cam->source());
-			writer_->start();
-		} else {
-			button->setTextColor(nanogui::Color(1.0f,1.0f,1.0f,1.0f));
-			if (writer_) {
-				writer_->stop();
-				delete writer_;
-				writer_ = nullptr;
+	auto 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 = new Button(recordpopup, "Virtual camera recording (.ftl)");
+	itembutton->setCallback([this,recordbutton]() {
+		std::cout << "Toggling video recording in itembutton callback." << '\n';
+		screen_->activeCamera()->toggleVideoRecording();
+		recordbutton->setCallback([this,recordbutton]() {
+			std::cout << "Toggling video recording in recordbutton callback." << '\n';
+			screen_->activeCamera()->toggleVideoRecording();
+			recordbutton->setCallback([]() {});
+			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->setTextColor(nanogui::Color(1.0f,0.1f,0.1f,1.0f));
+		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));
+				recordbutton->setCallback([this,recordbutton,configurable]() {
+					configurable->set("record", false);
+					recordbutton->setCallback([]() {});
+					recordbutton->setTextColor(nanogui::Color(1.0f,1.0f,1.0f,1.0f));
+					recordbutton->setPushed(false);
+				});
 			}
 		}
-		//if (state) ... start
-		//else ... stop
+		recordbutton->setPushed(false);
 	});
+	itembutton = new Button(recordpopup, "Detailed recording options");
 
 	button = new Button(this, "", ENTYPO_ICON_CONTROLLER_STOP);
 	button->setCallback([this]() {
diff --git a/applications/gui/src/screen.cpp b/applications/gui/src/screen.cpp
index c359e227d9be39ab98df2cd8e816323b6a0ecbef..1ee7425ab27f55108d9fad3edef4c683e945028f 100644
--- a/applications/gui/src/screen.cpp
+++ b/applications/gui/src/screen.cpp
@@ -6,7 +6,6 @@
 #include <nanogui/window.h>
 #include <nanogui/layout.h>
 #include <nanogui/imageview.h>
-#include <nanogui/combobox.h>
 #include <nanogui/label.h>
 #include <nanogui/toolbutton.h>
 #include <nanogui/popupbutton.h>
@@ -229,30 +228,29 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 		popup->setVisible(false);
 	});
 
-	popbutton = new PopupButton(innertool, "", ENTYPO_ICON_COG);
-	popbutton->setIconExtraScale(1.5f);
-	popbutton->setTheme(toolbuttheme);
-	popbutton->setTooltip("Settings");
-	popbutton->setFixedSize(Vector2i(40,40));
-	popbutton->setSide(Popup::Side::Right);
-	popbutton->setChevronIcon(0);
-	// popbutton->setPosition(Vector2i(5,height()-50));
-	popup = popbutton->popup();
-	popup->setLayout(new GroupLayout());
-	popup->setTheme(toolbuttheme);
+	itembutton = new Button(innertool, "", ENTYPO_ICON_COG);
+	itembutton->setIconExtraScale(1.5f);
+	itembutton->setTheme(toolbuttheme);
+	itembutton->setTooltip("Settings");
+	itembutton->setFixedSize(Vector2i(40,40));
+
+	itembutton->setCallback([this]() {
+		auto config_window = new ConfigWindow(this, ctrl_);
+		config_window->setTheme(windowtheme);
+	});
 
+	/*
 	//net_->onConnect([this,popup](ftl::net::Peer *p) {
 	{
 		LOG(INFO) << "NET CONNECT";
 		auto node_details = ctrl_->getSlaves();
-		std::vector<std::string> node_titles;
 
 		for (auto &d : node_details) {
 			LOG(INFO) << "ADDING TITLE: " << d.dump();
 			auto peer = ftl::UUID(d["id"].get<std::string>());
 			auto itembutton = new Button(popup, d["title"].get<std::string>());
 			itembutton->setCallback([this,popup,peer]() {
-				auto config_window = new ConfigWindow(this, ctrl_, peer);
+				auto config_window = new ConfigWindow(this, ctrl_);
 				config_window->setTheme(windowtheme);
 			});
 		}
@@ -264,6 +262,7 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 		auto config_window = new ConfigWindow(this, ctrl_);
 		config_window->setTheme(windowtheme);
 	});
+	*/
 
 	//configwindow_ = new ConfigWindow(parent, ctrl_);
 	cwindow_ = new ftl::gui::ControlWindow(this, controller);
diff --git a/applications/gui/src/src_window.cpp b/applications/gui/src/src_window.cpp
index 8e087d1dd42e25b00afd0efaf7b5dd22d5139a0d..0126810a511b85a1168fe06c638651e533f11a0d 100644
--- a/applications/gui/src/src_window.cpp
+++ b/applications/gui/src/src_window.cpp
@@ -63,7 +63,7 @@ SourceWindow::SourceWindow(ftl::gui::Screen *screen)
 		UNIQUE_LOCK(mutex_, lk);
 		_updateCameras(screen_->net()->findAll<string>("list_streams"));
 	});
-	
+
 	UNIQUE_LOCK(mutex_, lk);
 
 	std::vector<ftl::rgbd::Source*> srcs = ftl::createArray<ftl::rgbd::Source>(screen_->root(), "sources", screen_->net());
@@ -74,6 +74,14 @@ SourceWindow::SourceWindow(ftl::gui::Screen *screen)
 	_updateCameras(screen_->control()->getNet()->findAll<string>("list_streams"));
 }
 
+std::vector<ftl::gui::Camera*> SourceWindow::getCameras() {
+	auto cameras = std::vector<ftl::gui::Camera*>(cameras_.size());
+	for (const auto &kv : cameras_) {
+		cameras.push_back(kv.second);
+	}
+	return cameras;
+}
+
 void SourceWindow::_updateCameras(const vector<string> &netcams) {
 	for (auto s : netcams) {
 		if (cameras_.find(s) == cameras_.end()) {
diff --git a/applications/gui/src/src_window.hpp b/applications/gui/src/src_window.hpp
index b2fe8a9e0957f3345a719a0c37bac46bf473089c..dab3f5d4301de73d0dda2bbd32b321c3f796d160 100644
--- a/applications/gui/src/src_window.hpp
+++ b/applications/gui/src/src_window.hpp
@@ -25,7 +25,7 @@ class SourceWindow : public nanogui::Window {
 	explicit SourceWindow(ftl::gui::Screen *screen);
 	~SourceWindow();
 
-	const std::vector<ftl::gui::Camera*> &getCameras();
+	std::vector<ftl::gui::Camera*> getCameras();
 
 	virtual void draw(NVGcontext *ctx);
 
diff --git a/applications/reconstruct/src/main.cpp b/applications/reconstruct/src/main.cpp
index 0603dad4028d69223b0ce5ddc31d36eafc900c14..551a2dfe309bf8a5a29c17cc4856acb48f70f09c 100644
--- a/applications/reconstruct/src/main.cpp
+++ b/applications/reconstruct/src/main.cpp
@@ -241,6 +241,7 @@ 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 *virt = ftl::create<ftl::rgbd::VirtualSource>(root, "virtual");
+	root->set("tags", nlohmann::json::array({ root->getID()+"/virtual" }));
 	ftl::render::Triangular *splat = ftl::create<ftl::render::Triangular>(root, "renderer", &scene_B);
 	ftl::rgbd::Group *group = new ftl::rgbd::Group;
 	ftl::ILW *align = ftl::create<ftl::ILW>(root, "merge");
diff --git a/components/common/cpp/include/ftl/configuration.hpp b/components/common/cpp/include/ftl/configuration.hpp
index 94fcc7dfa6b57e9bfd59fbcf36c5be80e0b82638..9e4ba8aed4bcf8b5f2c8cec2c1079edb945359c3 100644
--- a/components/common/cpp/include/ftl/configuration.hpp
+++ b/components/common/cpp/include/ftl/configuration.hpp
@@ -72,6 +72,13 @@ json_t &resolveWait(const std::string &);
  */
 Configurable *find(const std::string &uri);
 
+/**
+ * Get all configurables that contain a specified tag. Tags are given under the
+ * "tags" property as an array of strings, but only during configurable
+ * construction.
+ */
+const std::vector<Configurable *> &findByTag(const std::string &tag);
+
 std::vector<std::string> list();
 
 /**
diff --git a/components/common/cpp/src/configuration.cpp b/components/common/cpp/src/configuration.cpp
index 89d539cbec5270b7d7a5bc57a0a113eb57456553..aa236c8a06772e71d59edd3886ce7a9cb14e2eec 100644
--- a/components/common/cpp/src/configuration.cpp
+++ b/components/common/cpp/src/configuration.cpp
@@ -165,6 +165,7 @@ static bool mergeConfig(const string path) {
 
 static std::map<std::string, json_t*> config_index;
 static std::map<std::string, ftl::Configurable*> config_instance;
+static std::map<std::string, std::vector<ftl::Configurable*>> tag_index;
 
 /*
  * Recursively URI index the JSON structure.
@@ -198,6 +199,10 @@ ftl::Configurable *ftl::config::find(const std::string &uri) {
 	else return (*ix).second;
 }
 
+const std::vector<Configurable*> &ftl::config::findByTag(const std::string &tag) {
+	return tag_index[tag];
+}
+
 std::vector<std::string> ftl::config::list() {
 	vector<string> r;
 	for (auto i : config_instance) {
@@ -219,6 +224,13 @@ void ftl::config::registerConfigurable(ftl::Configurable *cfg) {
 	} else {
 		config_instance[*uri] = cfg;
 		LOG(INFO) << "Registering instance: " << *uri;
+
+		auto tags = cfg->get<vector<string>>("tags");
+		if (tags) {
+			for (auto &t : *tags) {
+				tag_index[t].push_back(cfg);
+			}
+		}
 	}
 }
 
diff --git a/components/net/cpp/include/ftl/net_configurable.hpp b/components/net/cpp/include/ftl/net_configurable.hpp
index bdd21c4700e7664cff996672585054c30b6e8e6a..d9fd6e5288758de51de91bce65a1afd86831f19e 100644
--- a/components/net/cpp/include/ftl/net_configurable.hpp
+++ b/components/net/cpp/include/ftl/net_configurable.hpp
@@ -17,7 +17,7 @@ namespace ftl {
 
     private:
 	ftl::UUID peer;
-	const std::string &suri;
+	const std::string suri;
 	ftl::ctrl::Master &ctrl;
     };
 
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.