From bb78a21b3c71a38f975a9147b1bb67d62ef1ba91 Mon Sep 17 00:00:00 2001
From: Nicolas Pope <nicolas.pope@utu.fi>
Date: Sun, 30 Jun 2019 19:37:34 +0300
Subject: [PATCH] Implements #112 thumbnails

---
 applications/gui/CMakeLists.txt               |   1 +
 applications/gui/src/gltexture.cpp            |  13 +-
 applications/gui/src/gltexture.hpp            |   1 +
 applications/gui/src/media_panel.cpp          |  19 --
 applications/gui/src/screen.cpp               |  21 +-
 applications/gui/src/src_window.cpp           |  69 +++++-
 applications/gui/src/src_window.hpp           |   9 +
 applications/gui/src/thumbview.cpp            |  32 +++
 applications/gui/src/thumbview.hpp            |  29 +++
 .../include/ftl/virtual_source.hpp            |   2 +-
 .../reconstruct/src/virtual_source.cpp        |   2 +-
 .../include/ftl/rgbd/detail/source.hpp        |   6 +-
 .../rgbd-sources/include/ftl/rgbd/source.hpp  |   1 +
 .../include/ftl/rgbd/streamer.hpp             |   8 +-
 .../rgbd-sources/src/bitrate_settings.hpp     |  32 +++
 components/rgbd-sources/src/image.hpp         |   2 +-
 .../rgbd-sources/src/middlebury_source.cpp    |   2 +-
 .../rgbd-sources/src/middlebury_source.hpp    |   2 +-
 components/rgbd-sources/src/net.cpp           |  52 ++--
 components/rgbd-sources/src/net.hpp           |   4 +-
 .../rgbd-sources/src/realsense_source.cpp     |   2 +-
 .../rgbd-sources/src/realsense_source.hpp     |   2 +-
 .../rgbd-sources/src/snapshot_source.hpp      |   2 +-
 components/rgbd-sources/src/source.cpp        |  21 +-
 components/rgbd-sources/src/stereovideo.cpp   |   2 +-
 components/rgbd-sources/src/stereovideo.hpp   |   2 +-
 components/rgbd-sources/src/streamer.cpp      | 231 +++++++-----------
 components/rgbd-sources/test/source_unit.cpp  |  12 +-
 28 files changed, 358 insertions(+), 223 deletions(-)
 create mode 100644 applications/gui/src/thumbview.cpp
 create mode 100644 applications/gui/src/thumbview.hpp
 create mode 100644 components/rgbd-sources/src/bitrate_settings.hpp

diff --git a/applications/gui/CMakeLists.txt b/applications/gui/CMakeLists.txt
index cda2b89d2..baffb9bdd 100644
--- a/applications/gui/CMakeLists.txt
+++ b/applications/gui/CMakeLists.txt
@@ -12,6 +12,7 @@ set(GUISRC
 	src/gltexture.cpp
 	src/camera.cpp
 	src/media_panel.cpp
+	src/thumbview.cpp
 )
 
 add_executable(ftl-gui ${GUISRC})
diff --git a/applications/gui/src/gltexture.cpp b/applications/gui/src/gltexture.cpp
index 752894a3c..8b36e848f 100644
--- a/applications/gui/src/gltexture.cpp
+++ b/applications/gui/src/gltexture.cpp
@@ -14,20 +14,21 @@ GLTexture::~GLTexture() {
 }
 
 void GLTexture::update(cv::Mat &m) {
+	if (m.rows == 0) return;
 	if (glid_ == std::numeric_limits<unsigned int>::max()) {
 		glGenTextures(1, &glid_);
 		glBindTexture(GL_TEXTURE_2D, glid_);
-		cv::Mat m(cv::Size(100,100), CV_8UC3);
-		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, m.cols, m.rows, 0, GL_RGB, GL_UNSIGNED_BYTE, m.data);
+		//cv::Mat m(cv::Size(100,100), CV_8UC3);
+		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, m.cols, m.rows, 0, GL_BGR, GL_UNSIGNED_BYTE, m.data);
 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+	} else {
+		glBindTexture(GL_TEXTURE_2D, glid_);
+		// TODO Allow for other formats
+		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, m.cols, m.rows, 0, GL_BGR, GL_UNSIGNED_BYTE, m.data);
 	}
-	if (m.rows == 0) return;
-	glBindTexture(GL_TEXTURE_2D, glid_);
-	// TODO Allow for other formats
-	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, m.cols, m.rows, 0, GL_BGR, GL_UNSIGNED_BYTE, m.data);
 	auto err = glGetError();
 	if (err != 0) LOG(ERROR) << "OpenGL Texture error: " << err;
 }
