diff --git a/applications/gui/src/camera.cpp b/applications/gui/src/camera.cpp
index bb715383baaf1f355cba5d96fcec805b7ad932e6..b223e6f9cca81fd1b357919761d79708e853576d 100644
--- a/applications/gui/src/camera.cpp
+++ b/applications/gui/src/camera.cpp
@@ -189,6 +189,7 @@ void ftl::gui::Camera::setPose(const Eigen::Matrix4d &p) {
 }
 
 void ftl::gui::Camera::mouseMovement(int rx, int ry, int button) {
+	if (!src_->hasCapabilities(ftl::rgbd::kCapMovable)) return;
 	if (button == 1) {
 		float rrx = ((float)ry * 0.2f * delta_);
 		//orientation_[2] += std::cos(orientation_[1])*((float)rel[1] * 0.2f * delta_);
@@ -202,6 +203,7 @@ void ftl::gui::Camera::mouseMovement(int rx, int ry, int button) {
 }
 
 void ftl::gui::Camera::keyMovement(int key, int modifiers) {
+	if (!src_->hasCapabilities(ftl::rgbd::kCapMovable)) return;
 	if (key == 263 || key == 262) {
 		float mag = (modifiers & 0x1) ? 0.01f : 0.1f;
 		float scalar = (key == 263) ? -mag : mag;
@@ -249,7 +251,7 @@ const GLTexture &ftl::gui::Camera::captureFrame() {
 		Eigen::Affine3d t(trans);
 		Eigen::Matrix4d viewPose = t.matrix() * rotmat_;
 
-		src_->setPose(viewPose);
+		if (src_->hasCapabilities(ftl::rgbd::kCapMovable)) src_->setPose(viewPose);
 		src_->grab();
 		src_->getFrames(rgb, depth);
 
diff --git a/applications/reconstruct/src/virtual_source.cpp b/applications/reconstruct/src/virtual_source.cpp
index 245339376c7ded2093de988aa4366c2ef539a325..46eca475e0868a06317104fd4975778b705b1e58 100644
--- a/applications/reconstruct/src/virtual_source.cpp
+++ b/applications/reconstruct/src/virtual_source.cpp
@@ -24,6 +24,8 @@ VirtualSource::VirtualSource(ftl::rgbd::Source *host)
 	params_.maxDepth = rays_->value("max_depth", 10.0f);
 	params_.minDepth = rays_->value("min_depth", 0.1f);
 
+	capabilities_ = kCapMovable | kCapVideo | kCapStereo;
+
 	rgb_ = cv::Mat(cv::Size(params_.width,params_.height), CV_8UC3);
 	idepth_ = cv::Mat(cv::Size(params_.width,params_.height), CV_32SC1);
 	depth_ = cv::Mat(cv::Size(params_.width,params_.height), CV_32FC1);
diff --git a/components/rgbd-sources/include/ftl/rgbd/detail/source.hpp b/components/rgbd-sources/include/ftl/rgbd/detail/source.hpp
index cee6e61794db6a7d9cead54d9ffa2d035baee7f7..a047174de7e1b983706ffcca341b7996a3ec8b14 100644
--- a/components/rgbd-sources/include/ftl/rgbd/detail/source.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/detail/source.hpp
@@ -10,6 +10,13 @@ namespace rgbd {
 
 class Source;
 
+typedef unsigned int capability_t;
+
+static const capability_t kCapMovable	= 0x0001;	// A movable virtual cam
+static const capability_t kCapVideo		= 0x0002;	// Is a video feed
+static const capability_t kCapActive	= 0x0004;	// An active depth sensor
+static const capability_t kCapStereo	= 0x0005;	// Has right RGB
+
 namespace detail {
 
 class Source {
@@ -17,7 +24,7 @@ class Source {
 	friend class ftl::rgbd::Source;
 
 	public:
-	explicit Source(ftl::rgbd::Source *host) : host_(host), params_({0}) { }
+	explicit Source(ftl::rgbd::Source *host) : capabilities_(0), host_(host), params_({0}) { }
 	virtual ~Source() {}
 
 	virtual bool grab()=0;
@@ -25,6 +32,7 @@ class Source {
 	virtual void setPose(const Eigen::Matrix4d &pose) { };
 
 	protected:
+	capability_t capabilities_;
 	ftl::rgbd::Source *host_;
 	ftl::rgbd::Camera params_;
 	cv::Mat rgb_;
diff --git a/components/rgbd-sources/include/ftl/rgbd/source.hpp b/components/rgbd-sources/include/ftl/rgbd/source.hpp
index c43525ae4ba14b2843e87a50493a3222c648c084..3b5e53f18b3ed36b9c1adb6337f16d9c79a9b7ed 100644
--- a/components/rgbd-sources/include/ftl/rgbd/source.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/source.hpp
@@ -23,24 +23,15 @@ static inline bool isValidDepth(float d) { return (d > 0.01f) && (d < 39.99f); }
 
 class SnapshotReader;
 
-enum capability_t {
-	kCapColour,		// Has a colour feed
-	kCapDepth,		// Has a depth feed
-	kCapRight,		// It is possible to get a right image
-	kCapMovable,	// Camera is software movable
-	kCapVideo,		// It is a video feed, not static
-	kCapDisparity	// Raw disparity is available
-};
-
 typedef unsigned int channel_t;
 
-static const unsigned int kChanLeft = 1;
-static const unsigned int kChanDepth = 2;
-static const unsigned int kChanRight = 3;
-static const unsigned int kChanDisparity = 4;
-static const unsigned int kChanDeviation = 5;
+static const channel_t kChanLeft = 1;
+static const channel_t kChanDepth = 2;
+static const channel_t kChanRight = 3;
+static const channel_t kChanDisparity = 4;
+static const channel_t kChanDeviation = 5;
 
-static const unsigned int kChanOverlay1 = 100;
+static const channel_t kChanOverlay1 = 100;
 
 /**
  * RGBD Generic data source configurable entity. This class hides the
@@ -140,7 +131,9 @@ class Source : public ftl::Configurable {
 	/**
 	 * Check what features this source has available.
 	 */
-	virtual bool hasCapability(capability_t);
+	bool hasCapabilities(capability_t);
+
+	capability_t getCapabilities() const;
 
 	/**
 	 * Get a point in camera coordinates at specified pixel location.
diff --git a/components/rgbd-sources/src/middlebury_source.cpp b/components/rgbd-sources/src/middlebury_source.cpp
index c539acf0c3ed43fb954a0adf43979a126fd241ac..a1f490e1d2c0e725bb2a2cd8f4b826612a5731ec 100644
--- a/components/rgbd-sources/src/middlebury_source.cpp
+++ b/components/rgbd-sources/src/middlebury_source.cpp
@@ -63,6 +63,8 @@ MiddleburySource::MiddleburySource(ftl::rgbd::Source *host, const string &dir)
 
 	double scaling = host->value("scaling", 0.5);
 
+	capabilities_ = kCapStereo;
+
 	// Load params from txt file..
 	/*params_.fx = 3000.0 * scaling;
 	params_.width = 3000.0 * scaling;
diff --git a/components/rgbd-sources/src/net.cpp b/components/rgbd-sources/src/net.cpp
index 8667be5fc7747337dd7db8987ca80f3d3b6cc87e..62682d712665cc8f173c7447046103e2bc893d44 100644
--- a/components/rgbd-sources/src/net.cpp
+++ b/components/rgbd-sources/src/net.cpp
@@ -3,6 +3,7 @@
 #include <thread>
 #include <chrono>
 #include <shared_mutex>
+#include <tuple>
 
 #include "colour.hpp"
 
@@ -18,11 +19,14 @@ using std::unique_lock;
 using std::vector;
 using std::this_thread::sleep_for;
 using std::chrono::milliseconds;
+using std::tuple;
 
 bool NetSource::_getCalibration(Universe &net, const UUID &peer, const string &src, ftl::rgbd::Camera &p) {
 	try {
 		while(true) {
-			auto buf = net.call<vector<unsigned char>>(peer_, "source_calibration", src);
+			auto [cap,buf] = net.call<tuple<unsigned int,vector<unsigned char>>>(peer_, "source_details", src);
+
+			capabilities_ = cap;
 
 			if (buf.size() > 0) {
 				memcpy((char*)&p, buf.data(), buf.size());
diff --git a/components/rgbd-sources/src/realsense_source.cpp b/components/rgbd-sources/src/realsense_source.cpp
index 54fd1888d3a25446d57c8c91dbe2cfd3c9d45594..e1b66afb1a9b83d83333a92b75746ac4542d26c0 100644
--- a/components/rgbd-sources/src/realsense_source.cpp
+++ b/components/rgbd-sources/src/realsense_source.cpp
@@ -6,6 +6,8 @@ using std::string;
 
 RealsenseSource::RealsenseSource(ftl::rgbd::Source *host)
         : ftl::rgbd::detail::Source(host), align_to_depth_(RS2_STREAM_DEPTH) {
+	capabilities_ = kCapVideo;
+
     rs2::config cfg;
     cfg.enable_stream(RS2_STREAM_DEPTH, 1280, 720, RS2_FORMAT_Z16, 30);
     cfg.enable_stream(RS2_STREAM_COLOR, 1280, 720, RS2_FORMAT_BGRA8, 30);
diff --git a/components/rgbd-sources/src/source.cpp b/components/rgbd-sources/src/source.cpp
index a22c3e6308d5797c458d2567cf949e14b0fd9719..b5741520c5daedfca51745277384cf341e7c483d 100644
--- a/components/rgbd-sources/src/source.cpp
+++ b/components/rgbd-sources/src/source.cpp
@@ -195,8 +195,13 @@ const Eigen::Matrix4d &Source::getPose() const {
 	return pose_;
 }
 
-bool Source::hasCapability(capability_t) {
-	return false;
+bool Source::hasCapabilities(capability_t c) {
+	return getCapabilities() & c == c;
+}
+
+capability_t Source::getCapabilities() const {
+	if (impl_) return impl_->capabilities_;
+	else return 0;
 }
 
 void Source::reset() {
diff --git a/components/rgbd-sources/src/stereovideo.cpp b/components/rgbd-sources/src/stereovideo.cpp
index 0fb5fd864584aa53786392adcfc295b1aab23c2a..c0471643a0aae646ccbe98cfb37dad942f93b5ba 100644
--- a/components/rgbd-sources/src/stereovideo.cpp
+++ b/components/rgbd-sources/src/stereovideo.cpp
@@ -31,7 +31,8 @@ StereoVideoSource::~StereoVideoSource() {
 }
 
 void StereoVideoSource::init(const string &file) {
-	LOG(INFO) << "STEREOSOURCE = " << file;
+	capabilities_ = kCapVideo | kCapStereo;
+
 	if (ftl::is_video(file)) {
 		// Load video file
 		LOG(INFO) << "Using video file...";
diff --git a/components/rgbd-sources/src/streamer.cpp b/components/rgbd-sources/src/streamer.cpp
index 10ada28fce9b25af78c22c9ad9c1822855a7ca2d..e2daa0f0fc6fe5d258c22593691f249a93e62ef1 100644
--- a/components/rgbd-sources/src/streamer.cpp
+++ b/components/rgbd-sources/src/streamer.cpp
@@ -3,6 +3,7 @@
 #include <optional>
 #include <thread>
 #include <chrono>
+#include <tuple>
 
 using ftl::rgbd::Streamer;
 using ftl::rgbd::Source;
@@ -20,6 +21,8 @@ using std::unique_lock;
 using std::shared_lock;
 using std::this_thread::sleep_for;
 using std::chrono::milliseconds;
+using std::tuple;
+using std::make_tuple;
 
 
 Streamer::Streamer(nlohmann::json &config, Universe *net)
@@ -71,7 +74,7 @@ Streamer::Streamer(nlohmann::json &config, Universe *net)
 	});
 
 	// Allow remote users to access camera calibration matrix
-	net->bind("source_calibration", [this](const std::string &uri) -> vector<unsigned char> {
+	net->bind("source_details", [this](const std::string &uri) -> tuple<unsigned int,vector<unsigned char>> {
 		vector<unsigned char> buf;
 		SHARED_LOCK(mutex_,slk);
 
@@ -79,8 +82,10 @@ Streamer::Streamer(nlohmann::json &config, Universe *net)
 			buf.resize(sizeof(Camera));
 			LOG(INFO) << "Calib buf size = " << buf.size();
 			memcpy(buf.data(), &sources_[uri]->src->parameters(), buf.size());
+			return make_tuple(sources_[uri]->src->getCapabilities(), buf);
+		} else {
+			return make_tuple(0u,buf);
 		}
-		return buf;
 	});
 
 	net->bind("get_stream", [this](const string &source, int N, int rate, const UUID &peer, const string &dest) {