diff --git a/.gitignore b/.gitignore
index 23c1a956bc8381a05673482e04ecf2c193fc0980..cf767ae36de979c736483d294608c7d1dadd3b5e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,5 @@ lab-designs/viewer
 .vscode
 doc/
 web-service/npm-debug.log
-web-service/server/npm-debug.log
\ No newline at end of file
+web-service/server/npm-debug.log
+gmon.out
\ No newline at end of file
diff --git a/applications/gui/src/camera.cpp b/applications/gui/src/camera.cpp
index 3924410d4c4c1056634c01fe19efd9123c80ffe2..df514b2ffbf8e04bf5f68bbea77846c333eea03f 100644
--- a/applications/gui/src/camera.cpp
+++ b/applications/gui/src/camera.cpp
@@ -140,13 +140,6 @@ ftl::gui::Camera::Camera(ftl::gui::Screen *screen, int fsid, int fid, ftl::codec
 	sdepth_ = false;
 	ftime_ = (float)glfwGetTime();
 	pause_ = 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);
-	});*/
-	recording_ = false;
 
 #ifdef HAVE_OPENVR
 	vr_mode_ = false;
@@ -425,29 +418,6 @@ void ftl::gui::Camera::setChannel(Channel c) {
 #endif
 
 	channel_ = c;
-	/*switch (c) {
-	case Channel::Energy:
-	case Channel::Density:
-	case Channel::Flow:
-	case Channel::Confidence:
-	case Channel::Normals:
-	case Channel::ColourNormals:
-	case Channel::Right:
-		src_->setChannel(c);
-		break;
-
-	case Channel::Deviation:
-		if (stats_) { stats_->reset(); }
-		src_->setChannel(Channel::Depth);
-		break;
-	
-	case Channel::Depth:
-		src_->setChannel(c);
-		break;
-	
-	default: src_->setChannel(Channel::None);
-	}*/
-	// FIXME: Somehow send channel request...
 }
 
 static void visualizeDepthMap(	const cv::Mat &depth, cv::Mat &out,
diff --git a/applications/gui/src/camera.hpp b/applications/gui/src/camera.hpp
index 9146d3cce1f7384c503f6cda69390fdd40570001..adc346055891a6a489aed0559113c1727e2f577c 100644
--- a/applications/gui/src/camera.hpp
+++ b/applications/gui/src/camera.hpp
@@ -93,7 +93,6 @@ class Camera {
 	Screen *screen_;
 	int fsid_;
 	int fid_;
-	// TODO: Renderer
 
 	int width_;
 	int height_;
@@ -118,12 +117,6 @@ class Camera {
 	cv::Mat im1_; // first channel (left)
 	cv::Mat im2_; // second channel ("right")
 
-	// FIXME: Recording will now be broken?
-	bool recording_;
-	//std::ofstream *fileout_;
-	//ftl::codecs::Writer *writer_;
-	//ftl::rgbd::RawCallback recorder_;
-
 	ftl::render::Triangular *renderer_;
 	ftl::rgbd::Frame frame_;
 	ftl::rgbd::FrameState state_;
diff --git a/applications/gui/src/record_window.cpp b/applications/gui/src/record_window.cpp
index f74fd61aa65b392fc07e361a94dbead1bd7cd183..e358206fe727a97432044c95a8be75d2663b049f 100644
--- a/applications/gui/src/record_window.cpp
+++ b/applications/gui/src/record_window.cpp
@@ -38,16 +38,11 @@ RecordWindow::RecordWindow(nanogui::Widget *parent, ftl::gui::Screen *screen, co
         if (s == screen->activeCamera()) {
             ix = std::optional<int>(streamNames.size());
         }
-        // FIXME: Find alternative to source URI
-        //streamNames.push_back(s->source()->getURI());
+
 		streamNames.push_back(std::string("Stream")+std::to_string(i++));
     }
     auto streamSelect = new ComboBox(this, streamNames);
-    // TODO: The function availableChannels() only finds those channels that
-    // have been set in camera.cpp. The only channels that are set in
-    // camera.cpp currently are Colour and Depth. This means that currently,
-    // the list of channels returned by availableChannels() is not accurate
-    // and should be fixed.
+
     TabWidget *tabWidget = add<TabWidget>();
     tabWidget->setFixedWidth(400);
     auto snapshot2D = tabWidget->createTab("2D snapshot");
diff --git a/applications/gui/src/screen.cpp b/applications/gui/src/screen.cpp
index 787d209919ef339eb71e46bda70a371d461eec8a..bb7ebfa998817ab3b7beef63abcfbdb46a279ed6 100644
--- a/applications/gui/src/screen.cpp
+++ b/applications/gui/src/screen.cpp
@@ -363,7 +363,6 @@ void ftl::gui::Screen::setActiveCamera(ftl::gui::Camera *cam) {
 	camera_ = cam;
 
 	if (cam) {
-		// FIXME: Alternative to source URI
 		status_ = cam->name();
 		mwindow_->setVisible(true);
 		mwindow_->cameraChanged();
@@ -397,7 +396,7 @@ bool ftl::gui::Screen::mouseMotionEvent(const Eigen::Vector2i &p, const Eigen::V
 			}
 		}
 	}
-	return true; // TODO: return statement was missing; is true correct?
+	return true;
 }
 
 bool ftl::gui::Screen::mouseButtonEvent(const nanogui::Vector2i &p, int button, bool down, int modifiers) {
diff --git a/applications/gui/src/thumbview.cpp b/applications/gui/src/thumbview.cpp
index 6c567e516482785dbaa0d7d69aa08bef3ed1a8e4..5fdbd4ec4754c8a8042397c3e02a5835171dfe3c 100644
--- a/applications/gui/src/thumbview.cpp
+++ b/applications/gui/src/thumbview.cpp
@@ -18,9 +18,10 @@ ThumbView::~ThumbView() {
 bool ThumbView::mouseButtonEvent(const nanogui::Vector2i &p, int button, bool down, int modifiers) {
 	if (button == 0 && !down) {
 		screen_->setActiveCamera(cam_);
+		return true;
+	} else {
+		return false;
 	}
-
-	return false; // TODO: return statement was missing; is false correct?
 }
 
 void ThumbView::draw(NVGcontext *ctx) {
diff --git a/applications/reconstruct/src/main.cpp b/applications/reconstruct/src/main.cpp
index 8108a6b5d74a9b7effcd75df4f6651d1249102e4..9eabd9e0a6eae0deca2ccb95f604d2b1ad58309d 100644
--- a/applications/reconstruct/src/main.cpp
+++ b/applications/reconstruct/src/main.cpp
@@ -11,7 +11,6 @@
 #include <ftl/depth_camera.hpp>
 #include <ftl/rgbd.hpp>
 #include <ftl/rgbd/virtual.hpp>
-#include <ftl/rgbd/streamer.hpp>
 #include <ftl/master.hpp>
 #include <ftl/rgbd/group.hpp>
 #include <ftl/threads.hpp>
@@ -73,16 +72,6 @@ using std::chrono::milliseconds;
 using ftl::registration::loadTransformations;
 using ftl::registration::saveTransformations;
 
-static Eigen::Affine3d create_rotation_matrix(float ax, float ay, float az) {
-	Eigen::Affine3d rx =
-		Eigen::Affine3d(Eigen::AngleAxisd(ax, Eigen::Vector3d(1, 0, 0)));
-	Eigen::Affine3d ry =
-		Eigen::Affine3d(Eigen::AngleAxisd(ay, Eigen::Vector3d(0, 1, 0)));
-	Eigen::Affine3d rz =
-		Eigen::Affine3d(Eigen::AngleAxisd(az, Eigen::Vector3d(0, 0, 1)));
-	return rz * rx * ry;
-}
-
 /* Build a generator using a deprecated list of source objects. */
 static ftl::rgbd::Generator *createSourceGenerator(const std::vector<ftl::rgbd::Source*> &srcs) {
 	// Must find pose for each source...
@@ -217,6 +206,12 @@ static void run(ftl::Configurable *root) {
 			ftl::stream::Receiver *gen = ftl::create<ftl::stream::Receiver>(root, "receiver");
 			gen->setStream(stream);
 
+			sender->onRequest([stream](const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
+				if (spkt.channel == Channel::Colour) {
+					stream->reset();
+				}
+			});
+
 			int count = 0;
 			for (auto &s : stream_uris) {
 				LOG(INFO) << " --- found stream: " << s;
diff --git a/components/codecs/include/ftl/codecs/bitrates.hpp b/components/codecs/include/ftl/codecs/bitrates.hpp
deleted file mode 100644
index c1b9617a34d1b9dd307008176c47a529a53d567b..0000000000000000000000000000000000000000
--- a/components/codecs/include/ftl/codecs/bitrates.hpp
+++ /dev/null
@@ -1,144 +0,0 @@
-#ifndef _FTL_CODECS_BITRATES_HPP_
-#define _FTL_CODECS_BITRATES_HPP_
-
-#include <cstdint>
-#include <msgpack.hpp>
-
-namespace ftl {
-namespace codecs {
-
-/**
- * Compression format used.
- */
-enum struct codec_t : uint8_t {
-	JPG = 0,
-	PNG,
-	H264,
-	HEVC,  // H265
-	H264_LOSSLESS,
-	HEVC_LOSSLESS,
-
-	// TODO: Add audio codecs
-	WAV,
-
-	JSON = 100,		// A JSON string
-	CALIBRATION,	// Camera parameters object
-	POSE,			// 4x4 eigen matrix
-	MSGPACK,
-	STRING,			// Null terminated string
-	RAW,				// Some unknown binary format
-
-	Any = 255
-};
-
-/**
- * Resolution of encoding.
- */
-enum struct definition_t : uint8_t {
-	UHD8k = 0,
-	UHD4k = 1,
-	HD1080 = 2,
-	HD720 = 3,
-	SD576 = 4,
-	SD480 = 5,
-	LD360 = 6,
-	Any = 7,
-
-	HTC_VIVE = 8,
-
-	// TODO: Add audio definitions
-
-	Invalid
-};
-
-definition_t findDefinition(int width, int height);
-
-/**
- * Get width in pixels of definition.
- */
-int getWidth(definition_t);
-
-/**
- * Get height in pixels of definition.
- */
-int getHeight(definition_t);
-
-/**
- * General indication of desired quality. Exact bitrate numbers depends also
- * upon chosen definition and codec.
- */
-enum struct bitrate_t {
-	High,
-	Standard,
-	Low
-};
-
-/**
- * Pre-specified definition and quality levels for encoding where kPreset0 is
- * the best quality and kPreset9 is the worst. The use of presets is useful for
- * adaptive bitrate scenarios where the numbers are increased or decreased.
- */
-typedef int8_t preset_t;
-static const preset_t kPreset0 = 0;
-static const preset_t kPreset1 = 1;
-static const preset_t kPreset2 = 2;
-static const preset_t kPreset3 = 3;
-static const preset_t kPreset4 = 4;
-static const preset_t kPreset5 = 5;
-static const preset_t kPreset6 = 6;
-static const preset_t kPreset7 = 7;
-static const preset_t kPreset8 = 8;
-static const preset_t kPreset9 = 9;
-static const preset_t kPresetBest = 0;
-static const preset_t kPresetWorst = 9;
-static const preset_t kPresetLQThreshold = 4;
-
-static const preset_t kPresetHTCVive = -1;
-
-static const preset_t kPresetMinimum = -1;
-
-/**
- * Represents the details of each preset codec configuration.
- */
-struct CodecPreset {
-	definition_t res;
-	bitrate_t qual;
-};
-
-/**
- * Get preset details structure from preset number.
- */
-const CodecPreset &getPreset(preset_t);
-
-/**
- * Get preset based upon a requested definition. If multiple presets match then
- * the highest quality one is returned.
- */
-const CodecPreset &getPreset(definition_t, definition_t);
-
-/**
- * Get preset based upon a requested definition for colour channel.
- * If multiple presets match then the highest quality one is returned.
- */
-const CodecPreset &getPreset(definition_t);
-
-/**
- * Get the preset id nearest to requested definition for colour and depth.
- */
-preset_t findPreset(definition_t, definition_t);
-
-/**
- * Get the preset id nearest to requested colour definition. If multiple presets
- * match then return highest quality.
- */
-preset_t findPreset(definition_t);
-
-preset_t findPreset(size_t width, size_t height);
-
-}
-}
-
-MSGPACK_ADD_ENUM(ftl::codecs::codec_t);
-MSGPACK_ADD_ENUM(ftl::codecs::definition_t);
-
-#endif  // _FTL_CODECS_BITRATES_HPP_
diff --git a/components/codecs/include/ftl/codecs/codecs.hpp b/components/codecs/include/ftl/codecs/codecs.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4f643d406fa799822e7909ac42a971f0433675b0
--- /dev/null
+++ b/components/codecs/include/ftl/codecs/codecs.hpp
@@ -0,0 +1,98 @@
+#ifndef _FTL_CODECS_BITRATES_HPP_
+#define _FTL_CODECS_BITRATES_HPP_
+
+#include <cstdint>
+#include <msgpack.hpp>
+
+namespace ftl {
+namespace codecs {
+
+enum struct format_t {
+	BGRA8,
+	RGBA8,
+	VUYA16,
+	F32,
+	U16
+};
+
+static constexpr uint8_t kFlagFlipRGB = 0x01;		// Swap blue and red channels [deprecated]
+static constexpr uint8_t kFlagMappedDepth = 0x02;	// Use Yuv mapping for float [deprecated]
+static constexpr uint8_t kFlagFloat = 0x04;			// Floating point output
+static constexpr uint8_t kFlagMultiple = 0x80;		// Multiple video frames in single packet
+
+/**
+ * Compression format used.
+ */
+enum struct codec_t : uint8_t {
+	JPG = 0,
+	PNG,
+	H264,
+	HEVC,  // H265
+	H264_LOSSLESS,
+	HEVC_LOSSLESS,
+
+	// TODO: Add audio codecs
+	WAV,
+
+	JSON = 100,		// A JSON string
+	CALIBRATION,	// Camera parameters object
+	POSE,			// 4x4 eigen matrix
+	MSGPACK,
+	STRING,			// Null terminated string
+	RAW,				// Some unknown binary format
+
+	Invalid = 254,
+	Any = 255
+};
+
+/**
+ * Resolution of encoding.
+ */
+enum struct definition_t : uint8_t {
+	UHD8k = 0,
+	UHD4k = 1,
+	HD1080 = 2,
+	HD720 = 3,
+	SD576 = 4,
+	SD480 = 5,
+	LD360 = 6,
+	Any = 7,
+
+	HTC_VIVE = 8,
+
+	// TODO: Add audio definitions
+
+	Invalid
+};
+
+definition_t findDefinition(int width, int height);
+
+/**
+ * Get width in pixels of definition.
+ */
+int getWidth(definition_t);
+
+/**
+ * Get height in pixels of definition.
+ */
+int getHeight(definition_t);
+
+/**
+ * General indication of desired quality. Exact bitrate numbers depends also
+ * upon chosen definition and codec.
+ */
+enum struct bitrate_t {
+	High,
+	Standard,
+	Low
+};
+
+std::pair<int,int> chooseTileConfig(int size);
+
+}
+}
+
+MSGPACK_ADD_ENUM(ftl::codecs::codec_t);
+MSGPACK_ADD_ENUM(ftl::codecs::definition_t);
+
+#endif  // _FTL_CODECS_BITRATES_HPP_
diff --git a/components/codecs/include/ftl/codecs/decoder.hpp b/components/codecs/include/ftl/codecs/decoder.hpp
index 7684990e204a1231af0cd8c114eabbf4ef9290d5..25dcc6a2334539cff28bfe241e705dbe3ba6cefb 100644
--- a/components/codecs/include/ftl/codecs/decoder.hpp
+++ b/components/codecs/include/ftl/codecs/decoder.hpp
@@ -38,6 +38,11 @@ class Decoder {
 	virtual bool decode(const ftl::codecs::Packet &pkt, cv::cuda::GpuMat &out)=0;
 
 	virtual bool accepts(const ftl::codecs::Packet &)=0;
+
+	cv::cuda::Stream &stream() { return stream_; }
+
+	protected:
+	cv::cuda::Stream stream_;
 };
 
 }
diff --git a/components/codecs/src/depth_convert_cuda.hpp b/components/codecs/include/ftl/codecs/depth_convert_cuda.hpp
similarity index 100%
rename from components/codecs/src/depth_convert_cuda.hpp
rename to components/codecs/include/ftl/codecs/depth_convert_cuda.hpp
diff --git a/components/codecs/include/ftl/codecs/encoder.hpp b/components/codecs/include/ftl/codecs/encoder.hpp
index ae2d69f9d1fa549bcc85d456d67fe1c5930704dc..54b334a517853230fd4d0aad44b342d9662aec8f 100644
--- a/components/codecs/include/ftl/codecs/encoder.hpp
+++ b/components/codecs/include/ftl/codecs/encoder.hpp
@@ -5,14 +5,13 @@
 #include <opencv2/opencv.hpp>
 #include <opencv2/core/cuda.hpp>
 
-#include <ftl/codecs/bitrates.hpp>
+#include <ftl/codecs/codecs.hpp>
 #include <ftl/codecs/packet.hpp>
 
 namespace ftl {
 namespace codecs {
 
 static const unsigned int kVideoBufferSize = 10*1024*1024;
-static constexpr uint8_t kFlagMultiple = 0x80;
 
 class Encoder;
 
@@ -59,42 +58,31 @@ class Encoder {
 	virtual ~Encoder();
 
 	/**
-	 * Wrapper encode to allow use of presets.
-	 */
-	virtual bool encode(const cv::cuda::GpuMat &in, ftl::codecs::preset_t preset,
-			const std::function<void(const ftl::codecs::Packet&)> &cb);
-
-	/**
-	 * Encode a frame at specified preset and call a callback function for each
-	 * block as it is encoded. The callback may be called only once with the
-	 * entire frame or it may be called many times from many threads with
-	 * partial blocks of a frame. Each packet should be sent over the network
-	 * to all listening clients.
+	 * Encode a frame given as an opencv gpumat. The packet structure should
+	 * be filled before calling this function as the codec, definition and
+	 * bitrate given in the packet are used as suggestions for the encoder. The
+	 * encoder will modify the fields within the packet and will populate the
+	 * data element.
 	 * 
 	 * @param in An OpenCV image of the frame to compress
-	 * @param preset Codec preset for resolution and quality
-	 * @param iframe Full frame if true, else might be a delta frame
-	 * @param cb Callback containing compressed data
+	 * @param fmt Colour format used.
+	 * @param pkt Partially filled packet to be encoded into.
 	 * @return True if succeeded with encoding.
 	 */
-	virtual bool encode(
-			const cv::cuda::GpuMat &in,
-			ftl::codecs::definition_t definition,
-			ftl::codecs::bitrate_t bitrate,
-			const std::function<void(const ftl::codecs::Packet&)> &cb)=0;
-
-	// TODO: Eventually, use GPU memory directly since some encoders can support this
-	//virtual bool encode(const cv::cuda::GpuMat &in, std::vector<uint8_t> &out, bitrate_t bix, bool)=0;
+	virtual bool encode(const cv::cuda::GpuMat &in, ftl::codecs::Packet &pkt)=0;
 
 	virtual void reset() {}
 
 	virtual bool supports(ftl::codecs::codec_t codec)=0;
 
+	cv::cuda::Stream &stream() { return stream_; }
+
 	protected:
 	bool available;
 	const ftl::codecs::definition_t max_definition;
 	const ftl::codecs::definition_t min_definition;
 	const ftl::codecs::device_t device;
+	cv::cuda::Stream stream_;
 };
 
 }
diff --git a/components/codecs/include/ftl/codecs/nvpipe_decoder.hpp b/components/codecs/include/ftl/codecs/nvpipe_decoder.hpp
index 82a61feaed8a34d7d00d1fa00d127a8c0c2361f4..990865b6035bd9798642acf07017ddf417f274e8 100644
--- a/components/codecs/include/ftl/codecs/nvpipe_decoder.hpp
+++ b/components/codecs/include/ftl/codecs/nvpipe_decoder.hpp
@@ -26,7 +26,6 @@ class NvPipeDecoder : public ftl::codecs::Decoder {
 	MUTEX mutex_;
 	bool seen_iframe_;
 	cv::cuda::GpuMat tmp_;
-	cv::cuda::Stream stream_;
 
 	bool _checkIFrame(ftl::codecs::codec_t codec, const unsigned char *data, size_t size);
 };
diff --git a/components/codecs/include/ftl/codecs/nvpipe_encoder.hpp b/components/codecs/include/ftl/codecs/nvpipe_encoder.hpp
index ff0f3750b0e938650141c91befdf6e7452280613..607d8d40f134ec044e5cdf2c335bf4050fa51373 100644
--- a/components/codecs/include/ftl/codecs/nvpipe_encoder.hpp
+++ b/components/codecs/include/ftl/codecs/nvpipe_encoder.hpp
@@ -13,13 +13,7 @@ class NvPipeEncoder : public ftl::codecs::Encoder {
 			ftl::codecs::definition_t mindef);
 	~NvPipeEncoder();
 
-	bool encode(const cv::cuda::GpuMat &in, ftl::codecs::preset_t preset,
-			const std::function<void(const ftl::codecs::Packet&)> &cb) {
-		return Encoder::encode(in, preset, cb);
-	}
-
-	bool encode(const cv::cuda::GpuMat &in, ftl::codecs::definition_t definition, ftl::codecs::bitrate_t bitrate,
-			const std::function<void(const ftl::codecs::Packet&)>&) override;
+	bool encode(const cv::cuda::GpuMat &in, ftl::codecs::Packet &pkt) override;
 
 	//bool encode(const cv::cuda::GpuMat &in, std::vector<uint8_t> &out, bitrate_t bix, bool);
 
@@ -32,17 +26,17 @@ class NvPipeEncoder : public ftl::codecs::Encoder {
 
 	private:
 	NvPipe *nvenc_;
-	definition_t current_definition_;
-	bool is_float_channel_;
+	NvPipe_Codec codec_;
+	NvPipe_Format format_;
+	NvPipe_Compression compression_;
+	uint8_t last_bitrate_;
+
 	bool was_reset_;
-	ftl::codecs::codec_t preference_;
-	ftl::codecs::codec_t current_codec_;
 	cv::cuda::GpuMat tmp_;
 	cv::cuda::GpuMat tmp2_;
-	cv::cuda::Stream stream_;
 
-	bool _encoderMatch(const cv::cuda::GpuMat &in, definition_t def);
-	bool _createEncoder(const cv::cuda::GpuMat &in, definition_t def, bitrate_t rate);
+	bool _encoderMatch(const ftl::codecs::Packet &pkt, format_t fmt);
+	bool _createEncoder(const ftl::codecs::Packet &pkt, format_t fmt);
 	ftl::codecs::definition_t _verifiedDefinition(ftl::codecs::definition_t def, const cv::cuda::GpuMat &in);
 };
 
diff --git a/components/codecs/include/ftl/codecs/opencv_encoder.hpp b/components/codecs/include/ftl/codecs/opencv_encoder.hpp
index 82b0a5cccf32398ed6e94d9a7a84d0c0ee1b9f25..d500e3c5e64bb8d65f4fcd30d69d838bf8626819 100644
--- a/components/codecs/include/ftl/codecs/opencv_encoder.hpp
+++ b/components/codecs/include/ftl/codecs/opencv_encoder.hpp
@@ -20,13 +20,7 @@ class OpenCVEncoder : public ftl::codecs::Encoder {
 			ftl::codecs::definition_t mindef);
     ~OpenCVEncoder();
 
-	bool encode(const cv::cuda::GpuMat &in, ftl::codecs::preset_t preset,
-			const std::function<void(const ftl::codecs::Packet&)> &cb) {
-		return Encoder::encode(in, preset, cb);
-	}
-
-    bool encode(const cv::cuda::GpuMat &in, ftl::codecs::definition_t definition, ftl::codecs::bitrate_t bitrate,
-			const std::function<void(const ftl::codecs::Packet&)>&) override;
+	bool encode(const cv::cuda::GpuMat &in, ftl::codecs::Packet &pkt) override;
 
 	bool supports(ftl::codecs::codec_t codec) override;
 
@@ -35,14 +29,13 @@ class OpenCVEncoder : public ftl::codecs::Encoder {
 	private:
 	int chunk_count_;
 	int chunk_dim_;
-	ftl::codecs::preset_t current_preset_;
 	ftl::codecs::definition_t current_definition_;
 	std::atomic<int> jobs_;
 	std::mutex job_mtx_;
 	std::condition_variable job_cv_;
 	cv::Mat tmp_;
 
-	bool _encodeBlock(const cv::Mat &in, ftl::codecs::Packet &pkt, ftl::codecs::bitrate_t);
+	bool _encodeBlock(const cv::Mat &in, ftl::codecs::Packet &pkt);
 };
 
 }
diff --git a/components/codecs/include/ftl/codecs/packet.hpp b/components/codecs/include/ftl/codecs/packet.hpp
index 0788653c1b95781d56ba5fd419b6f8eacb7948f5..f4089188bef581af9969dab48700633fc6bb0b3d 100644
--- a/components/codecs/include/ftl/codecs/packet.hpp
+++ b/components/codecs/include/ftl/codecs/packet.hpp
@@ -3,7 +3,7 @@
 
 #include <cstdint>
 #include <vector>
-#include <ftl/codecs/bitrates.hpp>
+#include <ftl/codecs/codecs.hpp>
 #include <ftl/codecs/channels.hpp>
 
 #include <msgpack.hpp>
diff --git a/components/codecs/src/bitrates.cpp b/components/codecs/src/bitrates.cpp
index fc101f6bbd5733389db04fc75369b5512c4a3e8e..5781c4f5f80c3a6452214f3d7a7c86608893c9bc 100644
--- a/components/codecs/src/bitrates.cpp
+++ b/components/codecs/src/bitrates.cpp
@@ -1,27 +1,11 @@
 
-#include <ftl/codecs/bitrates.hpp>
+#include <ftl/codecs/codecs.hpp>
 #include <cmath>
 
-using ftl::codecs::CodecPreset;
-using ftl::codecs::bitrate_t;
-using ftl::codecs::preset_t;
 using ftl::codecs::definition_t;
 using ftl::codecs::codec_t;
 
 
-static const CodecPreset special_presets[] = {
-	definition_t::HTC_VIVE, bitrate_t::High
-};
-
-static const CodecPreset presets[] = {
-	definition_t::HD1080, bitrate_t::High,
-	definition_t::HD720, bitrate_t::High,
-	definition_t::SD576, bitrate_t::High,
-	definition_t::SD480, bitrate_t::High,
-	definition_t::LD360, bitrate_t::Standard,
-	definition_t::LD360, bitrate_t::Low
-};
-
 static const float kAspectRatio = 1.777778f;
 
 struct Resolution {
@@ -60,43 +44,6 @@ definition_t ftl::codecs::findDefinition(int width, int height) {
 		best++;
 	}
 
-	// TODO error!
-	return definition_t::Any;
+	return definition_t::Invalid;
 }
 
-/*
-const CodecPreset &ftl::codecs::getPreset(preset_t p) {
-	if (p < 0 && p >= -1) return special_presets[std::abs(p+1)];
-	if (p > kPresetWorst) return presets[kPresetWorst];
-	if (p < kPresetBest) return presets[kPresetBest];
-	return presets[p];
-}
-
-preset_t ftl::codecs::findPreset(size_t width, size_t height) {
-	int min_error = std::numeric_limits<int>::max();
-
-	// Find best definition
-	int best_def = (int)definition_t::Invalid;
-
-	for (int i=0; i<(int)definition_t::Invalid; ++i) {
-		int dw = resolutions[i].width - width;
-		int dh = resolutions[i].height - height;
-		int error = dw*dw + dh*dh;
-		if (error < min_error) {
-			min_error = error;
-			best_def = i;
-		}
-	}
-
-	// Find preset that matches this best definition
-	for (preset_t i=kPresetMinimum; i<=kPresetWorst; ++i) {
-		const auto &preset = getPreset(i);
-
-		if ((int)preset.res == best_def) {
-			return i;
-		}
-	}
-
-	return kPresetWorst;
-}
-*/
diff --git a/components/codecs/src/depth_convert.cu b/components/codecs/src/depth_convert.cu
index aeb814d5013478b792e07b6f5eb332ba218a1ea7..94fa5e4beb4ba300624630073aa37ffc7b1dae2e 100644
--- a/components/codecs/src/depth_convert.cu
+++ b/components/codecs/src/depth_convert.cu
@@ -1,4 +1,4 @@
-#include "depth_convert_cuda.hpp"
+#include <ftl/codecs/depth_convert_cuda.hpp>
 
 #include <opencv2/core/cuda_stream_accessor.hpp>
 
diff --git a/components/codecs/src/encoder.cpp b/components/codecs/src/encoder.cpp
index 7c7f9a35848441597cd4650fc7d8eaf87bdd01e4..4ef9ade5a0fb5a7ec32b9642638809da8570625c 100644
--- a/components/codecs/src/encoder.cpp
+++ b/components/codecs/src/encoder.cpp
@@ -7,14 +7,9 @@
 #include <loguru.hpp>
 
 using ftl::codecs::Encoder;
-using ftl::codecs::bitrate_t;
 using ftl::codecs::definition_t;
-using ftl::codecs::preset_t;
 using ftl::codecs::device_t;
 using ftl::codecs::codec_t;
-using ftl::codecs::kPresetBest;
-using ftl::codecs::kPresetWorst;
-using ftl::codecs::kPresetLQThreshold;
 
 namespace ftl {
 namespace codecs {
@@ -70,10 +65,17 @@ Encoder::~Encoder() {
 
 }
 
-bool Encoder::encode(const cv::cuda::GpuMat &in, preset_t preset,
-			const std::function<void(const ftl::codecs::Packet&)> &cb) {
-	const definition_t definition = ftl::codecs::findDefinition(in.size().width, in.size().height);
-	const bitrate_t bitrate = bitrate_t::High;
-
-	return encode(in, definition, bitrate, cb);
+std::pair<int,int> ftl::codecs::chooseTileConfig(int size) {
+	switch (size) {
+	case 1:		return {1,1};
+	case 2:		return {2,1};
+	case 3:		return {2,2};
+	case 4:		return {2,2};
+	case 5:		return {3,2};
+	case 6:		return {3,2};
+	case 7:		return {4,2};
+	case 8:		return {4,2};
+	case 9:		return {3,3};
+	}
+	return {3,3};
 }
diff --git a/components/codecs/src/nvpipe_decoder.cpp b/components/codecs/src/nvpipe_decoder.cpp
index 605586990f7dc048232e2539c104b304a311decf..339bb665bfbdb1b9be80f6cc8c1d1732c38b63c0 100644
--- a/components/codecs/src/nvpipe_decoder.cpp
+++ b/components/codecs/src/nvpipe_decoder.cpp
@@ -10,7 +10,7 @@
 
 #include <opencv2/core/cuda/common.hpp>
 
-#include "depth_convert_cuda.hpp"
+#include <ftl/codecs/depth_convert_cuda.hpp>
 
 using ftl::codecs::NvPipeDecoder;
 
@@ -47,7 +47,34 @@ bool NvPipeDecoder::decode(const ftl::codecs::Packet &pkt, cv::cuda::GpuMat &out
 	cudaSetDevice(0);
 	UNIQUE_LOCK(mutex_,lk);
 	if (pkt.codec != codec_t::HEVC && pkt.codec != codec_t::H264 && pkt.codec != codec_t::HEVC_LOSSLESS && pkt.codec != codec_t::H264_LOSSLESS) return false;
-	bool is_float_frame = out.type() == CV_32F;
+
+	bool is_float_frame = pkt.flags & ftl::codecs::kFlagFloat;
+	bool islossless = ((pkt.codec == ftl::codecs::codec_t::HEVC || pkt.codec == ftl::codecs::codec_t::H264) && is_float_frame &&
+		!(pkt.flags & 0x2)) || pkt.codec == ftl::codecs::codec_t::HEVC_LOSSLESS || pkt.codec == ftl::codecs::codec_t::H264_LOSSLESS; 
+
+	if (is_float_frame && !islossless && out.type() != CV_16UC4) {
+		LOG(ERROR) << "Invalid buffer for lossy float frame";
+		return false;
+	}
+
+	if (is_float_frame && islossless && out.type() != CV_16U) {
+		LOG(ERROR) << "Invalid buffer for lossless float frame";
+		return false;
+	}
+
+	if (!is_float_frame && out.type() != CV_8UC4) {
+		LOG(ERROR) << "Invalid buffer for lossy colour frame: " << out.type();
+		return false;
+	}
+
+	int width = ftl::codecs::getWidth(pkt.definition);
+	int height = ftl::codecs::getHeight(pkt.definition);
+	auto [tx,ty] = ftl::codecs::chooseTileConfig(pkt.frame_count);
+
+	if (tx*width != out.cols || ty*height != out.rows) {
+		LOG(ERROR) << "Received frame too large for output";
+		return false;
+	}
 
 	// Is the previous decoder still valid for current resolution and type?
 	if (nv_decoder_ != nullptr && (last_definition_ != pkt.definition || last_codec_ != pkt.codec || is_float_channel_ != is_float_frame)) {
@@ -59,15 +86,13 @@ bool NvPipeDecoder::decode(const ftl::codecs::Packet &pkt, cv::cuda::GpuMat &out
 	last_definition_ = pkt.definition;
 	last_codec_ = pkt.codec;
 
-	bool islossless = ((pkt.codec == ftl::codecs::codec_t::HEVC || pkt.codec == ftl::codecs::codec_t::H264) && is_float_frame && !(pkt.flags & 0x2)) || pkt.codec == ftl::codecs::codec_t::HEVC_LOSSLESS || pkt.codec == ftl::codecs::codec_t::H264_LOSSLESS; 
-
 	// Build a decoder instance of the correct kind
 	if (nv_decoder_ == nullptr) {
 		nv_decoder_ = NvPipe_CreateDecoder(
 				(is_float_frame) ? (islossless) ? NVPIPE_UINT16 : NVPIPE_YUV64 : NVPIPE_RGBA32,
 				(pkt.codec == codec_t::HEVC || pkt.codec == ftl::codecs::codec_t::HEVC_LOSSLESS) ? NVPIPE_HEVC : NVPIPE_H264,
-				ftl::codecs::getWidth(pkt.definition),
-				ftl::codecs::getHeight(pkt.definition));
+				out.cols,
+				out.rows);
 		if (!nv_decoder_) {
 			//LOG(INFO) << "Bitrate=" << (int)bitrate << " width=" << ABRController::getColourWidth(bitrate);
 			LOG(FATAL) << "Could not create decoder: " << NvPipe_GetError(NULL);
@@ -76,10 +101,10 @@ bool NvPipeDecoder::decode(const ftl::codecs::Packet &pkt, cv::cuda::GpuMat &out
 		seen_iframe_ = false;
 	}
 	
-	tmp_.create(cv::Size(ftl::codecs::getWidth(pkt.definition),ftl::codecs::getHeight(pkt.definition)), (!is_float_frame) ? CV_8UC4 : (islossless) ? CV_16U : CV_16UC4);
+	//tmp_.create(cv::Size(ftl::codecs::getWidth(pkt.definition),ftl::codecs::getHeight(pkt.definition)), (!is_float_frame) ? CV_8UC4 : (islossless) ? CV_16U : CV_16UC4);
 
 	// Final checks for validity
-	if (pkt.data.size() == 0 || tmp_.size() != out.size()) { // || !ftl::codecs::hevc::validNAL(pkt.data)) {
+	if (pkt.data.size() == 0) { // || !ftl::codecs::hevc::validNAL(pkt.data)) {
 		LOG(ERROR) << "Failed to decode packet";
 		return false;
 	}
@@ -100,7 +125,7 @@ bool NvPipeDecoder::decode(const ftl::codecs::Packet &pkt, cv::cuda::GpuMat &out
 				else return false;
 			}
 
-			rc = NvPipe_Decode(nv_decoder_, ptr, size, tmp_.data, tmp_.cols, tmp_.rows, tmp_.step);
+			rc = NvPipe_Decode(nv_decoder_, ptr, size, out.data, out.cols, out.rows, out.step);
 			if (rc == 0) LOG(ERROR) << "NvPipe decode error: " << NvPipe_GetError(nv_decoder_);
 			ptr += size;
 		}
@@ -111,17 +136,13 @@ bool NvPipeDecoder::decode(const ftl::codecs::Packet &pkt, cv::cuda::GpuMat &out
 			LOG(WARNING) << "P-Frame without I-Frame in decoder";
 			return false;
 		}
-		rc = NvPipe_Decode(nv_decoder_, pkt.data.data(), pkt.data.size(), tmp_.data, tmp_.cols, tmp_.rows, tmp_.step);
+		rc = NvPipe_Decode(nv_decoder_, pkt.data.data(), pkt.data.size(), out.data, out.cols, out.rows, out.step);
 		if (rc == 0) LOG(ERROR) << "NvPipe decode error: " << NvPipe_GetError(nv_decoder_);
 	}
 
-	if (is_float_frame) {
+	/*if (is_float_frame) {
 		if (!islossless) {
 			//cv::cuda::cvtColor(tmp_, tmp_, cv::COLOR_RGB2YUV, 4, stream_);
-			/*cv::Mat tmpHost;
-			tmp_.download(tmpHost);
-			cv::imshow("DEPTH", tmpHost);
-			cv::waitKey(1);*/
 
 			ftl::cuda::vuya_to_depth(out, tmp_, 16.0f, stream_);
 		} else {
@@ -132,9 +153,9 @@ bool NvPipeDecoder::decode(const ftl::codecs::Packet &pkt, cv::cuda::GpuMat &out
 		if (pkt.flags & 0x1) {
 			cv::cuda::cvtColor(tmp_, out, cv::COLOR_RGBA2BGRA, 0, stream_);
 		}
-	}
+	}*/
 
-	stream_.waitForCompletion();
+	//stream_.waitForCompletion();
 
 	return rc > 0;
 }
diff --git a/components/codecs/src/nvpipe_encoder.cpp b/components/codecs/src/nvpipe_encoder.cpp
index 2c3b27636999676578cc7eb34301a6b0fa1757db..d34078cc5798b0d8f739c74f1c7d0b4101e6c0ca 100644
--- a/components/codecs/src/nvpipe_encoder.cpp
+++ b/components/codecs/src/nvpipe_encoder.cpp
@@ -1,29 +1,24 @@
 #include <ftl/codecs/nvpipe_encoder.hpp>
 #include <loguru.hpp>
 #include <ftl/timer.hpp>
-#include <ftl/codecs/bitrates.hpp>
+#include <ftl/codecs/codecs.hpp>
 #include <ftl/cuda_util.hpp>
 
 #include <opencv2/core/cuda/common.hpp>
 
-#include "depth_convert_cuda.hpp"
+#include <ftl/codecs/depth_convert_cuda.hpp>
 
 using ftl::codecs::NvPipeEncoder;
 using ftl::codecs::bitrate_t;
 using ftl::codecs::codec_t;
 using ftl::codecs::definition_t;
-using ftl::codecs::preset_t;
-using ftl::codecs::CodecPreset;
+using ftl::codecs::format_t;
 using ftl::codecs::Packet;
 
 NvPipeEncoder::NvPipeEncoder(definition_t maxdef,
 			definition_t mindef) : Encoder(maxdef, mindef, ftl::codecs::device_t::Hardware) {
 	nvenc_ = nullptr;
-	current_definition_ = definition_t::HD1080;
-	is_float_channel_ = false;
 	was_reset_ = false;
-	preference_ = codec_t::Any;
-	current_codec_ = codec_t::HEVC;
 }
 
 NvPipeEncoder::~NvPipeEncoder() {
@@ -39,42 +34,114 @@ bool NvPipeEncoder::supports(ftl::codecs::codec_t codec) {
 	case codec_t::H264_LOSSLESS:
 	case codec_t::HEVC_LOSSLESS:
 	case codec_t::H264:
-	case codec_t::HEVC: preference_ = codec; return true;
+	case codec_t::HEVC: return true;
 	default: return false;
 	}
 }
 
 /* Check preset resolution is not better than actual resolution. */
-definition_t NvPipeEncoder::_verifiedDefinition(definition_t def, const cv::cuda::GpuMat &in) {
+/*definition_t NvPipeEncoder::_verifiedDefinition(definition_t def, const cv::cuda::GpuMat &in) {
 	int height = ftl::codecs::getHeight(def);
 
-	// FIXME: Make sure this can't go forever
 	while (height > in.rows) {
 		def = static_cast<definition_t>(int(def)+1);
 		height = ftl::codecs::getHeight(def);
 	}
 
 	return def;
-}
+}*/
 
 static bool isLossy(codec_t c) {
 	return !(c == codec_t::HEVC_LOSSLESS || c == codec_t::H264_LOSSLESS);
 }
 
-bool NvPipeEncoder::encode(const cv::cuda::GpuMat &in, definition_t odefinition, bitrate_t bitrate, const std::function<void(const ftl::codecs::Packet&)> &cb) {
+static bool sanityFormat(int type, ftl::codecs::format_t fmt) {
+	switch(fmt) {
+	case format_t::BGRA8	:
+	case format_t::RGBA8	: return type == CV_8UC4;
+	case format_t::VUYA16	: return type == CV_8UC4;
+	case format_t::F32		: return type == CV_32F;
+	case format_t::U16		: return type == CV_16U;
+	}
+	return false;
+}
+
+static ftl::codecs::format_t formatFromPacket(const ftl::codecs::Packet &pkt) {
+	if (pkt.flags & ftl::codecs::kFlagFloat) {
+		return (pkt.flags & ftl::codecs::kFlagMappedDepth) ? format_t::VUYA16 : format_t::U16;
+	} else {
+		return (pkt.flags & ftl::codecs::kFlagFlipRGB) ? format_t::BGRA8 : format_t::RGBA8;
+	}
+}
+
+static uint64_t calculateBitrate(definition_t def, float ratescale) {
+	float bitrate = 1.0f;  // Megabits
+	switch (def) {
+	case definition_t::UHD4k	: bitrate = 32.0f; break;
+	case definition_t::HTC_VIVE	: bitrate = 16.0f; break;
+	case definition_t::HD1080	: bitrate = 6.0f; break;
+	case definition_t::HD720	: bitrate = 4.0f; break;
+	case definition_t::SD576	:
+	case definition_t::SD480	: bitrate = 2.0f; break;
+	case definition_t::LD360	: bitrate = 1.0f; break;
+	default						: bitrate = 8.0f;
+	}
+
+	bitrate *= 1000.0f*1000.0f;
+	float minrate = 0.25f * bitrate;
+	return uint64_t((bitrate - minrate)*ratescale + minrate);
+}
+
+bool NvPipeEncoder::encode(const cv::cuda::GpuMat &in, ftl::codecs::Packet &pkt) {
 	cudaSetDevice(0);
-	auto definition = odefinition; //_verifiedDefinition(odefinition, in);
 
-	auto width = ftl::codecs::getWidth(definition);
-	auto height = ftl::codecs::getHeight(definition);
+	if (pkt.codec != codec_t::Any && !supports(pkt.codec)) {
+		pkt.codec = codec_t::Invalid;
+		return false;
+	}
+
+	// Correct for mising flag
+	if (pkt.codec == codec_t::HEVC && (pkt.flags & ftl::codecs::kFlagFloat) && in.type() == CV_8UC4) {
+		pkt.flags |= ftl::codecs::kFlagMappedDepth;
+	}
+
+	ftl::codecs::format_t fmt = formatFromPacket(pkt);
+
+	if (pkt.frame_count == 0) {
+		pkt.definition = definition_t::Invalid;
+		return false;
+	}
+
+	auto [tx,ty] = ftl::codecs::chooseTileConfig(pkt.frame_count);
+	pkt.definition = (pkt.definition == definition_t::Any) ? ftl::codecs::findDefinition(in.cols/tx, in.rows/ty) : pkt.definition;
+	if (pkt.definition == definition_t::Invalid || pkt.definition == definition_t::Any) {
+		LOG(ERROR) << "Could not find appropriate definition";
+		return false;
+	}
+
+	auto width = ftl::codecs::getWidth(pkt.definition);
+	auto height = ftl::codecs::getHeight(pkt.definition);
 
 	if (in.empty()) {
 		LOG(WARNING) << "No data";
 		return false;
 	}
 
+	if (!sanityFormat(in.type(), fmt)) {
+		LOG(ERROR) << "Input type does not match given format";
+		pkt.flags = 0;
+		return false;
+	}
+
+	if (tx*width != in.cols || ty*height != in.rows) {
+		// TODO: Resize if lower definition requested...
+		LOG(ERROR) << "Input size does not match expected: " << in.cols << " != " << tx*width;
+		pkt.definition = definition_t::Invalid;
+		return false;
+	}
+
 	cv::cuda::GpuMat tmp;
-	if (width != in.cols || height != in.rows) {
+	/*if (width != in.cols || height != in.rows) {
 		LOG(WARNING) << "Mismatch resolution with encoding resolution";
 		if (in.type() == CV_32F) {
 			cv::cuda::resize(in, tmp_, cv::Size(width,height), 0.0, 0.0, cv::INTER_NEAREST, stream_);
@@ -82,60 +149,75 @@ bool NvPipeEncoder::encode(const cv::cuda::GpuMat &in, definition_t odefinition,
 			cv::cuda::resize(in, tmp_, cv::Size(width,height), 0.0, 0.0, cv::INTER_LINEAR, stream_);
 		}
 		tmp = tmp_;
-	} else {
+	} else {*/
 		tmp = in;
-	}
+	//}
 
-	//LOG(INFO) << "Definition: " << ftl::codecs::getWidth(definition) << "x" << ftl::codecs::getHeight(definition);
+	//LOG(INFO) << "Definition: " << ftl::codecs::getWidth(pkt.definition) << "x" << ftl::codecs::getHeight(pkt.definition);
 
 	if (in.empty()) {
 		LOG(ERROR) << "Missing data for Nvidia encoder";
 		return false;
 	}
 
-	if (preference_ == codec_t::Any) preference_ = codec_t::HEVC;
+	if (pkt.codec == codec_t::Any)
+		pkt.codec = ((pkt.flags & ftl::codecs::kFlagFloat) && !(pkt.flags & ftl::codecs::kFlagMappedDepth)) ? codec_t::HEVC_LOSSLESS : codec_t::HEVC;
 
-	if (!_createEncoder(tmp, definition, bitrate)) return false;
+	if (!_createEncoder(pkt, fmt)) return false;
+
+	if (isLossy(pkt.codec) && pkt.bitrate != last_bitrate_) {
+		uint64_t bitrate = calculateBitrate(pkt.definition, float(pkt.bitrate)/255.0f) * pkt.frame_count;
+		const int fps = 1000/ftl::timer::getInterval();
+		LOG(INFO) << "Changing bitrate: " << bitrate;
+		NvPipe_SetBitrate(nvenc_, bitrate, fps);
+		last_bitrate_ = pkt.bitrate;
+	}
 
 	//LOG(INFO) << "NvPipe Encode: " << int(definition) << " " << in.cols;
 
+	//pkt.flags = 0;
+
 	//cv::Mat tmp;
-	if (tmp.type() == CV_32F) {
-		if (isLossy(preference_)) {
+	/*if (tmp.type() == CV_32F) {
+		if (isLossy(pkt.codec)) {
 			// Use special encoding transform
 			tmp2_.create(tmp.size(), CV_8UC4);
 			ftl::cuda::depth_to_vuya(tmp, tmp2_, 16.0f, stream_);
+			pkt.flags |= NvPipeEncoder::kFlagMappedDepth;
 		} else {
 			tmp.convertTo(tmp2_, CV_16UC1, 1000, stream_);
 		}
 	} else if (tmp.type() == CV_8UC3) {
 		cv::cuda::cvtColor(tmp, tmp2_, cv::COLOR_BGR2RGBA, 0, stream_);
 	} else if (tmp.type() == CV_8UC4) {
-		cv::cuda::cvtColor(tmp, tmp2_, cv::COLOR_BGRA2RGBA, 0, stream_);
+		if (fmt == format_t::BGRA8) {
+			cv::cuda::cvtColor(tmp, tmp2_, cv::COLOR_BGRA2RGBA, 0, stream_);
+			pkt.flags |= NvPipeEncoder::kFlagRGB;
+		} else if (fmt == format_t::VUYA16) {
+			tmp2_ = tmp;
+		}
+	//} else if (tmp.type() == CV_16UC4) {
+
 	} else {
 		LOG(ERROR) << "Unsupported cv::Mat type in Nvidia encoder";
 		return false;
-	}
+	}*/
 
 	// Make sure conversions complete...
-	stream_.waitForCompletion();
+	//stream_.waitForCompletion();
 
-	Packet pkt;
-	pkt.codec = preference_;
-	pkt.definition = definition;
-	pkt.frame_count = 1;
-	pkt.bitrate = 0;
-	pkt.flags = NvPipeEncoder::kFlagRGB | NvPipeEncoder::kFlagMappedDepth;
+	//pkt.flags = NvPipeEncoder::kFlagRGB | NvPipeEncoder::kFlagMappedDepth;
 
+	// TODO: Use page locked memory?
 	pkt.data.resize(ftl::codecs::kVideoBufferSize);
 	uint64_t cs = NvPipe_Encode(
 		nvenc_,
-		tmp2_.data,
-		tmp2_.step,
+		in.data,
+		in.step,
 		pkt.data.data(),
 		ftl::codecs::kVideoBufferSize,
-		tmp2_.cols,
-		tmp2_.rows,
+		in.cols,
+		in.rows,
 		was_reset_		// Force IFrame!
 	);
 	pkt.data.resize(cs);
@@ -145,82 +227,64 @@ bool NvPipeEncoder::encode(const cv::cuda::GpuMat &in, definition_t odefinition,
 		LOG(ERROR) << "Could not encode video frame: " << NvPipe_GetError(nvenc_);
 		return false;
 	} else {
-		cb(pkt);
 		return true;
 	}
 }
 
-bool NvPipeEncoder::_encoderMatch(const cv::cuda::GpuMat &in, definition_t def) {
-	return ((in.type() == CV_32F && is_float_channel_) ||
-		((in.type() == CV_8UC3 || in.type() == CV_8UC4) && !is_float_channel_)) && current_definition_ == def && current_codec_ == preference_;
+static NvPipe_Codec selectCodec(const Packet &pkt) {
+	return (pkt.codec == codec_t::HEVC || pkt.codec == codec_t::HEVC_LOSSLESS) ? NVPIPE_HEVC : NVPIPE_H264;
 }
 
-static uint64_t calculateBitrate(definition_t def, bitrate_t rate) {
-	float scale = 1.0f;
-	switch (rate) {
-	case bitrate_t::High		: break;
-	case bitrate_t::Standard	: scale = 0.5f; break;
-	case bitrate_t::Low			: scale = 0.25f; break;
+static NvPipe_Compression selectCompression(const Packet &pkt, format_t fmt) {
+	switch (fmt) {
+	case format_t::BGRA8	:
+	case format_t::RGBA8	: return NVPIPE_LOSSY;
+	case format_t::F32		: return (isLossy(pkt.codec)) ? NVPIPE_LOSSY_10BIT_420 : NVPIPE_LOSSLESS;
+	case format_t::VUYA16	: return NVPIPE_LOSSY_10BIT_420;  // FIXME: Check codec.
+	case format_t::U16		: return NVPIPE_LOSSLESS;
 	}
+	return NVPIPE_LOSSY;
+}
 
-	float bitrate = 1.0f;  // Megabits
-	switch (def) {
-	case definition_t::UHD4k	: bitrate = 24.0f; break;
-	case definition_t::HTC_VIVE	: bitrate = 16.0f; break;
-	case definition_t::HD1080	: bitrate = 16.0f; break;
-	case definition_t::HD720	: bitrate = 8.0f; break;
-	case definition_t::SD576	:
-	case definition_t::SD480	: bitrate = 4.0f; break;
-	case definition_t::LD360	: bitrate = 2.0f; break;
-	default						: bitrate = 8.0f;
+static NvPipe_Format selectFormat(const Packet &pkt, format_t fmt) {
+	switch (fmt) {
+	case format_t::BGRA8	:
+	case format_t::RGBA8	: return NVPIPE_RGBA32;
+	case format_t::F32		: return (isLossy(pkt.codec)) ? NVPIPE_YUV32 : NVPIPE_UINT16;
+	case format_t::U16		: return NVPIPE_UINT16;
+	case format_t::VUYA16	: return NVPIPE_YUV32;
 	}
-
-	return uint64_t(bitrate * 1000.0f * 1000.0f * scale);
+	return NVPIPE_RGBA32;
 }
 
-bool NvPipeEncoder::_createEncoder(const cv::cuda::GpuMat &in, definition_t def, bitrate_t rate) {
-	if (_encoderMatch(in, def) && nvenc_) return true;
+bool NvPipeEncoder::_encoderMatch(const ftl::codecs::Packet &pkt, format_t fmt) {
+	return	compression_ == selectCompression(pkt, fmt) &&
+			format_ == selectFormat(pkt, fmt) &&
+			codec_ == selectCodec(pkt);
+}
 
-	if (in.type() == CV_32F) is_float_channel_ = true;
-	else is_float_channel_ = false;
-	current_definition_ = def;
-	current_codec_ = preference_;
+bool NvPipeEncoder::_createEncoder(const ftl::codecs::Packet &pkt, format_t fmt) {
+	if (_encoderMatch(pkt, fmt) && nvenc_) return true;
 
-	uint64_t bitrate = calculateBitrate(def, rate);
-	if (is_float_channel_) bitrate *= 2.0f;
+	uint64_t bitrate = calculateBitrate(pkt.definition, float(pkt.bitrate)/255.0f) * pkt.frame_count;
+	//if (is_float_channel_) bitrate *= 2.0f;
 	//LOG(INFO) << "Calculated bitrate: " << bitrate;
-
-	NvPipe_Codec codec;
-	NvPipe_Format format;
-	NvPipe_Compression compression;
-
-	if (is_float_channel_) {
-		if (isLossy(preference_)) {
-			format = NVPIPE_YUV32;
-			compression = NVPIPE_LOSSY_10BIT_420;
-			codec = (preference_ == codec_t::HEVC) ? NVPIPE_HEVC : NVPIPE_H264;
-		} else {
-			format = NVPIPE_UINT16;
-			compression = NVPIPE_LOSSLESS;
-			codec = (preference_ == codec_t::HEVC_LOSSLESS) ? NVPIPE_HEVC : NVPIPE_H264;
-		}
-	} else {
-		format = NVPIPE_RGBA32;
-		compression = NVPIPE_LOSSY;
-		codec = (preference_ == codec_t::HEVC || preference_ == codec_t::HEVC_LOSSLESS) ? NVPIPE_HEVC : NVPIPE_H264;
-	}
-
+	
+	format_ = selectFormat(pkt, fmt);
+	compression_ = selectCompression(pkt, fmt);
+	codec_ = selectCodec(pkt);
+	last_bitrate_ = pkt.bitrate;
 
 	if (nvenc_) NvPipe_Destroy(nvenc_);
 	const int fps = 1000/ftl::timer::getInterval();
 	nvenc_ = NvPipe_CreateEncoder(
-		format,
-		codec,
-		compression,
+		format_,
+		codec_,
+		compression_,
 		bitrate,
 		fps,				// FPS
-		ftl::codecs::getWidth(def),	// Output Width
-		ftl::codecs::getHeight(def)	// Output Height
+		ftl::codecs::getWidth(pkt.definition),	// Output Width
+		ftl::codecs::getHeight(pkt.definition)	// Output Height
 	);
 
 	if (!nvenc_) {
diff --git a/components/codecs/src/opencv_encoder.cpp b/components/codecs/src/opencv_encoder.cpp
index 01310c41f55ba34dfbe08d1b99bdc256a70358cd..8edc1a616fd527bd748f7a7802ef7a28740ec44e 100644
--- a/components/codecs/src/opencv_encoder.cpp
+++ b/components/codecs/src/opencv_encoder.cpp
@@ -5,10 +5,7 @@
 
 using ftl::codecs::definition_t;
 using ftl::codecs::codec_t;
-using ftl::codecs::bitrate_t;
 using ftl::codecs::OpenCVEncoder;
-using ftl::codecs::preset_t;
-using ftl::codecs::CodecPreset;
 using std::vector;
 
 OpenCVEncoder::OpenCVEncoder(ftl::codecs::definition_t maxdef,
@@ -28,53 +25,53 @@ bool OpenCVEncoder::supports(ftl::codecs::codec_t codec) {
 	}
 }
 
-bool OpenCVEncoder::encode(const cv::cuda::GpuMat &in, definition_t definition, bitrate_t bitrate, const std::function<void(const ftl::codecs::Packet&)> &cb) {
-	bool is_colour = in.type() != CV_32F;
+bool OpenCVEncoder::encode(const cv::cuda::GpuMat &in, ftl::codecs::Packet &pkt) {
+	bool is_colour = !(pkt.flags & ftl::codecs::kFlagFloat);
+
+	if (is_colour && in.type() != CV_8UC4) return false;
+	if (!is_colour && in.type() == CV_8UC4) {
+		LOG(ERROR) << "OpenCV Encoder doesn't support lossy depth";
+		return false;
+	}
+
+	pkt.definition = (pkt.definition == definition_t::Any) ? ftl::codecs::findDefinition(in.cols, in.rows) : pkt.definition;
+
+	if (pkt.definition == definition_t::Invalid || pkt.definition == definition_t::Any) return false;
 
 	// Ensure definition does not exceed max
-	current_definition_ = ((int)definition < (int)max_definition) ? max_definition : definition;
+	current_definition_ = ((int)pkt.definition < (int)max_definition) ? max_definition : pkt.definition;
 
 	in.download(tmp_);
 	//CHECK(cv::Size(ftl::codecs::getWidth(definition), ftl::codecs::getHeight(definition)) == in.size()); 
 
+	int width = ftl::codecs::getWidth(current_definition_);
+	int height = ftl::codecs::getHeight(current_definition_);
+
 	// Scale down image to match requested definition...
-	if (ftl::codecs::getHeight(current_definition_) < in.rows) {
+	/*if (ftl::codecs::getHeight(current_definition_) < in.rows) {
 		cv::resize(tmp_, tmp_, cv::Size(ftl::codecs::getWidth(current_definition_), ftl::codecs::getHeight(current_definition_)), 0, 0, (is_colour) ? 1 : cv::INTER_NEAREST);
 	} else {
 		
-	}
-
-	// Represent float at 16bit int
-	if (!is_colour) {
-		tmp_.convertTo(tmp_, CV_16UC1, 1000);
-	}
+	}*/
+	if (width != in.cols || height != in.rows) return false;
 
-	// FIXME: Chunking is broken so forced to single chunk
-	chunk_dim_ = 1; //(definition == definition_t::LD360) ? 1 : 4;
-	chunk_count_ = chunk_dim_ * chunk_dim_;
-	jobs_ = chunk_count_;
+	if (pkt.codec == codec_t::Any) pkt.codec = (is_colour) ? codec_t::JPG : codec_t::PNG;
 
 	//for (int i=0; i<chunk_count_; ++i) {
 		// Add chunk job to thread pool
 		//ftl::pool.push([this,i,cb,is_colour,bitrate](int id) {
-			ftl::codecs::Packet pkt;
-			pkt.bitrate = 0;
-			pkt.frame_count = 1;
-			pkt.definition = current_definition_;
-			pkt.codec = (is_colour) ? codec_t::JPG : codec_t::PNG;
+			//ftl::codecs::Packet pkt;
+			//pkt.bitrate = 0;
+			//pkt.frame_count = 1;
+			//pkt.definition = current_definition_;
+			//pkt.codec = (is_colour) ? codec_t::JPG : codec_t::PNG;
 
 			try {
-				_encodeBlock(tmp_, pkt, bitrate);
+				_encodeBlock(tmp_, pkt);
 			} catch(...) {
 				LOG(ERROR) << "OpenCV encode block exception: ";
 			}
 
-			try {
-				cb(pkt);
-			} catch(...) {
-				LOG(ERROR) << "OpenCV encoder callback exception";
-			}
-
 			//std::unique_lock<std::mutex> lk(job_mtx_);
 			//--jobs_;
 			//if (jobs_ == 0) job_cv_.notify_one();
@@ -90,16 +87,16 @@ bool OpenCVEncoder::encode(const cv::cuda::GpuMat &in, definition_t definition,
 	return true;
 }
 
-bool OpenCVEncoder::_encodeBlock(const cv::Mat &in, ftl::codecs::Packet &pkt, bitrate_t bitrate) {
-	int chunk_width = in.cols / chunk_dim_;
-	int chunk_height = in.rows / chunk_dim_;
+bool OpenCVEncoder::_encodeBlock(const cv::Mat &in, ftl::codecs::Packet &pkt) {
+	//int chunk_width = in.cols / 1;
+	//int chunk_height = in.rows / 1;
 
 	// Build chunk heads
-	int cx = 0; //(pkt.block_number % chunk_dim_) * chunk_width;
-	int cy = 0; //(pkt.block_number / chunk_dim_) * chunk_height;
-	cv::Rect roi(cx,cy,chunk_width,chunk_height);
+	//int cx = 0; //(pkt.block_number % chunk_dim_) * chunk_width;
+	//int cy = 0; //(pkt.block_number / chunk_dim_) * chunk_height;
+	//cv::Rect roi(cx,cy,chunk_width,chunk_height);
 
-	cv::Mat chunkHead = in(roi);
+	cv::Mat chunkHead = in;
 
 	if (pkt.codec == codec_t::PNG) {
 		vector<int> params = {cv::IMWRITE_PNG_COMPRESSION, 1};
@@ -109,13 +106,7 @@ bool OpenCVEncoder::_encodeBlock(const cv::Mat &in, ftl::codecs::Packet &pkt, bi
 		}
 		return true;
 	} else if (pkt.codec == codec_t::JPG) {
-		int q = 95;
-
-		switch (bitrate) {
-		case bitrate_t::High		: q = 95; break;
-		case bitrate_t::Standard	: q = 75; break;
-		case bitrate_t::Low			: q = 50; break;
-		}
+		int q = int((95.0f - 50.0f) * (float(pkt.bitrate)/255.0f) + 50.0f);
 
 		vector<int> params = {cv::IMWRITE_JPEG_QUALITY, q};
 		cv::imencode(".jpg", chunkHead, pkt.data, params);
diff --git a/components/codecs/test/nvpipe_codec_unit.cpp b/components/codecs/test/nvpipe_codec_unit.cpp
index dccc65f9671a70ddc1879d12d0b8ef38aa9a1f01..2b32e28431890b0acc6e36a1ec8ce76968e7e122 100644
--- a/components/codecs/test/nvpipe_codec_unit.cpp
+++ b/components/codecs/test/nvpipe_codec_unit.cpp
@@ -1,12 +1,12 @@
 #include "catch.hpp"
 #include <ftl/codecs/nvpipe_encoder.hpp>
 #include <ftl/codecs/nvpipe_decoder.hpp>
+#include <ftl/codecs/hevc.hpp>
 #include <ftl/threads.hpp>
 
-using ftl::codecs::CodecPreset;
-using ftl::codecs::preset_t;
 using ftl::codecs::definition_t;
 using ftl::codecs::codec_t;
+using ftl::codecs::format_t;
 
 ctpl::thread_pool ftl::pool(4);
 
@@ -22,47 +22,251 @@ namespace ftl {
 	}
 }
 
-/*
-TEST_CASE( "NvPipeEncoder::encode() - A colour test image at preset 0" ) {
+
+TEST_CASE( "NvPipeEncoder::encode() - A valid colour image" ) {
 	ftl::codecs::NvPipeEncoder encoder(definition_t::HD1080, definition_t::SD480);
-	cv::cuda::GpuMat m(cv::Size(1920,1080), CV_8UC3, cv::Scalar(0,0,0));
+	cv::cuda::GpuMat m(cv::Size(1920,1080), CV_8UC4, cv::Scalar(0,0,0,0));
+
+	ftl::codecs::Packet pkt;
+	pkt.codec = codec_t::Any;
+	pkt.bitrate = 255;
+	pkt.definition = definition_t::Any;
+	pkt.flags = 0;
+	pkt.frame_count = 1;
+
+	SECTION("auto codec and definition, single frame") {
+		bool r = encoder.encode(m, pkt);
 
-	int block_total = 0;
-	std::atomic<int> block_count = 0;
-	encoder.encode()
-	bool r = encoder.encode(m, definition::H, [&block_total, &block_count, preset, m](const ftl::codecs::Packet &pkt) {
+		REQUIRE( r );
 		REQUIRE( pkt.codec == codec_t::HEVC );
-		REQUIRE( pkt.data.size() > 0 );
 		REQUIRE( pkt.definition == definition_t::HD1080 );
+		REQUIRE( pkt.flags == 0 );
+		REQUIRE( pkt.data.size() > 0 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
+
+	SECTION("auto codec and definition, single frame, 1 bitrate") {
+		pkt.bitrate = 1;
+
+		bool r = encoder.encode(m, pkt);
+
+		REQUIRE( r );
+		REQUIRE( pkt.bitrate == 1 );
+		REQUIRE( pkt.data.size() > 0 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
+
+	SECTION("invalid frame count of 2") {
+		pkt.frame_count = 2;
+
+		bool r = encoder.encode(m, pkt);
+
+		REQUIRE( !r );
+		REQUIRE( pkt.definition == definition_t::Invalid );
+		REQUIRE( pkt.data.size() == 0 );
+	}
+
+	SECTION("invalid frame count of 0") {
+		pkt.frame_count = 0;
+
+		bool r = encoder.encode(m, pkt);
+
+		REQUIRE( !r );
+		REQUIRE( pkt.definition == definition_t::Invalid );
+		REQUIRE( pkt.data.size() == 0 );
+	}
 
-		block_total = pkt.block_total;
-		block_count++;
-	});
+	SECTION("invalid float flag") {
+		pkt.flags = ftl::codecs::kFlagFloat;
 
-	REQUIRE( r );
-	REQUIRE( block_count == block_total );
+		bool r = encoder.encode(m, pkt);
+
+		REQUIRE( !r );
+		REQUIRE( pkt.flags == 0 );
+		REQUIRE( pkt.data.size() == 0 );
+	}
+
+	SECTION("invalid codec") {
+		pkt.codec = codec_t::JPG;
+
+		bool r = encoder.encode(m, pkt);
+
+		REQUIRE( !r );
+		REQUIRE( pkt.codec == codec_t::Invalid );
+		REQUIRE( pkt.data.size() == 0 );
+	}
+
+	SECTION("invalid definition") {
+		pkt.definition = definition_t::HD720;
+
+		bool r = encoder.encode(m, pkt);
+
+		REQUIRE( !r );
+		REQUIRE( pkt.definition == definition_t::Invalid );
+		REQUIRE( pkt.data.size() == 0 );
+	}
 }
 
-TEST_CASE( "NvPipeEncoder::encode() - A depth test image at preset 0" ) {
+TEST_CASE( "NvPipeEncoder::encode() - A tiled colour image" ) {
 	ftl::codecs::NvPipeEncoder encoder(definition_t::HD1080, definition_t::SD480);
-	cv::cuda::GpuMat m(cv::Size(1920,1080), CV_32F, cv::Scalar(0.0f));
+	cv::cuda::GpuMat m(cv::Size(2560,720), CV_8UC4, cv::Scalar(0,0,0,0));
+
+	SECTION("auto codec and definition, 2x1 frames") {
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.flags = 0;
+		pkt.frame_count = 2;
 
-	int block_total = 0;
-	std::atomic<int> block_count = 0;
+		bool r = encoder.encode(m, pkt);
 
-	bool r = encoder.encode(m, ftl::codecs::kPreset0, [&block_total, &block_count, preset](const ftl::codecs::Packet &pkt) {
+		REQUIRE( r );
 		REQUIRE( pkt.codec == codec_t::HEVC );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+		REQUIRE( pkt.flags == 0 );
 		REQUIRE( pkt.data.size() > 0 );
-		REQUIRE( pkt.definition == definition_t::HD1080 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
+}
+
+TEST_CASE( "NvPipeEncoder::encode() - A valid lossless float image" ) {
+	ftl::codecs::NvPipeEncoder encoder(definition_t::HD1080, definition_t::SD480);
+	cv::cuda::GpuMat m(cv::Size(1280,720), CV_16U, cv::Scalar(0));
+
+	SECTION("auto codec and definition, single frame") {
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.flags = ftl::codecs::kFlagFloat;
+		pkt.frame_count = 1;
+
+		bool r = encoder.encode(m, pkt);
+
+		REQUIRE( r );
+		REQUIRE( pkt.codec == codec_t::HEVC_LOSSLESS );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+		REQUIRE( pkt.flags == ftl::codecs::kFlagFloat );
+		REQUIRE( pkt.data.size() > 0 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
+
+	SECTION("missing float flag") {
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.flags = 0;
+		pkt.frame_count = 1;
+
+		bool r = encoder.encode(m, pkt);
+
+		REQUIRE( !r );
+		REQUIRE( pkt.data.size() == 0 );
+	}
+
+	SECTION("invalid lossy flag") {
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.flags = ftl::codecs::kFlagFloat & ftl::codecs::kFlagMappedDepth;
+		pkt.frame_count = 1;
+
+		bool r = encoder.encode(m, pkt);
+
+		REQUIRE( !r );
+		REQUIRE( pkt.data.size() == 0 );
+	}
+}
+
+TEST_CASE( "NvPipeEncoder::encode() - A valid lossy float image" ) {
+	ftl::codecs::NvPipeEncoder encoder(definition_t::HD1080, definition_t::SD480);
+	cv::cuda::GpuMat m(cv::Size(1280,720), CV_8UC4, cv::Scalar(0));
+
+	SECTION("auto codec and definition, single frame") {
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.flags = ftl::codecs::kFlagFloat | ftl::codecs::kFlagMappedDepth;
+		pkt.frame_count = 1;
+
+		bool r = encoder.encode(m, pkt);
+
+		REQUIRE( r );
+		REQUIRE( pkt.codec == codec_t::HEVC );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+		REQUIRE( pkt.flags == (ftl::codecs::kFlagFloat | ftl::codecs::kFlagMappedDepth) );
+		REQUIRE( pkt.data.size() > 0 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
+
+	SECTION("correct codec, missing flag") {
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::HEVC;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.flags = ftl::codecs::kFlagFloat;
+		pkt.frame_count = 1;
+
+		bool r = encoder.encode(m, pkt);
+
+		REQUIRE( r );
+		REQUIRE( pkt.codec == codec_t::HEVC );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+		REQUIRE( pkt.flags == (ftl::codecs::kFlagFloat | ftl::codecs::kFlagMappedDepth) );
+		REQUIRE( pkt.data.size() > 0 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
+}
+
+TEST_CASE( "NvPipeEncoder::encode() - A tiled lossy float image" ) {
+	ftl::codecs::NvPipeEncoder encoder(definition_t::HD1080, definition_t::SD480);
+	cv::cuda::GpuMat m(cv::Size(2560,720), CV_8UC4, cv::Scalar(0));
+
+	SECTION("auto codec and definition, 2x1 frame") {
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.flags = ftl::codecs::kFlagFloat & ftl::codecs::kFlagMappedDepth;
+		pkt.frame_count = 2;
+
+		bool r = encoder.encode(m, pkt);
+
+		REQUIRE( r );
+		REQUIRE( pkt.codec == codec_t::HEVC );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+		REQUIRE( pkt.flags == (ftl::codecs::kFlagFloat & ftl::codecs::kFlagMappedDepth) );
+		REQUIRE( pkt.data.size() > 0 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
+}
+
+TEST_CASE( "NvPipeEncoder::encode() - A large tiled lossy float image" ) {
+	ftl::codecs::NvPipeEncoder encoder(definition_t::HD1080, definition_t::SD480);
+	cv::cuda::GpuMat m(cv::Size(5120,1440), CV_8UC4, cv::Scalar(0));
+
+	SECTION("auto codec and definition, 4x2 frame") {
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.flags = ftl::codecs::kFlagFloat & ftl::codecs::kFlagMappedDepth;
+		pkt.frame_count = 7;
 
-		block_total = pkt.block_total;
-		block_count++;
-	});
+		bool r = encoder.encode(m, pkt);
 
-	REQUIRE( r );
-	REQUIRE( block_count == block_total );
+		REQUIRE( r );
+		REQUIRE( pkt.codec == codec_t::HEVC );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+		REQUIRE( pkt.flags == (ftl::codecs::kFlagFloat & ftl::codecs::kFlagMappedDepth) );
+		REQUIRE( pkt.data.size() > 0 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
 }
-*/
 
 TEST_CASE( "NvPipeDecoder::decode() - A colour test image" ) {
 	ftl::codecs::NvPipeEncoder encoder(definition_t::HD1080, definition_t::SD480);
@@ -70,53 +274,246 @@ TEST_CASE( "NvPipeDecoder::decode() - A colour test image" ) {
 
 	cv::cuda::GpuMat in;
 	cv::cuda::GpuMat out;
-	bool r = false;
 
-	SECTION("FHD in and out, FHD encoding") {
-		in = cv::cuda::GpuMat(cv::Size(1920,1080), CV_8UC3, cv::Scalar(255,0,0));
-		out = cv::cuda::GpuMat(cv::Size(1920,1080), CV_8UC3, cv::Scalar(0,0,0));
+	//SECTION("FHD in and out, FHD encoding") {
+		in = cv::cuda::GpuMat(cv::Size(1920,1080), CV_8UC4, cv::Scalar(255,0,0,0));
+		out = cv::cuda::GpuMat(cv::Size(1920,1080), CV_8UC4, cv::Scalar(0,0,0,0));
+
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.frame_count = 1;
+		pkt.flags = 0;
+		bool r = encoder.encode(in, pkt);
+
+		REQUIRE( r );
+		REQUIRE( decoder.decode(pkt, out) );
+		REQUIRE( out.cols == 1920 );
+		REQUIRE( out.type() == CV_8UC4 );
+	//}
+
+	REQUIRE( (cv::cuda::sum(out) != cv::Scalar(0,0,0)) );
+}
+
+TEST_CASE( "NvPipeDecoder::decode() - A tiled colour image" ) {
+	ftl::codecs::NvPipeEncoder encoder(definition_t::HD1080, definition_t::SD480);
+	ftl::codecs::NvPipeDecoder decoder;
+
+	cv::cuda::GpuMat in;
+	cv::cuda::GpuMat out;
+
+	//SECTION("FHD in and out, FHD encoding") {
+		in = cv::cuda::GpuMat(cv::Size(2560,720), CV_8UC4, cv::Scalar(255,0,0,0));
+		out = cv::cuda::GpuMat(cv::Size(2560,720), CV_8UC4, cv::Scalar(0,0,0,0));
+
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.frame_count = 2;
+		pkt.flags = 0;
+		bool r = encoder.encode(in, pkt);
+
+		REQUIRE( r );
+		REQUIRE( decoder.decode(pkt, out) );
+		REQUIRE( out.cols == 2560 );
+		REQUIRE( out.type() == CV_8UC4 );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+	//}
+
+	REQUIRE( (cv::cuda::sum(out) != cv::Scalar(0,0,0)) );
+}
+
+TEST_CASE( "NvPipeDecoder::decode() - A lossless depth image" ) {
+	ftl::codecs::NvPipeEncoder encoder(definition_t::HD1080, definition_t::SD480);
+	ftl::codecs::NvPipeDecoder decoder;
+
+	cv::cuda::GpuMat in;
+	cv::cuda::GpuMat out;
+
+	//SECTION("FHD in and out, FHD encoding") {
+		in = cv::cuda::GpuMat(cv::Size(1280,720), CV_16U, cv::Scalar(255));
+		out = cv::cuda::GpuMat(cv::Size(1280,720), CV_16U, cv::Scalar(0));
+
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.frame_count = 1;
+		pkt.flags = ftl::codecs::kFlagFloat;
+		bool r = encoder.encode(in, pkt);
+
+		REQUIRE( r );
+		REQUIRE( decoder.decode(pkt, out) );
+		REQUIRE( out.cols == 1280 );
+		REQUIRE( out.type() == CV_16U );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+	//}
+
+	REQUIRE( (cv::cuda::sum(out) != cv::Scalar(0)) );
+}
+
+TEST_CASE( "NvPipeDecoder::decode() - A lossy depth image" ) {
+	ftl::codecs::NvPipeEncoder encoder(definition_t::HD1080, definition_t::SD480);
+	ftl::codecs::NvPipeDecoder decoder;
+
+	cv::cuda::GpuMat in;
+	cv::cuda::GpuMat out;
+
+	//SECTION("FHD in and out, FHD encoding") {
+		in = cv::cuda::GpuMat(cv::Size(1280,720), CV_8UC4, cv::Scalar(255));
+		out = cv::cuda::GpuMat(cv::Size(1280,720), CV_16UC4, cv::Scalar(0));
+
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.frame_count = 1;
+		pkt.flags = ftl::codecs::kFlagFloat | ftl::codecs::kFlagMappedDepth;
+		bool r = encoder.encode(in, pkt);
+
+		REQUIRE( r );
+		REQUIRE( decoder.decode(pkt, out) );
+		REQUIRE( out.cols == 1280 );
+		REQUIRE( out.type() == CV_16UC4 );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+	//}
+
+	REQUIRE( (cv::cuda::sum(out) != cv::Scalar(0)) );
+}
+
+TEST_CASE( "NvPipeDecoder::decode() - corrupted packet" ) {
+	ftl::codecs::NvPipeEncoder encoder(definition_t::HD1080, definition_t::SD480);
+	ftl::codecs::NvPipeDecoder decoder;
 
-		r = encoder.encode(in, ftl::codecs::kPreset0, [&out,&decoder](const ftl::codecs::Packet &pkt) {
-			REQUIRE( decoder.decode(pkt, out) );
-		});
+	cv::cuda::GpuMat in;
+	cv::cuda::GpuMat out;
+
+	SECTION("Corrupted definition") {
+		in = cv::cuda::GpuMat(cv::Size(2560,720), CV_8UC4, cv::Scalar(255,0,0,0));
+		out = cv::cuda::GpuMat(cv::Size(2560,720), CV_8UC4, cv::Scalar(0,0,0,0));
+
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.frame_count = 2;
+		pkt.flags = 0;
+		bool r = encoder.encode(in, pkt);
+
+		pkt.definition = definition_t::HD1080;
+
+		REQUIRE( r );
+		REQUIRE( !decoder.decode(pkt, out) );
 	}
 
-	// No longer supported
-	/*SECTION("Full HD in, 720 out, FHD encoding") {
-		in = cv::cuda::GpuMat(cv::Size(1920,1080), CV_8UC3, cv::Scalar(255,0,0));
-		out = cv::cuda::GpuMat(cv::Size(1280,720), CV_8UC3, cv::Scalar(0,0,0));
+	SECTION("Corrupted but supported codec") {
+		in = cv::cuda::GpuMat(cv::Size(2560,720), CV_8UC4, cv::Scalar(255,0,0,0));
+		out = cv::cuda::GpuMat(cv::Size(2560,720), CV_8UC4, cv::Scalar(0,0,0,0));
 
-		r = encoder.encode(in, ftl::codecs::kPreset0, [&out,&decoder](const ftl::codecs::Packet &pkt) {
-			REQUIRE( decoder.decode(pkt, out) );
-		});
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.frame_count = 2;
+		pkt.flags = 0;
+		bool r = encoder.encode(in, pkt);
 
-		REQUIRE( (out.rows == 720) );
-	}*/
+		pkt.codec = codec_t::H264;
 
-	// No longer supported
-	/*SECTION("HHD in, FHD out, FHD encoding") {
-		in = cv::cuda::GpuMat(cv::Size(1280,720), CV_8UC3, cv::Scalar(255,0,0));
-		out = cv::cuda::GpuMat(cv::Size(1920,1080), CV_8UC3, cv::Scalar(0,0,0));
+		REQUIRE( r );
+		REQUIRE( !decoder.decode(pkt, out) );
+	}
 
-		r = encoder.encode(in, ftl::codecs::kPreset0, [&out,&decoder](const ftl::codecs::Packet &pkt) {
-			REQUIRE( decoder.decode(pkt, out) );
-		});
+	SECTION("Corrupted and unsupported codec") {
+		in = cv::cuda::GpuMat(cv::Size(2560,720), CV_8UC4, cv::Scalar(255,0,0,0));
+		out = cv::cuda::GpuMat(cv::Size(2560,720), CV_8UC4, cv::Scalar(0,0,0,0));
 
-		REQUIRE( (out.rows == 1080) );
-	}*/
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.frame_count = 2;
+		pkt.flags = 0;
+		bool r = encoder.encode(in, pkt);
 
-	// No longer supported
-	/*SECTION("FHD in, HHD out, SD encoding") {
-		in = cv::cuda::GpuMat(cv::Size(1920,1080), CV_8UC3, cv::Scalar(255,0,0));
-		out = cv::cuda::GpuMat(cv::Size(1280,720), CV_8UC3, cv::Scalar(0,0,0));
+		pkt.codec = codec_t::JPG;
 
-		r = encoder.encode(in, ftl::codecs::kPreset4, [&out,&decoder](const ftl::codecs::Packet &pkt) {
-			REQUIRE( decoder.decode(pkt, out) );
-		});
+		REQUIRE( r );
+		REQUIRE( !decoder.decode(pkt, out) );
+	}
 
-		REQUIRE( (out.rows == 720) );
-	}*/
+	SECTION("Corrupted float flag") {
+		in = cv::cuda::GpuMat(cv::Size(2560,720), CV_8UC4, cv::Scalar(255,0,0,0));
+		out = cv::cuda::GpuMat(cv::Size(2560,720), CV_8UC4, cv::Scalar(0,0,0,0));
 
-	REQUIRE( r );
-	REQUIRE( (cv::cuda::sum(out) != cv::Scalar(0,0,0)) );
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.frame_count = 2;
+		pkt.flags = 0;
+		bool r = encoder.encode(in, pkt);
+
+		pkt.flags = ftl::codecs::kFlagFloat;
+
+		REQUIRE( r );
+		REQUIRE( !decoder.decode(pkt, out) );
+	}
+
+	SECTION("Corrupted float mapped flags") {
+		in = cv::cuda::GpuMat(cv::Size(1280,720), CV_16U, cv::Scalar(255));
+		out = cv::cuda::GpuMat(cv::Size(1280,720), CV_16U, cv::Scalar(0));
+
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.frame_count = 1;
+		pkt.flags = ftl::codecs::kFlagFloat;
+		bool r = encoder.encode(in, pkt);
+
+		pkt.codec = codec_t::HEVC;
+		pkt.flags = ftl::codecs::kFlagFloat | ftl::codecs::kFlagMappedDepth;
+
+		REQUIRE( r );
+		REQUIRE( !decoder.decode(pkt, out) );
+	}
+
+	SECTION("Missing float flag - lossless") {
+		in = cv::cuda::GpuMat(cv::Size(1280,720), CV_16U, cv::Scalar(255));
+		out = cv::cuda::GpuMat(cv::Size(1280,720), CV_16U, cv::Scalar(0));
+
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.frame_count = 1;
+		pkt.flags = ftl::codecs::kFlagFloat;
+		bool r = encoder.encode(in, pkt);
+
+		pkt.flags = 0;
+
+		REQUIRE( r );
+		REQUIRE( !decoder.decode(pkt, out) );
+	}
+
+	SECTION("Missing data") {
+		in = cv::cuda::GpuMat(cv::Size(1280,720), CV_16U, cv::Scalar(255));
+		out = cv::cuda::GpuMat(cv::Size(1280,720), CV_16U, cv::Scalar(0));
+
+		ftl::codecs::Packet pkt;
+		pkt.codec = codec_t::Any;
+		pkt.bitrate = 255;
+		pkt.definition = definition_t::Any;
+		pkt.frame_count = 1;
+		pkt.flags = ftl::codecs::kFlagFloat;
+		bool r = encoder.encode(in, pkt);
+
+		pkt.data.resize(0);
+
+		REQUIRE( r );
+		REQUIRE( !decoder.decode(pkt, out) );
+	}
 }
diff --git a/components/codecs/test/opencv_codec_unit.cpp b/components/codecs/test/opencv_codec_unit.cpp
index 0982551bdc4ca36f6234f99346a183f5dd0d978d..ec3ec10c0e47d97b65b51529f2edc3ed3baf4e0f 100644
--- a/components/codecs/test/opencv_codec_unit.cpp
+++ b/components/codecs/test/opencv_codec_unit.cpp
@@ -3,8 +3,7 @@
 #include <ftl/codecs/opencv_decoder.hpp>
 #include <ftl/threads.hpp>
 
-using ftl::codecs::CodecPreset;
-using ftl::codecs::preset_t;
+using ftl::codecs::format_t;
 using ftl::codecs::definition_t;
 using ftl::codecs::codec_t;
 
@@ -87,12 +86,16 @@ TEST_CASE( "OpenCVDecoder::decode() - A colour test image no resolution change"
 	cv::cuda::GpuMat in(cv::Size(1024,576), CV_8UC4, cv::Scalar(255,0,0,0));
 	cv::cuda::GpuMat out(cv::Size(1024,576), CV_8UC4, cv::Scalar(0,0,0,0));
 
-	std::mutex mtx;
+	ftl::codecs::Packet pkt;
+	pkt.codec = codec_t::Any;
+	pkt.definition = definition_t::Any;
+	pkt.bitrate = 255;
+	pkt.flags = 0;
+	pkt.frame_count = 1;
+	bool r = encoder.encode(in, pkt);
 
-	bool r = encoder.encode(in, ftl::codecs::kPreset4, [&mtx, &out,&decoder](const ftl::codecs::Packet &pkt) {
-		std::unique_lock<std::mutex> lk(mtx);
-		REQUIRE( decoder.decode(pkt, out) );
-	});
+	REQUIRE( r );
+	REQUIRE( decoder.decode(pkt, out) );
 
-	REQUIRE( (cv::cuda::sum(out) != cv::Scalar(0,0,0)) );
+	REQUIRE( (cv::cuda::sum(out) != cv::Scalar(0,0,0,0)) );
 }
diff --git a/components/common/cpp/src/configuration.cpp b/components/common/cpp/src/configuration.cpp
index c53f50c78decd870c86228de2634588bdb313374..a752a3c1c540b69f1cf6a3cacefd4c7a2d0ef147 100644
--- a/components/common/cpp/src/configuration.cpp
+++ b/components/common/cpp/src/configuration.cpp
@@ -560,11 +560,13 @@ Configurable *ftl::config::configure(ftl::config::json_t &cfg) {
 
 static bool doing_cleanup = false;
 void ftl::config::cleanup() {
+	if (doing_cleanup) return;
 	doing_cleanup = true;
 	for (auto f : config_instance) {
 		delete f.second;
 	}
 	config_instance.clear();
+	doing_cleanup = false;
 }
 
 void ftl::config::removeConfigurable(Configurable *cfg) {
diff --git a/components/rgbd-sources/include/ftl/rgbd/format.hpp b/components/rgbd-sources/include/ftl/rgbd/format.hpp
index 2e86aaf96a5f64d13065385ddee360fd80ca9f8c..e52a6627fc58d523a1dff7872e64fab03e45c28a 100644
--- a/components/rgbd-sources/include/ftl/rgbd/format.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/format.hpp
@@ -4,7 +4,7 @@
 #include <opencv2/core.hpp>
 #include <opencv2/core/cuda.hpp>
 #include <ftl/cuda_util.hpp>
-#include <ftl/codecs/bitrates.hpp>
+#include <ftl/codecs/codecs.hpp>
 #include <ftl/traits.hpp>
 
 namespace ftl {
diff --git a/components/rgbd-sources/include/ftl/rgbd/frame.hpp b/components/rgbd-sources/include/ftl/rgbd/frame.hpp
index 85b97326b263ccd354924253f1a2652ea22959f9..f4dc769870fe87ef4614afe37a1cc288554bd89b 100644
--- a/components/rgbd-sources/include/ftl/rgbd/frame.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/frame.hpp
@@ -11,7 +11,7 @@
 #include <ftl/codecs/channels.hpp>
 #include <ftl/rgbd/format.hpp>
 #include <ftl/rgbd/camera.hpp>
-#include <ftl/codecs/bitrates.hpp>
+#include <ftl/codecs/codecs.hpp>
 #include <ftl/codecs/packet.hpp>
 
 #include <ftl/cuda_common.hpp>
diff --git a/components/rgbd-sources/src/sources/net/net.cpp b/components/rgbd-sources/src/sources/net/net.cpp
index 57b494032afa0331ad21bcb3ac4108df114dbf9b..7b21594cf48df7622c7990445800e96150fa07e8 100644
--- a/components/rgbd-sources/src/sources/net/net.cpp
+++ b/components/rgbd-sources/src/sources/net/net.cpp
@@ -8,7 +8,7 @@
 #include "colour.hpp"
 
 #include <ftl/rgbd/streamer.hpp>
-#include <ftl/codecs/bitrates.hpp>
+#include <ftl/codecs/codecs.hpp>
 
 using ftl::rgbd::detail::NetFrame;
 using ftl::rgbd::detail::NetFrameQueue;
diff --git a/components/streams/include/ftl/streams/filestream.hpp b/components/streams/include/ftl/streams/filestream.hpp
index 7baffd75aab8f842d80e658905046604455bc3a4..e1677b413c271554a1a99b0d908497a15583d2e6 100644
--- a/components/streams/include/ftl/streams/filestream.hpp
+++ b/components/streams/include/ftl/streams/filestream.hpp
@@ -37,7 +37,7 @@ class File : public Stream {
 	/**
 	 * Manually tick through the frames one per call.
 	 */
-	bool tick();
+	bool tick(int64_t);
 
 	enum class Mode {
 		Read,
diff --git a/components/streams/include/ftl/streams/netstream.hpp b/components/streams/include/ftl/streams/netstream.hpp
index 04cf48afede0172d7ec12feb24d640dd30b605cd..3c7aef3c64e917e4daac0722340414b2123dffd8 100644
--- a/components/streams/include/ftl/streams/netstream.hpp
+++ b/components/streams/include/ftl/streams/netstream.hpp
@@ -56,6 +56,8 @@ class Net : public Stream {
 	bool end() override;
 	bool active() override;
 
+	void reset() override;
+
 	inline const ftl::UUID &getPeer() const { return peer_; }
 
 	private:
diff --git a/components/streams/include/ftl/streams/receiver.hpp b/components/streams/include/ftl/streams/receiver.hpp
index e75c3b5987282caa445bcc4c0d612c4e28855e36..1d1fe816c0c8d6070b69b29d58a1d34089dc340e 100644
--- a/components/streams/include/ftl/streams/receiver.hpp
+++ b/components/streams/include/ftl/streams/receiver.hpp
@@ -54,6 +54,7 @@ class Receiver : public ftl::Configurable, public ftl::rgbd::Generator {
 		ftl::rgbd::FrameState state;
 		ftl::rgbd::Frame frame;
 		ftl::codecs::Decoder* decoders[32];
+		cv::cuda::GpuMat surface[32];
 		MUTEX mutex;
 		ftl::codecs::Channels<0> completed;
 	};
@@ -62,7 +63,7 @@ class Receiver : public ftl::Configurable, public ftl::rgbd::Generator {
 
 	void _processConfig(InternalStates &frame, const ftl::codecs::Packet &pkt);
 	void _createDecoder(InternalStates &frame, int chan, const ftl::codecs::Packet &pkt);
-	InternalStates &_getFrame(const ftl::codecs::StreamPacket &spkt);
+	InternalStates &_getFrame(const ftl::codecs::StreamPacket &spkt, int ix=0);
 };
 
 }
diff --git a/components/streams/include/ftl/streams/sender.hpp b/components/streams/include/ftl/streams/sender.hpp
index 2cfc57bb31d71cff17e322c36ec46960796ef13b..31022ee458fea27e6cef31ebcf1008f0c6741b47 100644
--- a/components/streams/include/ftl/streams/sender.hpp
+++ b/components/streams/include/ftl/streams/sender.hpp
@@ -29,16 +29,31 @@ class Sender : public ftl::Configurable {
 
 	//void onStateChange(const std::function<void(ftl::codecs::Channel, int, int)>&);
 
+	void onRequest(const ftl::stream::StreamCallback &);
+
 	private:
 	ftl::stream::Stream *stream_;
 	int64_t timestamp_;
 	SHARED_MUTEX mutex_;
 	std::atomic_flag do_inject_;
 	//std::function<void(ftl::codecs::Channel, int, int)> state_cb_;
-
-	std::unordered_map<int, ftl::codecs::Encoder*> encoders_;
-
-	ftl::codecs::Encoder *_getEncoder(int fsid, int fid, ftl::codecs::Channel c);
+	ftl::stream::StreamCallback reqcb_;
+
+	struct EncodingState {
+		uint8_t bitrate;
+		ftl::codecs::Encoder *encoder[2];
+		cv::cuda::GpuMat surface;
+		cudaStream_t stream;
+	};
+
+	std::unordered_map<int, EncodingState> state_;
+
+	//ftl::codecs::Encoder *_getEncoder(int fsid, int fid, ftl::codecs::Channel c);
+	void _encodeChannel(const ftl::rgbd::FrameSet &fs, ftl::codecs::Channel c, bool reset);
+	int _generateTiles(const ftl::rgbd::FrameSet &fs, int offset, ftl::codecs::Channel c, cv::cuda::Stream &stream, bool);
+	EncodingState &_getTile(int fsid, ftl::codecs::Channel c);
+	cv::Rect _generateROI(const ftl::rgbd::FrameSet &fs, ftl::codecs::Channel c, int offset);
+	float _selectFloatMax(ftl::codecs::Channel c);
 };
 
 }
diff --git a/components/streams/include/ftl/streams/stream.hpp b/components/streams/include/ftl/streams/stream.hpp
index 08064b6c9a6c69813930775ddac85d13f9ac3d9e..2502f8b69c7566b6e94c8d7cacb11ebf7614a904 100644
--- a/components/streams/include/ftl/streams/stream.hpp
+++ b/components/streams/include/ftl/streams/stream.hpp
@@ -56,6 +56,13 @@ class Stream : public ftl::Configurable {
 	 */
 	virtual bool active()=0;
 
+	/**
+	 * Perform a forced reset of the stream. This means something different
+	 * depending on stream type, for example with a network stream it involves
+	 * resending all stream requests as if a reconnection had occured.
+	 */
+	virtual void reset();
+
 	/**
 	 * Query available video channels for a frameset.
 	 */
@@ -70,7 +77,7 @@ class Stream : public ftl::Configurable {
 	/**
 	 * Change the video channel selection for a frameset.
 	 */
-	void select(int fs, const ftl::codecs::Channels<0> &);
+	void select(int fs, const ftl::codecs::Channels<0> &, bool make=false);
 
 	/**
 	 * Number of framesets in stream.
@@ -116,6 +123,8 @@ class Muxer : public Stream {
 	bool end() override;
 	bool active() override;
 
+	void reset() override;
+
 	private:
 	struct StreamEntry {
 		Stream *stream;
@@ -152,6 +161,8 @@ class Broadcast : public Stream {
 	bool end() override;
 	bool active() override;
 
+	void reset() override;
+
 	private:
 	std::list<Stream*> streams_;
 	StreamCallback cb_;
@@ -177,6 +188,8 @@ class Intercept : public Stream {
 	bool end() override;
 	bool active() override;
 
+	void reset() override;
+
 	private:
 	Stream *stream_;
 	StreamCallback cb_;
diff --git a/components/streams/src/filestream.cpp b/components/streams/src/filestream.cpp
index 232a929f6bf574baaa09e993e8a0be22ad7c9421..784b14fb686d49ff22f30f7ee133b63b3997f1b8 100644
--- a/components/streams/src/filestream.cpp
+++ b/components/streams/src/filestream.cpp
@@ -55,7 +55,7 @@ bool File::post(const ftl::codecs::StreamPacket &s, const ftl::codecs::Packet &p
 	return ostream_->good();
 }
 
-bool File::tick() {
+bool File::tick(int64_t ts) {
 	if (!active_) return false;
 	if (mode_ != Mode::Read) {
 		LOG(ERROR) << "Cannot read from a write only file";
@@ -77,6 +77,7 @@ bool File::tick() {
 		for (auto i = data_.begin(); i != data_.end(); ++i) {
 			if (std::get<0>(*i).timestamp <= timestamp_) {
 				++jobs_;
+				std::get<0>(*i).timestamp = ts;
 				ftl::pool.push([this,i](int id) {
 					auto &spkt = std::get<0>(*i);
 					auto &pkt = std::get<1>(*i);
@@ -141,6 +142,10 @@ bool File::tick() {
 		if (version_ < 4) {
 			std::get<0>(data).frame_number = std::get<0>(data).streamID;
 			std::get<0>(data).streamID = 0;
+			if (isFloatChannel(std::get<0>(data).channel)) std::get<1>(data).flags |= ftl::codecs::kFlagFloat;
+
+			auto codec = std::get<1>(data).codec;
+			if (codec == ftl::codecs::codec_t::HEVC) std::get<1>(data).codec = ftl::codecs::codec_t::HEVC_LOSSLESS;
 		}
 		std::get<0>(data).version = 4;
 
@@ -151,6 +156,7 @@ bool File::tick() {
 		// the data buffer is already several frames ahead so is processed
 		// above. Hence, no need to bother parallelising this bit.
 		if (std::get<0>(data).timestamp <= timestamp_) {
+			std::get<0>(data).timestamp = ts;
 			if (cb_) {
 				dlk.lock();
 				try {
@@ -188,7 +194,7 @@ bool File::tick() {
 
 bool File::run() {
 	timer_ = ftl::timer::add(ftl::timer::kTimerMain, [this](int64_t ts) {
-		tick();
+		tick(ts);
 		return active_;
 	});
 	return true;
@@ -219,10 +225,10 @@ bool File::begin(bool dorun) {
 		// Capture current time to adjust timestamps
 		timestart_ = (ftl::timer::get_time() / ftl::timer::getInterval()) * ftl::timer::getInterval();
 		active_ = true;
-		interval_ = ftl::timer::getInterval();
+		interval_ = 50; //ftl::timer::getInterval();
 		timestamp_ = timestart_;
 
-		tick(); // Do some now!
+		tick(timestart_); // Do some now!
 		if (dorun) run();
 	} else if (mode_ == Mode::Write) {
 		if (!ostream_) ostream_ = new std::ofstream;
diff --git a/components/streams/src/netstream.cpp b/components/streams/src/netstream.cpp
index b5039d71aeb34bfb1f31d1dc72ba21f8e2593e39..b11fdbb7d3908afcdef41820389ac4baac831380 100644
--- a/components/streams/src/netstream.cpp
+++ b/components/streams/src/netstream.cpp
@@ -37,6 +37,7 @@ Net::Net(nlohmann::json &config, ftl::net::Universe *net) : Stream(config), net_
 	}
 
 	last_frame_ = 0;
+	last_msg_ = 0;
 	time_peer_ = ftl::UUID(0);
 }
 
@@ -53,7 +54,7 @@ bool Net::post(const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet
 	if (!active_) return false;
 
 	// Check if the channel has been requested recently enough. If not then disable it.
-	if (host_ && pkt.data.size() > 0 && static_cast<int>(spkt.channel) >= 0 && static_cast<int>(spkt.channel) < 32) {
+	if (host_ && pkt.data.size() > 0 && spkt.frame_number == 0 && static_cast<int>(spkt.channel) >= 0 && static_cast<int>(spkt.channel) < 32) {
 		if (reqtally_[static_cast<int>(spkt.channel)] == 0) {
 			auto sel = selected(0);
 			sel -= spkt.channel;
@@ -75,9 +76,9 @@ bool Net::post(const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet
 				auto &client = *c;
 
 				// Quality filter the packets
-				if (client.quality >= 0 && pkt.bitrate != client.quality) {
+				if (pkt.bitrate > 0 && pkt.bitrate != client.quality) {
 					++c;
-					LOG(INFO) << "INCORRECT QUALITY";
+					LOG(INFO) << "Incorrect quality: " << (int)pkt.bitrate << " but requested " << (int)client.quality;
 					continue;
 				}
 
@@ -150,16 +151,18 @@ bool Net::begin() {
 			if (last_frame_ != spkt.timestamp) {
 				last_frame_ = spkt.timestamp;
 
-				auto sel = selected(0);
+				if (size() > 0) {
+					auto sel = selected(0);
 
-				// A change in channel selections, so send those requests now
-				if (sel != last_selected_) {
-					auto changed = sel - last_selected_;
-					last_selected_ = sel;
+					// A change in channel selections, so send those requests now
+					if (sel != last_selected_) {
+						auto changed = sel - last_selected_;
+						last_selected_ = sel;
 
-					if (size() > 0) {
-						for (auto c : changed) {
-							_sendRequest(c, kAllFramesets, kAllFrames, 30, 0);
+						if (size() > 0) {
+							for (auto c : changed) {
+								_sendRequest(c, kAllFramesets, kAllFrames, 30, 0);
+							}
 						}
 					}
 				}
@@ -168,6 +171,8 @@ bool Net::begin() {
 				if (tally_ <= 5) {
 					// Yes, so send new requests
 					if (size() > 0) {
+						auto sel = selected(0);
+						
 						for (auto c : sel) {
 							_sendRequest(c, kAllFramesets, kAllFrames, 30, 0);
 						}
@@ -188,7 +193,7 @@ bool Net::begin() {
 				for (int i=0; i<size(); ++i) {
 					select(i, selected(i) + spkt.channel);
 				}
-				reqtally_[static_cast<int>(spkt.channel)] = static_cast<int>(pkt.frame_count)*size()*kTallyScale;
+				reqtally_[static_cast<int>(spkt.channel)] = static_cast<int>(pkt.frame_count)*kTallyScale;
 			} else {
 				select(spkt.frameSetID(), selected(spkt.frameSetID()) + spkt.channel);
 			}
@@ -224,6 +229,19 @@ bool Net::begin() {
 	return true;
 }
 
+void Net::reset() {
+	UNIQUE_LOCK(mutex_, lk);
+
+	if (size() > 0) {
+		auto sel = selected(0);
+		
+		for (auto c : sel) {
+			_sendRequest(c, kAllFramesets, kAllFrames, 30, 0);
+		}
+	}
+	tally_ = 30*kTallyScale;
+}
+
 bool Net::_sendRequest(Channel c, uint8_t frameset, uint8_t frames, uint8_t count, uint8_t bitrate) {
 	if (!active_ || host_) return false;
 
@@ -286,7 +304,7 @@ bool Net::_processRequest(ftl::net::Peer &p, const ftl::codecs::Packet &pkt) {
 		if (!found) {
 			auto &client = clients_.emplace_back();
 			client.peerid = p.id();
-			client.quality = 0;  // TODO: Use quality given in packet
+			client.quality = 255;  // TODO: Use quality given in packet
 			client.txcount = 0;
 			client.txmax = static_cast<int>(pkt.frame_count)*kTallyScale;
 		}
diff --git a/components/streams/src/receiver.cpp b/components/streams/src/receiver.cpp
index 36b454759e84be7d63fbb12596daab67fa4b5d8d..ad74db5d72c6b7809e8721949a3dff2df9429589 100644
--- a/components/streams/src/receiver.cpp
+++ b/components/streams/src/receiver.cpp
@@ -1,4 +1,5 @@
 #include <ftl/streams/receiver.hpp>
+#include <ftl/codecs/depth_convert_cuda.hpp>
 
 #include "parsers.hpp"
 #include "injectors.hpp"
@@ -54,14 +55,16 @@ Receiver::InternalStates::InternalStates() {
 	for (int i=0; i<32; ++i) decoders[i] = nullptr;
 }
 
-Receiver::InternalStates &Receiver::_getFrame(const StreamPacket &spkt) {
+Receiver::InternalStates &Receiver::_getFrame(const StreamPacket &spkt, int ix) {
+	int fn = spkt.frameNumber()+ix;
+
 	UNIQUE_LOCK(mutex_, lk);
-	while (frames_.size() <= spkt.frameNumber()) {
+	while (frames_.size() <= fn) {
 		//frames_.resize(spkt.frameNumber()+1);
 		frames_.push_back(new InternalStates);
-		frames_[frames_.size()-1]->state.set("name",std::string("Source ")+std::to_string(spkt.frameNumber()+1));
+		frames_[frames_.size()-1]->state.set("name",std::string("Source ")+std::to_string(fn+1));
 	}
-	auto &f = *frames_[spkt.frameNumber()];
+	auto &f = *frames_[fn];
 	if (!f.frame.origin()) f.frame.setOrigin(&f.state);
 	return f;
 }
@@ -78,7 +81,7 @@ void Receiver::setStream(ftl::stream::Stream *s) {
 		const ftl::codecs::Channel rchan = spkt.channel;
 		const unsigned int channum = (unsigned int)spkt.channel;
 
-		//LOG(INFO) << "PACKET: " << spkt.timestamp << ", " << (int)spkt.channel << ", " << (int)pkt.codec;
+		//LOG(INFO) << "PACKET: " << spkt.timestamp << ", " << (int)spkt.channel << ", " << (int)pkt.codec << ", " << (int)pkt.definition;
 
 		// TODO: Allow for multiple framesets
 		if (spkt.frameSetID() > 0) return;
@@ -89,48 +92,67 @@ void Receiver::setStream(ftl::stream::Stream *s) {
 		// Dummy no data packet.
 		if (pkt.data.size() == 0) return;
 
-		InternalStates &frame = _getFrame(spkt);
+		InternalStates &iframe = _getFrame(spkt);
+
+		auto [tx,ty] = ftl::codecs::chooseTileConfig(pkt.frame_count);
+		int width = ftl::codecs::getWidth(pkt.definition);
+		int height = ftl::codecs::getHeight(pkt.definition);
 
 		//if (spkt.timestamp > frame.timestamp && !frame.completed.empty()) {
 		//	LOG(WARNING) << "Next frame received";
 			//return;
 		//}
 
-		// Deal with the special channels...
-		switch (rchan) {
-		case Channel::Configuration		: frame.state.getConfig() = nlohmann::json::parse(parseConfig(pkt)); return;
-		case Channel::Calibration		: frame.state.getLeft() = parseCalibration(pkt); return;
-		case Channel::Calibration2		: frame.state.getRight() = parseCalibration(pkt); return;
-		case Channel::Pose				: frame.state.getPose() = parsePose(pkt); return;
-		default: break;
+		if (channum >= 64) {
+			for (int i=0; i<pkt.frame_count; ++i) {
+				InternalStates &frame = _getFrame(spkt,i);
+
+				// Deal with the special channels...
+				switch (rchan) {
+				case Channel::Configuration		: frame.state.getConfig() = nlohmann::json::parse(parseConfig(pkt)); break;
+				case Channel::Calibration		: frame.state.getLeft() = parseCalibration(pkt); break;
+				case Channel::Calibration2		: frame.state.getRight() = parseCalibration(pkt); break;
+				case Channel::Pose				: frame.state.getPose() = parsePose(pkt); break;
+				default: break;
+				}
+			}
+			return;
 		}
 
-		// Packets are for unwanted channel.
-		//if (rchan != Channel::Colour && rchan != chan) return;
-
-		if (frame.frame.hasChannel(spkt.channel)) {
-			// FIXME: Is this a corruption in recording or in playback?
-			// Seems to occur in same place in ftl file, one channel is missing
-			LOG(ERROR) << "Previous frame not complete: " << frame.timestamp;
-			//LOG(ERROR) << " --- " << (string)spkt;
-			UNIQUE_LOCK(frame.mutex, lk);
-			frame.frame.reset();
-			frame.completed.clear();
-		}
-		frame.timestamp = spkt.timestamp;
+		for (int i=0; i<pkt.frame_count; ++i) {
+			InternalStates &frame = _getFrame(spkt,i);
 
-		// Add channel to frame and allocate memory if required
-		const cv::Size size = cv::Size(ftl::codecs::getWidth(pkt.definition), ftl::codecs::getHeight(pkt.definition));
-		frame.frame.create<cv::cuda::GpuMat>(spkt.channel).create(size, (isFloatChannel(rchan) ? CV_32FC1 : CV_8UC4));
+			// Packets are for unwanted channel.
+			//if (rchan != Channel::Colour && rchan != chan) return;
+
+			if (frame.frame.hasChannel(spkt.channel)) {
+				// FIXME: Is this a corruption in recording or in playback?
+				// Seems to occur in same place in ftl file, one channel is missing
+				LOG(ERROR) << "Previous frame not complete: " << frame.timestamp;
+				//LOG(ERROR) << " --- " << (string)spkt;
+				UNIQUE_LOCK(frame.mutex, lk);
+				frame.frame.reset();
+				frame.completed.clear();
+			}
+			frame.timestamp = spkt.timestamp;
+
+			// Add channel to frame and allocate memory if required
+			const cv::Size size = cv::Size(width, height);
+			frame.frame.create<cv::cuda::GpuMat>(spkt.channel).create(size, (isFloatChannel(rchan) ? CV_32FC1 : CV_8UC4));
+		}
 
 		Packet tmppkt = pkt;
-		frame.frame.pushPacket(spkt.channel, tmppkt);
+		iframe.frame.pushPacket(spkt.channel, tmppkt);
 
 		//LOG(INFO) << " CODEC = " << (int)pkt.codec << " " << (int)pkt.flags << " " << (int)spkt.channel;
+		//LOG(INFO) << "Decode surface: " << (width*tx) << "x" << (height*ty);
+
+		auto &surface = iframe.surface[static_cast<int>(spkt.channel)];
+		surface.create(height*ty, width*tx, ((isFloatChannel(spkt.channel)) ? ((pkt.flags & 0x2) ? CV_16UC4 : CV_16U) : CV_8UC4));
 
 		// Do the actual decode
-		_createDecoder(frame, channum, pkt);
-		auto *decoder = frame.decoders[channum];
+		_createDecoder(iframe, channum, pkt);
+		auto *decoder = iframe.decoders[channum];
 		if (!decoder) {
 			LOG(ERROR) << "No frame decoder available";
 			return;
@@ -138,40 +160,74 @@ void Receiver::setStream(ftl::stream::Stream *s) {
 
 		try {
 			//LOG(INFO) << "TYPE = " << frame.frame.get<cv::cuda::GpuMat>(spkt.channel).type();
-			decoder->decode(pkt, frame.frame.get<cv::cuda::GpuMat>(spkt.channel));
+			if (!decoder->decode(pkt, surface)) {
+				LOG(ERROR) << "Decode failed";
+				//return;
+			}
 		} catch (std::exception &e) {
 			LOG(ERROR) << "Decode failed for " << spkt.timestamp << ": " << e.what();
+			//return;
+		}
+
+		/*if (spkt.channel == Channel::Depth && (pkt.flags & 0x2)) {
+		cv::Mat tmp;
+		surface.download(tmp);
+		cv::imshow("Test", tmp);
+		cv::waitKey(1);
+		}*/
+
+		for (int i=0; i<pkt.frame_count; ++i) {
+			InternalStates &frame = _getFrame(spkt,i);
+
+			cv::Rect roi((i % tx)*width, (i / tx)*height, width, height);
+			cv::cuda::GpuMat sroi = surface(roi);
+
+			// Do colour conversion
+			if (isFloatChannel(rchan) && (pkt.flags & 0x2)) {
+				//LOG(INFO) << "VUYA Convert";
+				ftl::cuda::vuya_to_depth(frame.frame.get<cv::cuda::GpuMat>(spkt.channel), sroi, 16.0f, decoder->stream());
+			} else if (isFloatChannel(rchan)) {
+				sroi.convertTo(frame.frame.get<cv::cuda::GpuMat>(spkt.channel), CV_32FC1, 1.0f/1000.0f, decoder->stream());
+			} else {
+				cv::cuda::cvtColor(sroi, frame.frame.get<cv::cuda::GpuMat>(spkt.channel), cv::COLOR_RGBA2BGRA, 0, decoder->stream());
+			}
 		}
 
-		frame.completed += spkt.channel;
+		decoder->stream().waitForCompletion();
+
+		for (int i=0; i<pkt.frame_count; ++i) {
+			InternalStates &frame = _getFrame(spkt,i);
+			
+			frame.completed += spkt.channel;
 		
-		// Complete if all requested frames are found
-		auto sel = stream_->selected(spkt.frameSetID());
+			// Complete if all requested frames are found
+			auto sel = stream_->selected(spkt.frameSetID());
 
-		if ((frame.completed & sel) == sel) {
-			UNIQUE_LOCK(frame.mutex, lk);
 			if ((frame.completed & sel) == sel) {
-				timestamp_ = frame.timestamp;
+				UNIQUE_LOCK(frame.mutex, lk);
+				if ((frame.completed & sel) == sel) {
+					timestamp_ = frame.timestamp;
 
-				//LOG(INFO) << "BUILDER PUSH: " << timestamp_ << ", " << spkt.frameNumber();
+					//LOG(INFO) << "BUILDER PUSH: " << timestamp_ << ", " << spkt.frameNumber();
 
-				if (frame.state.getLeft().width == 0) {
-					LOG(WARNING) << "Missing calibration, skipping frame";
-					//frame.frame.reset();
-					//frame.completed.clear();
-					//return;
-				}
+					if (frame.state.getLeft().width == 0) {
+						LOG(WARNING) << "Missing calibration, skipping frame";
+						//frame.frame.reset();
+						//frame.completed.clear();
+						//return;
+					}
 
-				// TODO: Have multiple builders for different framesets.
-				builder_.push(frame.timestamp, spkt.frameNumber(), frame.frame);
+					// TODO: Have multiple builders for different framesets.
+					builder_.push(frame.timestamp, spkt.frameNumber()+i, frame.frame);
 
-				// Check for any state changes and send them back
-				if (frame.state.hasChanged(Channel::Pose)) injectPose(stream_, frame.frame, frame.timestamp, spkt.frameNumber());
-				if (frame.state.hasChanged(Channel::Calibration)) injectCalibration(stream_, frame.frame, frame.timestamp, spkt.frameNumber());
-				if (frame.state.hasChanged(Channel::Calibration2)) injectCalibration(stream_, frame.frame, frame.timestamp, spkt.frameNumber(), true);
+					// Check for any state changes and send them back
+					if (frame.state.hasChanged(Channel::Pose)) injectPose(stream_, frame.frame, frame.timestamp, spkt.frameNumber()+i);
+					if (frame.state.hasChanged(Channel::Calibration)) injectCalibration(stream_, frame.frame, frame.timestamp, spkt.frameNumber()+i);
+					if (frame.state.hasChanged(Channel::Calibration2)) injectCalibration(stream_, frame.frame, frame.timestamp, spkt.frameNumber()+i, true);
 
-				frame.frame.reset();
-				frame.completed.clear();
+					frame.frame.reset();
+					frame.completed.clear();
+				}
 			}
 		}
 	});
diff --git a/components/streams/src/sender.cpp b/components/streams/src/sender.cpp
index 275ad63f0cabc538004492dbd31e6e7c33acf040..261c8b2fec49e6e0ec0e7b066d3df852683c380f 100644
--- a/components/streams/src/sender.cpp
+++ b/components/streams/src/sender.cpp
@@ -1,4 +1,5 @@
 #include <ftl/streams/sender.hpp>
+#include <ftl/codecs/depth_convert_cuda.hpp>
 
 #include "injectors.hpp"
 
@@ -10,16 +11,21 @@ using ftl::codecs::Channel;
 using ftl::codecs::definition_t;
 using ftl::codecs::device_t;
 using ftl::codecs::codec_t;
+using ftl::codecs::format_t;
 using ftl::stream::injectCalibration;
 using ftl::stream::injectPose;
 using ftl::stream::injectConfig;
 
 Sender::Sender(nlohmann::json &config) : ftl::Configurable(config), stream_(nullptr) {
-	//do_inject_ = false;
+	do_inject_.test_and_set();
 }
 
 Sender::~Sender() {
     // Delete all encoders
+	for (auto c : state_) {
+		if (c.second.encoder[0]) ftl::codecs::free(c.second.encoder[0]);
+		if (c.second.encoder[1]) ftl::codecs::free(c.second.encoder[1]);
+	}
 }
 
 /*void Sender::onStateChange(const std::function<void(ftl::codecs::Channel,const ftl::rgbd::FrameState&)> &cb) {
@@ -31,9 +37,10 @@ void Sender::setStream(ftl::stream::Stream*s) {
 	if (stream_) stream_->onPacket(nullptr);
     stream_ = s;
 	stream_->onPacket([this](const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
-		LOG(INFO) << "SENDER REQUEST : " << (int)spkt.channel;
+		//LOG(INFO) << "SENDER REQUEST : " << (int)spkt.channel;
 
 		//if (state_cb_) state_cb_(spkt.channel, spkt.streamID, spkt.frame_number);
+		if (reqcb_) reqcb_(spkt,pkt);
 
 		// Inject state packets
 		//do_inject_ = true;
@@ -41,6 +48,10 @@ void Sender::setStream(ftl::stream::Stream*s) {
 	});
 }
 
+void Sender::onRequest(const ftl::stream::StreamCallback &cb) {
+	reqcb_ = cb;
+}
+
 template <typename T>
 static void writeValue(std::vector<unsigned char> &data, T value) {
 	unsigned char *pvalue_start = (unsigned char*)&value;
@@ -73,12 +84,12 @@ void Sender::post(const ftl::rgbd::FrameSet &fs) {
     if (!stream_) return;
 
     Channels selected;
-	if (stream_->size() > 0) selected = stream_->selected(0);
-
 	Channels available;  // but not selected and actually sent.
+	Channels needencoding;
+
+	if (stream_->size() > 0) selected = stream_->selected(0);
 
 	bool do_inject = !do_inject_.test_and_set();
-	//do_inject_ = false;
 
     for (int i=0; i<fs.frames.size(); ++i) {
         const auto &frame = fs.frames[i];
@@ -124,22 +135,7 @@ void Sender::post(const ftl::rgbd::FrameSet &fs) {
 						//}
 					}
 				} else  {
-					auto *enc = _getEncoder(fs.id, i, cc);
-
-					if (enc) {
-						// FIXME: Timestamps may not always be aligned to interval.
-						//if (do_inject || fs.timestamp % (10*ftl::timer::getInterval()) == 0) enc->reset();
-						if (do_inject) enc->reset();
-						try {
-							enc->encode(frame.get<cv::cuda::GpuMat>(cc), 0, [this,&spkt](const ftl::codecs::Packet &pkt){
-								stream_->post(spkt, pkt);
-							});
-						} catch (std::exception &e) {
-							LOG(ERROR) << "Exception in encoder: " << e.what();
-						}
-					} else {
-						LOG(ERROR) << "No encoder";
-					}
+					needencoding += c;
 				}
 			} else {
 				available += c;
@@ -164,23 +160,167 @@ void Sender::post(const ftl::rgbd::FrameSet &fs) {
 		stream_->post(spkt, pkt);
 	}
 
+	for (auto c : needencoding) {
+		// TODO: One thread per channel.
+		_encodeChannel(fs, c, do_inject);
+	}
+
 	//do_inject_ = false;
 }
 
-ftl::codecs::Encoder *Sender::_getEncoder(int fsid, int fid, Channel c) {
-	int id = (fsid << 16) | (fid << 8) | (int)c;
+void Sender::_encodeChannel(const ftl::rgbd::FrameSet &fs, Channel c, bool reset) {
+	bool lossless = value("lossless", false);
+	int max_bitrate = std::max(0, std::min(255, value("max_bitrate", 255)));
+	int min_bitrate = std::max(0, std::min(255, value("min_bitrate", 0)));
+	codec_t codec = value("codec", codec_t::Any);
+
+	int offset = 0;
+	while (offset < fs.frames.size()) {
+		StreamPacket spkt;
+		spkt.version = 4;
+		spkt.timestamp = fs.timestamp;
+		spkt.streamID = 0; // FIXME: fs.id;
+		spkt.frame_number = offset;
+		spkt.channel = c;
+
+		auto &tile = _getTile(fs.id, c);
+
+		ftl::codecs::Encoder *enc = tile.encoder[(offset==0)?0:1];
+		if (!enc) {
+			enc = ftl::codecs::allocateEncoder(
+				definition_t::HD1080, device_t::Any, codec);
+			tile.encoder[(offset==0)?0:1] = enc;
+		}
+
+		if (!enc) {
+			LOG(ERROR) << "No encoder";
+			return;
+		}
+
+		int count = _generateTiles(fs, offset, c, enc->stream(), lossless);
+		if (count <= 0) {
+			LOG(ERROR) << "Could not generate tiles.";
+			break;
+		}
+
+		if (enc) {
+			// FIXME: Timestamps may not always be aligned to interval.
+			//if (do_inject || fs.timestamp % (10*ftl::timer::getInterval()) == 0) enc->reset();
+			if (reset) enc->reset();
+
+			try {
+				ftl::codecs::Packet pkt;
+				pkt.frame_count = count;
+				pkt.codec = codec;
+				pkt.definition = definition_t::Any;
+				pkt.bitrate = max_bitrate;
+				pkt.flags = 0;
+
+				if (!lossless && ftl::codecs::isFloatChannel(c)) pkt.flags = ftl::codecs::kFlagFloat | ftl::codecs::kFlagMappedDepth;
+				else if (lossless && ftl::codecs::isFloatChannel(c)) pkt.flags = ftl::codecs::kFlagFloat;
+				else pkt.flags = ftl::codecs::kFlagFlipRGB;
+
+				// Choose correct region of interest into the surface.
+				cv::Rect roi = _generateROI(fs, c, offset);
+				cv::cuda::GpuMat sroi = tile.surface(roi);
+
+				if (enc->encode(sroi, pkt)) {
+					stream_->post(spkt, pkt);
+
+					/*cv::Mat tmp;
+					tile.surface.download(tmp);
+					cv::imshow("Test", tmp);
+					cv::waitKey(1);*/
+				} else {
+					LOG(ERROR) << "Encoding failed";
+				}
+			} catch (std::exception &e) {
+				LOG(ERROR) << "Exception in encoder: " << e.what();
+			}
+		} else {
+			LOG(ERROR) << "No encoder";
+		}
+
+		offset += count;
+	}
+}
+
+cv::Rect Sender::_generateROI(const ftl::rgbd::FrameSet &fs, ftl::codecs::Channel c, int offset) {
+	const ftl::rgbd::Frame *cframe = &fs.frames[offset];
+	int rwidth = cframe->get<cv::cuda::GpuMat>(c).cols;
+	int rheight = cframe->get<cv::cuda::GpuMat>(c).rows;
+	auto [tx,ty] = ftl::codecs::chooseTileConfig(fs.frames.size()-offset);
+	return cv::Rect(0, 0, tx*rwidth, ty*rheight);
+}
+
+Sender::EncodingState &Sender::_getTile(int fsid, Channel c) {
+	int id = (fsid << 8) | (int)c;
 
 	{
 		SHARED_LOCK(mutex_, lk);
-		auto i = encoders_.find(id);
-		if (i != encoders_.end()) {
+		auto i = state_.find(id);
+		if (i != state_.end()) {
 			return (*i).second;
 		}
 	}
 
-	auto *enc = ftl::codecs::allocateEncoder(
-					definition_t::HD1080, device_t::Any, codec_t::Any);
 	UNIQUE_LOCK(mutex_, lk);
-	encoders_[id] = enc;
-	return enc;
+	state_[id] = {
+		uint8_t(255),
+		{nullptr,nullptr},
+		cv::cuda::GpuMat(),
+		0
+	};
+	return state_[id];
+}
+
+float Sender::_selectFloatMax(Channel c) {
+	switch (c) {
+	case Channel::Depth		: return 16.0f;
+	default					: return 1.0f;
+	}
+}
+
+int Sender::_generateTiles(const ftl::rgbd::FrameSet &fs, int offset, Channel c, cv::cuda::Stream &stream, bool lossless) {
+	auto &surface = _getTile(fs.id, c);
+
+	const ftl::rgbd::Frame *cframe = &fs.frames[offset];
+	auto &m = cframe->get<cv::cuda::GpuMat>(c);
+
+	// Choose tile configuration and allocate memory
+	auto [tx,ty] = ftl::codecs::chooseTileConfig(fs.frames.size());
+	int rwidth = cframe->get<cv::cuda::GpuMat>(c).cols;
+	int rheight = cframe->get<cv::cuda::GpuMat>(c).rows;
+	int width = tx * rwidth;
+	int height = ty * rheight;
+	int tilecount = tx*ty;
+	int count = 0;
+
+	surface.surface.create(height, width, (lossless && m.type() == CV_32F) ? CV_16U : CV_8UC4);
+
+	// Loop over tiles with ROI mats and do colour conversions.
+	while (tilecount > 0 && count+offset < fs.frames.size()) {
+		auto &m = cframe->get<cv::cuda::GpuMat>(c);
+		cv::Rect roi((count % tx)*rwidth, (count / tx)*rheight, rwidth, rheight);
+		cv::cuda::GpuMat sroi = surface.surface(roi);
+
+		if (m.type() == CV_32F) {
+			if (lossless) {
+				m.convertTo(sroi, CV_16UC1, 1000, stream);
+			} else {
+				ftl::cuda::depth_to_vuya(m, sroi, _selectFloatMax(c), stream);
+			}
+		} else if (m.type() == CV_8UC4) {
+			cv::cuda::cvtColor(m, sroi, cv::COLOR_BGRA2RGBA, 0, stream);
+		} else {
+			LOG(ERROR) << "Unsupported colour format";
+			return 0;
+		}
+
+		++count;
+		--tilecount;
+		cframe = &fs.frames[offset+count];
+	}
+
+	return count;
 }
diff --git a/components/streams/src/stream.cpp b/components/streams/src/stream.cpp
index 4a01287f479bd815c4a1456940ac211b9d58069b..2259ba09204ec8dbe9e40b878aa63bec13f6a2a1 100644
--- a/components/streams/src/stream.cpp
+++ b/components/streams/src/stream.cpp
@@ -17,9 +17,10 @@ const ftl::codecs::Channels<0> &Stream::selected(int fs) const {
 	return state_[fs].selected;
 }
 
-void Stream::select(int fs, const ftl::codecs::Channels<0> &s) {
+void Stream::select(int fs, const ftl::codecs::Channels<0> &s, bool make) {
 	UNIQUE_LOCK(mtx_, lk);
-	if (fs < 0 || fs >= state_.size()) throw ftl::exception("Frameset index out-of-bounds");
+	if (fs < 0 || (!make && fs >= state_.size())) throw ftl::exception("Frameset index out-of-bounds");
+	if (fs >= state_.size()) state_.resize(fs+1);
 	state_[fs].selected = s;
 }
 
@@ -30,6 +31,10 @@ ftl::codecs::Channels<0> &Stream::available(int fs) {
 	return state_[fs].available;
 }
 
+void Stream::reset() {
+	// Clear available and selected?
+}
+
 // ==== Muxer ==================================================================
 
 Muxer::Muxer(nlohmann::json &config) : Stream(config), nid_(0) {
@@ -108,6 +113,12 @@ bool Muxer::active() {
 	return r;
 }
 
+void Muxer::reset() {
+	for (auto &s : streams_) {
+		s.stream->reset();
+	}
+}
+
 int Muxer::_lookup(int sid, int ssid) {
 	SHARED_LOCK(mutex_, lk);
 	auto &se = streams_[sid];
@@ -190,6 +201,12 @@ bool Broadcast::active() {
 	return r;
 }
 
+void Broadcast::reset() {
+	for (auto &s : streams_) {
+		s->reset();
+	}
+}
+
 // ==== Intercept ==============================================================
 
 Intercept::Intercept(nlohmann::json &config) : Stream(config) {
@@ -244,3 +261,7 @@ bool Intercept::end() {
 bool Intercept::active() {
 	return stream_->active();
 }
+
+void Intercept::reset() {
+	stream_->reset();
+}
diff --git a/components/streams/test/CMakeLists.txt b/components/streams/test/CMakeLists.txt
index 6fec3245c6eae652c93a68549bbedc513334212d..859e1bcbd8c41b1fbcbe0bd11bef30b3d0ca1691 100644
--- a/components/streams/test/CMakeLists.txt
+++ b/components/streams/test/CMakeLists.txt
@@ -34,3 +34,33 @@ add_test(FileStreamUnitTest filestream_unit)
 #	ftlcommon ftlcodecs ftlrgbd ftlnet)
 
 #add_test(NetStreamUnitTest netstream_unit)
+
+### Sender Unit ################################################################
+add_executable(sender_unit
+	./tests.cpp
+	./sender_unit.cpp
+	../src/sender.cpp
+	../src/stream.cpp
+	../src/injectors.cpp
+	../src/parsers.cpp
+)
+target_include_directories(sender_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include")
+target_link_libraries(sender_unit
+	ftlcommon ftlcodecs ftlrgbd)
+
+add_test(SenderUnitTest sender_unit)
+
+### Receiver Unit ##############################################################
+add_executable(receiver_unit
+	./tests.cpp
+	./receiver_unit.cpp
+	../src/receiver.cpp
+	../src/stream.cpp
+	../src/injectors.cpp
+	../src/parsers.cpp
+)
+target_include_directories(receiver_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include")
+target_link_libraries(receiver_unit
+	ftlcommon ftlcodecs ftlrgbd)
+
+add_test(ReceiverUnitTest receiver_unit)
diff --git a/components/streams/test/filestream_unit.cpp b/components/streams/test/filestream_unit.cpp
index 54895357e8a29cb8b4a49672b1d6e7872127f9ee..c58ffaf0e40ebc37c2a4f8ed3812c007b574b33e 100644
--- a/components/streams/test/filestream_unit.cpp
+++ b/components/streams/test/filestream_unit.cpp
@@ -72,7 +72,7 @@ TEST_CASE("ftl::stream::File write and read", "[stream]") {
 		//reader->tick();
 
 		REQUIRE( count == 3 );
-		REQUIRE( tspkt.timestamp == 0 );
+		REQUIRE( tspkt.timestamp > 0 );
 		REQUIRE( tspkt.streamID == 2 );
 		REQUIRE( tspkt.channel == ftl::codecs::Channel::Screen );
 	}
@@ -104,21 +104,21 @@ TEST_CASE("ftl::stream::File write and read", "[stream]") {
 
 		REQUIRE( count == 1 );
 		//REQUIRE( tspkt.timestamp == 0 );
-		auto itime = tspkt.timestamp;
+		//auto itime = tspkt.timestamp;
 
 		count = 0;
-		reader->tick();
+		reader->tick(0);
 		std::this_thread::sleep_for(std::chrono::milliseconds(10));
 
 		REQUIRE( count == 1 );
-		REQUIRE( tspkt.timestamp == itime+ftl::timer::getInterval() );
+		//REQUIRE( tspkt.timestamp == itime+ftl::timer::getInterval() );
 
 		count = 0;
-		reader->tick();
+		reader->tick(0);
 		std::this_thread::sleep_for(std::chrono::milliseconds(10));
 
 		REQUIRE( count == 1 );
-		REQUIRE( tspkt.timestamp == itime+2*ftl::timer::getInterval() );
+		//REQUIRE( tspkt.timestamp == itime+2*ftl::timer::getInterval() );
 		
 	}
 }
diff --git a/components/streams/test/receiver_unit.cpp b/components/streams/test/receiver_unit.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..86ce60b6c118eec5d8ff08789feed5d437723ed8
--- /dev/null
+++ b/components/streams/test/receiver_unit.cpp
@@ -0,0 +1,219 @@
+#include "catch.hpp"
+
+#include <ftl/streams/receiver.hpp>
+#include <ftl/codecs/nvpipe_encoder.hpp>
+#include "../src/injectors.hpp"
+
+using ftl::codecs::definition_t;
+using ftl::codecs::codec_t;
+using ftl::stream::Receiver;
+using ftl::rgbd::Frame;
+using ftl::rgbd::FrameSet;
+using ftl::codecs::Channel;
+using ftl::codecs::Channels;
+using ftl::config::json_t;
+
+class TestStream : public ftl::stream::Stream {
+	public:
+	explicit TestStream(nlohmann::json &config) : ftl::stream::Stream(config) {};
+	~TestStream() {};
+
+	bool onPacket(const std::function<void(const ftl::codecs::StreamPacket &, const ftl::codecs::Packet &)> &cb) {
+		cb_ = cb;
+		return true;
+	}
+
+	bool post(const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
+		available(spkt.streamID) += spkt.channel;
+		if (pkt.data.size() == 0) {
+			if (spkt.frameSetID() == 255) {
+				for (int i=0; i<size(); ++i) {
+					select(i, selected(i) + spkt.channel);
+				}
+			} else {
+				select(spkt.frameSetID(), selected(spkt.frameSetID()) + spkt.channel);
+			}
+		}
+		if (cb_) cb_(spkt, pkt);
+		return true;
+	}
+
+	bool begin() override { return true; }
+	bool end() override { return true; }
+	bool active() override { return true; }
+
+	private:
+	std::function<void(const ftl::codecs::StreamPacket &, const ftl::codecs::Packet &)> cb_;
+};
+
+
+TEST_CASE( "Receiver generating onFrameSet" ) {
+	json_t global = json_t{{"$id","ftl://test"}};
+	ftl::config::configure(global);
+
+	json_t cfg = json_t{
+		{"$id","ftl://test/1"}
+	};
+	auto *receiver = ftl::create<Receiver>(cfg);
+
+	json_t cfg2 = json_t{
+		{"$id","ftl://test/2"}
+	};
+	TestStream stream(cfg2);
+	receiver->setStream(&stream);
+
+	ftl::codecs::NvPipeEncoder encoder(definition_t::HD1080, definition_t::SD480);
+
+	ftl::codecs::Packet pkt;
+	pkt.codec = codec_t::Any;
+	pkt.bitrate = 255;
+	pkt.definition = definition_t::Any;
+	pkt.flags = 0;
+	pkt.frame_count = 1;
+
+	ftl::codecs::StreamPacket spkt;
+	spkt.version = 4;
+	spkt.timestamp = 10;
+	spkt.frame_number = 0;
+	spkt.channel = Channel::Colour;
+	spkt.streamID = 0;
+
+	ftl::rgbd::Frame dummy;
+	ftl::rgbd::FrameState state;
+	state.getLeft().width = 1280;
+	state.getLeft().height = 720;
+	dummy.setOrigin(&state);
+	ftl::stream::injectCalibration(&stream, dummy, 0, 0);
+
+	ftl::timer::start(false);
+
+	SECTION("a single colour frame") {
+		cv::cuda::GpuMat m(cv::Size(1280,720), CV_8UC4, cv::Scalar(0));
+
+		bool r = encoder.encode(m, pkt);
+		REQUIRE( r );
+
+		stream.post(spkt, pkt);
+
+		int count = 0;
+		receiver->onFrameSet([&count](ftl::rgbd::FrameSet &fs) {
+			++count;
+
+			REQUIRE( fs.timestamp == 10 );
+			REQUIRE( fs.frames.size() == 1 );
+			REQUIRE( fs.frames[0].hasChannel(Channel::Colour) );
+			REQUIRE( fs.frames[0].get<cv::cuda::GpuMat>(Channel::Colour).rows == 720 );
+			REQUIRE( fs.frames[0].get<cv::cuda::GpuMat>(Channel::Colour).type() == CV_8UC4 );
+
+			return true;
+		});
+
+		int i=10;
+		while (i-- > 0 && count < 1) std::this_thread::sleep_for(std::chrono::milliseconds(10));
+
+		REQUIRE( count == 1 );
+	}
+
+	SECTION("a tiled colour frame") {
+		cv::cuda::GpuMat m(cv::Size(2560,720), CV_8UC4, cv::Scalar(0));
+		ftl::stream::injectCalibration(&stream, dummy, 0, 1);
+
+		pkt.frame_count = 2;
+		bool r = encoder.encode(m, pkt);
+		REQUIRE( r );
+
+		stream.post(spkt, pkt);
+
+		int count = 0;
+		receiver->onFrameSet([&count](ftl::rgbd::FrameSet &fs) {
+			++count;
+
+			REQUIRE( fs.timestamp == 10 );
+			REQUIRE( fs.frames.size() == 2 );
+			REQUIRE( fs.frames[0].hasChannel(Channel::Colour) );
+			REQUIRE( fs.frames[0].get<cv::cuda::GpuMat>(Channel::Colour).rows == 720 );
+			REQUIRE( fs.frames[0].get<cv::cuda::GpuMat>(Channel::Colour).type() == CV_8UC4 );
+			REQUIRE( fs.frames[1].hasChannel(Channel::Colour) );
+			REQUIRE( fs.frames[1].get<cv::cuda::GpuMat>(Channel::Colour).rows == 720 );
+			REQUIRE( fs.frames[1].get<cv::cuda::GpuMat>(Channel::Colour).type() == CV_8UC4 );
+
+			return true;
+		});
+
+		int i=10;
+		while (i-- > 0 && count < 1) std::this_thread::sleep_for(std::chrono::milliseconds(10));
+
+		REQUIRE( count == 1 );
+	}
+
+	SECTION("a tiled lossy depth frame") {
+		cv::cuda::GpuMat m(cv::Size(2560,720), CV_8UC4, cv::Scalar(0));
+		ftl::stream::injectCalibration(&stream, dummy, 0, 1);
+
+		spkt.channel = Channel::Depth;
+		pkt.frame_count = 2;
+		pkt.flags = ftl::codecs::kFlagFloat | ftl::codecs::kFlagMappedDepth;
+		bool r = encoder.encode(m, pkt);
+		REQUIRE( r );
+
+		stream.post(spkt, pkt);
+
+		int count = 0;
+		receiver->onFrameSet([&count](ftl::rgbd::FrameSet &fs) {
+			++count;
+
+			REQUIRE( fs.timestamp == 10 );
+			REQUIRE( fs.frames.size() == 2 );
+			REQUIRE( fs.frames[0].hasChannel(Channel::Depth) );
+			REQUIRE( fs.frames[0].get<cv::cuda::GpuMat>(Channel::Depth).rows == 720 );
+			REQUIRE( fs.frames[0].get<cv::cuda::GpuMat>(Channel::Depth).type() == CV_32F );
+			REQUIRE( fs.frames[1].hasChannel(Channel::Depth) );
+			REQUIRE( fs.frames[1].get<cv::cuda::GpuMat>(Channel::Depth).rows == 720 );
+			REQUIRE( fs.frames[1].get<cv::cuda::GpuMat>(Channel::Depth).type() == CV_32F );
+
+			return true;
+		});
+
+		int i=10;
+		while (i-- > 0 && count < 1) std::this_thread::sleep_for(std::chrono::milliseconds(10));
+
+		REQUIRE( count == 1 );
+	}
+
+	SECTION("a tiled lossless depth frame") {
+		cv::cuda::GpuMat m(cv::Size(2560,720), CV_16U, cv::Scalar(0));
+		ftl::stream::injectCalibration(&stream, dummy, 0, 1);
+
+		spkt.channel = Channel::Depth;
+		pkt.frame_count = 2;
+		pkt.flags = ftl::codecs::kFlagFloat;
+		bool r = encoder.encode(m, pkt);
+		REQUIRE( r );
+
+		stream.post(spkt, pkt);
+
+		int count = 0;
+		receiver->onFrameSet([&count](ftl::rgbd::FrameSet &fs) {
+			++count;
+
+			REQUIRE( fs.timestamp == 10 );
+			REQUIRE( fs.frames.size() == 2 );
+			REQUIRE( fs.frames[0].hasChannel(Channel::Depth) );
+			REQUIRE( fs.frames[0].get<cv::cuda::GpuMat>(Channel::Depth).rows == 720 );
+			REQUIRE( fs.frames[0].get<cv::cuda::GpuMat>(Channel::Depth).type() == CV_32F );
+			REQUIRE( fs.frames[1].hasChannel(Channel::Depth) );
+			REQUIRE( fs.frames[1].get<cv::cuda::GpuMat>(Channel::Depth).rows == 720 );
+			REQUIRE( fs.frames[1].get<cv::cuda::GpuMat>(Channel::Depth).type() == CV_32F );
+
+			return true;
+		});
+
+		int i=10;
+		while (i-- > 0 && count < 1) std::this_thread::sleep_for(std::chrono::milliseconds(10));
+
+		REQUIRE( count == 1 );
+	}
+
+	ftl::timer::stop(true);
+	delete receiver;
+}
diff --git a/components/streams/test/sender_unit.cpp b/components/streams/test/sender_unit.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e6d945b51cc5717df4217bfa7b367e163657437c
--- /dev/null
+++ b/components/streams/test/sender_unit.cpp
@@ -0,0 +1,299 @@
+#include "catch.hpp"
+
+#include <ftl/streams/sender.hpp>
+#include <ftl/codecs/hevc.hpp>
+
+using ftl::codecs::definition_t;
+using ftl::codecs::codec_t;
+using ftl::stream::Sender;
+using ftl::rgbd::Frame;
+using ftl::rgbd::FrameSet;
+using ftl::codecs::Channel;
+using ftl::codecs::Channels;
+using ftl::config::json_t;
+
+class TestStream : public ftl::stream::Stream {
+	public:
+	explicit TestStream(nlohmann::json &config) : ftl::stream::Stream(config) {};
+	~TestStream() {};
+
+	bool onPacket(const std::function<void(const ftl::codecs::StreamPacket &, const ftl::codecs::Packet &)> &cb) {
+		cb_ = cb;
+		return true;
+	}
+
+	bool onIntercept(const std::function<void(const ftl::codecs::StreamPacket &, const ftl::codecs::Packet &)> &cb) {
+		icb_ = cb;
+		return true;
+	}
+
+	bool post(const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
+		available(spkt.streamID) += spkt.channel;
+		if (pkt.data.size() == 0) {
+			if (spkt.frameSetID() == 255) {
+				for (int i=0; i<size(); ++i) {
+					select(i, selected(i) + spkt.channel);
+				}
+			} else {
+				select(spkt.frameSetID(), selected(spkt.frameSetID()) + spkt.channel);
+			}
+			if (cb_) cb_(spkt, pkt);
+		}
+		if (icb_) icb_(spkt, pkt);
+		return true;
+	}
+
+	bool begin() override { return true; }
+	bool end() override { return true; }
+	bool active() override { return true; }
+
+	private:
+	std::function<void(const ftl::codecs::StreamPacket &, const ftl::codecs::Packet &)> cb_;
+	std::function<void(const ftl::codecs::StreamPacket &, const ftl::codecs::Packet &)> icb_;
+};
+
+
+TEST_CASE( "Sender::post() video frames" ) {
+	json_t global = json_t{{"$id","ftl://test"}};
+	ftl::config::configure(global);
+
+	json_t cfg = json_t{
+		{"$id","ftl://test/1"}
+	};
+	auto *sender = ftl::create<Sender>(cfg);
+	
+	FrameSet fs;
+	fs.frames.emplace_back();
+	fs.timestamp = 1000;
+
+	json_t cfg2 = json_t{
+		{"$id","ftl://test/2"}
+	};
+	TestStream stream(cfg2);
+	sender->setStream(&stream);
+
+	ftl::codecs::StreamPacket spkt;
+	ftl::codecs::Packet pkt;
+	int count = 0;
+
+	stream.onIntercept([&count,&spkt,&pkt](const ftl::codecs::StreamPacket &pspkt, const ftl::codecs::Packet &ppkt) {
+		spkt = pspkt;
+		pkt = ppkt;
+		++count;
+	});
+
+	SECTION("a single colour frame") {
+		stream.select(0, Channels(Channel::Colour), true);
+
+		fs.count = 1;
+		fs.frames[0].create<cv::cuda::GpuMat>(Channel::Colour).create(cv::Size(1280,720), CV_8UC4);
+		fs.frames[0].get<cv::cuda::GpuMat>(Channel::Colour).setTo(cv::Scalar(0));
+
+		sender->post(fs);
+
+		REQUIRE( count == 1 );
+		REQUIRE( spkt.version == 4 );
+		REQUIRE( spkt.timestamp == 1000 );
+		REQUIRE( (int)spkt.frame_number == 0 );
+		REQUIRE( spkt.streamID == 0 );
+		REQUIRE( spkt.channel == Channel::Colour );
+		REQUIRE( pkt.codec == codec_t::HEVC );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+		REQUIRE( pkt.data.size() > 0 );
+		REQUIRE( pkt.frame_count == 1 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
+
+	SECTION("two colour frames tiled") {
+		stream.select(0, Channels(Channel::Colour), true);
+
+		fs.count = 2;
+		fs.frames[0].create<cv::cuda::GpuMat>(Channel::Colour).create(cv::Size(1280,720), CV_8UC4);
+		fs.frames[0].get<cv::cuda::GpuMat>(Channel::Colour).setTo(cv::Scalar(0));
+		fs.frames.emplace_back();
+		fs.frames[1].create<cv::cuda::GpuMat>(Channel::Colour).create(cv::Size(1280,720), CV_8UC4);
+		fs.frames[1].get<cv::cuda::GpuMat>(Channel::Colour).setTo(cv::Scalar(0));
+
+		sender->post(fs);
+
+		REQUIRE( count == 1 );
+		REQUIRE( spkt.version == 4 );
+		REQUIRE( spkt.timestamp == 1000 );
+		REQUIRE( (int)spkt.frame_number == 0 );
+		REQUIRE( spkt.streamID == 0 );
+		REQUIRE( spkt.channel == Channel::Colour );
+		REQUIRE( pkt.codec == codec_t::HEVC );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+		REQUIRE( pkt.data.size() > 0 );
+		REQUIRE( pkt.frame_count == 2 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
+
+	SECTION("two depth frames tiled") {
+		stream.select(0, Channels(Channel::Depth), true);
+
+		fs.count = 2;
+		fs.frames[0].create<cv::cuda::GpuMat>(Channel::Depth).create(cv::Size(1280,720), CV_32F);
+		fs.frames[0].get<cv::cuda::GpuMat>(Channel::Depth).setTo(cv::Scalar(0.0f));
+		fs.frames.emplace_back();
+		fs.frames[1].create<cv::cuda::GpuMat>(Channel::Depth).create(cv::Size(1280,720), CV_32F);
+		fs.frames[1].get<cv::cuda::GpuMat>(Channel::Depth).setTo(cv::Scalar(0.0f));
+
+		sender->post(fs);
+
+		REQUIRE( count == 1 );
+		REQUIRE( spkt.version == 4 );
+		REQUIRE( spkt.timestamp == 1000 );
+		REQUIRE( (int)spkt.frame_number == 0 );
+		REQUIRE( spkt.streamID == 0 );
+		REQUIRE( spkt.channel == Channel::Depth );
+		REQUIRE( pkt.codec == codec_t::HEVC );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+		REQUIRE( pkt.data.size() > 0 );
+		REQUIRE( pkt.flags == (ftl::codecs::kFlagFloat | ftl::codecs::kFlagMappedDepth) );
+		REQUIRE( pkt.frame_count == 2 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
+
+	SECTION("10 depth frames tiled") {
+		stream.select(0, Channels(Channel::Depth), true);
+
+		fs.count = 10;
+		fs.frames[0].create<cv::cuda::GpuMat>(Channel::Depth).create(cv::Size(1280,720), CV_32F);
+		fs.frames[0].get<cv::cuda::GpuMat>(Channel::Depth).setTo(cv::Scalar(0.0f));
+
+		for (int i=1; i<10; ++i) {
+			fs.frames.emplace_back();
+			fs.frames[i].create<cv::cuda::GpuMat>(Channel::Depth).create(cv::Size(1280,720), CV_32F);
+			fs.frames[i].get<cv::cuda::GpuMat>(Channel::Depth).setTo(cv::Scalar(0.0f));
+		}
+
+		sender->post(fs);
+
+		REQUIRE( count == 2 );
+		REQUIRE( spkt.version == 4 );
+		REQUIRE( spkt.timestamp == 1000 );
+		REQUIRE( (int)spkt.frame_number == 9 );
+		REQUIRE( spkt.streamID == 0 );
+		REQUIRE( spkt.channel == Channel::Depth );
+		REQUIRE( pkt.codec == codec_t::HEVC );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+		REQUIRE( pkt.data.size() > 0 );
+		REQUIRE( pkt.flags == (ftl::codecs::kFlagFloat | ftl::codecs::kFlagMappedDepth) );
+		REQUIRE( pkt.frame_count == 1 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
+
+	SECTION("two lossless depth frames tiled") {
+		stream.select(0, Channels(Channel::Depth), true);
+
+		fs.count = 2;
+		fs.frames[0].create<cv::cuda::GpuMat>(Channel::Depth).create(cv::Size(1280,720), CV_32F);
+		fs.frames[0].get<cv::cuda::GpuMat>(Channel::Depth).setTo(cv::Scalar(0.0f));
+		fs.frames.emplace_back();
+		fs.frames[1].create<cv::cuda::GpuMat>(Channel::Depth).create(cv::Size(1280,720), CV_32F);
+		fs.frames[1].get<cv::cuda::GpuMat>(Channel::Depth).setTo(cv::Scalar(0.0f));
+
+		sender->set("lossless", true);
+		sender->post(fs);
+
+		REQUIRE( count == 1 );
+		REQUIRE( spkt.version == 4 );
+		REQUIRE( spkt.timestamp == 1000 );
+		REQUIRE( (int)spkt.frame_number == 0 );
+		REQUIRE( spkt.streamID == 0 );
+		REQUIRE( spkt.channel == Channel::Depth );
+		REQUIRE( pkt.codec == codec_t::HEVC_LOSSLESS );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+		REQUIRE( pkt.data.size() > 0 );
+		REQUIRE( pkt.flags == (ftl::codecs::kFlagFloat) );
+		REQUIRE( pkt.frame_count == 2 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
+
+	SECTION("one frame and two channels") {
+		stream.select(0, Channel::Colour + Channel::Depth, true);
+
+		fs.count = 1;
+		fs.frames[0].create<cv::cuda::GpuMat>(Channel::Colour).create(cv::Size(1280,720), CV_8UC4);
+		fs.frames[0].get<cv::cuda::GpuMat>(Channel::Colour).setTo(cv::Scalar(0));
+		fs.frames[0].create<cv::cuda::GpuMat>(Channel::Depth).create(cv::Size(1280,720), CV_32F);
+		fs.frames[0].get<cv::cuda::GpuMat>(Channel::Depth).setTo(cv::Scalar(0.0f));
+
+		sender->post(fs);
+
+		REQUIRE( count == 2 );
+		REQUIRE( spkt.version == 4 );
+		REQUIRE( spkt.timestamp == 1000 );
+		REQUIRE( (int)spkt.frame_number == 0 );
+		REQUIRE( spkt.streamID == 0 );
+		REQUIRE( spkt.channel == Channel::Depth );
+		REQUIRE( pkt.codec == codec_t::HEVC );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+		REQUIRE( pkt.data.size() > 0 );
+		REQUIRE( pkt.frame_count == 1 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
+
+	//ftl::config::cleanup();
+	delete sender;
+}
+
+TEST_CASE( "Sender request to control encoding" ) {
+	json_t global = json_t{{"$id","ftl://test"}};
+	ftl::config::configure(global);
+
+	json_t cfg = json_t{
+		{"$id","ftl://test/1"}
+	};
+	auto *sender = ftl::create<Sender>(cfg);
+	
+	FrameSet fs;
+	fs.frames.emplace_back();
+	fs.timestamp = 1000;
+
+	json_t cfg2 = json_t{
+		{"$id","ftl://test/2"}
+	};
+	TestStream stream(cfg2);
+	sender->setStream(&stream);
+
+	ftl::codecs::StreamPacket spkt;
+	ftl::codecs::Packet pkt;
+	int count = 0;
+
+	stream.onIntercept([&count,&spkt,&pkt](const ftl::codecs::StreamPacket &pspkt, const ftl::codecs::Packet &ppkt) {
+		spkt = pspkt;
+		pkt = ppkt;
+		++count;
+	});
+
+	SECTION("a single colour frame request") {
+		//stream.select(0, Channels(Channel::Colour), true);
+
+		stream.post({
+			4, 1000, 0, 255, Channel::Colour
+		},{
+			codec_t::Any, definition_t::Any, 255, 255, 0, {}
+		});
+
+		fs.count = 1;
+		fs.frames[0].create<cv::cuda::GpuMat>(Channel::Colour).create(cv::Size(1280,720), CV_8UC4);
+		fs.frames[0].get<cv::cuda::GpuMat>(Channel::Colour).setTo(cv::Scalar(0));
+
+		count = 0;
+		sender->post(fs);
+
+		REQUIRE( count == 5 );
+		REQUIRE( spkt.version == 4 );
+		REQUIRE( spkt.timestamp == 1000 );
+		REQUIRE( (int)spkt.frame_number == 0 );
+		REQUIRE( spkt.streamID == 0 );
+		REQUIRE( spkt.channel == Channel::Colour );
+		REQUIRE( pkt.codec == codec_t::HEVC );
+		REQUIRE( pkt.definition == definition_t::HD720 );
+		REQUIRE( pkt.data.size() > 0 );
+		REQUIRE( pkt.frame_count == 1 );
+		REQUIRE( ftl::codecs::hevc::validNAL(pkt.data.data(), pkt.data.size()) );
+	}
+}
diff --git a/python/ftl/ftltype.py b/python/ftl/ftltype.py
index 4120fdb18ca2c9fd6c7f98da4a315a0cebb45edf..9efad0172ced6891cddb79af0c7234e94f5a6381 100644
--- a/python/ftl/ftltype.py
+++ b/python/ftl/ftltype.py
@@ -58,7 +58,7 @@ _float_channels = [
 def is_float_channel(channel):
     return channel in _float_channels
 
-# components/codecs/include/ftl/codecs/bitrates.hpp
+# components/codecs/include/ftl/codecs/codecs.hpp
 class codec_t(IntEnum):
     JPG = 0
     PNG = 1