From cda58132ae4aa7b9b22bdde0d7a3444ceaabdd7d Mon Sep 17 00:00:00 2001
From: Nicolas Pope <nicolas.pope@utu.fi>
Date: Tue, 15 Oct 2019 09:27:57 +0300
Subject: [PATCH] Fix stream bugs

---
 applications/ftl2mkv/src/main.cpp             |   6 +-
 applications/player/src/main.cpp              |  99 ++++++++--------
 applications/reconstruct/src/main.cpp         |   6 +-
 components/codecs/CMakeLists.txt              |   1 +
 .../codecs/include/ftl/codecs/channels.hpp    |   3 +
 components/codecs/include/ftl/codecs/hevc.hpp | 112 ++++++++++++++++++
 .../include/ftl/codecs/nvpipe_decoder.hpp     |   1 +
 .../codecs/include/ftl/codecs/reader.hpp      |   2 +-
 components/codecs/src/channels.cpp            |  93 +++++++++++++++
 components/codecs/src/nvpipe_decoder.cpp      |  13 +-
 components/codecs/src/reader.cpp              |  34 ++++--
 components/renderers/cpp/src/splat_render.cpp |   2 +
 .../include/ftl/rgbd/detail/netframe.hpp      |   5 +-
 .../rgbd-sources/include/ftl/rgbd/frame.hpp   |   7 +-
 components/rgbd-sources/src/file_source.cpp   |  28 ++++-
 components/rgbd-sources/src/file_source.hpp   |   1 +
 components/rgbd-sources/src/group.cpp         |   7 +-
 components/rgbd-sources/src/net.cpp           |  46 ++++---
 components/rgbd-sources/src/streamer.cpp      |   1 +
 19 files changed, 368 insertions(+), 99 deletions(-)
 create mode 100644 components/codecs/include/ftl/codecs/hevc.hpp
 create mode 100644 components/codecs/src/channels.cpp

diff --git a/applications/ftl2mkv/src/main.cpp b/applications/ftl2mkv/src/main.cpp
index 4ee909fee..b555dcbf7 100644
--- a/applications/ftl2mkv/src/main.cpp
+++ b/applications/ftl2mkv/src/main.cpp
@@ -3,6 +3,7 @@
 #include <ftl/codecs/reader.hpp>
 #include <ftl/codecs/packet.hpp>
 #include <ftl/rgbd/camera.hpp>
+#include <ftl/codecs/hevc.hpp>
 
 #include <fstream>
 
