diff --git a/applications/reconstruct/src/main.cpp b/applications/reconstruct/src/main.cpp
index d86eb5ea16b7a24c02f2ed526e9fa8db74dcaef1..c1afeed96955b310adbc90ea76a2dc07e5473dc7 100644
--- a/applications/reconstruct/src/main.cpp
+++ b/applications/reconstruct/src/main.cpp
@@ -95,6 +95,9 @@ static void writeSourceProperties(ftl::codecs::Writer &writer, int id, ftl::rgbd
 static void run(ftl::Configurable *root) {
 	Universe *net = ftl::create<Universe>(root, "net");
 	ftl::ctrl::Slave slave(net, root);
+
+	// Controls
+	auto *controls = ftl::create<ftl::Configurable>(root, "controls");
 	
 	net->start();
 	net->waitConnections();
@@ -236,10 +239,11 @@ static void run(ftl::Configurable *root) {
 
 	group->setLatency(4);
 	group->setName("ReconGroup");
-	group->sync([splat,virt,&busy,&slave,&scene_A,&scene_B,&align](ftl::rgbd::FrameSet &fs) -> bool {
+	group->sync([splat,virt,&busy,&slave,&scene_A,&scene_B,&align,controls](ftl::rgbd::FrameSet &fs) -> bool {
 		//cudaSetDevice(scene->getCUDADevice());
 
-		if (slave.isPaused()) return true;
+		//if (slave.isPaused()) return true;
+		if (controls->value("paused", false)) return true;
 		
 		if (busy) {
 			LOG(INFO) << "Group frameset dropped: " << fs.timestamp;
diff --git a/components/common/cpp/src/configuration.cpp b/components/common/cpp/src/configuration.cpp
index 6881c181ded6db678bd450d19002f1a6ee8cfc26..bd9d9feec20538cebc210cafba72a98f651b243c 100644
--- a/components/common/cpp/src/configuration.cpp
+++ b/components/common/cpp/src/configuration.cpp
@@ -185,7 +185,15 @@ static void _indexConfig(json_t &cfg) {
 }
 
 ftl::Configurable *ftl::config::find(const std::string &uri) {
-	auto ix = config_instance.find(uri);
+	std::string actual_uri = uri;
+	if (uri[0] == '/') {
+		if (uri.size() == 1) {
+			return rootCFG;
+		} else {
+			actual_uri = rootCFG->getID() + uri;
+		}
+	}
+	auto ix = config_instance.find(actual_uri);
 	if (ix == config_instance.end()) return nullptr;
 	else return (*ix).second;
 }
diff --git a/components/rgbd-sources/CMakeLists.txt b/components/rgbd-sources/CMakeLists.txt
index e064f91a259874b68879627e2fbb8ff44b24f2ca..0dbd5ccf8cca5aecea9b270c6fc28d4b53b3b4ac 100644
--- a/components/rgbd-sources/CMakeLists.txt
+++ b/components/rgbd-sources/CMakeLists.txt
@@ -1,13 +1,13 @@
 set(RGBDSRC
-	src/calibrate.cpp
-	src/local.cpp
+	src/sources/stereovideo/calibrate.cpp
+	src/sources/stereovideo/local.cpp
 	src/disparity.cpp
 	src/source.cpp
 	src/frame.cpp
 	src/frameset.cpp
-	src/stereovideo.cpp
-	src/middlebury_source.cpp
-	src/net.cpp
+	src/sources/stereovideo/stereovideo.cpp
+	src/sources/middlebury/middlebury_source.cpp
+	src/sources/net/net.cpp
 	src/streamer.cpp
 	src/colour.cpp
 	src/group.cpp
@@ -18,18 +18,19 @@ set(RGBDSRC
 	src/cb_segmentation.cpp
 	src/abr.cpp
 	src/offilter.cpp
-	src/virtual.cpp
-	src/file_source.cpp
+	src/sources/virtual/virtual.cpp
+	src/sources/ftlfile/file_source.cpp
+	src/sources/ftlfile/player.cpp
 )
 
 if (HAVE_REALSENSE)
-	list(APPEND RGBDSRC "src/realsense_source.cpp")
+	list(APPEND RGBDSRC "src/sources/realsense/realsense_source.cpp")
 endif()
 
 if (LibArchive_FOUND)
 	list(APPEND RGBDSRC
-		src/snapshot.cpp
-		src/snapshot_source.cpp
+		src/sources/snapshot/snapshot.cpp
+		src/sources/snapshot/snapshot_source.cpp
 	)
 endif (LibArchive_FOUND)
 
diff --git a/components/rgbd-sources/include/ftl/rgbd/source.hpp b/components/rgbd-sources/include/ftl/rgbd/source.hpp
index 62286ee55475803e59c0234c7a581103f0686b4c..862d472f16b170fc8de28974462b2a0f9df63e54 100644
--- a/components/rgbd-sources/include/ftl/rgbd/source.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/source.hpp
@@ -9,7 +9,6 @@
 #include <ftl/uri.hpp>
 #include <ftl/rgbd/detail/source.hpp>
 #include <ftl/codecs/packet.hpp>
-#include <ftl/codecs/reader.hpp>
 #include <opencv2/opencv.hpp>
 #include <Eigen/Eigen>
 #include <string>
@@ -30,6 +29,7 @@ static inline bool isValidDepth(float d) { return (d > 0.01f) && (d < 39.99f); }
 
 class SnapshotReader;
 class VirtualSource;
+class Player;
 
 /**
  * RGBD Generic data source configurable entity. This class hides the
@@ -247,9 +247,9 @@ class Source : public ftl::Configurable {
 	detail::Source *_createNetImpl(const ftl::URI &uri);
 	detail::Source *_createDeviceImpl(const ftl::URI &uri);
 
-	static ftl::codecs::Reader *__createReader(const std::string &path);
+	static ftl::rgbd::Player *__createReader(const std::string &path);
 
-	static std::map<std::string, ftl::codecs::Reader*> readers__;
+	static std::map<std::string, ftl::rgbd::Player*> readers__;
 };
 
 }
diff --git a/components/rgbd-sources/src/source.cpp b/components/rgbd-sources/src/source.cpp
index 114ba269394d315a6b88c33e21eb6cef8150e0e2..610f9d08a6c067063f3061b90b6a343080f245fd 100644
--- a/components/rgbd-sources/src/source.cpp
+++ b/components/rgbd-sources/src/source.cpp
@@ -2,20 +2,20 @@
 #include <ftl/rgbd/source.hpp>
 #include <ftl/threads.hpp>
 
-#include "net.hpp"
-#include "stereovideo.hpp"
-#include "image.hpp"
-#include "middlebury_source.hpp"
+#include "sources/net/net.hpp"
+#include "sources/stereovideo/stereovideo.hpp"
+#include "sources/image/image.hpp"
+#include "sources/middlebury/middlebury_source.hpp"
 
 #ifdef HAVE_LIBARCHIVE
 #include <ftl/rgbd/snapshot.hpp>
-#include "snapshot_source.hpp"
+#include "sources/snapshot/snapshot_source.hpp"
 #endif
 
-#include "file_source.hpp"
+#include "sources/ftlfile/file_source.hpp"
 
 #ifdef HAVE_REALSENSE
-#include "realsense_source.hpp"
+#include "sources/realsense/realsense_source.hpp"
 using ftl::rgbd::detail::RealsenseSource;
 #endif
 
@@ -30,7 +30,7 @@ using ftl::rgbd::capability_t;
 using ftl::codecs::Channel;
 using ftl::rgbd::detail::FileSource;
 
-std::map<std::string, ftl::codecs::Reader*> Source::readers__;
+std::map<std::string, ftl::rgbd::Player*> Source::readers__;
 
 Source::Source(ftl::config::json_t &cfg) : Configurable(cfg), pose_(Eigen::Matrix4d::Identity()), net_(nullptr) {
 	impl_ = nullptr;
@@ -120,7 +120,7 @@ ftl::rgbd::detail::Source *Source::_createFileImpl(const ftl::URI &uri) {
 		string ext = path.substr(eix+1);
 
 		if (ext == "ftl") {
-			ftl::codecs::Reader *reader = __createReader(path);
+			ftl::rgbd::Player *reader = __createReader(path);
 			LOG(INFO) << "Playing track: " << uri.getFragment();
 			return new FileSource(this, reader, std::stoi(uri.getFragment()));
 		} else if (ext == "png" || ext == "jpg") {
@@ -146,7 +146,7 @@ ftl::rgbd::detail::Source *Source::_createFileImpl(const ftl::URI &uri) {
 	return nullptr;
 }
 
-ftl::codecs::Reader *Source::__createReader(const std::string &path) {
+ftl::rgbd::Player *Source::__createReader(const std::string &path) {
 	if (readers__.find(path) != readers__.end()) {
 		return readers__[path];
 	}
@@ -156,7 +156,7 @@ ftl::codecs::Reader *Source::__createReader(const std::string &path) {
 
 	// FIXME: This is a memory leak, must delete ifstream somewhere.
 
-	auto *r = new ftl::codecs::Reader(*file);
+	auto *r = new ftl::rgbd::Player(*file);
 	readers__[path] = r;
 	r->begin();
 	return r;
diff --git a/components/rgbd-sources/src/file_source.cpp b/components/rgbd-sources/src/sources/ftlfile/file_source.cpp
similarity index 96%
rename from components/rgbd-sources/src/file_source.cpp
rename to components/rgbd-sources/src/sources/ftlfile/file_source.cpp
index 38acf1a7c47c35c3a2c88f505a801b3756168ffd..323f1ae72179c46ad816ec653ffc940d87cad0f7 100644
--- a/components/rgbd-sources/src/file_source.cpp
+++ b/components/rgbd-sources/src/sources/ftlfile/file_source.cpp
@@ -20,7 +20,7 @@ void FileSource::_createDecoder(int ix, const ftl::codecs::Packet &pkt) {
 	decoders_[ix] = ftl::codecs::allocateDecoder(pkt);
 }
 
-FileSource::FileSource(ftl::rgbd::Source *s, ftl::codecs::Reader *r, int sid) : ftl::rgbd::detail::Source(s) {
+FileSource::FileSource(ftl::rgbd::Source *s, ftl::rgbd::Player *r, int sid) : ftl::rgbd::detail::Source(s) {
     reader_ = r;
 	has_calibration_ = false;
 	decoders_[0] = nullptr;
diff --git a/components/rgbd-sources/src/file_source.hpp b/components/rgbd-sources/src/sources/ftlfile/file_source.hpp
similarity index 86%
rename from components/rgbd-sources/src/file_source.hpp
rename to components/rgbd-sources/src/sources/ftlfile/file_source.hpp
index 1f2241ea07c2bc639162c3fba68f4f45a7d0b28c..80f3b368b1ec30f5e5b1ebd9ac2f051ceba20901 100644
--- a/components/rgbd-sources/src/file_source.hpp
+++ b/components/rgbd-sources/src/sources/ftlfile/file_source.hpp
@@ -5,7 +5,7 @@
 #include <loguru.hpp>
 
 #include <ftl/rgbd/source.hpp>
-#include <ftl/codecs/reader.hpp>
+#include "player.hpp"
 #include <ftl/codecs/decoder.hpp>
 
 #include <list>
@@ -16,7 +16,7 @@ namespace detail {
 
 class FileSource : public detail::Source {
 	public:
-	FileSource(ftl::rgbd::Source *, ftl::codecs::Reader *, int sid);
+	FileSource(ftl::rgbd::Source *, ftl::rgbd::Player *, int sid);
 	~FileSource();
 
 	bool capture(int64_t ts);
@@ -27,7 +27,7 @@ class FileSource : public detail::Source {
 
 	//void reset();
 	private:
-	ftl::codecs::Reader *reader_;
+	ftl::rgbd::Player *reader_;
 	bool has_calibration_;
 
 	struct PacketPair {
diff --git a/components/rgbd-sources/src/sources/ftlfile/player.cpp b/components/rgbd-sources/src/sources/ftlfile/player.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..cabbce6425586f6473156cdb6e548add0648d770
--- /dev/null
+++ b/components/rgbd-sources/src/sources/ftlfile/player.cpp
@@ -0,0 +1,79 @@
+#include "player.hpp"
+#include <ftl/configuration.hpp>
+
+using ftl::rgbd::Player;
+
+Player::Player(std::istream &s) : stream_(&s), reader_(s) {
+    auto *c = ftl::config::find("/controls");
+
+    offset_ = 0;
+	
+    if (c) {
+        paused_ = c->value("paused", false);
+        c->on("paused", [this,c](const ftl::config::Event &e) {
+            UNIQUE_LOCK(mtx_, lk);
+            paused_ = c->value("paused", false);
+            if (paused_) {
+                pause_time_ = last_ts_;
+            } else {
+                offset_ += last_ts_ - pause_time_;
+            }
+        });
+
+        looping_ = c->value("looping", true);
+        c->on("looping", [this,c](const ftl::config::Event &e) {
+            looping_ = c->value("looping", false);
+        });
+
+        speed_ = c->value("speed", 1.0f);
+        c->on("speed", [this,c](const ftl::config::Event &e) {
+            speed_ = c->value("speed", 1.0f);
+        });
+
+        reversed_ = c->value("reverse", false);
+        c->on("reverse", [this,c](const ftl::config::Event &e) {
+            reversed_ = c->value("reverse", false);
+        });
+    }
+}
+
+Player::~Player() {
+    // TODO: Remove callbacks
+}
+
+bool Player::read(int64_t ts) {
+    std::unique_lock<std::mutex> lk(mtx_, std::defer_lock);
+	if (!lk.try_lock()) return true;
+
+    last_ts_ = ts;
+    if (paused_) return true;
+
+    int64_t adjusted_ts = int64_t(float(ts - reader_.getStartTime()) * speed_) + reader_.getStartTime() + offset_;
+    bool res = reader_.read(adjusted_ts);
+
+    if (looping_ && !res) {
+        reader_.end();
+        offset_ = 0;
+        stream_->clear();
+        stream_->seekg(0);
+        if (!reader_.begin()) {
+            LOG(ERROR) << "Could not loop";
+            return false;
+        }
+        return true;
+    }
+
+    return res;
+}
+
+void Player::onPacket(int streamID, const std::function<void(const ftl::codecs::StreamPacket &, ftl::codecs::Packet &)> &f) {
+    reader_.onPacket(streamID, f);
+}
+
+bool Player::begin() {
+    return reader_.begin();
+}
+
+bool Player::end() {
+    return reader_.end();
+}
diff --git a/components/rgbd-sources/src/sources/ftlfile/player.hpp b/components/rgbd-sources/src/sources/ftlfile/player.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f826caabaf6b461cc77b719427381e3b1c5828c4
--- /dev/null
+++ b/components/rgbd-sources/src/sources/ftlfile/player.hpp
@@ -0,0 +1,47 @@
+#ifndef _FTL_RGBD_PLAYER_HPP_
+#define _FTL_RGBD_PLAYER_HPP_
+
+#include <iostream>
+#include <ftl/codecs/reader.hpp>
+#include <ftl/threads.hpp>
+
+namespace ftl {
+namespace rgbd {
+
+/**
+ * Simple wrapper around stream reader to control file playback.
+ */
+class Player {
+    public:
+    explicit Player(std::istream &s);
+    ~Player();
+
+    bool read(int64_t ts);
+
+	void onPacket(int streamID, const std::function<void(const ftl::codecs::StreamPacket &, ftl::codecs::Packet &)> &);
+
+	bool begin();
+	bool end();
+
+    inline int64_t getStartTime() { return reader_.getStartTime(); }
+
+    private:
+    std::istream *stream_;
+    ftl::codecs::Reader reader_;
+
+    bool paused_;
+    bool reversed_;
+    bool looping_;
+    float speed_;
+
+    int64_t pause_time_;
+    int64_t offset_;
+    int64_t last_ts_;
+
+    MUTEX mtx_;
+};
+
+}
+}
+
+#endif  // _FTL_RGBD_PLAYER_HPP_
diff --git a/components/rgbd-sources/src/image.hpp b/components/rgbd-sources/src/sources/image/image.hpp
similarity index 100%
rename from components/rgbd-sources/src/image.hpp
rename to components/rgbd-sources/src/sources/image/image.hpp
diff --git a/components/rgbd-sources/src/middlebury_source.cpp b/components/rgbd-sources/src/sources/middlebury/middlebury_source.cpp
similarity index 100%
rename from components/rgbd-sources/src/middlebury_source.cpp
rename to components/rgbd-sources/src/sources/middlebury/middlebury_source.cpp
diff --git a/components/rgbd-sources/src/middlebury_source.hpp b/components/rgbd-sources/src/sources/middlebury/middlebury_source.hpp
similarity index 100%
rename from components/rgbd-sources/src/middlebury_source.hpp
rename to components/rgbd-sources/src/sources/middlebury/middlebury_source.hpp
diff --git a/components/rgbd-sources/src/net.cpp b/components/rgbd-sources/src/sources/net/net.cpp
similarity index 100%
rename from components/rgbd-sources/src/net.cpp
rename to components/rgbd-sources/src/sources/net/net.cpp
diff --git a/components/rgbd-sources/src/net.hpp b/components/rgbd-sources/src/sources/net/net.hpp
similarity index 100%
rename from components/rgbd-sources/src/net.hpp
rename to components/rgbd-sources/src/sources/net/net.hpp
diff --git a/components/rgbd-sources/src/realsense_source.cpp b/components/rgbd-sources/src/sources/realsense/realsense_source.cpp
similarity index 100%
rename from components/rgbd-sources/src/realsense_source.cpp
rename to components/rgbd-sources/src/sources/realsense/realsense_source.cpp
diff --git a/components/rgbd-sources/src/realsense_source.hpp b/components/rgbd-sources/src/sources/realsense/realsense_source.hpp
similarity index 100%
rename from components/rgbd-sources/src/realsense_source.hpp
rename to components/rgbd-sources/src/sources/realsense/realsense_source.hpp
diff --git a/components/rgbd-sources/src/snapshot.cpp b/components/rgbd-sources/src/sources/snapshot/snapshot.cpp
similarity index 100%
rename from components/rgbd-sources/src/snapshot.cpp
rename to components/rgbd-sources/src/sources/snapshot/snapshot.cpp
diff --git a/components/rgbd-sources/src/snapshot_source.cpp b/components/rgbd-sources/src/sources/snapshot/snapshot_source.cpp
similarity index 100%
rename from components/rgbd-sources/src/snapshot_source.cpp
rename to components/rgbd-sources/src/sources/snapshot/snapshot_source.cpp
diff --git a/components/rgbd-sources/src/snapshot_source.hpp b/components/rgbd-sources/src/sources/snapshot/snapshot_source.hpp
similarity index 100%
rename from components/rgbd-sources/src/snapshot_source.hpp
rename to components/rgbd-sources/src/sources/snapshot/snapshot_source.hpp
diff --git a/components/rgbd-sources/src/calibrate.cpp b/components/rgbd-sources/src/sources/stereovideo/calibrate.cpp
similarity index 100%
rename from components/rgbd-sources/src/calibrate.cpp
rename to components/rgbd-sources/src/sources/stereovideo/calibrate.cpp
diff --git a/components/rgbd-sources/src/calibrate.hpp b/components/rgbd-sources/src/sources/stereovideo/calibrate.hpp
similarity index 100%
rename from components/rgbd-sources/src/calibrate.hpp
rename to components/rgbd-sources/src/sources/stereovideo/calibrate.hpp
diff --git a/components/rgbd-sources/src/local.cpp b/components/rgbd-sources/src/sources/stereovideo/local.cpp
similarity index 100%
rename from components/rgbd-sources/src/local.cpp
rename to components/rgbd-sources/src/sources/stereovideo/local.cpp
diff --git a/components/rgbd-sources/src/local.hpp b/components/rgbd-sources/src/sources/stereovideo/local.hpp
similarity index 100%
rename from components/rgbd-sources/src/local.hpp
rename to components/rgbd-sources/src/sources/stereovideo/local.hpp
diff --git a/components/rgbd-sources/src/stereovideo.cpp b/components/rgbd-sources/src/sources/stereovideo/stereovideo.cpp
similarity index 100%
rename from components/rgbd-sources/src/stereovideo.cpp
rename to components/rgbd-sources/src/sources/stereovideo/stereovideo.cpp
diff --git a/components/rgbd-sources/src/stereovideo.hpp b/components/rgbd-sources/src/sources/stereovideo/stereovideo.hpp
similarity index 100%
rename from components/rgbd-sources/src/stereovideo.hpp
rename to components/rgbd-sources/src/sources/stereovideo/stereovideo.hpp
diff --git a/components/rgbd-sources/src/virtual.cpp b/components/rgbd-sources/src/sources/virtual/virtual.cpp
similarity index 100%
rename from components/rgbd-sources/src/virtual.cpp
rename to components/rgbd-sources/src/sources/virtual/virtual.cpp
diff --git a/components/rgbd-sources/test/source_unit.cpp b/components/rgbd-sources/test/source_unit.cpp
index ca66273a99f75f58d118d16237f558017911ee20..1b69631aa0ec0312cf1cba06533967f585babf56 100644
--- a/components/rgbd-sources/test/source_unit.cpp
+++ b/components/rgbd-sources/test/source_unit.cpp
@@ -18,6 +18,14 @@ class SnapshotReader {
 	Snapshot readArchive() { return Snapshot(); };
 };
 
+class Player {
+	public:
+	explicit Player(std::istream &) {}
+
+	bool begin() { return true; }
+	bool end() { return true; }
+};
+
 namespace detail {
 
 class ImageSource : public ftl::rgbd::detail::Source {
@@ -76,7 +84,7 @@ class SnapshotSource : public ftl::rgbd::detail::Source {
 
 class FileSource : public ftl::rgbd::detail::Source {
 	public:
-	FileSource(ftl::rgbd::Source *host, ftl::codecs::Reader *r, int) : ftl::rgbd::detail::Source(host) {
+	FileSource(ftl::rgbd::Source *host, ftl::rgbd::Player *r, int) : ftl::rgbd::detail::Source(host) {
 		last_type = "filesource";
 	}