From 92565996b36d32c8c20eab4fe81f7e4ea9e446b4 Mon Sep 17 00:00:00 2001
From: Nicolas Pope <nicolas.pope@utu.fi>
Date: Sat, 14 Mar 2020 15:46:11 +0200
Subject: [PATCH] C SDK for writing FTL files

---
 CMakeLists.txt                                |  11 +-
 SDK/C/CMakeLists.txt                          |  22 ++
 SDK/C/examples/image_write/CMakeLists.txt     |   2 +
 SDK/C/examples/image_write/main.cpp           |  48 +++
 SDK/C/examples/video_write/CMakeLists.txt     |   2 +
 SDK/C/examples/video_write/main.cpp           |  42 +++
 SDK/C/include/ftl/ftl.h                       | 195 +++++++++++++
 SDK/C/src/common.cpp                          |   3 +
 SDK/C/src/streams.cpp                         | 276 ++++++++++++++++++
 components/streams/src/filestream.cpp         |   2 +
 .../structures/include/ftl/data/frame.hpp     |   3 +
 11 files changed, 603 insertions(+), 3 deletions(-)
 create mode 100644 SDK/C/CMakeLists.txt
 create mode 100644 SDK/C/examples/image_write/CMakeLists.txt
 create mode 100644 SDK/C/examples/image_write/main.cpp
 create mode 100644 SDK/C/examples/video_write/CMakeLists.txt
 create mode 100644 SDK/C/examples/video_write/main.cpp
 create mode 100644 SDK/C/include/ftl/ftl.h
 create mode 100644 SDK/C/src/common.cpp
 create mode 100644 SDK/C/src/streams.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 338ed2cb5..376b06b36 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,7 +4,7 @@ include (CheckIncludeFileCXX)
 include (CheckFunctionExists)
 include(CheckLanguage)
 
-project (ftl.utu.fi)
+project (ftl.utu.fi VERSION 0.0.4)
 
 include(GNUInstallDirs)
 include(CTest)
@@ -263,7 +263,7 @@ endif()
 check_language(CUDA)
 if (CUDA_TOOLKIT_ROOT_DIR)
 enable_language(CUDA)
-set(CMAKE_CUDA_FLAGS "")
+set(CMAKE_CUDA_FLAGS "-Xcompiler -fPIC")
 set(CMAKE_CUDA_FLAGS_DEBUG "--gpu-architecture=compute_61 -g -DDEBUG -D_DEBUG")
 set(CMAKE_CUDA_FLAGS_RELEASE "--gpu-architecture=compute_61")
 set(HAVE_CUDA TRUE)
@@ -352,7 +352,7 @@ if (WIN32) # TODO(nick) Should do based upon compiler (VS)
 	set(OS_LIBS "")
 else()
 	add_definitions(-DUNIX)
-	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -msse3 -Werror -Wall")
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -fPIC -msse3 -Werror -Wall")
 	set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -D_DEBUG -pg")
 	set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -mfpmath=sse")
 	set(OS_LIBS "dl")
@@ -378,6 +378,11 @@ add_subdirectory(components/calibration)
 #add_subdirectory(applications/merger)
 add_subdirectory(applications/tools)
 
+# SDK only compiles on linux currently
+if (NOT WIN32)
+	add_subdirectory(SDK/C)
+endif()
+
 if (HAVE_AVFORMAT)
 	add_subdirectory(applications/ftl2mkv)
 endif()
