diff --git a/CMakeLists.txt b/CMakeLists.txt
index 17ef352e58e8191f84a7a9388f6d3e823d5a1535..008047b9e567579fc3bc8f40ff2dcab5a993273e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -30,6 +30,8 @@ find_package( URIParser REQUIRED )
 find_package( MsgPack REQUIRED )
 find_package( Eigen3 REQUIRED )
 
+# find_package( ffmpeg )
+
 if (WITH_OPTFLOW)
 	# TODO check that cudaoptflow.hpp exists (OpenCV built with correct contrib modules)
 	set(HAVE_OPTFLOW true)
@@ -179,6 +181,9 @@ find_library(UUID_LIBRARIES NAMES uuid libuuid)
 else()
 endif()
 
+# For ftl2mkv
+check_include_file("libavformat/avformat.h" HAVE_AVFORMAT)
+
 # Check for optional opencv components
 set(CMAKE_REQUIRED_INCLUDES ${OpenCV_INCLUDE_DIRS})
 check_include_file_cxx("opencv2/viz.hpp" HAVE_VIZ)
@@ -222,6 +227,10 @@ add_subdirectory(applications/calibration)
 add_subdirectory(applications/groupview)
 add_subdirectory(applications/player)
 
+if (HAVE_AVFORMAT)
+	add_subdirectory(applications/ftl2mkv)
+endif()
+
 if (BUILD_RENDERER)
 	add_subdirectory(components/renderers)
 	set(HAVE_RENDER)
