diff --git a/applications/groupview/src/main.cpp b/applications/groupview/src/main.cpp
index 3a821a01323fbe51dbc8b6fdef98597f0bd20853..d1dab2607ce15ea1ec5defe63aa4ff2b48ee95b6 100644
--- a/applications/groupview/src/main.cpp
+++ b/applications/groupview/src/main.cpp
@@ -90,15 +90,17 @@ void modeLeftRight(ftl::Configurable *root) {
 	group.sync([&mutex, &new_frames, &rgb_new](ftl::rgbd::FrameSet &frames) {
 		mutex.lock();
 		bool good = true;
-		for (size_t i = 0; i < frames.channel1.size(); i ++) {
-			if (frames.channel1[i].empty()) good = false;
-			if (frames.channel1[i].empty()) good = false;
-			if (frames.channel1[i].channels() != 3) good = false; // ASSERT
-			if (frames.channel2[i].channels() != 3) good = false;
+		for (size_t i = 0; i < frames.frames.size(); i ++) {
+			auto &chan1 = frames.frames[i].getChannel<cv::Mat>(ftl::rgbd::kChanColour);
+			auto &chan2 = frames.frames[i].getChannel<cv::Mat>(frames.sources[i]->getChannel());
+			if (chan1.empty()) good = false;
+			if (chan2.empty()) good = false;
+			if (chan1.channels() != 3) good = false; // ASSERT
+			if (chan2.channels() != 3) good = false;
 			if (!good) break;
 			
-			frames.channel1[i].copyTo(rgb_new[2 * i]);
-			frames.channel2[i].copyTo(rgb_new[2 * i + 1]);
+			chan1.copyTo(rgb_new[2 * i]);
+			chan2.copyTo(rgb_new[2 * i + 1]);
 		}
 
 		new_frames = good;
@@ -180,14 +182,19 @@ void modeFrame(ftl::Configurable *root, int frames=1) {
 		//LOG(INFO) << "Complete set: " << fs.timestamp;
 		if (!ftl::running) { return false; }
 		
+		std::vector<cv::Mat> frames;
 
 		for (size_t i=0; i<fs.sources.size(); ++i) {
-			if (fs.channel1[i].empty() || fs.channel2[i].empty()) return true;	
+			auto &chan1 = fs.frames[i].getChannel<cv::Mat>(ftl::rgbd::kChanColour);
+			auto &chan2 = fs.frames[i].getChannel<cv::Mat>(fs.sources[i]->getChannel());
+			if (chan1.empty() || chan2.empty()) return true;
+
+			frames.push_back(chan1);
 		}
 
 		cv::Mat show;
 
-		stack(fs.channel1, show);
+		stack(frames, show);
 
 		cv::resize(show, show, cv::Size(1280,720));
 		cv::namedWindow("Cameras", cv::WINDOW_KEEPRATIO | cv::WINDOW_NORMAL);
@@ -206,9 +213,12 @@ void modeFrame(ftl::Configurable *root, int frames=1) {
 			auto writer = ftl::rgbd::SnapshotWriter(std::string(timestamp) + ".tar.gz");
 
 			for (size_t i=0; i<fs.sources.size(); ++i) {
+				auto &chan1 = fs.frames[i].getChannel<cv::Mat>(ftl::rgbd::kChanColour);
+				auto &chan2 = fs.frames[i].getChannel<cv::Mat>(fs.sources[i]->getChannel());
+
 				writer.addSource(fs.sources[i]->getURI(), fs.sources[i]->parameters(), fs.sources[i]->getPose());
-				LOG(INFO) << "SAVE: " << fs.channel1[i].cols << ", " << fs.channel2[i].type();
-				writer.addRGBD(i, fs.channel1[i], fs.channel2[i]);
+				//LOG(INFO) << "SAVE: " << fs.channel1[i].cols << ", " << fs.channel2[i].type();
+				writer.addRGBD(i, chan1, chan2);
 			}
 		}
 #endif  // HAVE_LIBARCHIVE
diff --git a/applications/reconstruct/src/voxel_scene.cpp b/applications/reconstruct/src/voxel_scene.cpp
index cba6e61e9e9134845e59d71dd6e69d4ad7ae8beb..ab8b2c1dd25b530e1ffb4fcf4a29c42bd86dfbb6 100644
--- a/applications/reconstruct/src/voxel_scene.cpp
+++ b/applications/reconstruct/src/voxel_scene.cpp
@@ -233,6 +233,8 @@ int SceneRep::upload(ftl::rgbd::FrameSet &fs) {
 
 	for (size_t i=0; i<cameras_.size(); ++i) {
 		auto &cam = cameras_[i];
+		auto &chan1 = fs.frames[i].getChannel<cv::Mat>(ftl::rgbd::kChanColour);
+		auto &chan2 = fs.frames[i].getChannel<cv::Mat>(fs.sources[i]->getChannel());
 
 		// Get the RGB-Depth frame from input
 		Source *input = cam.source;
@@ -247,12 +249,12 @@ int SceneRep::upload(ftl::rgbd::FrameSet &fs) {
 
 		// Must be in RGBA for GPU
 		Mat rgbt, rgba;
-		cv::cvtColor(fs.channel1[i],rgbt, cv::COLOR_BGR2Lab);
+		cv::cvtColor(chan1,rgbt, cv::COLOR_BGR2Lab);
 		cv::cvtColor(rgbt,rgba, cv::COLOR_BGR2BGRA);
 
 		// Send to GPU and merge view into scene
 		//cam.gpu.updateParams(cam.params);
-		cam.gpu.updateData(fs.channel2[i], rgba, cam.stream);
+		cam.gpu.updateData(chan2, rgba, cam.stream);
 
 		//setLastRigidTransform(input->getPose().cast<float>());
 
diff --git a/components/rgbd-sources/include/ftl/rgbd/frame.hpp b/components/rgbd-sources/include/ftl/rgbd/frame.hpp
index 227913e5de056f15380390201da96b66ec23e27f..a1822fd163174bf9a55a697fa7736c0ac3d9a5bb 100644
--- a/components/rgbd-sources/include/ftl/rgbd/frame.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/frame.hpp
@@ -12,6 +12,7 @@ namespace rgbd {
 typedef unsigned int channel_t;
 
 static const channel_t kChanNone = 0;
+static const channel_t kChanColour = 0x0001;
 static const channel_t kChanLeft = 0x0001;		// CV_8UC3
 static const channel_t kChanDepth = 0x0002;		// CV_32FC1
 static const channel_t kChanRight = 0x0004;		// CV_8UC3
@@ -58,7 +59,7 @@ public:
 	 */
 	bool hasChannel(const ftl::rgbd::channel_t& channel)
 	{
-		return available_[_channelIdx(channel)];
+		return (channel == ftl::rgbd::kChanNone) ? true : available_[_channelIdx(channel)];
 	}
 
 	/* @brief	Method to get reference to the channel content
diff --git a/components/rgbd-sources/include/ftl/rgbd/frameset.hpp b/components/rgbd-sources/include/ftl/rgbd/frameset.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..93839efda9c6dc8cfe53e6775293949d5c1747e8
--- /dev/null
+++ b/components/rgbd-sources/include/ftl/rgbd/frameset.hpp
@@ -0,0 +1,32 @@
+#ifndef _FTL_RGBD_FRAMESET_HPP_
+#define _FTL_RGBD_FRAMESET_HPP_
+
+#include <ftl/rgbd/frame.hpp>
+
+#include <opencv2/opencv.hpp>
+#include <vector>
+
+namespace ftl {
+namespace rgbd {
+
+class Source;
+
+/**
+ * Represents a set of synchronised frames, each with two channels. This is
+ * used to collect all frames from multiple computers that have the same
+ * timestamp.
+ */
+struct FrameSet {
+	int64_t timestamp;				// Millisecond timestamp of all frames
+	std::vector<Source*> sources;	// All source objects involved.
+	std::vector<ftl::rgbd::Frame> frames;
+	std::atomic<int> count;				// Number of valid frames
+	std::atomic<unsigned int> mask;		// Mask of all sources that contributed
+	bool stale;						// True if buffers have been invalidated
+	SHARED_MUTEX mtx;
+};
+
+}
+}
+
+#endif  // _FTL_RGBD_FRAMESET_HPP_
diff --git a/components/rgbd-sources/include/ftl/rgbd/group.hpp b/components/rgbd-sources/include/ftl/rgbd/group.hpp
index 000eea0ae60303d816e09298613ddc30f0a8146a..e4ffb5984e625b1b9859a3f193a7f4b44c956684 100644
--- a/components/rgbd-sources/include/ftl/rgbd/group.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/group.hpp
@@ -3,6 +3,8 @@
 
 #include <ftl/threads.hpp>
 #include <ftl/timer.hpp>
+#include <ftl/rgbd/frame.hpp>
+#include <ftl/rgbd/frameset.hpp>
 
 #include <opencv2/opencv.hpp>
 #include <vector>
@@ -12,22 +14,6 @@ namespace rgbd {
 
 class Source;
 
-/**
- * Represents a set of synchronised frames, each with two channels. This is
- * used to collect all frames from multiple computers that have the same
- * timestamp.
- */
-struct FrameSet {
-	int64_t timestamp;				// Millisecond timestamp of all frames
-	std::vector<Source*> sources;	// All source objects involved.
-	std::vector<cv::Mat> channel1;	// RGB
-	std::vector<cv::Mat> channel2;	// Depth (usually)
-	std::atomic<int> count;				// Number of valid frames
-	std::atomic<unsigned int> mask;		// Mask of all sources that contributed
-	bool stale;						// True if buffers have been invalidated
-	SHARED_MUTEX mtx;
-};
-
 // Allows a latency of 20 frames maximum
 static const size_t kFrameBufferSize = 20;
 
diff --git a/components/rgbd-sources/src/group.cpp b/components/rgbd-sources/src/group.cpp
index e61289220ad92fff8e278905a075f01d93b6ad9a..57d770ba12db4f4b15aebf48cedd33aa2b880fa5 100644
--- a/components/rgbd-sources/src/group.cpp
+++ b/components/rgbd-sources/src/group.cpp
@@ -52,6 +52,8 @@ void Group::addSource(ftl::rgbd::Source *src) {
 	src->setCallback([this,ix,src](int64_t timestamp, cv::Mat &rgb, cv::Mat &depth) {
 		if (timestamp == 0) return;
 
+		auto chan = src->getChannel();
+
 		//LOG(INFO) << "SRC CB: " << timestamp << " (" << framesets_[head_].timestamp << ")";
 
 		UNIQUE_LOCK(mutex_, lk);
@@ -73,11 +75,15 @@ void Group::addSource(ftl::rgbd::Source *src) {
 
 				//LOG(INFO) << "Adding frame: " << ix << " for " << timestamp;
 				// Ensure channels match source mat format
-				fs.channel1[ix].create(rgb.size(), rgb.type());
-				fs.channel2[ix].create(depth.size(), depth.type());
+				//fs.channel1[ix].create(rgb.size(), rgb.type());
+				//fs.channel2[ix].create(depth.size(), depth.type());
+				fs.frames[ix].setChannel<cv::Mat>(ftl::rgbd::kChanColour).create(rgb.size(), rgb.type());
+				fs.frames[ix].setChannel<cv::Mat>(chan).create(depth.size(), depth.type());
 
-				cv::swap(rgb, fs.channel1[ix]);
-				cv::swap(depth, fs.channel2[ix]);
+				//cv::swap(rgb, fs.channel1[ix]);
+				//cv::swap(depth, fs.channel2[ix]);
+				cv::swap(rgb, fs.frames[ix].setChannel<cv::Mat>(ftl::rgbd::kChanColour));
+				cv::swap(depth, fs.frames[ix].setChannel<cv::Mat>(chan));
 
 				++fs.count;
 				fs.mask |= (1 << ix);
@@ -271,8 +277,9 @@ void Group::_addFrameset(int64_t timestamp) {
 		framesets_[head_].count = 0;
 		framesets_[head_].mask = 0;
 		framesets_[head_].stale = false;
-		framesets_[head_].channel1.resize(sources_.size());
-		framesets_[head_].channel2.resize(sources_.size());
+		//framesets_[head_].channel1.resize(sources_.size());
+		//framesets_[head_].channel2.resize(sources_.size());
+		framesets_[head_].frames.resize(sources_.size());
 
 		if (framesets_[head_].sources.size() != sources_.size()) {
 			framesets_[head_].sources.clear();
@@ -301,8 +308,9 @@ void Group::_addFrameset(int64_t timestamp) {
 		framesets_[head_].count = 0;
 		framesets_[head_].mask = 0;
 		framesets_[head_].stale = false;
-		framesets_[head_].channel1.resize(sources_.size());
-		framesets_[head_].channel2.resize(sources_.size());
+		//framesets_[head_].channel1.resize(sources_.size());
+		//framesets_[head_].channel2.resize(sources_.size());
+		framesets_[head_].frames.resize(sources_.size());
 
 		if (framesets_[head_].sources.size() != sources_.size()) {
 			framesets_[head_].sources.clear();
diff --git a/components/rgbd-sources/src/streamer.cpp b/components/rgbd-sources/src/streamer.cpp
index 4a9ccf529fbfcfac285312e1651d5a4b292fb849..c2676ac6a07b2c47914da9affffde5823d0fb9ca 100644
--- a/components/rgbd-sources/src/streamer.cpp
+++ b/components/rgbd-sources/src/streamer.cpp
@@ -365,7 +365,8 @@ void Streamer::_process(ftl::rgbd::FrameSet &fs) {
 		if (!src) continue;
 		if (!fs.sources[j]->isReady()) continue;
 		if (src->clientCount == 0) continue;
-		if (fs.channel1[j].empty() || (fs.sources[j]->getChannel() != ftl::rgbd::kChanNone && fs.channel2[j].empty())) continue;
+		//if (fs.channel1[j].empty() || (fs.sources[j]->getChannel() != ftl::rgbd::kChanNone && fs.channel2[j].empty())) continue;
+		if (!fs.frames[j].hasChannel(ftl::rgbd::kChanColour) || !fs.frames[j].hasChannel(fs.sources[j]->getChannel())) continue;
 
 		bool hasChan2 = fs.sources[j]->getChannel() != ftl::rgbd::kChanNone;
 
@@ -387,14 +388,14 @@ void Streamer::_process(ftl::rgbd::FrameSet &fs) {
 				// Receiver only waits for channel 1 by default
 				// TODO: Each encode could be done in own thread
 				if (hasChan2) {
-					enc2->encode(fs.channel2[j], src->hq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
+					enc2->encode(fs.frames[j].getChannel<cv::Mat>(fs.sources[j]->getChannel()), src->hq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
 						_transmitPacket(src, blk, 1, hasChan2, true);
 					});
 				} else {
 					if (enc2) enc2->reset();
 				}
 
-				enc1->encode(fs.channel1[j], src->hq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
+				enc1->encode(fs.frames[j].getChannel<cv::Mat>(ftl::rgbd::kChanColour), src->hq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
 					_transmitPacket(src, blk, 0, hasChan2, true);
 				});
 			}
@@ -415,14 +416,14 @@ void Streamer::_process(ftl::rgbd::FrameSet &fs) {
 				// Important to send channel 2 first if needed...
 				// Receiver only waits for channel 1 by default
 				if (hasChan2) {
-					enc2->encode(fs.channel2[j], src->lq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
+					enc2->encode(fs.frames[j].getChannel<cv::Mat>(fs.sources[j]->getChannel()), src->lq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
 						_transmitPacket(src, blk, 1, hasChan2, false);
 					});
 				} else {
 					if (enc2) enc2->reset();
 				}
 
-				enc1->encode(fs.channel1[j], src->lq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
+				enc1->encode(fs.frames[j].getChannel<cv::Mat>(ftl::rgbd::kChanColour), src->lq_bitrate, [this,src,hasChan2](const ftl::codecs::Packet &blk){
 					_transmitPacket(src, blk, 0, hasChan2, false);
 				});
 			}