diff --git a/applications/gui/src/gltexture.hpp b/applications/gui/src/gltexture.hpp
index 4fb0bdaef..f83eabea2 100644
--- a/applications/gui/src/gltexture.hpp
+++ b/applications/gui/src/gltexture.hpp
@@ -13,6 +13,7 @@ class GLTexture {
 
 	void update(cv::Mat &m);
 	unsigned int texture() const { return glid_; }
+	bool isValid() const { return glid_ != std::numeric_limits<unsigned int>::max(); }
 
 	private:
 	unsigned int glid_;
diff --git a/applications/gui/src/media_panel.cpp b/applications/gui/src/media_panel.cpp
index 2e0f5fae2..84b45e9fc 100644
--- a/applications/gui/src/media_panel.cpp
+++ b/applications/gui/src/media_panel.cpp
@@ -26,25 +26,6 @@ MediaPanel::MediaPanel(ftl::gui::Screen *screen) : nanogui::Window(screen, ""),
     //setFixedSize(size);
     setPosition(Vector2i(screen->width() / 2 - size[0]/2, screen->height() - 30 - size[1]));
 
-	Theme *mediatheme = new Theme(*theme());
-	mediatheme->mIconScale = 1.2f;
-	mediatheme->mWindowDropShadowSize = 0;
-	mediatheme->mWindowFillFocused = nanogui::Color(45, 150);
-	mediatheme->mWindowFillUnfocused = nanogui::Color(45, 80);
-	mediatheme->mButtonGradientTopUnfocused = nanogui::Color(0,0);
-	mediatheme->mButtonGradientBotUnfocused = nanogui::Color(0,0);
-	mediatheme->mButtonGradientTopFocused = nanogui::Color(80,230);
-	mediatheme->mButtonGradientBotFocused = nanogui::Color(80,230);
-	mediatheme->mIconColor = nanogui::Color(255,255);
-    mediatheme->mTextColor = nanogui::Color(1.0f,1.0f,1.0f,1.0f);
-	mediatheme->mBorderDark = nanogui::Color(0,0);
-	mediatheme->mBorderMedium = nanogui::Color(0,0);
-	mediatheme->mBorderLight = nanogui::Color(0,0);
-	mediatheme->mDropShadow = nanogui::Color(0,0);
-	mediatheme->mButtonFontSize = 30;
-
-	setTheme(mediatheme);
-
     auto button = new Button(this, "", ENTYPO_ICON_EDIT);
 	button->setTooltip("Edit camera properties");
     button->setCallback([this]() {
diff --git a/applications/gui/src/screen.cpp b/applications/gui/src/screen.cpp
index 00d46ee58..9f3d03bdf 100644
--- a/applications/gui/src/screen.cpp
+++ b/applications/gui/src/screen.cpp
@@ -72,6 +72,24 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 	toolbuttheme->mButtonGradientTopPushed = nanogui::Color(60,180);
 	toolbuttheme->mButtonGradientBotPushed = nanogui::Color(60,180);
 
+	Theme *mediatheme = new Theme(*theme());
+	mediatheme->mIconScale = 1.2f;
+	mediatheme->mWindowDropShadowSize = 0;
+	mediatheme->mWindowFillFocused = nanogui::Color(45, 150);
+	mediatheme->mWindowFillUnfocused = nanogui::Color(45, 80);
+	mediatheme->mButtonGradientTopUnfocused = nanogui::Color(0,0);
+	mediatheme->mButtonGradientBotUnfocused = nanogui::Color(0,0);
+	mediatheme->mButtonGradientTopFocused = nanogui::Color(80,230);
+	mediatheme->mButtonGradientBotFocused = nanogui::Color(80,230);
+	mediatheme->mIconColor = nanogui::Color(255,255);
+    mediatheme->mTextColor = nanogui::Color(1.0f,1.0f,1.0f,1.0f);
+	mediatheme->mBorderDark = nanogui::Color(0,0);
+	mediatheme->mBorderMedium = nanogui::Color(0,0);
+	mediatheme->mBorderLight = nanogui::Color(0,0);
+	mediatheme->mDropShadow = nanogui::Color(0,0);
+	mediatheme->mButtonFontSize = 30;
+	mediatheme->mStandardFontSize = 20;
+
 	windowtheme = new Theme(*theme());
 	windowtheme->mWindowFillFocused = nanogui::Color(220, 200);
 	windowtheme->mWindowFillUnfocused = nanogui::Color(220, 200);
@@ -196,6 +214,7 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 	swindow_ = new ftl::gui::SourceWindow(this);
 	mwindow_ = new ftl::gui::MediaPanel(this);
 	mwindow_->setVisible(false);
+	mwindow_->setTheme(mediatheme);
 
 	cwindow_->setPosition(Eigen::Vector2i(80, 20));
 	//swindow_->setPosition(Eigen::Vector2i(80, 400));
@@ -203,7 +222,7 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 	swindow_->setVisible(true);
 	swindow_->center();
 	cwindow_->setTheme(windowtheme);
-	swindow_->setTheme(windowtheme);
+	swindow_->setTheme(mediatheme);
 
 	mShader.init("RGBDShader", defaultImageViewVertexShader,
 				defaultImageViewFragmentShader);
diff --git a/applications/gui/src/src_window.cpp b/applications/gui/src/src_window.cpp
index 124285573..c654a9877 100644
--- a/applications/gui/src/src_window.cpp
+++ b/applications/gui/src/src_window.cpp
@@ -12,11 +12,14 @@
 #include <nanogui/glutil.h>
 #include <nanogui/screen.h>
 #include <nanogui/layout.h>
+#include <nanogui/vscrollpanel.h>
 
 #ifdef HAVE_LIBARCHIVE
 #include "ftl/rgbd/snapshot.hpp"
 #endif
 
+#include "thumbview.hpp"
+
 using ftl::gui::SourceWindow;
 using ftl::gui::Screen;
 using ftl::rgbd::Source;
@@ -25,7 +28,7 @@ using ftl::config::json_t;
 
 SourceWindow::SourceWindow(ftl::gui::Screen *screen)
 		: nanogui::Window(screen, ""), screen_(screen) {
-	setLayout(new nanogui::GroupLayout());
+	setLayout(new nanogui::BoxLayout(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 20, 5));
 
 	using namespace nanogui;
 	
@@ -39,24 +42,33 @@ SourceWindow::SourceWindow(ftl::gui::Screen *screen)
 	//    tools->setLayout(new BoxLayout(Orientation::Horizontal,
 	//                                   Alignment::Middle, 0, 6));
 
-	new Label(this, "Select source","sans-bold");
+	auto label = new Label(this, "Select Camera","sans-bold",20);
+
 	available_ = screen_->control()->getNet()->findAll<string>("list_streams");
-	auto select = new ComboBox(this, available_);
-	select->setCallback([this,select](int ix) {
+	//auto select = new ComboBox(this, available_);
+	//select->setCallback([this,select](int ix) {
 		//src_->set("uri", available_[ix]);
 		// TODO(Nick) Check camera exists first
-		screen_->setActiveCamera(cameras_[available_[ix]]);
-		LOG(INFO) << "Change source: " << ix;
-	});
+	//	screen_->setActiveCamera(cameras_[available_[ix]]);
+	//	LOG(INFO) << "Change source: " << ix;
+	//});
 
-	_updateCameras();
+	auto vscroll = new VScrollPanel(this);
+	ipanel_ = new Widget(vscroll);
+	ipanel_->setLayout(new GridLayout(nanogui::Orientation::Horizontal, 2,
+		nanogui::Alignment::Middle, 0, 5));
+	//ipanel_ = new ImageView(vscroll, 0);
 
-	screen->net()->onConnect([this,select](ftl::net::Peer *p) {
+	screen->net()->onConnect([this](ftl::net::Peer *p) {
+		UNIQUE_LOCK(mutex_, lk);
 		available_ = screen_->net()->findAll<string>("list_streams");
-		select->setItems(available_);
+		//select->setItems(available_);
 		_updateCameras();
 	});
 
+	UNIQUE_LOCK(mutex_, lk);
+	_updateCameras();
+
 	/*new Label(this, "Source Options","sans-bold");
 
 	auto tools = new Widget(this);
@@ -118,6 +130,11 @@ SourceWindow::SourceWindow(ftl::gui::Screen *screen)
 }
 
 void SourceWindow::_updateCameras() {
+	refresh_thumbs_ = true;
+	if (thumbs_.size() != available_.size()) {
+		thumbs_.resize(available_.size());
+	}
+
 	for (auto s : available_) {
 		if (cameras_.find(s) == cameras_.end()) {
 			json_t srcjson;
@@ -126,6 +143,8 @@ void SourceWindow::_updateCameras() {
 			std::vector<ftl::rgbd::Source*> srcs = ftl::createArray<ftl::rgbd::Source>(screen_->root(), "sources", screen_->net());
 			auto *src = srcs[srcs.size()-1];
 
+			LOG(INFO) << "Making camera: " << src->getURI();
+
 			auto *cam = new ftl::gui::Camera(screen_, src);
 			cameras_[s] = cam;
 		} else {
@@ -137,3 +156,33 @@ void SourceWindow::_updateCameras() {
 SourceWindow::~SourceWindow() {
 
 }
+
+void SourceWindow::draw(NVGcontext *ctx) {
+	if (refresh_thumbs_) {
+		UNIQUE_LOCK(mutex_, lk);
+		//refresh_thumbs_ = false;
+
+		for (size_t i=0; i<thumbs_.size(); ++i) {
+			cv::Mat t;
+			auto *cam = cameras_[available_[i]];
+			if (cam) {
+				if (cam->source()->thumbnail(t)) {
+					thumbs_[i].update(t);
+				} else {
+					refresh_thumbs_ = true;
+				}
+			}
+
+			if (ipanel_->childCount() < i+1) {
+				new ftl::gui::ThumbView(ipanel_, screen_, cam);
+			}
+			if (thumbs_[i].isValid()) dynamic_cast<nanogui::ImageView*>(ipanel_->childAt(i))->bindImage(thumbs_[i].texture());
+		}
+
+		// TODO(Nick) remove excess image views
+
+		center();
+	}
+
+	nanogui::Window::draw(ctx);
+}
diff --git a/applications/gui/src/src_window.hpp b/applications/gui/src/src_window.hpp
index 77f529c8c..de14d0d05 100644
--- a/applications/gui/src/src_window.hpp
+++ b/applications/gui/src/src_window.hpp
@@ -2,12 +2,15 @@
 #define _FTL_GUI_SRCWINDOW_HPP_
 
 #include <nanogui/window.h>
+#include <nanogui/imageview.h>
 #include <ftl/master.hpp>
 #include <ftl/uuid.hpp>
 #include <ftl/rgbd/source.hpp>
+#include <ftl/threads.hpp>
 #include <vector>
 #include <map>
 #include <string>
+#include "gltexture.hpp"
 
 class VirtualCameraView;
 
@@ -24,10 +27,16 @@ class SourceWindow : public nanogui::Window {
 
 	const std::vector<ftl::gui::Camera*> &getCameras();
 
+	virtual void draw(NVGcontext *ctx);
+
 	private:
 	ftl::gui::Screen *screen_;
 	std::map<std::string, ftl::gui::Camera*> cameras_; 
 	std::vector<std::string> available_;
+	std::vector<GLTexture> thumbs_;
+	bool refresh_thumbs_;
+	nanogui::Widget *ipanel_;
+	std::mutex mutex_;
 
 	void _updateCameras();
 
diff --git a/applications/gui/src/thumbview.cpp b/applications/gui/src/thumbview.cpp
new file mode 100644
index 000000000..4b2d03a75
--- /dev/null
+++ b/applications/gui/src/thumbview.cpp
@@ -0,0 +1,32 @@
+#include "thumbview.hpp"
+#include "screen.hpp"
+#include "camera.hpp"
+
+using ftl::gui::ThumbView;
+using ftl::gui::Screen;
+using ftl::gui::Camera;
+
+ThumbView::ThumbView(nanogui::Widget *parent, ftl::gui::Screen *screen, ftl::gui::Camera *cam)
+ : ImageView(parent, 0), screen_(screen), cam_(cam) {
+	 setCursor(nanogui::Cursor::Hand);
+}
+
+ThumbView::~ThumbView() {
+
+}
+
+bool ThumbView::mouseButtonEvent(const nanogui::Vector2i &p, int button, bool down, int modifiers) {
+	if (button == 0 && !down) {
+		screen_->setActiveCamera(cam_);
+	}
+}
+
+void ThumbView::draw(NVGcontext *ctx) {
+	ImageView::draw(ctx);
+
+	nvgScissor(ctx, mPos.x(), mPos.y(), mSize.x(), mSize.y());
+	nvgFontSize(ctx, 14);
+	nvgFontFace(ctx, "sans-bold");
+	nvgText(ctx, mPos.x() + 10, mPos.y()+mSize.y() - 10, cam_->source()->getURI().c_str(), NULL);
+	nvgResetScissor(ctx);
+}
diff --git a/applications/gui/src/thumbview.hpp b/applications/gui/src/thumbview.hpp
new file mode 100644
index 000000000..9bbac8097
--- /dev/null
+++ b/applications/gui/src/thumbview.hpp
@@ -0,0 +1,29 @@
+#ifndef _FTL_GUI_THUMBVIEW_HPP_
+#define _FTL_GUI_THUMBVIEW_HPP_
+
+#include <nanogui/imageview.h>
+
+namespace ftl {
+namespace gui {
+
+class Screen;
+class Camera;
+
+class ThumbView : public nanogui::ImageView {
+	public:
+	ThumbView(nanogui::Widget *parent, ftl::gui::Screen *screen, ftl::gui::Camera *cam);
+	~ThumbView();
+
+	bool mouseButtonEvent(const nanogui::Vector2i &p, int button, bool down, int modifiers);
+
+	void draw(NVGcontext *ctx);
+
+	private:
+	Screen *screen_;
+	Camera *cam_;
+};
+
+}
+}
+
+#endif  // _FTL_GUI_THUMBVIEW_HPP_
diff --git a/applications/reconstruct/include/ftl/virtual_source.hpp b/applications/reconstruct/include/ftl/virtual_source.hpp
index ff1a89d78..931bdd5eb 100644
--- a/applications/reconstruct/include/ftl/virtual_source.hpp
+++ b/applications/reconstruct/include/ftl/virtual_source.hpp
@@ -26,7 +26,7 @@ class VirtualSource : public ftl::rgbd::detail::Source {
 
 	void setScene(ftl::voxhash::SceneRep *);
 
-	bool grab();
+	bool grab(int n, int b);
 	//void getRGBD(cv::Mat &rgb, cv::Mat &depth);
 	bool isReady();
 
diff --git a/applications/reconstruct/src/virtual_source.cpp b/applications/reconstruct/src/virtual_source.cpp
index 46eca475e..cf6d268c1 100644
--- a/applications/reconstruct/src/virtual_source.cpp
+++ b/applications/reconstruct/src/virtual_source.cpp
@@ -53,7 +53,7 @@ void VirtualSource::setScene(ftl::voxhash::SceneRep *scene) {
 	scene_ = scene;
 }
 
-bool VirtualSource::grab() {
+bool VirtualSource::grab(int n, int b) {
 	if (scene_) {
 		// Ensure this host thread is using correct GPU.
 
diff --git a/components/rgbd-sources/include/ftl/rgbd/detail/source.hpp b/components/rgbd-sources/include/ftl/rgbd/detail/source.hpp
index a047174de..8ebdab720 100644
--- a/components/rgbd-sources/include/ftl/rgbd/detail/source.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/detail/source.hpp
@@ -27,7 +27,11 @@ class Source {
 	explicit Source(ftl::rgbd::Source *host) : capabilities_(0), host_(host), params_({0}) { }
 	virtual ~Source() {}
 
-	virtual bool grab()=0;
+	/**
+	 * @param n Number of frames to request in batch. Default -1 means automatic (10)
+	 * @param b Bit rate setting. -1 = automatic, 0 = best quality, 9 = lowest quality
+	 */
+	virtual bool grab(int n, int b)=0;
 	virtual bool isReady() { return false; };
 	virtual void setPose(const Eigen::Matrix4d &pose) { };
 
diff --git a/components/rgbd-sources/include/ftl/rgbd/source.hpp b/components/rgbd-sources/include/ftl/rgbd/source.hpp
index 3b5e53f18..3cf1a2b52 100644
--- a/components/rgbd-sources/include/ftl/rgbd/source.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/source.hpp
@@ -173,6 +173,7 @@ class Source : public ftl::Configurable {
 	detail::Source *impl_;
 	cv::Mat rgb_;
 	cv::Mat depth_;
+	cv::Mat thumb_;
 	Camera params_;		// TODO Find better solution
 	Eigen::Matrix4d pose_;
 	ftl::net::Universe *net_;
diff --git a/components/rgbd-sources/include/ftl/rgbd/streamer.hpp b/components/rgbd-sources/include/ftl/rgbd/streamer.hpp
index 665f9af9d..5dac8fd13 100644
--- a/components/rgbd-sources/include/ftl/rgbd/streamer.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/streamer.hpp
@@ -17,14 +17,15 @@ namespace ftl {
 namespace rgbd {
 
 static const int kChunkDim = 4;
+static constexpr int kChunkCount = kChunkDim * kChunkDim;
 
 namespace detail {
 
 struct StreamClient {
 	std::string uri;
 	ftl::UUID peerid;
-	int txcount;		// Frames sent since last request
-	int txmax;			// Frames to send in request
+	std::atomic<int> txcount;	// Frames sent since last request
+	int txmax;					// Frames to send in request
 };
 
 static const unsigned int kGrabbed = 0x1;
@@ -34,11 +35,12 @@ static const unsigned int kDepth = 0x4;
 struct StreamSource {
 	ftl::rgbd::Source *src;
 	std::atomic<unsigned int> jobs;				// Busy or ready to swap?
+	std::atomic<unsigned int> clientCount;
 	cv::Mat rgb;									// Tx buffer
 	cv::Mat depth;									// Tx buffer
 	cv::Mat prev_rgb;
 	cv::Mat prev_depth;
-	std::vector<detail::StreamClient> clients[10];	// One list per bitrate
+	std::list<detail::StreamClient> clients[10];	// One list per bitrate
 	std::shared_mutex mutex;
 	unsigned long long frame;
 };
diff --git a/components/rgbd-sources/src/bitrate_settings.hpp b/components/rgbd-sources/src/bitrate_settings.hpp
new file mode 100644
index 000000000..6b6e1508e
--- /dev/null
+++ b/components/rgbd-sources/src/bitrate_settings.hpp
@@ -0,0 +1,32 @@
+#ifndef _FTL_RGBD_BITRATESETTINGS_HPP_
+#define _FTL_RGBD_BITRATESETTINGS_HPP_
+
+namespace ftl {
+namespace rgbd {
+namespace detail {
+
+struct BitrateSetting {
+	int width;
+	int height;
+	int jpg_quality;
+	int png_compression;
+};
+
+static const BitrateSetting bitrate_settings[] = {
+	1280, 720, 95, 1,
+	1280, 720, 95, 1,
+	1280, 720, 95, 1,
+	1280, 720, 75, 1,
+	640, 360, 95, 1,
+	640, 360, 75, 5,
+	640, 360, 50, 5,
+	320, 160, 95, 5,
+	320, 160, 75, 5,
+	320, 160, 50, 9
+};
+
+}
+}
+}
+
+#endif  // _FTL_RGBD_BITRATESETTINGS_HPP_
diff --git a/components/rgbd-sources/src/image.hpp b/components/rgbd-sources/src/image.hpp
index 296a1c876..b389fd176 100644
--- a/components/rgbd-sources/src/image.hpp
+++ b/components/rgbd-sources/src/image.hpp
@@ -14,7 +14,7 @@ class ImageSource : public ftl::rgbd::detail::Source {
 
 	}
 
-	bool grab() { return false; };
+	bool grab(int n, int b) { return false; };
 	bool isReady() { return false; };
 };
 
diff --git a/components/rgbd-sources/src/middlebury_source.cpp b/components/rgbd-sources/src/middlebury_source.cpp
index a1f490e1d..d335f8dbf 100644
--- a/components/rgbd-sources/src/middlebury_source.cpp
+++ b/components/rgbd-sources/src/middlebury_source.cpp
@@ -150,7 +150,7 @@ void MiddleburySource::_performDisparity() {
 	stream_.waitForCompletion();
 }
 
-bool MiddleburySource::grab() {
+bool MiddleburySource::grab(int n, int b) {
 	//_performDisparity();
 	return true;
 }
diff --git a/components/rgbd-sources/src/middlebury_source.hpp b/components/rgbd-sources/src/middlebury_source.hpp
index 10cd47d44..5f0e2be53 100644
--- a/components/rgbd-sources/src/middlebury_source.hpp
+++ b/components/rgbd-sources/src/middlebury_source.hpp
@@ -19,7 +19,7 @@ class MiddleburySource : public detail::Source {
 	MiddleburySource(ftl::rgbd::Source *, const std::string &dir);
 	~MiddleburySource() {};
 
-	bool grab();
+	bool grab(int n, int b);
 	bool isReady() { return ready_; }
 
 	private:
diff --git a/components/rgbd-sources/src/net.cpp b/components/rgbd-sources/src/net.cpp
index 62682d712..7752f8d1f 100644
--- a/components/rgbd-sources/src/net.cpp
+++ b/components/rgbd-sources/src/net.cpp
@@ -56,7 +56,7 @@ bool NetSource::_getCalibration(Universe &net, const UUID &peer, const string &s
 }
 
 NetSource::NetSource(ftl::rgbd::Source *host)
-		: ftl::rgbd::detail::Source(host), active_(false) {
+		: ftl::rgbd::detail::Source(host), active_(false), minB_(9), maxN_(1) {
 
 	gamma_ = host->value("gamma", 1.0f);
 	temperature_ = host->value("temperature", 6500);
@@ -120,9 +120,20 @@ void NetSource::_recvChunk(int frame, int chunk, bool delta, const vector<unsign
 	cv::Mat chunkRGB = rgb_(roi);
 	cv::Mat chunkDepth = depth_(roi);
 
-	tmp_rgb.copyTo(chunkRGB);
-	tmp_depth.convertTo(chunkDepth, CV_32FC1, 1.0f/1000.0f); //(16.0f*10.0f));
-	if (chunk == 0) N_--;
+	// Original size so just copy
+	if (tmp_rgb.cols == chunkRGB.cols) {
+		tmp_rgb.copyTo(chunkRGB);
+		tmp_depth.convertTo(chunkDepth, CV_32FC1, 1.0f/1000.0f); //(16.0f*10.0f));
+	// Downsized so needs a scale up
+	} else {
+		cv::resize(tmp_rgb, chunkRGB, chunkRGB.size());
+		tmp_depth.convertTo(tmp_depth, CV_32FC1, 1.0f/1000.0f);
+		cv::resize(tmp_depth, chunkDepth, chunkDepth.size());
+	}
+
+	if (chunk == 0) {
+		N_--;
+	}
 }
 
 void NetSource::setPose(const Eigen::Matrix4d &pose) {
@@ -162,14 +173,14 @@ void NetSource::_updateURI() {
 			_recvChunk(frame, chunk, delta, jpg, d);
 		});
 
-		N_ = 1;
+		N_ = 0;
 
 		// Initiate stream with request for first 10 frames
-		try {
-			host_->getNet()->send(peer_, "get_stream", *uri, N_, 0, host_->getNet()->id(), *uri);
-		} catch(...) {
-			LOG(ERROR) << "Could not connect to stream " << *uri;
-		}
+		//try {
+		//	host_->getNet()->send(peer_, "get_stream", *uri, N_, 0, host_->getNet()->id(), *uri);
+		//} catch(...) {
+		//	LOG(ERROR) << "Could not connect to stream " << *uri;
+		//}
 
 		// Update chunk details
 		chunks_dim_ = ftl::rgbd::kChunkDim;
@@ -188,13 +199,24 @@ void NetSource::_updateURI() {
 	}
 }
 
-bool NetSource::grab() {
-	// Send one frame before end to prevent unwanted pause
-	if (N_ <= 2) {
-		N_ = 10;
-		if (!host_->getNet()->send(peer_, "get_stream", *host_->get<string>("uri"), N_, 0, host_->getNet()->id(), *host_->get<string>("uri"))) {
+bool NetSource::grab(int n, int b) {
+	// Choose highest requested number of frames
+	maxN_ = std::max(maxN_,(n == -1) ? 10 : n);
+
+	// Choose best requested quality
+	minB_ = std::min(minB_,(b == -1) ? 0 : b);
+
+	// Send k frames before end to prevent unwanted pause
+	// Unless only a single frame is requested
+	if ((N_ <= 2 && maxN_ > 1) || N_ == 0) {
+		N_ = maxN_;
+
+		if (!host_->getNet()->send(peer_, "get_stream", *host_->get<string>("uri"), N_, minB_, host_->getNet()->id(), *host_->get<string>("uri"))) {
 			active_ = false;
 		}
+
+		maxN_ = 1;  // Reset to single frame
+		minB_ = 9;  // Reset to worst quality
 	}
 	return true;
 }
diff --git a/components/rgbd-sources/src/net.hpp b/components/rgbd-sources/src/net.hpp
index 5a5639273..61bbbe7d7 100644
--- a/components/rgbd-sources/src/net.hpp
+++ b/components/rgbd-sources/src/net.hpp
@@ -22,7 +22,7 @@ class NetSource : public detail::Source {
 	explicit NetSource(ftl::rgbd::Source *);
 	~NetSource();
 
-	bool grab();
+	bool grab(int n, int b);
 	bool isReady();
 
 	void setPose(const Eigen::Matrix4d &pose);
@@ -43,6 +43,8 @@ class NetSource : public detail::Source {
 	cv::Mat idepth_;
 	float gamma_;
 	int temperature_;
+	int minB_;
+	int maxN_;
 
 	bool _getCalibration(ftl::net::Universe &net, const ftl::UUID &peer, const std::string &src, ftl::rgbd::Camera &p);
 	void _recv(const std::vector<unsigned char> &jpg, const std::vector<unsigned char> &d);
diff --git a/components/rgbd-sources/src/realsense_source.cpp b/components/rgbd-sources/src/realsense_source.cpp
index e1b66afb1..a4f34c236 100644
--- a/components/rgbd-sources/src/realsense_source.cpp
+++ b/components/rgbd-sources/src/realsense_source.cpp
@@ -38,7 +38,7 @@ RealsenseSource::~RealsenseSource() {
 
 }
 
-bool RealsenseSource::grab() {
+bool RealsenseSource::grab(int n, int b) {
     rs2::frameset frames = pipe_.wait_for_frames();
     //rs2::align align(RS2_STREAM_DEPTH);
     frames = align_to_depth_.process(frames); //align_to_depth_.process(frames);
diff --git a/components/rgbd-sources/src/realsense_source.hpp b/components/rgbd-sources/src/realsense_source.hpp
index 2a48ac2db..2af26bbaf 100644
--- a/components/rgbd-sources/src/realsense_source.hpp
+++ b/components/rgbd-sources/src/realsense_source.hpp
@@ -17,7 +17,7 @@ class RealsenseSource : public ftl::rgbd::detail::Source {
 	RealsenseSource(ftl::rgbd::Source *host);
 	~RealsenseSource();
 
-	bool grab();
+	bool grab(int n=-1, int b=-1);
 	bool isReady();
 
 	private:
diff --git a/components/rgbd-sources/src/snapshot_source.hpp b/components/rgbd-sources/src/snapshot_source.hpp
index 7b5b491fc..38b9d875a 100644
--- a/components/rgbd-sources/src/snapshot_source.hpp
+++ b/components/rgbd-sources/src/snapshot_source.hpp
@@ -17,7 +17,7 @@ class SnapshotSource : public detail::Source {
 	SnapshotSource(ftl::rgbd::Source *, ftl::rgbd::SnapshotReader &reader, const std::string &id);
 	~SnapshotSource() {};
 
-	bool grab() override {};
+	bool grab(int n, int b) override { return true; };
 	bool isReady() { return true; }
 
 	//void reset();
diff --git a/components/rgbd-sources/src/source.cpp b/components/rgbd-sources/src/source.cpp
index b5741520c..1f6c521cc 100644
--- a/components/rgbd-sources/src/source.cpp
+++ b/components/rgbd-sources/src/source.cpp
@@ -196,7 +196,7 @@ const Eigen::Matrix4d &Source::getPose() const {
 }
 
 bool Source::hasCapabilities(capability_t c) {
-	return getCapabilities() & c == c;
+	return (getCapabilities() & c) == c;
 }
 
 capability_t Source::getCapabilities() const {
@@ -212,10 +212,27 @@ void Source::reset() {
 
 bool Source::grab() {
 	UNIQUE_LOCK(mutex_,lk);
-	if (impl_ && impl_->grab()) {
+	if (impl_ && impl_->grab(-1,-1)) {
 		impl_->rgb_.copyTo(rgb_);
 		impl_->depth_.copyTo(depth_);
 		return true;
 	}
 	return false;
 }
+
+bool Source::thumbnail(cv::Mat &t) {
+	// TODO(Nick) periodic refresh
+	if (impl_) {
+		UNIQUE_LOCK(mutex_,lk);
+		impl_->grab(1, 9);
+		impl_->rgb_.copyTo(rgb_);
+		impl_->depth_.copyTo(depth_);
+	}
+	if (!rgb_.empty()) {
+		SHARED_LOCK(mutex_,lk);
+		// Downsize and square the rgb_ image
+		cv::resize(rgb_, thumb_, cv::Size(320,180));
+	}
+	t = thumb_;
+	return !thumb_.empty();
+}
diff --git a/components/rgbd-sources/src/stereovideo.cpp b/components/rgbd-sources/src/stereovideo.cpp
index c0471643a..9442eefde 100644
--- a/components/rgbd-sources/src/stereovideo.cpp
+++ b/components/rgbd-sources/src/stereovideo.cpp
@@ -123,7 +123,7 @@ static void disparityToDepth(const cv::cuda::GpuMat &disparity, cv::cuda::GpuMat
 	cv::cuda::divide(val, disparity, depth, 1.0f / 1000.0f, -1, stream);
 }
 
-bool StereoVideoSource::grab() {
+bool StereoVideoSource::grab(int n, int b) {
 	lsrc_->get(left_, right_, stream_);
 	if (depth_tmp_.empty()) depth_tmp_ = cv::cuda::GpuMat(left_.size(), CV_32FC1);
 	if (disp_tmp_.empty()) disp_tmp_ = cv::cuda::GpuMat(left_.size(), CV_32FC1);
diff --git a/components/rgbd-sources/src/stereovideo.hpp b/components/rgbd-sources/src/stereovideo.hpp
index a5db20fb9..28fe6b90d 100644
--- a/components/rgbd-sources/src/stereovideo.hpp
+++ b/components/rgbd-sources/src/stereovideo.hpp
@@ -26,7 +26,7 @@ class StereoVideoSource : public detail::Source {
 	StereoVideoSource(ftl::rgbd::Source*, const std::string &);
 	~StereoVideoSource();
 
-	bool grab();
+	bool grab(int n, int b);
 	bool isReady();
 
 	//const cv::Mat &getRight() const { return right_; }
diff --git a/components/rgbd-sources/src/streamer.cpp b/components/rgbd-sources/src/streamer.cpp
index e2daa0f0f..3dc57428b 100644
--- a/components/rgbd-sources/src/streamer.cpp
+++ b/components/rgbd-sources/src/streamer.cpp
@@ -5,10 +5,13 @@
 #include <chrono>
 #include <tuple>
 
+#include "bitrate_settings.hpp"
+
 using ftl::rgbd::Streamer;
 using ftl::rgbd::Source;
 using ftl::rgbd::detail::StreamSource;
 using ftl::rgbd::detail::StreamClient;
+using ftl::rgbd::detail::bitrate_settings;
 using ftl::net::Universe;
 using std::string;
 using std::list;
@@ -123,6 +126,7 @@ void Streamer::add(Source *src) {
 		//s->prev_depth = cv::Mat(cv::Size(src->parameters().width, src->parameters().height), CV_16SC1, 0);
 		s->jobs = 0;
 		s->frame = 0;
+		s->clientCount = 0;
 		sources_[src->getID()] = s;
 	}
 
@@ -133,37 +137,38 @@ void Streamer::add(Source *src) {
 void Streamer::_addClient(const string &source, int N, int rate, const ftl::UUID &peer, const string &dest) {
 	StreamSource *s = nullptr;
 
-	//{
+	{
 		UNIQUE_LOCK(mutex_,slk);
 		if (sources_.find(source) == sources_.end()) return;
 
 		if (rate < 0 || rate >= 10) return;
 		if (N < 0 || N > ftl::rgbd::kMaxFrames) return;
 
-		DLOG(INFO) << "Adding Stream Peer: " << peer.to_string();
+		LOG(INFO) << "Adding Stream Peer: " << peer.to_string() << " rate=" << rate << " N=" << N;
 
 		s = sources_[source];
-	//}
+	}
 
-	if (!s) return;
+	if (!s) return;  // No matching stream
 
+	SHARED_LOCK(mutex_, slk);
 	UNIQUE_LOCK(s->mutex, lk2);
-	for (int i=0; i<s->clients[rate].size(); i++) {
-		if (s->clients[rate][i].peerid == peer) {
-			StreamClient &c = s->clients[rate][i];
-			c.txmax = N;
-			c.txcount = 0;
+	for (auto &client : s->clients[rate]) {
+		// If already listening, just update chunk counters
+		if (client.peerid == peer) {
+			client.txmax = N * kChunkCount;
+			client.txcount = 0;
 			return;
 		}
 	}
 
-	StreamClient c;
+	// Not an existing client so add one
+	StreamClient &c = s->clients[rate].emplace_back();
 	c.peerid = peer;
 	c.uri = dest;
 	c.txcount = 0;
-	c.txmax = N;
-
-	s->clients[rate].push_back(c);
+	c.txmax = N * kChunkCount;
+	++s->clientCount;
 }
 
 void Streamer::remove(Source *) {
@@ -176,7 +181,7 @@ void Streamer::remove(const std::string &) {
 
 void Streamer::stop() {
 	active_ = false;
-	//pool_.stop();
+	wait();
 }
 
 void Streamer::poll() {
@@ -222,14 +227,17 @@ void Streamer::_swap(StreamSource *src) {
 	if (src->jobs == 0) {
 		UNIQUE_LOCK(src->mutex,lk);
 
-		auto i = src->clients[0].begin();
-		while (i != src->clients[0].end()) {
-			(*i).txcount++;
-			if ((*i).txcount >= (*i).txmax) {
-				LOG(INFO) << "Remove client: " << (*i).uri;
-				i = src->clients[0].erase(i);
-			} else {
-				i++;
+		for (unsigned int b=0; b<10; ++b) {
+			auto i = src->clients[b].begin();
+			while (i != src->clients[b].end()) {
+				// Client request completed so remove from list
+				if ((*i).txcount >= (*i).txmax) {
+					LOG(INFO) << "Remove client: " << (*i).uri;
+					i = src->clients[b].erase(i);
+					--src->clientCount;
+				} else {
+					i++;
+				}
 			}
 		}
 
@@ -268,7 +276,7 @@ void Streamer::_schedule() {
 		string uri = s.first;
 
 		// No point in doing work if no clients
-		if (s.second->clients[0].size() == 0) {
+		if (s.second->clientCount == 0) {
 			continue;
 		}
 
@@ -283,8 +291,7 @@ void Streamer::_schedule() {
 
 		// Grab job
 		ftl::pool.push([this,src](int id) {
-			//StreamSource *src = sources_[uri];
-			auto start = std::chrono::high_resolution_clock::now();
+			//auto start = std::chrono::high_resolution_clock::now();
 			try {
 				src->src->grab();
 			} catch (std::exception &ex) {
@@ -295,14 +302,11 @@ void Streamer::_schedule() {
 				LOG(ERROR) << "Unknown exception when grabbing frame";
 			}
 
-			std::chrono::duration<double> elapsed =
-				std::chrono::high_resolution_clock::now() - start;
-			LOG(INFO) << "Grab in " << elapsed.count() << "s";
+			//std::chrono::duration<double> elapsed =
+			//	std::chrono::high_resolution_clock::now() - start;
+			//LOG(INFO) << "Grab in " << elapsed.count() << "s";
 
-			// CHECK (Nick) Can state be an atomic instead?
-			//UNIQUE_LOCK(src->mutex, lk);
 			src->jobs--;
-			//src->state |= ftl::rgbd::detail::kGrabbed;
 			_swap(src);
 
 			// Mark job as finished
@@ -311,154 +315,83 @@ void Streamer::_schedule() {
 		});
 
 		// Create jobs for each chunk
-		for (int i=0; i<(kChunkDim*kChunkDim); i++) {
+		for (int i=0; i<kChunkCount; ++i) {
+			// Add chunk job to thread pool
 			ftl::pool.push([this,src](int id, int chunk) {
 				if (!src->rgb.empty() && !src->depth.empty()) {
-					bool delta = (chunk+src->frame) % 8 > 0;
+					bool delta = (chunk+src->frame) % 8 > 0;  // Do XOR or not
 					int chunk_width = src->rgb.cols / kChunkDim;
 					int chunk_height = src->rgb.rows / kChunkDim;
 
-					// Build chunk head
+					// Build chunk heads
 					int cx = (chunk % kChunkDim) * chunk_width;
 					int cy = (chunk / kChunkDim) * chunk_height;
-
 					cv::Rect roi(cx,cy,chunk_width,chunk_height);
 					vector<unsigned char> rgb_buf;
 					cv::Mat chunkRGB = src->rgb(roi);
 					cv::Mat chunkDepth = src->depth(roi);
 					//cv::Mat chunkDepthPrev = src->prev_depth(roi);
 
-					cv::imencode(".jpg", chunkRGB, rgb_buf);
-
 					cv::Mat d2, d3;
 					vector<unsigned char> d_buf;
 					chunkDepth.convertTo(d2, CV_16UC1, 1000); // 16*10);
 					//if (delta) d3 = (d2 * 2) - chunkDepthPrev;
 					//else d3 = d2;
 					//d2.copyTo(chunkDepthPrev);
-					vector<int> pngparams = {cv::IMWRITE_PNG_COMPRESSION, compress_level_}; // Default is 1 for fast, 9 = small but slow.
-					cv::imencode(".png", d2, d_buf, pngparams);
-
-					//LOG(INFO) << "Sending chunk " << chunk << " : size = " << (d_buf.size()+rgb_buf.size()) / 1024 << "kb";
-
-					SHARED_LOCK(src->mutex,lk);
-					auto i = src->clients[0].begin();
-					while (i != src->clients[0].end()) {
-						try {
-							// TODO(Nick) Send pose and timestamp
-							if (!net_->send((*i).peerid, (*i).uri, 0, chunk, delta, rgb_buf, d_buf)) {
-								(*i).txcount = (*i).txmax;
+
+					// For each allowed bitrate setting (0 = max quality)
+					for (unsigned int b=0; b<10; ++b) {
+						{
+							//SHARED_LOCK(src->mutex,lk);
+							if (src->clients[b].size() == 0) continue;
+						}
+						
+						// Max bitrate means no changes
+						if (b == 0) {
+							cv::imencode(".jpg", chunkRGB, rgb_buf);
+							vector<int> pngparams = {cv::IMWRITE_PNG_COMPRESSION, compress_level_}; // Default is 1 for fast, 9 = small but slow.
+							cv::imencode(".png", d2, d_buf, pngparams);
+
+						// Otherwise must downscale and change compression params
+						// TODO(Nick) could reuse downscales
+						} else {
+							cv::Mat downrgb, downdepth;
+							cv::resize(chunkRGB, downrgb, cv::Size(bitrate_settings[b].width / kChunkDim, bitrate_settings[b].height / kChunkDim));
+							cv::resize(d2, downdepth, cv::Size(bitrate_settings[b].width / kChunkDim, bitrate_settings[b].height / kChunkDim));
+							vector<int> jpgparams = {cv::IMWRITE_JPEG_QUALITY, bitrate_settings[b].jpg_quality};
+							cv::imencode(".jpg", downrgb, rgb_buf, jpgparams);
+							vector<int> pngparams = {cv::IMWRITE_PNG_COMPRESSION, bitrate_settings[b].png_compression}; // Default is 1 for fast, 9 = small but slow.
+							cv::imencode(".png", downdepth, d_buf, pngparams);
+						}
+
+						//if (chunk == 0) LOG(INFO) << "Sending chunk " << chunk << " : size = " << (d_buf.size()+rgb_buf.size()) << "bytes";
+
+						// Lock to prevent clients being added / removed
+						SHARED_LOCK(src->mutex,lk);
+						auto c = src->clients[b].begin();
+						while (c != src->clients[b].end()) {
+							try {
+								// TODO(Nick) Send pose and timestamp
+								if (!net_->send((*c).peerid, (*c).uri, 0, chunk, delta, rgb_buf, d_buf)) {
+									// Send failed so mark as client stream completed
+									(*c).txcount = (*c).txmax;
+								} else {
+									++(*c).txcount;
+								}
+							} catch(...) {
+								(*c).txcount = (*c).txmax;
 							}
-						} catch(...) {
-							(*i).txcount = (*i).txmax;
+							++c;
 						}
-						i++;
 					}
 				}
 
-				//src->state |= ftl::rgbd::detail::kRGB;
 				src->jobs--;
 				_swap(src);
 				--jobs_;
 				job_cv_.notify_one();
 			}, i);
 		}
-
-		// Compress colour job
-		/*pool_.push([this,src](int id) {
-			if (!src->rgb.empty()) {
-				auto start = std::chrono::high_resolution_clock::now();
-
-				//vector<unsigned char> src->rgb_buf;
-				cv::imencode(".jpg", src->rgb, src->rgb_buf);
-			}
-
-			src->state |= ftl::rgbd::detail::kRGB;
-			_swap(src);
-			--jobs_;
-			job_cv_.notify_one();
-		});
-
-		// Compress depth job
-		ftl::pool.push([this,src](int id) {
-			auto start = std::chrono::high_resolution_clock::now();
-
-			if (!src->depth.empty()) {
-				cv::Mat d2;
-				src->depth.convertTo(d2, CV_16UC1, 16*100);
-				//vector<unsigned char> d_buf;
-
-				// Setting 1 = fast but large
-				// Setting 9 = small but slow
-				// Anything up to 8 causes minimal if any impact on frame rate
-				// on my (Nicks) laptop, but 9 halves the frame rate.
-				vector<int> pngparams = {cv::IMWRITE_PNG_COMPRESSION, 1}; // Default is 1 for fast, 9 = small but slow.
-				cv::imencode(".png", d2, src->d_buf, pngparams);
-			}
-
-			std::chrono::duration<double> elapsed =
-				std::chrono::high_resolution_clock::now() - start;
-			LOG(INFO) << "Depth Compress in " << elapsed.count() << "s";
-
-			src->state |= ftl::rgbd::detail::kDepth;
-			_swap(src);
-			--jobs_;
-			job_cv_.notify_one();
-		});*/
-
-		// Transmit job
-		// For any single source and bitrate there is only one thread
-		// meaning that no lock is required here since outer shared_lock
-		// prevents addition of new clients.
-		// TODO, could do one for each bitrate...
-		/* pool_.push([this,src](int id) {
-			//StreamSource *src = sources_[uri];
-
-			try {
-			if (src && src->rgb.rows > 0 && src->depth.rows > 0 && src->clients[0].size() > 0) {
-				auto start = std::chrono::high_resolution_clock::now();
-
-				vector<unsigned char> rgb_buf;
-				cv::imencode(".jpg", src->rgb, rgb_buf);
-
-				std::chrono::duration<double> elapsed =
-					std::chrono::high_resolution_clock::now() - start;
-				LOG(INFO) << "JPG in " << elapsed.count() << "s";
-				
-				cv::Mat d2;
-				src->depth.convertTo(d2, CV_16UC1, 16*100);
-				vector<unsigned char> d_buf;
-
-				// Setting 1 = fast but large
-				// Setting 9 = small but slow
-				// Anything up to 8 causes minimal if any impact on frame rate
-				// on my (Nicks) laptop, but 9 halves the frame rate.
-				vector<int> pngparams = {cv::IMWRITE_PNG_COMPRESSION, 1}; // Default is 1 for fast, 9 = small but slow.
-				cv::imencode(".png", d2, d_buf, pngparams);
-
-				//LOG(INFO) << "Data size: " << ((rgb_buf.size() + d_buf.size()) / 1024) << "kb";
-
-				
-			}
-			} catch(...) {
-				LOG(ERROR) << "Error in transmission loop";
-			}
-
-			// CHECK (Nick) Could state be an atomic?
-			//UNIQUE_LOCK(src->mutex,lk);
-			//LOG(INFO) << "Tx Frame: " << uri;
-			src->state |= ftl::rgbd::detail::kTransmitted;
-			_swap(*src);
-			//lk.unlock();
-
-			// Mark job as finished
-			//UNIQUE_LOCK(job_mtx_,ulk);
-			//jobs_--;
-			//ulk.unlock();
-
-			--jobs_;
-			job_cv_.notify_one();
-		});*/
 	}
 }
 
diff --git a/components/rgbd-sources/test/source_unit.cpp b/components/rgbd-sources/test/source_unit.cpp
index 8454c011b..b27b72e07 100644
--- a/components/rgbd-sources/test/source_unit.cpp
+++ b/components/rgbd-sources/test/source_unit.cpp
@@ -26,7 +26,7 @@ class ImageSource : public ftl::rgbd::detail::Source {
 		last_type = "image";
 	}
 
-	bool grab() { return true; };
+	bool grab(int n, int b) { return true; };
 	bool isReady() { return true; };
 };
 
@@ -39,7 +39,7 @@ class StereoVideoSource : public ftl::rgbd::detail::Source {
 		last_type = "video";
 	}
 
-	bool grab() { return true; };
+	bool grab(int n, int b) { return true; };
 	bool isReady() { return true; };
 };
 
@@ -49,7 +49,7 @@ class NetSource : public ftl::rgbd::detail::Source {
 		last_type = "net";
 	}
 
-	bool grab() { return true; };
+	bool grab(int n, int b) { return true; };
 	bool isReady() { return true; };
 };
 
@@ -59,7 +59,7 @@ class SnapshotSource : public ftl::rgbd::detail::Source {
 		last_type = "snapshot";
 	}
 
-	bool grab() { return true; };
+	bool grab(int n, int b) { return true; };
 	bool isReady() { return true; };
 };
 
@@ -69,7 +69,7 @@ class RealsenseSource : public ftl::rgbd::detail::Source {
 		last_type = "realsense";
 	}
 
-	bool grab() { return true; };
+	bool grab(int n, int b) { return true; };
 	bool isReady() { return true; };
 };
 
@@ -79,7 +79,7 @@ class MiddleburySource : public ftl::rgbd::detail::Source {
 		last_type = "middlebury";
 	}
 
-	bool grab() { return true; };
+	bool grab(int n, int b) { return true; };
 	bool isReady() { return true; };
 };
 
-- 
GitLab