diff --git a/applications/tools/CMakeLists.txt b/applications/tools/CMakeLists.txt
index c4f3bc012d6cf25340be837f9f0da780f0db5baa..8a6d662a3de011e2a0a2670716ddb5a924fe9ddd 100644
--- a/applications/tools/CMakeLists.txt
+++ b/applications/tools/CMakeLists.txt
@@ -5,3 +5,4 @@
 #endif()
 
 add_subdirectory(middlebury_gen)
+add_subdirectory(simple_viewer)
diff --git a/applications/tools/simple_viewer/CMakeLists.txt b/applications/tools/simple_viewer/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c7d6e0c2ad6a5e91722612b77bb6ee55916afa73
--- /dev/null
+++ b/applications/tools/simple_viewer/CMakeLists.txt
@@ -0,0 +1,12 @@
+# Need to include staged files and libs
+#include_directories(${PROJECT_SOURCE_DIR}/reconstruct/include)
+#include_directories(${PROJECT_BINARY_DIR})
+
+set(SIMPVIEWSRC
+	main.cpp
+)
+
+add_executable(simple-viewer ${SIMPVIEWSRC})
+
+#target_include_directories(cv-node PUBLIC ${PROJECT_SOURCE_DIR}/include)
+target_link_libraries(simple-viewer ftlcommon ftlrgbd Threads::Threads ${OpenCV_LIBS} ftlctrl ftlnet ftlrender ftloperators ftlstreams ftlaudio)
diff --git a/applications/tools/simple_viewer/main.cpp b/applications/tools/simple_viewer/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ae7fd54cc253796a60d6a3a73b8a6b24085ae5f1
--- /dev/null
+++ b/applications/tools/simple_viewer/main.cpp
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2019 Nicolas Pope. All rights reserved.
+ *
+ * See LICENSE.
+ */
+
+#define LOGURU_WITH_STREAMS 1
+#include <loguru.hpp>
+#include <ftl/config.h>
+#include <ftl/configuration.hpp>
+#include <ftl/master.hpp>
+#include <ftl/threads.hpp>
+#include <ftl/codecs/channels.hpp>
+#include <ftl/codecs/depth_convert_cuda.hpp>
+#include <ftl/data/framepool.hpp>
+
+#include <nlohmann/json.hpp>
+
+#include <fstream>
+#include <string>
+#include <vector>
+#include <thread>
+#include <chrono>
+
+#include <opencv2/opencv.hpp>
+#include <opencv2/quality/qualitypsnr.hpp>
+#include <ftl/net/universe.hpp>
+
+#include <ftl/streams/filestream.hpp>
+#include <ftl/streams/receiver.hpp>
+#include <ftl/streams/sender.hpp>
+#include <ftl/streams/netstream.hpp>
+
+#include <ftl/operators/colours.hpp>
+#include <ftl/operators/mask.hpp>
+#include <ftl/operators/segmentation.hpp>
+#include <ftl/operators/depth.hpp>
+
+#ifdef WIN32
+#pragma comment(lib, "Rpcrt4.lib")
+#endif
+
+using ftl::net::Universe;
+using std::string;
+using std::vector;
+using ftl::config::json_t;
+using ftl::codecs::Channel;
+using ftl::codecs::codec_t;
+using ftl::codecs::definition_t;
+
+using json = nlohmann::json;
+using std::this_thread::sleep_for;
+using std::chrono::milliseconds;
+
+static ftl::data::Generator *createFileGenerator(ftl::Configurable *root, ftl::data::Pool *pool, const std::string &filename) {
+	ftl::stream::File *stream = ftl::create<ftl::stream::File>(root, "player");
+	stream->set("filename", filename);
+
+	ftl::stream::Receiver *gen = ftl::create<ftl::stream::Receiver>(root, "receiver", pool);
+	gen->setStream(stream);
+
+	stream->begin();
+	stream->select(0, Channel::Colour + Channel::Depth);  // TODO: Choose these elsewhere
+	return gen;
+}
+
+static void visualizeDepthMap(	const cv::Mat &depth, cv::Mat &out,
+								const float max_depth)
+{
+	DCHECK(max_depth > 0.0);
+
+	depth.convertTo(out, CV_8U, 255.0f / max_depth);
+	out = 255 - out;
+	//cv::Mat mask = (depth >= max_depth); // TODO (mask for invalid pixels)
+	
+	applyColorMap(out, out, cv::COLORMAP_JET);
+	//out.setTo(cv::Scalar(0), mask);
+	//cv::cvtColor(out,out, cv::COLOR_BGR2BGRA);
+}
+
+static void run(ftl::Configurable *root) {
+	Universe *net = ftl::create<Universe>(root, "net");
+	ftl::ctrl::Master ctrl(root, net);
+
+	net->start();
+	net->waitConnections();
+
+	std::list<ftl::Handle> handles;
+	ftl::data::Pool pool(2,10);
+
+	// Check paths for FTL files to load.
+	auto paths = (*root->get<nlohmann::json>("paths"));
+	int i = 0; //groups.size();
+	for (auto &x : paths.items()) {
+		std::string path = x.value().get<std::string>();
+		auto eix = path.find_last_of('.');
+		auto ext = path.substr(eix+1);
+
+		// Command line path is ftl file
+		if (ext == "ftl") {
+			auto *gen = createFileGenerator(root, &pool, path);
+
+			handles.push_back(std::move(gen->onFrameSet([&](std::shared_ptr<ftl::data::FrameSet> fs) {
+				
+				LOG(INFO) << "Got frameset: " << fs->timestamp();
+				for (auto &f : fs->frames) {
+					if (f.has(Channel::Colour)) {
+						cv::Mat tmp;
+						f.get<cv::cuda::GpuMat>(Channel::Colour).download(tmp);
+						cv::imshow(std::string("Frame")+std::to_string(f.id().id), tmp);
+					}
+				}
+
+				cv::waitKey(1);
+
+				return true;
+			})));
+
+			++i;
+		}
+	}
+
+	LOG(INFO) << "Start timer";
+	ftl::timer::start(true);
+
+	LOG(INFO) << "Shutting down...";
+	ftl::timer::stop();
+	ftl::pool.stop(true);
+	ctrl.stop();
+	net->shutdown();
+
+	//cudaProfilerStop();
+
+	LOG(INFO) << "Deleting...";
+
+	delete net;
+
+	ftl::config::cleanup();  // Remove any last configurable objects.
+	LOG(INFO) << "Done.";
+}
+
+int main(int argc, char **argv) {
+	run(ftl::configure(argc, argv, "tools_default"));
+}
diff --git a/components/streams/CMakeLists.txt b/components/streams/CMakeLists.txt
index 41d265defd55fd4bc267b8ac70f7e39d96e9b925..8513da77c659f9468d9ed155b54a2b9ef3c600d8 100644
--- a/components/streams/CMakeLists.txt
+++ b/components/streams/CMakeLists.txt
@@ -14,6 +14,7 @@ set(STREAMSRC
 	src/netstream.cpp
 	src/injectors.cpp
 	src/parsers.cpp
+	src/builder.cpp
 )
 
 add_library(ftlstreams ${STREAMSRC})