@@ -188,10 +189,7 @@ int main(int argc, char **argv) {
 
 			bool keyframe = false;
 			if (pkt.codec == codec_t::HEVC) {
-				// Obtain NAL unit type
-				int nal_type = (pkt.data[4] >> 1) & 0x3F;
-				// A type of 32 = VPS unit (so in this case a key frame)
-				if (nal_type == 32) {
+				if (ftl::codecs::hevc::isIFrame(pkt.data)) {
 					seen_key[spkt.streamID] = true;
 					keyframe = true;
 				}
diff --git a/applications/player/src/main.cpp b/applications/player/src/main.cpp
index 751d9969a..cc0f5039a 100644
--- a/applications/player/src/main.cpp
+++ b/applications/player/src/main.cpp
@@ -4,6 +4,7 @@
 #include <ftl/codecs/decoder.hpp>
 #include <ftl/codecs/packet.hpp>
 #include <ftl/rgbd/camera.hpp>
+#include <ftl/timer.hpp>
 
 #include <fstream>
 
@@ -56,53 +57,57 @@ int main(int argc, char **argv) {
     int current_stream = 0;
     int current_channel = 0;
 
-    bool res = r.read(90000000000000, [&current_stream,&current_channel,&r](const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
-        if (spkt.channel != static_cast<ftl::codecs::Channel>(current_channel)) return;
-        if (spkt.streamID == current_stream) {
-
-            if (pkt.codec == codec_t::POSE) {
-                Eigen::Matrix4d p = Eigen::Map<Eigen::Matrix4d>((double*)pkt.data.data());
-                LOG(INFO) << "Have pose: " << p;
-                return;
-            }
-
-            if (pkt.codec == codec_t::CALIBRATION) {
-                ftl::rgbd::Camera *camera = (ftl::rgbd::Camera*)pkt.data.data();
-                LOG(INFO) << "Have calibration: " << camera->fx;
-                return;
-            }
-
-            LOG(INFO) << "Reading packet: (" << (int)spkt.streamID << "," << (int)spkt.channel << ") " << (int)pkt.codec << ", " << (int)pkt.definition;
-
-            cv::Mat frame(cv::Size(ftl::codecs::getWidth(pkt.definition),ftl::codecs::getHeight(pkt.definition)), (spkt.channel == Channel::Depth) ? CV_32F : CV_8UC3);
-            createDecoder(pkt);
-
-            try {
-                decoder->decode(pkt, frame);
-            } catch (std::exception &e) {
-                LOG(INFO) << "Decoder exception: " << e.what();
-            }
-
-            if (!frame.empty()) {
-                if (spkt.channel == Channel::Depth) {
-                    visualizeDepthMap(frame, frame, 8.0f);
-                }
-                double time = (double)(spkt.timestamp - r.getStartTime()) / 1000.0;
-                cv::putText(frame, std::string("Time: ") + std::to_string(time) + std::string("s"), cv::Point(10,20), cv::FONT_HERSHEY_PLAIN, 1, cv::Scalar(0,0,255));
-                cv::imshow("Player", frame);
-            }
-            int key = cv::waitKey(20);
-            if (key >= 48 && key <= 57) {
-                current_stream = key - 48;
-            } else if (key == 'd') {
-                current_channel = (current_channel == 0) ? 1 : 0;
-            } else if (key == 27) {
-                r.end();
-            }
-        }
-    });
-
-    if (!res) LOG(ERROR) << "No frames left";
+	ftl::timer::add(ftl::timer::kTimerMain, [&current_stream,&current_channel,&r](int64_t ts) {
+		bool res = r.read(ts, [&current_stream,&current_channel,&r](const ftl::codecs::StreamPacket &spkt, const ftl::codecs::Packet &pkt) {
+			if (spkt.channel != static_cast<ftl::codecs::Channel>(current_channel)) return;
+			if (spkt.streamID == current_stream) {
+
+				if (pkt.codec == codec_t::POSE) {
+					Eigen::Matrix4d p = Eigen::Map<Eigen::Matrix4d>((double*)pkt.data.data());
+					LOG(INFO) << "Have pose: " << p;
+					return;
+				}
+
+				if (pkt.codec == codec_t::CALIBRATION) {
+					ftl::rgbd::Camera *camera = (ftl::rgbd::Camera*)pkt.data.data();
+					LOG(INFO) << "Have calibration: " << camera->fx;
+					return;
+				}
+
+				//LOG(INFO) << "Reading packet: (" << (int)spkt.streamID << "," << (int)spkt.channel << ") " << (int)pkt.codec << ", " << (int)pkt.definition;
+
+				cv::Mat frame(cv::Size(ftl::codecs::getWidth(pkt.definition),ftl::codecs::getHeight(pkt.definition)), (spkt.channel == Channel::Depth) ? CV_32F : CV_8UC3);
+				createDecoder(pkt);
+
+				try {
+					decoder->decode(pkt, frame);
+				} catch (std::exception &e) {
+					LOG(INFO) << "Decoder exception: " << e.what();
+				}
+
+				if (!frame.empty()) {
+					if (spkt.channel == Channel::Depth) {
+						visualizeDepthMap(frame, frame, 8.0f);
+					}
+					double time = (double)(spkt.timestamp - r.getStartTime()) / 1000.0;
+					cv::putText(frame, std::string("Time: ") + std::to_string(time) + std::string("s"), cv::Point(10,20), cv::FONT_HERSHEY_PLAIN, 1, cv::Scalar(0,0,255));
+					cv::imshow("Player", frame);
+				}
+				int key = cv::waitKey(1);
+				if (key >= 48 && key <= 57) {
+					current_stream = key - 48;
+				} else if (key == 'd') {
+					current_channel = (current_channel == 0) ? 1 : 0;
+				} else if (key == 27) {
+					ftl::timer::stop(false);
+				}
+			}
+		});
+		if (!res) ftl::timer::stop(false);
+		return res;
+	});
+
+	ftl::timer::start(true);
 
     r.end();
 
diff --git a/applications/reconstruct/src/main.cpp b/applications/reconstruct/src/main.cpp
index 50d7986f3..d86eb5ea1 100644
--- a/applications/reconstruct/src/main.cpp
+++ b/applications/reconstruct/src/main.cpp
@@ -228,13 +228,13 @@ static void run(ftl::Configurable *root) {
 
 	// -------------------------------------------------------------------------
 
-	stream->setLatency(5);  // FIXME: This depends on source!?
-	stream->add(group);
+	stream->setLatency(6);  // FIXME: This depends on source!?
+	//stream->add(group);
 	stream->run();
 
 	bool busy = false;
 
-	group->setLatency(5);
+	group->setLatency(4);
 	group->setName("ReconGroup");
 	group->sync([splat,virt,&busy,&slave,&scene_A,&scene_B,&align](ftl::rgbd::FrameSet &fs) -> bool {
 		//cudaSetDevice(scene->getCUDADevice());
diff --git a/components/codecs/CMakeLists.txt b/components/codecs/CMakeLists.txt
index db41431d4..63e311b24 100644
--- a/components/codecs/CMakeLists.txt
+++ b/components/codecs/CMakeLists.txt
@@ -7,6 +7,7 @@ set(CODECSRC
 	src/generate.cpp
 	src/writer.cpp
 	src/reader.cpp
+	src/channels.cpp
 )
 
 if (HAVE_NVPIPE)
diff --git a/components/codecs/include/ftl/codecs/channels.hpp b/components/codecs/include/ftl/codecs/channels.hpp
index 8ef470972..d1a0e265b 100644
--- a/components/codecs/include/ftl/codecs/channels.hpp
+++ b/components/codecs/include/ftl/codecs/channels.hpp
@@ -43,6 +43,9 @@ inline bool isVideo(Channel c) { return (int)c < 32; };
 inline bool isAudio(Channel c) { return (int)c >= 32 && (int)c < 64; };
 inline bool isData(Channel c) { return (int)c >= 64; };
 
+std::string name(Channel c);
+int type(Channel c);
+
 class Channels {
     public:
 
diff --git a/components/codecs/include/ftl/codecs/hevc.hpp b/components/codecs/include/ftl/codecs/hevc.hpp
new file mode 100644
index 000000000..f658635d6
--- /dev/null
+++ b/components/codecs/include/ftl/codecs/hevc.hpp
@@ -0,0 +1,112 @@
+#ifndef _FTL_CODECS_HEVC_HPP_
+#define _FTL_CODECS_HEVC_HPP_
+
+namespace ftl {
+namespace codecs {
+
+/**
+ * H.265 / HEVC codec utility functions.
+ */
+namespace hevc {
+
+/**
+ * HEVC Network Abstraction Layer Unit types.
+ */
+enum class NALType : int {
+	CODED_SLICE_TRAIL_N = 0,
+    CODED_SLICE_TRAIL_R = 1,
+
+    CODED_SLICE_TSA_N = 2,
+    CODED_SLICE_TSA_R = 3,
+
+    CODED_SLICE_STSA_N = 4,
+    CODED_SLICE_STSA_R = 5,
+
+    CODED_SLICE_RADL_N = 6,
+    CODED_SLICE_RADL_R = 7,
+
+    CODED_SLICE_RASL_N = 8,
+    CODED_SLICE_RASL_R = 9,
+
+    RESERVED_VCL_N10 = 10,
+    RESERVED_VCL_R11 = 11,
+    RESERVED_VCL_N12 = 12,
+    RESERVED_VCL_R13 = 13,
+    RESERVED_VCL_N14 = 14,
+    RESERVED_VCL_R15 = 15,
+
+    CODED_SLICE_BLA_W_LP = 16,
+    CODED_SLICE_BLA_W_RADL = 17,
+    CODED_SLICE_BLA_N_LP = 18,
+    CODED_SLICE_IDR_W_RADL = 19,
+    CODED_SLICE_IDR_N_LP = 20,
+    CODED_SLICE_CRA = 21,
+    RESERVED_IRAP_VCL22 = 22,
+    RESERVED_IRAP_VCL23 = 23,
+
+    RESERVED_VCL24 = 24,
+    RESERVED_VCL25 = 25,
+    RESERVED_VCL26 = 26,
+    RESERVED_VCL27 = 27,
+    RESERVED_VCL28 = 28,
+    RESERVED_VCL29 = 29,
+    RESERVED_VCL30 = 30,
+    RESERVED_VCL31 = 31,
+
+    VPS = 32,
+    SPS = 33,
+    PPS = 34,
+    ACCESS_UNIT_DELIMITER = 35,
+    EOS = 36,
+    EOB = 37,
+    FILLER_DATA = 38,
+    PREFIX_SEI = 39,
+    SUFFIX_SEI = 40,
+
+    RESERVED_NVCL41 = 41,
+    RESERVED_NVCL42 = 42,
+    RESERVED_NVCL43 = 43,
+    RESERVED_NVCL44 = 44,
+    RESERVED_NVCL45 = 45,
+    RESERVED_NVCL46 = 46,
+    RESERVED_NVCL47 = 47,
+    UNSPECIFIED_48 = 48,
+    UNSPECIFIED_49 = 49,
+    UNSPECIFIED_50 = 50,
+    UNSPECIFIED_51 = 51,
+    UNSPECIFIED_52 = 52,
+    UNSPECIFIED_53 = 53,
+    UNSPECIFIED_54 = 54,
+    UNSPECIFIED_55 = 55,
+    UNSPECIFIED_56 = 56,
+    UNSPECIFIED_57 = 57,
+    UNSPECIFIED_58 = 58,
+    UNSPECIFIED_59 = 59,
+    UNSPECIFIED_60 = 60,
+    UNSPECIFIED_61 = 61,
+    UNSPECIFIED_62 = 62,
+    UNSPECIFIED_63 = 63,
+    INVALID = 64
+};
+
+/**
+ * Extract the NAL unit type from the first NAL header.
+ * With NvPipe, the 5th byte contains the NAL Unit header.
+ */
+inline NALType getNALType(const std::vector<uint8_t> &data) {
+	return static_cast<NALType>((data[4] >> 1) & 0x3F);
+}
+
+/**
+ * Check the HEVC bitstream for an I-Frame. With NvPipe, all I-Frames start
+ * with a VPS NAL unit so just check for this.
+ */
+inline bool isIFrame(const std::vector<uint8_t> &data) {
+	return getNALType(data) == NALType::VPS;
+}
+
+}
+}
+}
+
+#endif  // _FTL_CODECS_HEVC_HPP_
diff --git a/components/codecs/include/ftl/codecs/nvpipe_decoder.hpp b/components/codecs/include/ftl/codecs/nvpipe_decoder.hpp
index c13b26ec0..75807f05e 100644
--- a/components/codecs/include/ftl/codecs/nvpipe_decoder.hpp
+++ b/components/codecs/include/ftl/codecs/nvpipe_decoder.hpp
@@ -23,6 +23,7 @@ class NvPipeDecoder : public ftl::codecs::Decoder {
 	bool is_float_channel_;
 	ftl::codecs::definition_t last_definition_;
 	MUTEX mutex_;
+	bool seen_iframe_;
 };
 
 }
diff --git a/components/codecs/include/ftl/codecs/reader.hpp b/components/codecs/include/ftl/codecs/reader.hpp
index 9e607c77f..cdc50cad3 100644
--- a/components/codecs/include/ftl/codecs/reader.hpp
+++ b/components/codecs/include/ftl/codecs/reader.hpp
@@ -45,7 +45,7 @@ class Reader {
 	private:
 	std::istream *stream_;
 	msgpack::unpacker buffer_;
-	std::tuple<StreamPacket,Packet> data_;
+	std::list<std::tuple<StreamPacket,Packet>> data_;
 	bool has_data_;
 	int64_t timestart_;
 	bool playing_;
diff --git a/components/codecs/src/channels.cpp b/components/codecs/src/channels.cpp
new file mode 100644
index 000000000..25b06b533
--- /dev/null
+++ b/components/codecs/src/channels.cpp
@@ -0,0 +1,93 @@
+#include <ftl/codecs/channels.hpp>
+
+#include <opencv2/opencv.hpp>
+
+struct ChannelInfo {
+	const char *name;
+	int type;
+};
+
+static ChannelInfo info[] = {
+    "Colour", CV_8UC3,
+    "Depth", CV_32F,
+    "Right", CV_8UC3,
+    "DepthRight", CV_32F,
+    "Deviation", CV_32F,
+    "Normals", CV_32FC4,
+    "Points", CV_32FC4,
+    "Confidence", CV_32F,
+    "EnergyVector", CV_32FC4,
+    "Flow", CV_32F,
+    "Energy", CV_32F,
+	"Mask", CV_32S,
+	"Density", CV_32F,
+    "LeftGray", CV_8U,
+    "RightGray", CV_8U,
+    "Overlay1", CV_8UC3,
+
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+
+	"AudioLeft", 0,
+	"AudioRight", 0,
+
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+	"NoName", 0,
+
+	"Configuration", 0,
+	"Calibration", 0,
+	"Pose", 0,
+	"Data", 0
+};
+
+std::string ftl::codecs::name(Channel c) {
+	if (c == Channel::None) return "None";
+	else return info[(int)c].name;
+}
+
+int ftl::codecs::type(Channel c)  {
+	if (c == Channel::None) return 0;
+	else return info[(int)c].type;
+}
diff --git a/components/codecs/src/nvpipe_decoder.cpp b/components/codecs/src/nvpipe_decoder.cpp
index fefd5ead5..b5e358388 100644
--- a/components/codecs/src/nvpipe_decoder.cpp
+++ b/components/codecs/src/nvpipe_decoder.cpp
@@ -3,6 +3,7 @@
 #include <loguru.hpp>
 
 #include <ftl/cuda_util.hpp>
+#include <ftl/codecs/hevc.hpp>
 //#include <cuda_runtime.h>
 
 #include <opencv2/core/cuda/common.hpp>
@@ -11,6 +12,7 @@ using ftl::codecs::NvPipeDecoder;
 
 NvPipeDecoder::NvPipeDecoder() {
 	nv_decoder_ = nullptr;
+	seen_iframe_ = false;
 }
 
 NvPipeDecoder::~NvPipeDecoder() {
@@ -46,13 +48,22 @@ bool NvPipeDecoder::decode(const ftl::codecs::Packet &pkt, cv::Mat &out) {
 			//LOG(INFO) << "Bitrate=" << (int)bitrate << " width=" << ABRController::getColourWidth(bitrate);
 			LOG(FATAL) << "Could not create decoder: " << NvPipe_GetError(NULL);
 		} else {
-			LOG(INFO) << "Decoder created";
+			DLOG(INFO) << "Decoder created";
 		}
+
+		seen_iframe_ = false;
 	}
 
 	// TODO: (Nick) Move to member variable to prevent re-creation
 	cv::Mat tmp(cv::Size(ftl::codecs::getWidth(pkt.definition),ftl::codecs::getHeight(pkt.definition)), (is_float_frame) ? CV_16U : CV_8UC4);
 
+	if (pkt.codec == ftl::codecs::codec_t::HEVC) {
+		// Obtain NAL unit type
+		if (ftl::codecs::hevc::isIFrame(pkt.data)) seen_iframe_ = true;
+	}
+
+	if (!seen_iframe_) return false;
+
 	int rc = NvPipe_Decode(nv_decoder_, pkt.data.data(), pkt.data.size(), tmp.data, tmp.cols, tmp.rows);
 	if (rc == 0) LOG(ERROR) << "NvPipe decode error: " << NvPipe_GetError(nv_decoder_);
 
diff --git a/components/codecs/src/reader.cpp b/components/codecs/src/reader.cpp
index b2d146422..41d0697f5 100644
--- a/components/codecs/src/reader.cpp
+++ b/components/codecs/src/reader.cpp
@@ -34,14 +34,18 @@ bool Reader::read(int64_t ts, const std::function<void(const ftl::codecs::Stream
 	std::unique_lock<std::mutex> lk(mtx_, std::defer_lock);
 	if (!lk.try_lock()) return true;
 
-	if (has_data_ && get<0>(data_).timestamp <= ts) {
-		f(get<0>(data_), get<1>(data_));
-		has_data_ = false;
-	} else if (has_data_) {
-		return false;
+	// Check buffer first for frames already read
+	for (auto i = data_.begin(); i != data_.end();) {
+		if (get<0>(*i).timestamp <= ts) {
+			f(get<0>(*i), get<1>(*i));
+			i = data_.erase(i);
+		} else {
+			++i;
+		}
 	}
 
 	bool partial = false;
+	int64_t extended_ts = ts + 200;  // Buffer 200ms ahead
 
 	while (playing_ && stream_->good() || buffer_.nonparsed_size() > 0) {
 		if (buffer_.nonparsed_size() == 0 || (partial && buffer_.nonparsed_size() < 10000000)) {
@@ -50,7 +54,7 @@ bool Reader::read(int64_t ts, const std::function<void(const ftl::codecs::Stream
 			//if (stream_->bad()) return false;
 
 			int bytes = stream_->gcount();
-			if (bytes == 0) return false;
+			if (bytes == 0) break;
 			buffer_.buffer_consumed(bytes);
 			partial = false;
 		}
@@ -61,10 +65,10 @@ bool Reader::read(int64_t ts, const std::function<void(const ftl::codecs::Stream
 			continue;
 		}
 
-		std::tuple<StreamPacket,Packet> data;
+		//std::tuple<StreamPacket,Packet> data;
 		msgpack::object obj = msg.get();
 		try {
-			obj.convert(data);
+			obj.convert(data_.emplace_back());
 		} catch (std::exception &e) {
 			LOG(INFO) << "Corrupt message: " << buffer_.nonparsed_size();
 			//partial = true;
@@ -72,19 +76,25 @@ bool Reader::read(int64_t ts, const std::function<void(const ftl::codecs::Stream
 			return false;
 		}
 
+		auto &data = data_.back();
+
 		// Adjust timestamp
 		get<0>(data).timestamp += timestart_;
 
+		// TODO: Need to read ahead a few frames because there may be a
+		// smaller timestamp after this one... requires a buffer. Ideally this
+		// should be resolved during the write process.
 		if (get<0>(data).timestamp <= ts) {
 			f(get<0>(data),get<1>(data));
-		} else {
-			data_ = data;
-			has_data_ = true;
+			data_.pop_back();
+		} else if (get<0>(data).timestamp > extended_ts) {
+			//data_ = data;
+			//has_data_ = true;
 			return true;
 		}
 	}
 
-	return false;
+	return data_.size() > 0;
 }
 
 bool Reader::read(int64_t ts) {
diff --git a/components/renderers/cpp/src/splat_render.cpp b/components/renderers/cpp/src/splat_render.cpp
index 9a34d4679..7cba3d7b8 100644
--- a/components/renderers/cpp/src/splat_render.cpp
+++ b/components/renderers/cpp/src/splat_render.cpp
@@ -301,6 +301,8 @@ bool Splatter::render(ftl::rgbd::VirtualSource *src, ftl::rgbd::Frame &out) {
 	SHARED_LOCK(scene_->mtx, lk);
 	if (!src->isReady()) return false;
 
+	scene_->upload(Channel::Colour + Channel::Depth, stream_);
+
 	const auto &camera = src->parameters();
 
 	//cudaSafeCall(cudaSetDevice(scene_->getCUDADevice()));
diff --git a/components/rgbd-sources/include/ftl/rgbd/detail/netframe.hpp b/components/rgbd-sources/include/ftl/rgbd/detail/netframe.hpp
index f01154e12..1abb624c9 100644
--- a/components/rgbd-sources/include/ftl/rgbd/detail/netframe.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/detail/netframe.hpp
@@ -17,8 +17,9 @@ struct NetFrame {
 	cv::Mat channel1;
 	cv::Mat channel2;
 	volatile int64_t timestamp;
-	std::atomic<int> chunk_count;
-	int chunk_total;
+	std::atomic<int> chunk_count[2];
+	std::atomic<int> channel_count;
+	int chunk_total[2];
 	std::atomic<int> tx_size;
 	int64_t tx_latency;
 	MUTEX mtx;
diff --git a/components/rgbd-sources/include/ftl/rgbd/frame.hpp b/components/rgbd-sources/include/ftl/rgbd/frame.hpp
index e6025b41a..5e38fe5cc 100644
--- a/components/rgbd-sources/include/ftl/rgbd/frame.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/frame.hpp
@@ -215,10 +215,10 @@ ftl::cuda::TextureObject<T> &Frame::createTexture(ftl::codecs::Channel c, const
 	// TODO: Check tex cvType
 
 	if (m.tex.devicePtr() == nullptr) {
-		LOG(INFO) << "Creating texture object";
+		//LOG(INFO) << "Creating texture object";
 		m.tex = ftl::cuda::TextureObject<T>(m.gpu);
 	} else if (m.tex.cvType() != ftl::traits::OpenCVType<T>::value || m.tex.width() != m.gpu.cols || m.tex.height() != m.gpu.rows) {
-		LOG(INFO) << "Recreating texture object";
+		LOG(INFO) << "Recreating texture object for '" << ftl::codecs::name(c) << "'";
 		m.tex.free();
 		m.tex = ftl::cuda::TextureObject<T>(m.gpu);
 	}
@@ -248,9 +248,10 @@ ftl::cuda::TextureObject<T> &Frame::createTexture(ftl::codecs::Channel c) {
 	// TODO: Check tex cvType
 
 	if (m.tex.devicePtr() == nullptr) {
-		LOG(INFO) << "Creating texture object";
+		//LOG(INFO) << "Creating texture object";
 		m.tex = ftl::cuda::TextureObject<T>(m.gpu);
 	} else if (m.tex.cvType() != ftl::traits::OpenCVType<T>::value || m.tex.width() != m.gpu.cols || m.tex.height() != m.gpu.rows || m.tex.devicePtr() != m.gpu.data) {
+		LOG(INFO) << "Recreating texture object for '" << ftl::codecs::name(c) << "'";
 		m.tex.free();
 		m.tex = ftl::cuda::TextureObject<T>(m.gpu);
 	}
diff --git a/components/rgbd-sources/src/file_source.cpp b/components/rgbd-sources/src/file_source.cpp
index bef04d6a4..38acf1a7c 100644
--- a/components/rgbd-sources/src/file_source.cpp
+++ b/components/rgbd-sources/src/file_source.cpp
@@ -1,5 +1,6 @@
 #include "file_source.hpp"
 
+#include <ftl/codecs/hevc.hpp>
 #include <ftl/timer.hpp>
 
 using ftl::rgbd::detail::FileSource;
@@ -15,7 +16,7 @@ void FileSource::_createDecoder(int ix, const ftl::codecs::Packet &pkt) {
 		}
 	}
 
-	LOG(INFO) << "Create a decoder: " << ix;
+	DLOG(INFO) << "Create a decoder: " << ix;
 	decoders_[ix] = ftl::codecs::allocateDecoder(pkt);
 }
 
@@ -28,8 +29,10 @@ FileSource::FileSource(ftl::rgbd::Source *s, ftl::codecs::Reader *r, int sid) :
 	cache_write_ = 0;
 	realtime_ = host_->value("realtime", true);
 	timestamp_ = r->getStartTime();
+	sourceid_ = sid;
 
     r->onPacket(sid, [this](const ftl::codecs::StreamPacket &spkt, ftl::codecs::Packet &pkt) {
+		host_->notifyRaw(spkt, pkt);
 		if (pkt.codec == codec_t::POSE) {
 			Eigen::Matrix4d p = Eigen::Map<Eigen::Matrix4d>((double*)pkt.data.data());
 			host_->setPose(p);
@@ -40,10 +43,7 @@ FileSource::FileSource(ftl::rgbd::Source *s, ftl::codecs::Reader *r, int sid) :
 			has_calibration_ = true;
 		} else {
 			if (pkt.codec == codec_t::HEVC) {
-				// Obtain NAL unit type
-				int nal_type = (pkt.data[4] >> 1) & 0x3F;
-				// A type of 32 = VPS unit, hence I-Frame in this case so skip past packets
-				if (nal_type == 32) _removeChannel(spkt.channel);
+				if (ftl::codecs::hevc::isIFrame(pkt.data)) _removeChannel(spkt.channel);
 			}
 			cache_[cache_write_].emplace_back();
 			auto &c = cache_[cache_write_].back();
@@ -93,12 +93,23 @@ void FileSource::swap() {
 
 bool FileSource::compute(int n, int b) {
 	if (cache_read_ < 0) return false;
+	if (cache_[cache_read_].size() == 0) return false;
+
+	int64_t lastts = 0;
+	int lastc = 0;
 
 	for (auto i=cache_[cache_read_].begin(); i!=cache_[cache_read_].end(); ++i) {
 		auto &c = *i;
 
+		if (c.spkt.timestamp > lastts) {
+			lastts = c.spkt.timestamp;
+			lastc = 1;
+		} else if (c.spkt.timestamp == lastts) {
+			lastc++;
+		}
+
 		if (c.spkt.channel == Channel::Colour) {
-			rgb_.create(cv::Size(ftl::codecs::getWidth(c.pkt.definition),ftl::codecs::getHeight(c.pkt.definition)), CV_8UC4);
+			rgb_.create(cv::Size(ftl::codecs::getWidth(c.pkt.definition),ftl::codecs::getHeight(c.pkt.definition)), CV_8UC3);
 		} else {
 			depth_.create(cv::Size(ftl::codecs::getWidth(c.pkt.definition),ftl::codecs::getHeight(c.pkt.definition)), CV_32F);
 		}
@@ -112,6 +123,11 @@ bool FileSource::compute(int n, int b) {
 		}
 	}
 
+	if (lastc != 2) {
+		LOG(ERROR) << "Channels not in sync (" << sourceid_ << "): " << lastts;
+		return false;
+	}
+
 	cache_[cache_read_].clear();
 
 	if (rgb_.empty() || depth_.empty()) return false;
diff --git a/components/rgbd-sources/src/file_source.hpp b/components/rgbd-sources/src/file_source.hpp
index a9bdc4b36..1f2241ea0 100644
--- a/components/rgbd-sources/src/file_source.hpp
+++ b/components/rgbd-sources/src/file_source.hpp
@@ -38,6 +38,7 @@ class FileSource : public detail::Source {
 	std::list<PacketPair> cache_[2];
 	int cache_read_;
 	int cache_write_;
+	int sourceid_;
 
 	ftl::codecs::Decoder *decoders_[2];
 
diff --git a/components/rgbd-sources/src/group.cpp b/components/rgbd-sources/src/group.cpp
index 278ce8285..4e178c024 100644
--- a/components/rgbd-sources/src/group.cpp
+++ b/components/rgbd-sources/src/group.cpp
@@ -257,8 +257,9 @@ ftl::rgbd::FrameSet *Group::_getFrameset(int f) {
 		int idx = (head_+kFrameBufferSize-i)%kFrameBufferSize;
 
 		if (framesets_[idx].timestamp == lookfor && framesets_[idx].count != sources_.size()) {
-			LOG(INFO) << "Required frame not complete (timestamp="  << (framesets_[idx].timestamp) << " buffer=" << i << ")";
+			LOG(WARNING) << "Required frame not complete in '" << name_ << "' (timestamp="  << (framesets_[idx].timestamp) << " buffer=" << i << ")";
 			//framesets_[idx].stale = true;
+			//return &framesets_[idx];
 			continue;
 		}
 
@@ -302,6 +303,8 @@ void Group::_addFrameset(int64_t timestamp) {
 		//framesets_[head_].channel2.resize(sources_.size());
 		framesets_[head_].frames.resize(sources_.size());
 
+		for (auto &f : framesets_[head_].frames) f.reset();
+
 		if (framesets_[head_].sources.size() != sources_.size()) {
 			framesets_[head_].sources.clear();
 			for (auto s : sources_) framesets_[head_].sources.push_back(s);
@@ -333,6 +336,8 @@ void Group::_addFrameset(int64_t timestamp) {
 		//framesets_[head_].channel2.resize(sources_.size());
 		framesets_[head_].frames.resize(sources_.size());
 
+		for (auto &f : framesets_[head_].frames) f.reset();
+
 		if (framesets_[head_].sources.size() != sources_.size()) {
 			framesets_[head_].sources.clear();
 			for (auto s : sources_) framesets_[head_].sources.push_back(s);
diff --git a/components/rgbd-sources/src/net.cpp b/components/rgbd-sources/src/net.cpp
index 91a990090..dd97e9618 100644
--- a/components/rgbd-sources/src/net.cpp
+++ b/components/rgbd-sources/src/net.cpp
@@ -46,8 +46,11 @@ NetFrame &NetFrameQueue::getFrame(int64_t ts, const cv::Size &s, int c1type, int
 	for (auto &f : frames_) {
 		if (f.timestamp == -1) {
 			f.timestamp = ts;
-			f.chunk_count = 0;
-			f.chunk_total = 0;
+			f.chunk_count[0] = 0;
+			f.chunk_count[1] = 0;
+			f.chunk_total[0] = 0;
+			f.chunk_total[1] = 0;
+			f.channel_count = 0;
 			f.tx_size = 0;
 			f.channel1.create(s, c1type);
 			f.channel2.create(s, c2type);
@@ -63,8 +66,11 @@ NetFrame &NetFrameQueue::getFrame(int64_t ts, const cv::Size &s, int c1type, int
 		// Force release of frame!
 		if (f.timestamp == oldest) {
 			f.timestamp = ts;
-			f.chunk_count = 0;
-			f.chunk_total = 0;
+			f.chunk_count[0] = 0;
+			f.chunk_count[1] = 0;
+			f.chunk_total[0] = 0;
+			f.chunk_total[1] = 0;
+			f.channel_count = 0;
 			f.tx_size = 0;
 			f.channel1.create(s, c1type);
 			f.channel2.create(s, c2type);
@@ -246,16 +252,17 @@ void NetSource::_recvPacket(short ttimeoff, const ftl::codecs::StreamPacket &spk
 	host_->notifyRaw(spkt, pkt);
 
 	const ftl::codecs::Channel chan = host_->getChannel();
-	const ftl::codecs::Channel rchan = spkt.channel; // & 0x1;
+	const ftl::codecs::Channel rchan = spkt.channel;
+	const int channum = (rchan == Channel::Colour) ? 0 : 1;
 
-	NetFrame &frame = queue_.getFrame(spkt.timestamp, cv::Size(params_.width, params_.height), CV_8UC4, (isFloatChannel(chan) ? CV_32FC1 : CV_8UC4));
+	NetFrame &frame = queue_.getFrame(spkt.timestamp, cv::Size(params_.width, params_.height), CV_8UC3, (isFloatChannel(chan) ? CV_32FC1 : CV_8UC3));
 
 	// Update frame statistics
 	frame.tx_size += pkt.data.size();
 
 	// Only decode if this channel is wanted.
 	if (rchan == Channel::Colour || rchan == chan) {
-		_createDecoder((rchan == Channel::Colour) ? 0 : 1, pkt);
+		_createDecoder(channum, pkt);
 		auto *decoder = (rchan == Channel::Colour) ? decoder_c1_ : decoder_c2_;
 		if (!decoder) {
 			LOG(ERROR) << "No frame decoder available";
@@ -282,30 +289,31 @@ void NetSource::_recvPacket(short ttimeoff, const ftl::codecs::StreamPacket &spk
 		return;
 	}
 
-	// Calculate how many packets to expect for this frame
-	if (frame.chunk_total == 0) {
-		// Getting a second channel first means expect double packets
-		// FIXME: Assumes each packet has same number of blocks!
-		frame.chunk_total = pkt.block_total * spkt.channel_count;
+	// Calculate how many packets to expect for this channel
+	if (frame.chunk_total[channum] == 0) {
+		frame.chunk_total[channum] = pkt.block_total;
 	}		
 
-	++frame.chunk_count;
+	++frame.chunk_count[channum];
+	++frame.channel_count;
 
-	if (frame.chunk_count > frame.chunk_total) LOG(FATAL) << "TOO MANY CHUNKS";
+	if (frame.chunk_count[channum] > frame.chunk_total[channum]) LOG(FATAL) << "TOO MANY CHUNKS";
 
 	// Capture tx time of first received chunk
-	if (frame.chunk_count == 1) {
+	if (frame.channel_count == 1 && frame.chunk_count[channum] == 1) {
 		UNIQUE_LOCK(frame.mtx, flk);
-		if (frame.chunk_count == 1) {
+		if (frame.chunk_count[channum] == 1) {
 			frame.tx_latency = int64_t(ttimeoff);
 		}
 	}
 
-	// Last chunk now received
-	if (frame.chunk_count == frame.chunk_total) {
+	// Last chunk of both channels now received
+	if (frame.channel_count == spkt.channel_count &&
+			frame.chunk_count[0] == frame.chunk_total[0] &&
+			frame.chunk_count[1] == frame.chunk_total[1]) {
 		UNIQUE_LOCK(frame.mtx, flk);
 
-		if (frame.timestamp >= 0 && frame.chunk_count == frame.chunk_total) {
+		if (frame.timestamp >= 0 && frame.chunk_count[0] == frame.chunk_total[0] && frame.chunk_count[1] == frame.chunk_total[1]) {
 			timestamp_ = frame.timestamp;
 			frame.tx_latency = now-(spkt.timestamp+frame.tx_latency);
 
diff --git a/components/rgbd-sources/src/streamer.cpp b/components/rgbd-sources/src/streamer.cpp
index cab8f99d8..c446445c9 100644
--- a/components/rgbd-sources/src/streamer.cpp
+++ b/components/rgbd-sources/src/streamer.cpp
@@ -48,6 +48,7 @@ Streamer::Streamer(nlohmann::json &config, Universe *net)
 
 	//group_.setFPS(value("fps", 20));
 	group_.setLatency(4);
+	group_.setName("StreamGroup");
 
 	compress_level_ = value("compression", 1);
 	
-- 
GitLab