diff --git a/applications/ftl2mkv/CMakeLists.txt b/applications/ftl2mkv/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..96ed5b890040e77516aec786ca767f6e8d430117
--- /dev/null
+++ b/applications/ftl2mkv/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(FTL2MKVSRC
+	src/main.cpp
+)
+
+add_executable(ftl2mkv ${FTL2MKVSRC})
+
+target_include_directories(ftl2mkv PRIVATE src)
+
+target_link_libraries(ftl2mkv ftlcommon ftlcodecs ftlrgbd Threads::Threads ${OpenCV_LIBS} avutil avformat avcodec)
+
+
diff --git a/applications/ftl2mkv/src/main.cpp b/applications/ftl2mkv/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4ee909feefc277a9134e19c432268452d8c67ebc
--- /dev/null
+++ b/applications/ftl2mkv/src/main.cpp
@@ -0,0 +1,241 @@
+#include <loguru.hpp>
+#include <ftl/configuration.hpp>
+#include <ftl/codecs/reader.hpp>
+#include <ftl/codecs/packet.hpp>
+#include <ftl/rgbd/camera.hpp>
+
+#include <fstream>
+
+#include <Eigen/Eigen>
+
+extern "C" {
+ #include <libavformat/avformat.h>
+}
+
+using ftl::codecs::codec_t;
+using ftl::codecs::Channel;
+
+static AVStream *add_video_stream(AVFormatContext *oc, const ftl::codecs::Packet &pkt)
+{
+    AVCodecContext *c;
+    AVStream *st;
+
+    st = avformat_new_stream(oc, 0);
+    if (!st) {
+        fprintf(stderr, "Could not alloc stream\n");
+        exit(1);
+    }
+
+	AVCodecID codec_id;
+	switch (pkt.codec) {
+	case codec_t::HEVC : codec_id = AV_CODEC_ID_HEVC; break;
+	}
+
+    //c = st->codec;
+    //c->codec_id = codec_id;
+    //c->codec_type = AVMEDIA_TYPE_VIDEO;
+
+	st->time_base.den = 20;
+	st->time_base.num = 1;
+	//st->id = oc->nb_streams-1;
+	//st->nb_frames = 0;
+	st->codecpar->codec_id = codec_id;
+	st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+	st->codecpar->width = ftl::codecs::getWidth(pkt.definition);
+	st->codecpar->height = ftl::codecs::getHeight(pkt.definition);
+	st->codecpar->format = AV_PIX_FMT_NV12;
+	st->codecpar->bit_rate = 4000000;
+
+    /* put sample parameters */
+    //c->bit_rate = 4000000;
+    /* resolution must be a multiple of two */
+    //c->width = ftl::codecs::getWidth(pkt.definition);
+   // c->height = ftl::codecs::getHeight(pkt.definition);
+    /* time base: this is the fundamental unit of time (in seconds) in terms
+      of which frame timestamps are represented. for fixed-fps content,
+       timebase should be 1/framerate and timestamp increments should be
+       identically 1. */
+    //c->time_base.den = 20;  // Frame rate
+    //c->time_base.num = 1;
+    //c->gop_size = 10; /* emit one intra frame every twelve frames at most */
+    //c->pix_fmt = AV_PIX_FMT_ABGR;
+
+    // some formats want stream headers to be separate
+    //if(oc->oformat->flags & AVFMT_GLOBALHEADER)
+    //    st->codecpar-> |= CODEC_FLAG_GLOBAL_HEADER;
+
+    return st;
+}
+
+
+int main(int argc, char **argv) {
+    std::string filename;
+
+	auto root = ftl::configure(argc, argv, "player_default");
+
+	std::string outputfile = root->value("out", std::string("output.mkv"));
+	std::vector<std::string> paths = *root->get<std::vector<std::string>>("paths");
+
+	if (paths.size() == 0) {
+		LOG(ERROR) << "Missing input ftl file.";
+		return -1;
+	} else {
+		filename = paths[0];
+	}
+
+	std::ifstream f;
+    f.open(filename);
+    if (!f.is_open()) {
+		LOG(ERROR) << "Could not open file: " << filename;
+		return -1;
+	}
+
+    ftl::codecs::Reader r(f);
+    if (!r.begin()) {
+		LOG(ERROR) << "Bad ftl file format";
+		return -1;
+	}
+
+	AVOutputFormat *fmt;
+	AVFormatContext *oc;
+	AVStream *video_st[10] = {nullptr};
+
+	av_register_all();
+
+	fmt = av_guess_format(NULL, outputfile.c_str(), NULL);
+
+	if (!fmt) {
+        LOG(ERROR) << "Could not find suitable output format";
+        return -1;
+    }
+
+    /* allocate the output media context */
+    oc = avformat_alloc_context();
+    if (!oc) {
+        LOG(ERROR) << "Memory error";
+        return -1;
+    }
+    oc->oformat = fmt;
+	snprintf(oc->filename, sizeof(oc->filename), "%s", outputfile.c_str());
+
+	/* open the output file, if needed */
+    if (!(fmt->flags & AVFMT_NOFILE)) {
+        if (avio_open(&oc->pb, outputfile.c_str(), AVIO_FLAG_WRITE) < 0) {
+            LOG(ERROR) << "Could not open output file: " << outputfile;
+            return -1;
+        }
+    }
+
+    LOG(INFO) << "Converting...";
+
+    int current_stream = root->value("stream", 0);
+    int current_channel = 0;
+
+	//bool stream_added[10] = {false};
+
+	// TODO: In future, find a better way to discover number of streams...
+	// Read entire file to find all streams before reading again to write data
+	bool res = r.read(90000000000000, [&current_stream,&current_channel,&r,&video_st,oc](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 || current_stream == 255) {
+
+            if (pkt.codec == codec_t::POSE) {
+                return;
+            }
+
+            if (pkt.codec == codec_t::CALIBRATION) {
+                return;
+            }
+
+			if (spkt.streamID >= 10) return;  // TODO: Allow for more than 10
+
+			if (video_st[spkt.streamID] == nullptr) {
+				video_st[spkt.streamID] = add_video_stream(oc, pkt);
+			}
+		}
+	});
+
+	r.end();
+	f.clear();
+	f.seekg(0);
+	if (!r.begin()) {
+		LOG(ERROR) << "Bad ftl file format";
+		return -1;
+	}
+
+	av_dump_format(oc, 0, "output.mkv", 1);
+	if (avformat_write_header(oc, NULL) < 0) {
+		LOG(ERROR) << "Failed to write stream header";
+	}
+
+	bool seen_key[10] = {false};
+
+    res = r.read(90000000000000, [&current_stream,&current_channel,&r,&video_st,oc,&seen_key](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 || current_stream == 255) {
+
+            if (pkt.codec == codec_t::POSE) {
+                return;
+            }
+
+            if (pkt.codec == codec_t::CALIBRATION) {
+                return;
+            }
+
+            //LOG(INFO) << "Reading packet: (" << (int)spkt.streamID << "," << (int)spkt.channel << ") " << (int)pkt.codec << ", " << (int)pkt.definition;
+
+			if (spkt.streamID >= 10) return;  // TODO: Allow for more than 10
+
+			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) {
+					seen_key[spkt.streamID] = true;
+					keyframe = true;
+				}
+			}
+			if (!seen_key[spkt.streamID]) return;
+
+            AVPacket avpkt;
+			av_init_packet(&avpkt);
+			if (keyframe) avpkt.flags |= AV_PKT_FLAG_KEY;
+			avpkt.pts = spkt.timestamp - r.getStartTime();
+			avpkt.dts = avpkt.pts;
+			avpkt.stream_index= video_st[spkt.streamID]->index;
+			avpkt.data= const_cast<uint8_t*>(pkt.data.data());
+			avpkt.size= pkt.data.size();
+			avpkt.duration = 1;
+
+			//LOG(INFO) << "write frame: " << avpkt.pts << "," << avpkt.stream_index << "," << avpkt.size;
+
+			/* write the compressed frame in the media file */
+			auto ret = av_write_frame(oc, &avpkt);
+			if (ret != 0) {
+				LOG(ERROR) << "Error writing frame: " << ret;
+			}
+        }
+    });
+
+	av_write_trailer(oc);
+	//avcodec_close(video_st->codec);
+
+	for (int i=0; i<10; ++i) {
+		if (video_st[i]) av_free(video_st[i]);
+	}
+
+	if (!(fmt->flags & AVFMT_NOFILE)) {
+         /* close the output file */
+        avio_close(oc->pb);
+    }
+
+	av_free(oc);
+
+    if (!res) LOG(INFO) << "Done.";
+
+    r.end();
+
+	ftl::running = false;
+	return 0;
+}
diff --git a/components/codecs/src/reader.cpp b/components/codecs/src/reader.cpp
index c12c6c6dce50b3ac28b57cba76016216823bb12a..b2d146422abefc388a57036e6fe86286ee8439e9 100644
--- a/components/codecs/src/reader.cpp
+++ b/components/codecs/src/reader.cpp
@@ -57,7 +57,6 @@ bool Reader::read(int64_t ts, const std::function<void(const ftl::codecs::Stream
 
 		msgpack::object_handle msg;
 		if (!buffer_.next(msg)) {
-			LOG(INFO) << "NO Message: " << buffer_.nonparsed_size();
 			partial = true;
 			continue;
 		}