diff --git a/SDK/C/CMakeLists.txt b/SDK/C/CMakeLists.txt
new file mode 100644
index 000000000..e34a99214
--- /dev/null
+++ b/SDK/C/CMakeLists.txt
@@ -0,0 +1,22 @@
+set(SDKSRC
+	src/common.cpp
+	src/streams.cpp
+)
+
+add_library(ftl-dev SHARED ${SDKSRC})
+set_target_properties(ftl-dev PROPERTIES VERSION ${PROJECT_VERSION})
+set_target_properties(ftl-dev PROPERTIES SOVERSION 0)
+
+target_include_directories(ftl-dev PUBLIC
+	$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
+	$<INSTALL_INTERFACE:include>
+	PRIVATE src)
+
+target_link_libraries(ftl-dev ftlcommon ftlrgbd ftlstreams Threads::Threads ${OpenCV_LIBS} ftlnet)
+
+install(TARGETS ftl-dev
+    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+	PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
+	
+add_subdirectory(examples/image_write)
+add_subdirectory(examples/video_write)
diff --git a/SDK/C/examples/image_write/CMakeLists.txt b/SDK/C/examples/image_write/CMakeLists.txt
new file mode 100644
index 000000000..9415ced85
--- /dev/null
+++ b/SDK/C/examples/image_write/CMakeLists.txt
@@ -0,0 +1,2 @@
+add_executable(image_write main.cpp)
+target_link_libraries(image_write ftl-dev)
diff --git a/SDK/C/examples/image_write/main.cpp b/SDK/C/examples/image_write/main.cpp
new file mode 100644
index 000000000..4cb9bd425
--- /dev/null
+++ b/SDK/C/examples/image_write/main.cpp
@@ -0,0 +1,48 @@
+#include <ftl/ftl.h>
+#include <opencv2/core/mat.hpp>
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
+#include <Eigen/Eigen>
+
+static void ftlCheck(ftlError_t err) {
+	if (err != FTLERROR_OK) {
+		LOG(ERROR) << "FTL Stream Error: " << err;
+		exit(-1);
+	}
+}
+
+int main(int argc, char **argv) {
+	ftlStream_t s = ftlCreateWriteStream("./out.ftl");
+	if (!s) ftlCheck(ftlGetLastStreamError(s));
+
+	// Two test images, red and green
+	cv::Mat test_image1(720, 1280, CV_8UC4, cv::Scalar(0,0,255,255));
+	cv::Mat test_image2(720, 1280, CV_8UC4, cv::Scalar(0,255,0,255));
+
+	// Two test depth maps
+	cv::Mat test_depth1(720, 1280, CV_32F, cv::Scalar(3.0f));
+	cv::Mat test_depth2(720, 1280, CV_32F, cv::Scalar(2.0f));
+
+	// Write red image
+	ftlCheck(ftlIntrinsicsWriteLeft(s, 0, 1280, 720, 300.0f, -1280.0f/2.0f, -720.0f/2.0f, 0.1f, 0.1f, 8.0f));
+	ftlCheck(ftlImageWrite(s, 0, FTLCHANNEL_Colour, FTLIMAGE_BGRA, test_image1.step, test_image1.data));
+
+	// Write green image
+	ftlCheck(ftlIntrinsicsWriteLeft(s, 1, 1280, 720, 300.0f, -1280.0f/2.0f, -720.0f/2.0f, 0.1f, 0.1f, 8.0f));
+	ftlCheck(ftlImageWrite(s, 1, FTLCHANNEL_Colour, FTLIMAGE_BGRA, test_image2.step, test_image2.data));
+
+	// Write depth images
+	ftlCheck(ftlImageWrite(s, 0, FTLCHANNEL_Depth, FTLIMAGE_FLOAT, test_depth1.step, test_depth1.data));
+	ftlCheck(ftlImageWrite(s, 1, FTLCHANNEL_Depth, FTLIMAGE_FLOAT, test_depth2.step, test_depth2.data));
+
+	// Set pose for second source
+	Eigen::Translation3f trans(1.0f, 0.5f, 0.0f);
+	Eigen::Affine3f t(trans);
+	Eigen::Matrix4f viewPose = t.matrix();
+	ftlCheck(ftlPoseWrite(s, 1, viewPose.data()));
+
+	ftlCheck(ftlDestroyStream(s));
+
+	return 0;
+}
diff --git a/SDK/C/examples/video_write/CMakeLists.txt b/SDK/C/examples/video_write/CMakeLists.txt
new file mode 100644
index 000000000..b0048d762
--- /dev/null
+++ b/SDK/C/examples/video_write/CMakeLists.txt
@@ -0,0 +1,2 @@
+add_executable(video_write main.cpp)
+target_link_libraries(video_write ftl-dev)
diff --git a/SDK/C/examples/video_write/main.cpp b/SDK/C/examples/video_write/main.cpp
new file mode 100644
index 000000000..94cd6fa72
--- /dev/null
+++ b/SDK/C/examples/video_write/main.cpp
@@ -0,0 +1,42 @@
+#include <ftl/ftl.h>
+#include <opencv2/core/mat.hpp>
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
+#include <Eigen/Eigen>
+
+static void ftlCheck(ftlError_t err) {
+	if (err != FTLERROR_OK) {
+		LOG(ERROR) << "FTL Stream Error: " << err;
+		exit(-1);
+	}
+}
+
+int main(int argc, char **argv) {
+	ftlStream_t s = ftlCreateWriteStream("./out.ftl");
+	if (!s) ftlCheck(ftlGetLastStreamError(s));
+
+	ftlCheck(ftlSetFrameRate(s, 20.0f));
+
+	// Two test frames, red and green
+	cv::Mat test_image1(720, 1280, CV_8UC4, cv::Scalar(0,0,255,255));
+	cv::Mat test_image2(720, 1280, CV_8UC4, cv::Scalar(0,255,0,255));
+
+	ftlCheck(ftlIntrinsicsWriteLeft(s, 0, 1280, 720, 300.0f, -1280.0f/2.0f, -720.0f/2.0f, 0.1f, 0.1f, 8.0f));
+
+	// Write a number of frames, alternating images
+	for (int i=0; i<100; ++i) {
+		if (i&1) {
+			ftlCheck(ftlImageWrite(s, 0, FTLCHANNEL_Colour, FTLIMAGE_BGRA, test_image2.step, test_image2.data));
+		} else {
+			ftlCheck(ftlImageWrite(s, 0, FTLCHANNEL_Colour, FTLIMAGE_BGRA, test_image1.step, test_image1.data));
+		}
+
+		ftlCheck(ftlNextFrame(s));
+	}
+	
+
+	ftlCheck(ftlDestroyStream(s));
+
+	return 0;
+}
diff --git a/SDK/C/include/ftl/ftl.h b/SDK/C/include/ftl/ftl.h
new file mode 100644
index 000000000..33ff215da
--- /dev/null
+++ b/SDK/C/include/ftl/ftl.h
@@ -0,0 +1,195 @@
+#ifndef _FTL_SDK_HPP_
+#define _FTL_SDK_HPP_
+
+#ifdef __cplusplus
+#include <cstdint>
+#else
+#include <stdint.h>
+#endif
+
+struct FTLStream;
+typedef FTLStream* ftlStream_t;
+
+
+enum ftlError_t {
+	FTLERROR_OK,
+	FTLERROR_UNKNOWN,
+	FTLERROR_STREAM_READONLY,
+	FTLERROR_STREAM_WRITEONLY,
+	FTLERROR_STREAM_NO_FILE,
+	FTLERROR_STREAM_LISTEN_FAILED,
+	FTLERROR_STREAM_FILE_CREATE_FAILED,
+	FTLERROR_STREAM_NET_CREATE_FAILED,
+	FTLERROR_STREAM_INVALID_STREAM,
+	FTLERROR_STREAM_INVALID_PARAMETER,
+	FTLERROR_STREAM_ENCODE_FAILED,
+	FTLERROR_STREAM_DECODE_FAILED,
+	FTLERROR_STREAM_BAD_CHANNEL,
+	FTLERROR_STREAM_BAD_TIMESTAMP,
+	FTLERROR_STREAM_BAD_URI,
+	FTLERROR_STREAM_BAD_IMAGE_TYPE,
+	FTLERROR_STREAM_BAD_DATA,
+	FTLERROR_STREAM_BAD_IMAGE_SIZE,
+	FTLERROR_STREAM_NO_INTRINSICS,
+	FTLERROR_STREAM_NO_DATA,
+	FTLERROR_STREAM_DUPLICATE
+};
+
+enum ftlChannel_t {
+	FTLCHANNEL_None				= -1,
+	FTLCHANNEL_Colour			= 0,	// 8UC3 or 8UC4
+	FTLCHANNEL_Left				= 0,
+	FTLCHANNEL_Depth			= 1,	// 32S or 32F
+	FTLCHANNEL_Right			= 2,	// 8UC3 or 8UC4
+	FTLCHANNEL_Colour2			= 2,
+	FTLCHANNEL_Depth2			= 3,
+	FTLCHANNEL_Deviation		= 4,
+	FTLCHANNEL_Screen			= 4,
+	FTLCHANNEL_Normals			= 5,	// 16FC4
+	FTLCHANNEL_Weights			= 6,	// short
+	FTLCHANNEL_Confidence		= 7,	// 32F
+	FTLCHANNEL_Contribution		= 7,	// 32F
+	FTLCHANNEL_EnergyVector		= 8,	// 32FC4
+	FTLCHANNEL_Flow				= 9,	// 32F
+	FTLCHANNEL_Energy			= 10,	// 32F
+	FTLCHANNEL_Mask				= 11,	// 32U
+	FTLCHANNEL_Density			= 12,	// 32F
+	FTLCHANNEL_Support1			= 13,	// 8UC4 (currently)
+	FTLCHANNEL_Support2			= 14,	// 8UC4 (currently)
+	FTLCHANNEL_Segmentation		= 15,	// 32S?
+	FTLCHANNEL_Normals2			= 16,	// 16FC4
+	FTLCHANNEL_ColourHighRes	= 17,	// 8UC3 or 8UC4
+	FTLCHANNEL_LeftHighRes		= 17,	// 8UC3 or 8UC4
+	FTLCHANNEL_Disparity		= 18,
+	FTLCHANNEL_Smoothing		= 19,	// 32F
+	FTLCHANNEL_RightHighRes		= 20,	// 8UC3 or 8UC4
+	FTLCHANNEL_Colour2HighRes	= 20,
+	FTLCHANNEL_Overlay			= 21,   // 8UC4
+	FTLCHANNEL_GroundTruth		= 22,	// 32F
+
+	FTLCHANNEL_Audio			= 32,
+	FTLCHANNEL_AudioMono		= 32,
+	FTLCHANNEL_AudioStereo		= 33,
+
+	FTLCHANNEL_Configuration	= 64,	// JSON Data
+	FTLCHANNEL_Settings1		= 65,
+	FTLCHANNEL_Calibration		= 65,	// Camera Parameters Object
+	FTLCHANNEL_Pose				= 66,	// Eigen::Matrix4d
+	FTLCHANNEL_Settings2		= 67,
+	FTLCHANNEL_Calibration2		= 67,	// Right camera parameters
+	FTLCHANNEL_Index           	= 68,
+	FTLCHANNEL_Control			= 69,	// For stream and encoder control
+	FTLCHANNEL_Settings3		= 70,
+
+	FTLCHANNEL_Data				= 2048,	// Custom data, any codec.
+	FTLCHANNEL_Faces			= 2049, // Data about detected faces
+	FTLCHANNEL_Transforms		= 2050,	// Transformation matrices for framesets
+	FTLCHANNEL_Shapes3D			= 2051,	// Labeled 3D shapes
+	FTLCHANNEL_Messages			= 2052	// Vector of Strings
+};
+
+enum ftlImageFormat_t {
+	FTLIMAGE_FLOAT,
+	FTLIMAGE_BGRA,
+	FTLIMAGE_RGBA,
+	FTLIMAGE_RGB,
+	FTLIMAGE_BGR
+};
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Get the last error for a particular stream. Or use NULL stream to get errors
+ * when creating streams.
+ */
+ftlError_t ftlGetLastStreamError(ftlStream_t stream);
+
+// ==== FTL Stream API =========================================================
+
+/**
+ * Create a new file or net stream from a URI. This is for writing or sending
+ * data and makes a write only stream.
+ */
+ftlStream_t ftlCreateWriteStream(const char *uri);
+
+/**
+ * Open an existing file or network stream from the URI. These streams are
+ * read only.
+ */
+ftlStream_t ftlCreateReadStream(const char *uri);
+
+/**
+ * Write raw image data to a frame. The width and height of
+ * the image contained in `data` are contained within the intrinsics data which
+ * must be written to a stream before calling `ftlImageWrite`. The `pitch`
+ * argument determines the number of bytes per row of the image, if set to 0
+ * the pitch is assumed to be width multiplied by size of pixel in bytes. Note
+ * that `ftlNextFrame` must be called before calling this again for the same
+ * source and channel.
+ * 
+ * @param stream As created with `ftlCreateWriteStream`
+ * @param sourceId Unique consecutive ID for a camera or source
+ * @param channel The image channel
+ * @param type The image format
+ * @param pitch Bytes per row, in case different from width x sizeof(type)
+ * @param data Raw image data, pitch x height bytes in size
+ * 
+ * @return FTLERROR_OK on success
+ */
+ftlError_t ftlImageWrite(
+	ftlStream_t stream,
+	int32_t sourceId,
+	ftlChannel_t channel,
+	ftlImageFormat_t type,
+	uint32_t pitch,
+	const void *data);
+
+/**
+ * Only for writing streams, this determines the timestamp interval between
+ * frames when `ftlNextFrame` is called. When reading an FTL file this is
+ * determined automatically. Default is 25 fps. Advised to call this only once
+ * before any call to `ftlNextFrame`.
+ * 
+ * @param stream As created with `ftlCreateWriteStream`
+ * @param fps Frames per second of video output
+ */
+ftlError_t ftlSetFrameRate(ftlStream_t stream, float fps);
+
+/**
+ * Move a write stream to the next frame. It is an error to call
+ * `ftlImageWrite` multiple times for the same source and channel without
+ * calling this function. This will also be used when reading FTL files.
+ */
+ftlError_t ftlNextFrame(ftlStream_t stream);
+
+/**
+ * Write of 4x4 transformation matrix into the stream for a given source. The
+ * `data` pointer must contain 16 float values packed and in Eigen::Matrix4f
+ * form.
+ */
+ftlError_t ftlPoseWrite(ftlStream_t stream, int32_t sourceId, const float *data);
+
+/**
+ * This should be called before any other function for a given `sourceId`.
+ * The width and height information here is used implicitely by other API calls.
+ */
+ftlError_t ftlIntrinsicsWriteLeft(ftlStream_t stream, int32_t sourceId, int32_t width, int32_t height, float f, float cx, float cy, float baseline, float minDepth, float maxDepth);
+
+/**
+ * Call this after the left intrinsics have been set.
+ */
+ftlError_t ftlIntrinsicsWriteRight(ftlStream_t stream, int32_t sourceId, int32_t width, int32_t height, float f, float cx, float cy, float baseline, float minDepth, float maxDepth);
+
+/**
+ * Close and destroy the stream, ensuring all read/write operations have
+ * completed. Network connections are terminated and files closed.
+ */
+ftlError_t ftlDestroyStream(ftlStream_t stream);
+
+#ifdef __cplusplus
+};
+#endif
+
+#endif
diff --git a/SDK/C/src/common.cpp b/SDK/C/src/common.cpp
new file mode 100644
index 000000000..6e1e252f4
--- /dev/null
+++ b/SDK/C/src/common.cpp
@@ -0,0 +1,3 @@
+#include <ftl/ftl.h>
+
+
diff --git a/SDK/C/src/streams.cpp b/SDK/C/src/streams.cpp
new file mode 100644
index 000000000..e9dc6ff07
--- /dev/null
+++ b/SDK/C/src/streams.cpp
@@ -0,0 +1,276 @@
+#include <ftl/ftl.h>
+#include <ftl/uri.hpp>
+#include <ftl/rgbd/camera.hpp>
+#include <ftl/streams/sender.hpp>
+#include <ftl/streams/filestream.hpp>
+#include <ftl/streams/netstream.hpp>
+
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
+static ftlError_t last_error = FTLERROR_OK;
+static ftl::Configurable *root = nullptr;
+
+struct FTLStream {
+	bool readonly;
+	ftl::stream::Sender *sender;
+	ftl::stream::Stream *stream;
+	ftlError_t last_error;
+	int64_t interval;
+	bool has_fresh_data;
+
+	std::vector<ftl::rgbd::FrameState> video_states;
+	ftl::rgbd::FrameSet video_fs;
+};
+
+ftlError_t ftlGetLastStreamError(ftlStream_t stream) {
+	return (stream == nullptr) ? last_error : stream->last_error;
+}
+
+static void createFileWriteStream(FTLStream *s, const ftl::URI &uri) {
+	if (!root) {
+		int argc = 1;
+		const char *argv[] = {"SDK",0};
+		root = ftl::configure(argc, const_cast<char**>(argv), "sdk_default");
+	}
+
+	auto *fs = ftl::create<ftl::stream::File>(root, "ftlfile");
+	fs->set("filename", uri.getPath());
+	fs->setMode(ftl::stream::File::Mode::Write);
+	s->stream = fs;
+};
+
+ftlStream_t ftlCreateWriteStream(const char *uri) {
+	std::string uristr(uri);
+	ftl::URI u(uristr);
+
+	if (!u.isValid()) {
+		last_error = FTLERROR_STREAM_BAD_URI;
+		return nullptr;
+	}
+
+	FTLStream *s = new FTLStream;
+	s->last_error = FTLERROR_OK;
+	s->stream = nullptr;
+	s->sender = nullptr;
+	s->video_fs.id = 0;
+	s->video_fs.count = 0;
+	s->video_fs.mask = 0;
+	s->interval = 40;
+	s->video_fs.frames.reserve(32);
+	s->video_states.resize(32);
+
+	switch (u.getScheme()) {
+		case ftl::URI::SCHEME_FILE	: createFileWriteStream(s, u); break;
+		default						: last_error = FTLERROR_STREAM_BAD_URI;
+									  return nullptr;
+	}
+
+	if (s->last_error == FTLERROR_OK) {
+		s->sender = ftl::create<ftl::stream::Sender>(root, "sender");
+		s->sender->setStream(s->stream);
+		if (!s->stream->begin()) {
+			last_error = FTLERROR_STREAM_FILE_CREATE_FAILED;
+			return nullptr;
+		}
+	}
+	last_error = FTLERROR_OK;
+
+	s->video_fs.timestamp = ftl::timer::get_time();
+
+	return s;
+}
+
+ftlError_t ftlImageWrite(
+	ftlStream_t stream,
+	int32_t sourceId,
+	ftlChannel_t channel,
+	ftlImageFormat_t type,
+	uint32_t pitch,
+	const void *data)
+{
+	if (!stream || !stream->stream)
+		return FTLERROR_STREAM_INVALID_STREAM;
+	if (sourceId < 0 || sourceId >= 32)
+		return FTLERROR_STREAM_INVALID_PARAMETER;
+	if (static_cast<int>(channel) < 0 || static_cast<int>(channel) > 32)
+		return FTLERROR_STREAM_BAD_CHANNEL;
+	if (!stream->video_fs.hasFrame(sourceId))
+		return FTLERROR_STREAM_NO_INTRINSICS;
+	if (!data) return FTLERROR_STREAM_NO_DATA;
+	if (stream->video_fs.hasChannel(static_cast<ftl::codecs::Channel>(channel)))
+		return FTLERROR_STREAM_DUPLICATE;
+
+	try {
+		auto &frame = stream->video_fs.frames[sourceId];
+		auto &img = frame.create<cv::cuda::GpuMat>(static_cast<ftl::codecs::Channel>(channel));
+		auto &intrin = frame.getLeft();
+
+		if (intrin.width == 0) {
+			return FTLERROR_STREAM_NO_INTRINSICS;
+		}
+
+		switch (type) {
+		case FTLIMAGE_FLOAT		: img.upload(cv::Mat(intrin.height, intrin.width, CV_32F, const_cast<void*>(data), pitch)); break;
+		case FTLIMAGE_BGRA		: img.upload(cv::Mat(intrin.height, intrin.width, CV_8UC4, const_cast<void*>(data), pitch)); break;
+		case FTLIMAGE_RGBA		:
+		case FTLIMAGE_BGR		:
+		case FTLIMAGE_RGB		:
+		default					: return FTLERROR_STREAM_BAD_IMAGE_TYPE;
+		}
+
+		if (img.empty()) return FTLERROR_STREAM_NO_DATA;
+
+		ftl::codecs::Channels<0> channels;
+		if (stream->stream->size() > static_cast<unsigned int>(stream->video_fs.id)) channels = stream->stream->selected(stream->video_fs.id);
+		channels += static_cast<ftl::codecs::Channel>(channel);
+		stream->stream->select(stream->video_fs.id, channels, true);
+
+	} catch (const std::exception &e) {
+		return FTLERROR_UNKNOWN;
+	}
+
+	stream->has_fresh_data = true;
+	return FTLERROR_OK;
+}
+
+ftlError_t ftlIntrinsicsWriteLeft(ftlStream_t stream, int32_t sourceId, int32_t width, int32_t height, float f, float cx, float cy, float baseline, float minDepth, float maxDepth) {
+	if (!stream || !stream->stream)
+		return FTLERROR_STREAM_INVALID_STREAM;
+	if (sourceId < 0 || sourceId >= 32)
+		return FTLERROR_STREAM_INVALID_PARAMETER;
+
+	while (stream->video_fs.frames.size() <= static_cast<unsigned int>(sourceId)) {
+		stream->video_fs.frames.emplace_back();
+	}
+
+	if (stream->video_fs.hasFrame(sourceId)) {
+		return FTLERROR_STREAM_DUPLICATE;
+	}
+
+	ftl::rgbd::Camera cam;
+	cam.fx = f;
+	cam.fy = f;
+	cam.cx = cx;
+	cam.cy = cy;
+	cam.width = width;
+	cam.height = height;
+	cam.minDepth = minDepth;
+	cam.maxDepth = maxDepth;
+	cam.baseline = baseline;
+	cam.doffs = 0.0f;
+	stream->video_fs.mask |= 1 << sourceId;
+	stream->video_fs.count++;
+	if (!stream->video_fs.frames[sourceId].origin()) {
+		stream->video_fs.frames[sourceId].setOrigin(&stream->video_states[sourceId]);
+	}
+	stream->video_fs.frames[sourceId].setLeft(cam);
+	stream->has_fresh_data = true;
+
+	return FTLERROR_OK;
+}
+
+ftlError_t ftlIntrinsicsWriteRight(ftlStream_t stream, int32_t sourceId, int32_t width, int32_t height, float f, float cx, float cy, float baseline, float minDepth, float maxDepth) {
+	if (!stream || !stream->stream)
+		return FTLERROR_STREAM_INVALID_STREAM;
+	if (sourceId < 0 || sourceId >= 32)
+		return FTLERROR_STREAM_INVALID_PARAMETER;
+	if (!stream->video_fs.hasFrame(sourceId))
+		return FTLERROR_STREAM_NO_INTRINSICS;
+
+	ftl::rgbd::Camera cam;
+	cam.fx = f;
+	cam.fy = f;
+	cam.cx = cx;
+	cam.cy = cy;
+	cam.width = width;
+	cam.height = height;
+	cam.minDepth = minDepth;
+	cam.maxDepth = maxDepth;
+	cam.baseline = baseline;
+	cam.doffs = 0.0f;
+	stream->video_fs.frames[sourceId].setRight(cam);
+	stream->has_fresh_data = true;
+
+	return FTLERROR_OK;
+}
+
+ftlError_t ftlPoseWrite(ftlStream_t stream, int32_t sourceId, const float *data) {
+	if (!stream) return FTLERROR_STREAM_INVALID_STREAM;
+	if (!stream->stream) return FTLERROR_STREAM_INVALID_STREAM;
+	if (sourceId < 0 || sourceId >= 32)
+		return FTLERROR_STREAM_INVALID_PARAMETER;
+	if (!stream->video_fs.hasFrame(sourceId))
+		return FTLERROR_STREAM_NO_INTRINSICS;
+	if (!data) return FTLERROR_STREAM_NO_DATA;
+
+	Eigen::Matrix4f pose;
+	for (int i=0; i<16; ++i) pose.data()[i] = data[i];
+
+	auto &frame = stream->video_fs.frames[sourceId];
+	frame.setPose(pose.cast<double>());
+
+	return FTLERROR_OK;
+}
+
+ftlError_t ftlSetFrameRate(ftlStream_t stream, float fps) {
+	if (!stream) return FTLERROR_STREAM_INVALID_STREAM;
+	if (!stream->stream) return FTLERROR_STREAM_INVALID_STREAM;
+
+	stream->interval = int64_t(1000.0f / fps);
+
+	return FTLERROR_OK;
+}
+
+ftlError_t ftlNextFrame(ftlStream_t stream) {
+	if (!stream) return FTLERROR_STREAM_INVALID_STREAM;
+	if (!stream->stream) return FTLERROR_STREAM_INVALID_STREAM;
+	if (!stream->has_fresh_data) return FTLERROR_STREAM_NO_DATA;
+
+	try {
+		stream->sender->post(stream->video_fs);
+	} catch (const std::exception &e) {
+		return FTLERROR_STREAM_ENCODE_FAILED;
+	}
+
+	// Reset the frameset.
+	for (size_t i=0; i<stream->video_fs.frames.size(); ++i) {
+		if (!stream->video_fs.hasFrame(i)) continue;
+
+		auto &f = stream->video_fs.frames[i];
+		f.reset();
+		f.setOrigin(&stream->video_states[i]);
+	}
+
+	// FIXME: These should be reset each time
+	//stream->video_fs.count = 0;
+	//stream->video_fs.mask = 0;
+	stream->video_fs.timestamp += stream->interval;
+	stream->has_fresh_data = false;
+	return FTLERROR_OK;
+}
+
+ftlError_t ftlDestroyStream(ftlStream_t stream) {
+	if (!stream) return FTLERROR_STREAM_INVALID_STREAM;
+	if (!stream->stream) return FTLERROR_STREAM_INVALID_STREAM;
+
+	ftlError_t err = FTLERROR_OK;
+
+	if (stream->has_fresh_data) {
+		try {
+			stream->sender->post(stream->video_fs);
+		} catch (const std::exception &e) {
+			err = FTLERROR_STREAM_ENCODE_FAILED;
+		}
+	}
+
+	if (!stream->stream->end()) {
+		err = FTLERROR_STREAM_FILE_CREATE_FAILED;
+	}
+	if (stream->sender) delete stream->sender;
+	delete stream->stream;
+	stream->sender = nullptr;
+	stream->stream = nullptr;
+	delete stream;
+	return err;
+}
diff --git a/components/streams/src/filestream.cpp b/components/streams/src/filestream.cpp
index 9766298b1..412594814 100644
--- a/components/streams/src/filestream.cpp
+++ b/components/streams/src/filestream.cpp
@@ -91,6 +91,8 @@ bool File::post(const ftl::codecs::StreamPacket &s, const ftl::codecs::Packet &p
 		return false;
 	}
 
+	LOG(INFO) << "WRITE: " << s.timestamp << " " << (int)s.channel << " " << p.data.size();
+
 	// Don't write dummy packets to files.
 	if (p.data.size() == 0) return true;
 
diff --git a/components/structures/include/ftl/data/frame.hpp b/components/structures/include/ftl/data/frame.hpp
index 637621169..c304e4e97 100644
--- a/components/structures/include/ftl/data/frame.hpp
+++ b/components/structures/include/ftl/data/frame.hpp
@@ -525,6 +525,7 @@ void ftl::data::Frame<BASE,N,STATE,DATA>::setPose(const Eigen::Matrix4d &pose, b
 		if (mark) origin_->setPose(pose);
 		else origin_->getPose() = pose;
 	}
+	state_.setPose(pose);
 }
 
 template <int BASE, int N, typename STATE, typename DATA>
@@ -535,11 +536,13 @@ void ftl::data::Frame<BASE,N,STATE,DATA>::patchPose(const Eigen::Matrix4d &pose)
 template <int BASE, int N, typename STATE, typename DATA>
 void ftl::data::Frame<BASE,N,STATE,DATA>::setLeft(const typename STATE::Settings &c) {
 	if (origin_) origin_->setLeft(c);
+	state_.setLeft(c);
 }
 
 template <int BASE, int N, typename STATE, typename DATA>
 void ftl::data::Frame<BASE,N,STATE,DATA>::setRight(const typename STATE::Settings &c) {
 	if (origin_) origin_->setRight(c);
+	state_.setRight(c);
 }
 
 template <int BASE, int N, typename STATE, typename DATA>
-- 
GitLab