diff --git a/cscore.gradle b/cscore.gradle index 56e4611bec..095adfe9d3 100644 --- a/cscore.gradle +++ b/cscore.gradle @@ -143,6 +143,29 @@ def cscoreSetupExamplesModel = { project -> } } } + + httpcvstream(NativeExecutableSpec) { + if (project.isArm) { + targetPlatform 'arm' + } else { + //targetPlatform 'x86' + targetPlatform 'x64' + } + setupDefines(project, binaries) + sources { + cpp { + source { + srcDir "${rootDir}/examples/httpcvstream" + include '**/*.cpp' + } + exportedHeaders { + srcDirs = ["${rootDir}/include", "${rootDir}/wpiutil/include", project.openCvInclude] + include '**/*.h' + } + lib library: 'cscore', linkage: 'static' + } + } + } } } } diff --git a/examples/httpcvstream/httpcvstream.cpp b/examples/httpcvstream/httpcvstream.cpp new file mode 100644 index 0000000000..e631218e95 --- /dev/null +++ b/examples/httpcvstream/httpcvstream.cpp @@ -0,0 +1,28 @@ +#include "cscore.h" +#include "opencv2/core/core.hpp" + +#include +#include + +int main() { + cs::HttpCamera camera{"httpcam", "http://localhost:8081/?action=stream"}; + camera.SetVideoMode(cs::VideoMode::kMJPEG, 320, 240, 30); + cs::CvSink cvsink{"cvsink"}; + cvsink.SetSource(camera); + cs::CvSource cvsource{"cvsource", cs::VideoMode::kMJPEG, 320, 240, 30}; + cs::MjpegServer cvMjpegServer{"cvhttpserver", 8083}; + cvMjpegServer.SetSource(cvsource); + + cv::Mat test; + cv::Mat flip; + for (;;) { + uint64_t time = cvsink.GrabFrame(test); + if (time == 0) { + std::cout << "error: " << cvsink.GetError() << std::endl; + continue; + } + std::cout << "got frame at time " << time << " size " << test.size() << std::endl; + cv::flip(test, flip, 0); + cvsource.PutFrame(flip); + } +} diff --git a/include/cscore_c.h b/include/cscore_c.h index cd038a1ac1..b814f654fd 100644 --- a/include/cscore_c.h +++ b/include/cscore_c.h @@ -51,7 +51,9 @@ enum CS_StatusValue { CS_INVALID_PROPERTY = -2002, CS_WRONG_PROPERTY_TYPE = -2003, CS_READ_FAILED = -2004, - CS_SOURCE_IS_DISCONNECTED = -2005 + CS_SOURCE_IS_DISCONNECTED = -2005, + CS_EMPTY_VALUE = -2006, + CS_BAD_URL = -2007 }; // @@ -112,6 +114,16 @@ enum CS_SourceKind { CS_SOURCE_CV = 4 }; +// +// HTTP Camera kinds +// +enum CS_HttpCameraKind { + CS_HTTP_UNKNOWN = 0, + CS_HTTP_MJPGSTREAMER = 1, + CS_HTTP_CSCORE = 2, + CS_HTTP_AXIS = 3 +}; + // // Sink kinds // @@ -190,7 +202,10 @@ CS_Source CS_CreateUsbCameraDev(const char* name, int dev, CS_Status* status); CS_Source CS_CreateUsbCameraPath(const char* name, const char* path, CS_Status* status); CS_Source CS_CreateHttpCamera(const char* name, const char* url, - CS_Status* status); + enum CS_HttpCameraKind kind, CS_Status* status); +CS_Source CS_CreateHttpCameraMulti(const char* name, const char** urls, + int count, enum CS_HttpCameraKind kind, + CS_Status* status); CS_Source CS_CreateCvSource(const char* name, const CS_VideoMode* mode, CS_Status* status); @@ -232,6 +247,14 @@ void CS_ReleaseSource(CS_Source source, CS_Status* status); // char* CS_GetUsbCameraPath(CS_Source source, CS_Status* status); +// +// HttpCamera Source Functions +// +CS_HttpCameraKind CS_GetHttpCameraKind(CS_Source source, CS_Status* status); +void CS_SetHttpCameraUrls(CS_Source source, const char** urls, int count, + CS_Status* status); +char** CS_GetHttpCameraUrls(CS_Source source, int* count, CS_Status* status); + // // OpenCV Source Functions // @@ -329,6 +352,7 @@ void CS_ReleaseEnumeratedSinks(CS_Sink* sinks, int count); void CS_FreeString(char* str); void CS_FreeEnumPropertyChoices(char** choices, int count); +void CS_FreeHttpCameraUrls(char** urls, int count); void CS_FreeEnumeratedProperties(CS_Property* properties, int count); void CS_FreeEnumeratedVideoModes(CS_VideoMode* modes, int count); diff --git a/include/cscore_cpp.h b/include/cscore_cpp.h index 0e5c84b784..b6e2546c6e 100644 --- a/include/cscore_cpp.h +++ b/include/cscore_cpp.h @@ -162,7 +162,10 @@ CS_Source CreateUsbCameraDev(llvm::StringRef name, int dev, CS_Status* status); CS_Source CreateUsbCameraPath(llvm::StringRef name, llvm::StringRef path, CS_Status* status); CS_Source CreateHttpCamera(llvm::StringRef name, llvm::StringRef url, - CS_Status* status); + CS_HttpCameraKind kind, CS_Status* status); +CS_Source CreateHttpCamera(llvm::StringRef name, + llvm::ArrayRef urls, + CS_HttpCameraKind kind, CS_Status* status); CS_Source CreateCvSource(llvm::StringRef name, const VideoMode& mode, CS_Status* status); @@ -205,6 +208,14 @@ void ReleaseSource(CS_Source source, CS_Status* status); // std::string GetUsbCameraPath(CS_Source source, CS_Status* status); +// +// HttpCamera Source Functions +// +CS_HttpCameraKind GetHttpCameraKind(CS_Source source, CS_Status* status); +void SetHttpCameraUrls(CS_Source source, llvm::ArrayRef urls, + CS_Status* status); +std::vector GetHttpCameraUrls(CS_Source source, CS_Status* status); + // // OpenCV Source Functions // diff --git a/include/cscore_oo.h b/include/cscore_oo.h index 3c2b4b7630..d0c347b67d 100644 --- a/include/cscore_oo.h +++ b/include/cscore_oo.h @@ -254,10 +254,37 @@ class UsbCamera : public VideoSource { /// A source that represents a MJPEG-over-HTTP (IP) camera. class HttpCamera : public VideoSource { public: + enum CameraKind { + kUnknown = CS_HTTP_UNKNOWN, + kMJPGStreamer = CS_HTTP_MJPGSTREAMER, + kCSCore = CS_HTTP_CSCORE, + kAxis = CS_HTTP_AXIS + }; + /// Create a source for a MJPEG-over-HTTP (IP) camera. /// @param name Source name (arbitrary unique identifier) /// @param url Camera URL (e.g. "http://10.x.y.11/video/stream.mjpg") - HttpCamera(llvm::StringRef name, llvm::StringRef url); + /// @param kind Camera kind (e.g. kAxis) + HttpCamera(llvm::StringRef name, llvm::StringRef url, + CameraKind kind = kUnknown); + + /// Create a source for a MJPEG-over-HTTP (IP) camera. + /// @param name Source name (arbitrary unique identifier) + /// @param urls Array of Camera URLs + /// @param kind Camera kind (e.g. kAxis) + HttpCamera(llvm::StringRef name, llvm::ArrayRef urls, + CameraKind kind = kUnknown); + + /// Get the kind of HTTP camera. + /// Autodetection can result in returning a different value than the camera + /// was created with. + CameraKind GetCameraKind() const; + + /// Change the URLs used to connect to the camera. + void SetUrls(llvm::ArrayRef urls); + + /// Get the URLs used to connect to the camera. + std::vector GetUrls() const; }; /// A source for user code to provide OpenCV images as video frames. diff --git a/include/cscore_oo.inl b/include/cscore_oo.inl index 17d6da8099..55022930e2 100644 --- a/include/cscore_oo.inl +++ b/include/cscore_oo.inl @@ -222,8 +222,35 @@ inline void UsbCamera::SetExposureManual(int value) { GetProperty(kPropExValue).Set(value); } -inline HttpCamera::HttpCamera(llvm::StringRef name, llvm::StringRef url) { - m_handle = CreateHttpCamera(name, url, &m_status); +inline HttpCamera::HttpCamera(llvm::StringRef name, llvm::StringRef url, + CameraKind kind) { + m_handle = CreateHttpCamera( + name, url, static_cast(static_cast(kind)), + &m_status); +} + +inline HttpCamera::HttpCamera(llvm::StringRef name, + llvm::ArrayRef urls, + CameraKind kind) { + m_handle = CreateHttpCamera( + name, urls, static_cast(static_cast(kind)), + &m_status); +} + +inline HttpCamera::CameraKind HttpCamera::GetCameraKind() const { + m_status = 0; + return static_cast( + static_cast(::cs::GetHttpCameraKind(m_handle, &m_status))); +} + +inline void HttpCamera::SetUrls(llvm::ArrayRef urls) { + m_status = 0; + ::cs::SetHttpCameraUrls(m_handle, urls, &m_status); +} + +inline std::vector HttpCamera::GetUrls() const { + m_status = 0; + return ::cs::GetHttpCameraUrls(m_handle, &m_status); } inline CvSource::CvSource(llvm::StringRef name, const VideoMode& mode) { diff --git a/java/lib/CameraServerJNI.cpp b/java/lib/CameraServerJNI.cpp index 5c69111ae5..faa0a48729 100644 --- a/java/lib/CameraServerJNI.cpp +++ b/java/lib/CameraServerJNI.cpp @@ -152,6 +152,12 @@ static void ReportError(JNIEnv *env, CS_Status status) { case CS_SOURCE_IS_DISCONNECTED: msg = "source is disconnected"; break; + case CS_EMPTY_VALUE: + msg = "empty value"; + break; + case CS_BAD_URL: + msg = "bad URL"; + break; default: { llvm::raw_svector_ostream oss{msg}; oss << "unknown error code=" << status; @@ -410,11 +416,11 @@ JNIEXPORT jint JNICALL Java_edu_wpi_cscore_CameraServerJNI_createUsbCameraPath /* * Class: edu_wpi_cscore_CameraServerJNI - * Method: createHTTPCamera - * Signature: (Ljava/lang/String;Ljava/lang/String;)I + * Method: createHttpCamera + * Signature: (Ljava/lang/String;Ljava/lang/String;I)I */ JNIEXPORT jint JNICALL Java_edu_wpi_cscore_CameraServerJNI_createHttpCamera - (JNIEnv *env, jclass, jstring name, jstring url) + (JNIEnv *env, jclass, jstring name, jstring url, jint kind) { if (!name) { nullPointerEx.Throw(env, "name cannot be null"); @@ -425,8 +431,45 @@ JNIEXPORT jint JNICALL Java_edu_wpi_cscore_CameraServerJNI_createHttpCamera return 0; } CS_Status status = 0; - auto val = cs::CreateHttpCamera(JStringRef{env, name}, - JStringRef{env, url}, &status); + auto val = + cs::CreateHttpCamera(JStringRef{env, name}, JStringRef{env, url}, + static_cast(kind), &status); + CheckStatus(env, status); + return val; +} + +/* + * Class: edu_wpi_cscore_CameraServerJNI + * Method: createHttpCameraMulti + * Signature: (Ljava/lang/String;[Ljava/lang/String;I)I + */ +JNIEXPORT jint JNICALL Java_edu_wpi_cscore_CameraServerJNI_createHttpCameraMulti + (JNIEnv *env, jclass, jstring name, jobjectArray urls, jint kind) +{ + if (!name) { + nullPointerEx.Throw(env, "name cannot be null"); + return 0; + } + if (!urls) { + nullPointerEx.Throw(env, "urls cannot be null"); + return 0; + } + size_t len = env->GetArrayLength(urls); + llvm::SmallVector vec; + vec.reserve(len); + for (size_t i = 0; i < len; ++i) { + JLocal elem{ + env, static_cast(env->GetObjectArrayElement(urls, i))}; + if (!elem) { + // TODO + return 0; + } + vec.push_back(JStringRef{env, elem}.str()); + } + CS_Status status = 0; + auto val = + cs::CreateHttpCamera(JStringRef{env, name}, vec, + static_cast(kind), &status); CheckStatus(env, status); return val; } @@ -713,6 +756,63 @@ JNIEXPORT jstring JNICALL Java_edu_wpi_cscore_CameraServerJNI_getUsbCameraPath return MakeJString(env, str); } +/* + * Class: edu_wpi_cscore_CameraServerJNI + * Method: getHttpCameraKind + * Signature: (I)I + */ +JNIEXPORT jint JNICALL Java_edu_wpi_cscore_CameraServerJNI_getHttpCameraKind + (JNIEnv *env, jclass, jint source) +{ + CS_Status status = 0; + auto kind = cs::GetHttpCameraKind(source, &status); + if (!CheckStatus(env, status)) return 0; + return kind; +} + +/* + * Class: edu_wpi_cscore_CameraServerJNI + * Method: setHttpCameraUrls + * Signature: (I[Ljava/lang/String;)V + */ +JNIEXPORT void JNICALL Java_edu_wpi_cscore_CameraServerJNI_setHttpCameraUrls + (JNIEnv *env, jclass, jint source, jobjectArray urls) +{ + if (!urls) { + nullPointerEx.Throw(env, "urls cannot be null"); + return; + } + size_t len = env->GetArrayLength(urls); + llvm::SmallVector vec; + vec.reserve(len); + for (size_t i = 0; i < len; ++i) { + JLocal elem{ + env, static_cast(env->GetObjectArrayElement(urls, i))}; + if (!elem) { + // TODO + return; + } + vec.push_back(JStringRef{env, elem}.str()); + } + CS_Status status = 0; + cs::SetHttpCameraUrls(source, vec, &status); + CheckStatus(env, status); +} + +/* + * Class: edu_wpi_cscore_CameraServerJNI + * Method: getHttpCameraUrls + * Signature: (I)[Ljava/lang/String; + */ +JNIEXPORT jobjectArray JNICALL Java_edu_wpi_cscore_CameraServerJNI_getHttpCameraUrls + (JNIEnv *env, jclass, jint source) +{ + CS_Status status = 0; + auto arr = cs::GetHttpCameraUrls(source, &status); + if (!CheckStatus(env, status)) return nullptr; + return MakeJStringArray(env, arr); +} + /* * Class: edu_wpi_cscore_CameraServerJNI * Method: putSourceFrame diff --git a/java/src/edu/wpi/cscore/CameraServerJNI.java b/java/src/edu/wpi/cscore/CameraServerJNI.java index c846885de6..2344aa0d2c 100644 --- a/java/src/edu/wpi/cscore/CameraServerJNI.java +++ b/java/src/edu/wpi/cscore/CameraServerJNI.java @@ -104,7 +104,8 @@ public class CameraServerJNI { // public static native int createUsbCameraDev(String name, int dev); public static native int createUsbCameraPath(String name, String path); - public static native int createHttpCamera(String name, String url); + public static native int createHttpCamera(String name, String url, int kind); + public static native int createHttpCameraMulti(String name, String[] urls, int kind); public static native int createCvSource(String name, int pixelFormat, int width, int height, int fps); // @@ -132,6 +133,13 @@ public class CameraServerJNI { // public static native String getUsbCameraPath(int source); + // + // HttpCamera Source Functions + // + public static native int getHttpCameraKind(int source); + public static native void setHttpCameraUrls(int source, String[] urls); + public static native String[] getHttpCameraUrls(int source); + // // OpenCV Source Functions // diff --git a/java/src/edu/wpi/cscore/HttpCamera.java b/java/src/edu/wpi/cscore/HttpCamera.java index ca1cd0b1c3..d6269fa3a1 100644 --- a/java/src/edu/wpi/cscore/HttpCamera.java +++ b/java/src/edu/wpi/cscore/HttpCamera.java @@ -9,10 +9,72 @@ package edu.wpi.cscore; /// A source that represents a MJPEG-over-HTTP (IP) camera. public class HttpCamera extends VideoSource { + public enum CameraKind { + kUnknown(0), kMJPGStreamer(1), kCSCore(2), kAxis(3); + private int value; + + private CameraKind(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + public static CameraKind getCameraKindFromInt(int kind) { + switch (kind) { + case 1: return CameraKind.kMJPGStreamer; + case 2: return CameraKind.kCSCore; + case 3: return CameraKind.kAxis; + default: return CameraKind.kUnknown; + } + } + /// Create a source for a MJPEG-over-HTTP (IP) camera. /// @param name Source name (arbitrary unique identifier) /// @param url Camera URL (e.g. "http://10.x.y.11/video/stream.mjpg") public HttpCamera(String name, String url) { - super(CameraServerJNI.createHttpCamera(name, url)); + super(CameraServerJNI.createHttpCamera(name, url, CameraKind.kUnknown.getValue())); + } + + /// Create a source for a MJPEG-over-HTTP (IP) camera. + /// @param name Source name (arbitrary unique identifier) + /// @param url Camera URL (e.g. "http://10.x.y.11/video/stream.mjpg") + /// @param kind Camera kind (e.g. kAxis) + public HttpCamera(String name, String url, CameraKind kind) { + super(CameraServerJNI.createHttpCamera(name, url, kind.getValue())); + } + + /// Create a source for a MJPEG-over-HTTP (IP) camera. + /// @param name Source name (arbitrary unique identifier) + /// @param urls Array of Camera URLs + public HttpCamera(String name, String[] urls) { + super(CameraServerJNI.createHttpCameraMulti(name, urls, CameraKind.kUnknown.getValue())); + } + + /// Create a source for a MJPEG-over-HTTP (IP) camera. + /// @param name Source name (arbitrary unique identifier) + /// @param urls Array of Camera URLs + /// @param kind Camera kind (e.g. kAxis) + public HttpCamera(String name, String[] urls, CameraKind kind) { + super(CameraServerJNI.createHttpCameraMulti(name, urls, kind.getValue())); + } + + /// Get the kind of HTTP camera. + /// Autodetection can result in returning a different value than the camera + /// was created with. + CameraKind getCameraKind() { + return getCameraKindFromInt(CameraServerJNI.getHttpCameraKind(m_handle)); + } + + /// Change the URLs used to connect to the camera. + void setUrls(String[] urls) { + CameraServerJNI.setHttpCameraUrls(m_handle, urls); + } + + /// Get the URLs used to connect to the camera. + String[] getUrls() { + return CameraServerJNI.getHttpCameraUrls(m_handle); } } diff --git a/src/HttpCameraImpl.cpp b/src/HttpCameraImpl.cpp new file mode 100644 index 0000000000..c5cc04c386 --- /dev/null +++ b/src/HttpCameraImpl.cpp @@ -0,0 +1,548 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) FIRST 2016. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "HttpCameraImpl.h" + +#include "llvm/STLExtras.h" +#include "support/timestamp.h" +#include "tcpsockets/TCPConnector.h" + +#include "c_util.h" +#include "Handle.h" +#include "JpegUtil.h" +#include "Log.h" +#include "Notifier.h" + +using namespace cs; + +HttpCameraImpl::HttpCameraImpl(llvm::StringRef name, CS_HttpCameraKind kind) + : SourceImpl{name}, m_kind{kind} {} + +HttpCameraImpl::~HttpCameraImpl() { + m_active = false; + + // Close file if it's open + { + std::lock_guard lock(m_mutex); + if (m_streamConn) m_streamConn->stream->close(); + if (m_settingsConn) m_settingsConn->stream->close(); + } + + // force wakeup of camera thread in case it's waiting on cv + m_sinkEnabledCond.notify_one(); + + // join camera thread + if (m_streamThread.joinable()) m_streamThread.join(); + + // force wakeup of settings thread + m_settingsCond.notify_one(); + + // join settings thread + if (m_settingsThread.joinable()) m_settingsThread.join(); +} + +void HttpCameraImpl::Start() { + // Kick off the stream and settings threads + m_streamThread = std::thread(&HttpCameraImpl::StreamThreadMain, this); + m_settingsThread = std::thread(&HttpCameraImpl::SettingsThreadMain, this); +} + +static inline void DoFdSet(int fd, fd_set* set, int* nfds) { + if (fd >= 0) { + FD_SET(fd, set); + if ((fd + 1) > *nfds) *nfds = fd + 1; + } +} + +void HttpCameraImpl::StreamThreadMain() { + while (m_active) { + SetConnected(false); + + // sleep between retries + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + + // disconnect if no one is listening + if (m_numSinksEnabled == 0) { + std::unique_lock lock(m_mutex); + if (m_streamConn) m_streamConn->stream->close(); + // Wait for a sink to enable + m_sinkEnabledCond.wait( + lock, [=] { return !m_active || m_numSinksEnabled != 0; }); + if (!m_active) return; + } + + // connect + llvm::SmallString<64> boundary; + HttpConnection* conn = DeviceStreamConnect(boundary); + + if (!m_active) break; + + // keep retrying + if (!conn) continue; + + // update connected since we're actually connected + SetConnected(true); + + // stream + DeviceStream(conn->is, boundary); + } + + SDEBUG("Camera Thread exiting"); + SetConnected(false); +} + +HttpConnection* HttpCameraImpl::DeviceStreamConnect( + llvm::SmallVectorImpl& boundary) { + // Build the request + HttpRequest req; + { + std::lock_guard lock(m_mutex); + if (m_locations.empty()) { + SERROR("locations array is empty!?"); + std::this_thread::sleep_for(std::chrono::seconds(1)); + return nullptr; + } + if (m_nextLocation >= m_locations.size()) m_nextLocation = 0; + req = HttpRequest{m_locations[m_nextLocation++], m_streamSettings}; + m_streamSettingsUpdated = false; + } + + // Try to connect + auto stream = wpi::TCPConnector::connect(req.host.c_str(), req.port, + Logger::GetInstance(), 1); + + if (!m_active || !stream) return nullptr; + + auto connPtr = llvm::make_unique(std::move(stream), 1); + HttpConnection* conn = connPtr.get(); + + // update m_streamConn + { + std::lock_guard lock(m_mutex); + m_streamConn = std::move(connPtr); + } + + if (!conn->Handshake(req, GetName())) { + std::lock_guard lock(m_mutex); + m_streamConn = nullptr; + return nullptr; + } + + // Parse Content-Type header to get the boundary + llvm::StringRef mediaType, contentType; + std::tie(mediaType, contentType) = conn->contentType.str().split(';'); + mediaType = mediaType.trim(); + if (mediaType != "multipart/x-mixed-replace") { + SWARNING("\"" << req.host << "\": unrecognized Content-Type \"" << mediaType + << "\""); + std::lock_guard lock(m_mutex); + m_streamConn = nullptr; + return nullptr; + } + + // media parameters + boundary.clear(); + while (!contentType.empty()) { + llvm::StringRef keyvalue; + std::tie(keyvalue, contentType) = contentType.split(';'); + contentType = contentType.ltrim(); + llvm::StringRef key, value; + std::tie(key, value) = keyvalue.split('='); + if (key.trim() == "boundary") { + value = value.trim().trim('"'); // value may be quoted + boundary.append(value.begin(), value.end()); + } + } + + if (boundary.empty()) { + SWARNING("\"" << req.host + << "\": empty multi-part boundary or no Content-Type"); + std::lock_guard lock(m_mutex); + m_streamConn = nullptr; + return nullptr; + } + + return conn; +} + +void HttpCameraImpl::DeviceStream(wpi::raw_istream& is, + llvm::StringRef boundary) { + // Stored here so we reuse it from frame to frame + std::string imageBuf; + + // keep track of number of bad images received; if we receive 3 bad images + // in a row, we reconnect + int numErrors = 0; + + // streaming loop + while (m_active && !is.has_error() && m_numSinksEnabled > 0 && + numErrors < 3 && !m_streamSettingsUpdated) { + if (!FindMultipartBoundary(is, boundary, nullptr)) break; + + // Read the next two characters after the boundary (normally \r\n) + char eol[2]; + is.read(eol, 2); + if (!m_active || is.has_error()) break; + // End-of-stream is indicated with trailing -- + if (eol[0] == '-' && eol[1] == '-') break; + + if (!DeviceStreamFrame(is, imageBuf)) + ++numErrors; + else + numErrors = 0; + } +} + +bool HttpCameraImpl::DeviceStreamFrame(wpi::raw_istream& is, + std::string& imageBuf) { + // Read the headers + llvm::SmallString<64> contentTypeBuf; + llvm::SmallString<64> contentLengthBuf; + if (!ParseHttpHeaders(is, &contentTypeBuf, &contentLengthBuf)) { + SWARNING("disconnected during headers"); + PutError("disconnected during headers", wpi::Now()); + return false; + } + + // Check the content type (if present) + if (!contentTypeBuf.str().empty() && + !contentTypeBuf.str().startswith("image/jpeg")) { + llvm::SmallString<64> errBuf; + llvm::raw_svector_ostream errMsg{errBuf}; + errMsg << "received unknown Content-Type \"" << contentTypeBuf << "\""; + SWARNING(errMsg.str()); + PutError(errMsg.str(), wpi::Now()); + return false; + } + + unsigned int contentLength = 0; + if (contentLengthBuf.str().getAsInteger(10, contentLength)) { + // Ugh, no Content-Length? Read the blocks of the JPEG file. + int width, height; + if (!ReadJpeg(is, imageBuf, &width, &height)) { + SWARNING("did not receive a JPEG image"); + PutError("did not receive a JPEG image", wpi::Now()); + return false; + } + PutFrame(VideoMode::PixelFormat::kMJPEG, width, height, imageBuf, + wpi::Now()); + return true; + } + + // We know how big it is! Just get a frame of the right size and read + // the data directly into it. + auto image = AllocImage(VideoMode::PixelFormat::kMJPEG, 0, 0, contentLength); + is.read(image->data(), contentLength); + if (!m_active || is.has_error()) return false; + int width, height; + if (!GetJpegSize(image->str(), &width, &height)) { + SWARNING("did not receive a JPEG image"); + PutError("did not receive a JPEG image", wpi::Now()); + return false; + } + image->width = width; + image->height = height; + PutFrame(std::move(image), wpi::Now()); + return true; +} + +void HttpCameraImpl::SettingsThreadMain() { + for (;;) { + HttpRequest req; + { + std::unique_lock lock(m_mutex); + m_settingsCond.wait(lock, [=] { + return !m_active || (m_prefLocation != -1 && !m_settings.empty()); + }); + if (!m_active) break; + + // Build the request + req = HttpRequest{m_locations[m_prefLocation], m_settings}; + } + + DeviceSendSettings(req); + } + + SDEBUG("Settings Thread exiting"); +} + +void HttpCameraImpl::DeviceSendSettings(HttpRequest& req) { + // Try to connect + auto stream = wpi::TCPConnector::connect(req.host.c_str(), req.port, + Logger::GetInstance(), 1); + + if (!m_active || !stream) return; + + auto connPtr = llvm::make_unique(std::move(stream), 1); + HttpConnection* conn = connPtr.get(); + + // update m_settingsConn + { + std::lock_guard lock(m_mutex); + m_settingsConn = std::move(connPtr); + } + + // Just need a handshake as settings are sent via GET parameters + conn->Handshake(req, GetName()); + + conn->stream->close(); +} + +CS_HttpCameraKind HttpCameraImpl::GetKind() const { + std::lock_guard lock(m_mutex); + return m_kind; +} + +bool HttpCameraImpl::SetUrls(llvm::ArrayRef urls, + CS_Status* status) { + std::vector locations; + for (const auto& url : urls) { + bool error = false; + locations.emplace_back(url, &error, GetName()); + if (error) { + *status = CS_BAD_URL; + return false; + } + } + + std::lock_guard lock(m_mutex); + m_locations.swap(locations); + m_nextLocation = 0; + return true; +} + +std::vector HttpCameraImpl::GetUrls() const { + std::lock_guard lock(m_mutex); + std::vector urls; + for (const auto& loc : m_locations) urls.push_back(loc.url); + return urls; +} + +void HttpCameraImpl::CreateProperty(llvm::StringRef name, + llvm::StringRef httpParam, bool viaSettings, + CS_PropertyKind kind, int minimum, + int maximum, int step, int defaultValue, + int value) const { + std::lock_guard lock(m_mutex); + m_propertyData.emplace_back(llvm::make_unique( + name, httpParam, viaSettings, kind, minimum, maximum, step, defaultValue, + value)); + + Notifier::GetInstance().NotifySourceProperty( + *this, CS_SOURCE_PROPERTY_CREATED, m_propertyData.size() + 1, kind, value, + llvm::StringRef{}); +} + +template +void HttpCameraImpl::CreateEnumProperty( + llvm::StringRef name, llvm::StringRef httpParam, bool viaSettings, + int defaultValue, int value, std::initializer_list choices) const { + std::lock_guard lock(m_mutex); + m_propertyData.emplace_back(llvm::make_unique( + name, httpParam, viaSettings, CS_PROP_ENUM, 0, choices.size() - 1, 1, + defaultValue, value)); + + auto& enumChoices = m_propertyData.back()->enumChoices; + enumChoices.clear(); + for (const auto& choice : choices) enumChoices.emplace_back(choice); + + Notifier::GetInstance().NotifySourceProperty( + *this, CS_SOURCE_PROPERTY_CREATED, m_propertyData.size() + 1, + CS_PROP_ENUM, value, llvm::StringRef{}); + Notifier::GetInstance().NotifySourceProperty( + *this, CS_SOURCE_PROPERTY_CHOICES_UPDATED, m_propertyData.size() + 1, + CS_PROP_ENUM, value, llvm::StringRef{}); +} + +std::unique_ptr HttpCameraImpl::CreateEmptyProperty( + llvm::StringRef name) const { + return llvm::make_unique(name); +} + +bool HttpCameraImpl::CacheProperties(CS_Status* status) const { + std::lock_guard lock(m_mutex); + + // Pretty typical set of video modes + m_videoModes.clear(); + m_videoModes.emplace_back(VideoMode::kMJPEG, 640, 480, 30); + m_videoModes.emplace_back(VideoMode::kMJPEG, 320, 240, 30); + m_videoModes.emplace_back(VideoMode::kMJPEG, 160, 120, 30); + + m_properties_cached = true; + return true; +} + +void HttpCameraImpl::SetProperty(int property, int value, CS_Status* status) { + // TODO +} + +void HttpCameraImpl::SetStringProperty(int property, llvm::StringRef value, + CS_Status* status) { + // TODO +} + +bool HttpCameraImpl::SetVideoMode(const VideoMode& mode, CS_Status* status) { + if (mode.pixelFormat != VideoMode::kMJPEG) return false; + std::lock_guard lock(m_mutex); + m_mode = mode; + m_streamSettingsUpdated = true; + return true; +} + +void HttpCameraImpl::NumSinksChanged() { + // ignore +} + +void HttpCameraImpl::NumSinksEnabledChanged() { + m_sinkEnabledCond.notify_one(); +} + +bool AxisCameraImpl::CacheProperties(CS_Status* status) const { + CreateProperty("brightness", "ImageSource.I0.Sensor.Brightness", true, + CS_PROP_INTEGER, 0, 100, 1, 50, 50); + CreateEnumProperty("white_balance", "ImageSource.I0.Sensor.WhiteBalance", + true, 0, 0, + {"auto", "hold", "fixed_outdoor1", "fixed_outdoor2", + "fixed_indoor", "fixed_fluor1", "fixed_fluor2"}); + CreateProperty("color_level", "ImageSource.I0.Sensor.ColorLevel", true, + CS_PROP_INTEGER, 0, 100, 1, 50, 50); + CreateEnumProperty("exposure", "ImageSource.I0.Sensor.Exposure", true, 0, 0, + {"auto", "hold", "flickerfree50", "flickerfree60"}); + CreateProperty("exposure_priority", "ImageSource.I0.Sensor.ExposurePriority", + true, CS_PROP_INTEGER, 0, 100, 1, 50, 50); + + // TODO: get video modes from device + std::lock_guard lock(m_mutex); + m_videoModes.clear(); + m_videoModes.emplace_back(VideoMode::kMJPEG, 640, 480, 30); + m_videoModes.emplace_back(VideoMode::kMJPEG, 480, 360, 30); + m_videoModes.emplace_back(VideoMode::kMJPEG, 320, 240, 30); + m_videoModes.emplace_back(VideoMode::kMJPEG, 240, 180, 30); + m_videoModes.emplace_back(VideoMode::kMJPEG, 176, 144, 30); + m_videoModes.emplace_back(VideoMode::kMJPEG, 160, 120, 30); + + m_properties_cached = true; + return true; +} + +namespace cs { + +CS_Source CreateHttpCamera(llvm::StringRef name, llvm::StringRef url, + CS_HttpCameraKind kind, CS_Status* status) { + std::shared_ptr source; + switch (kind) { + case CS_HTTP_AXIS: + source = std::make_shared(name); + break; + default: + source = std::make_shared(name, kind); + break; + } + std::string urlCopy{url}; + if (!source->SetUrls(urlCopy, status)) return 0; + auto handle = Sources::GetInstance().Allocate(CS_SOURCE_HTTP, source); + auto& notifier = Notifier::GetInstance(); + notifier.NotifySource(name, handle, CS_SOURCE_CREATED); + source->Start(); + return handle; +} + +CS_Source CreateHttpCamera(llvm::StringRef name, + llvm::ArrayRef urls, + CS_HttpCameraKind kind, CS_Status* status) { + if (urls.empty()) { + *status = CS_EMPTY_VALUE; + return 0; + } + auto source = std::make_shared(name, kind); + if (!source->SetUrls(urls, status)) return 0; + auto handle = Sources::GetInstance().Allocate(CS_SOURCE_HTTP, source); + auto& notifier = Notifier::GetInstance(); + notifier.NotifySource(name, handle, CS_SOURCE_CREATED); + source->Start(); + return handle; +} + +CS_HttpCameraKind GetHttpCameraKind(CS_Source source, CS_Status* status) { + auto data = Sources::GetInstance().Get(source); + if (!data || data->kind != CS_SOURCE_HTTP) { + *status = CS_INVALID_HANDLE; + return CS_HTTP_UNKNOWN; + } + return static_cast(*data->source).GetKind(); +} + +void SetHttpCameraUrls(CS_Source source, llvm::ArrayRef urls, + CS_Status* status) { + if (urls.empty()) { + *status = CS_EMPTY_VALUE; + return; + } + auto data = Sources::GetInstance().Get(source); + if (!data || data->kind != CS_SOURCE_HTTP) { + *status = CS_INVALID_HANDLE; + return; + } + static_cast(*data->source).SetUrls(urls, status); +} + +std::vector GetHttpCameraUrls(CS_Source source, + CS_Status* status) { + auto data = Sources::GetInstance().Get(source); + if (!data || data->kind != CS_SOURCE_HTTP) { + *status = CS_INVALID_HANDLE; + return std::vector{}; + } + return static_cast(*data->source).GetUrls(); +} + +} // namespace cs + +extern "C" { + +CS_Source CS_CreateHttpCamera(const char* name, const char* url, + CS_HttpCameraKind kind, CS_Status* status) { + return cs::CreateHttpCamera(name, url, kind, status); +} + +CS_Source CS_CreateHttpCameraMulti(const char* name, const char** urls, + int count, CS_HttpCameraKind kind, + CS_Status* status) { + llvm::SmallVector vec; + vec.reserve(count); + for (int i = 0; i < count; ++i) vec.push_back(urls[i]); + return cs::CreateHttpCamera(name, vec, kind, status); +} + +CS_HttpCameraKind CS_GetHttpCameraKind(CS_Source source, CS_Status* status) { + return cs::GetHttpCameraKind(source, status); +} + +void CS_SetHttpCameraUrls(CS_Source source, const char** urls, int count, + CS_Status* status) { + llvm::SmallVector vec; + vec.reserve(count); + for (int i = 0; i < count; ++i) vec.push_back(urls[i]); + cs::SetHttpCameraUrls(source, vec, status); +} + +char** CS_GetHttpCameraUrls(CS_Source source, int* count, CS_Status* status) { + auto urls = cs::GetHttpCameraUrls(source, status); + char** out = static_cast(std::malloc(urls.size() * sizeof(char*))); + *count = urls.size(); + for (std::size_t i = 0; i < urls.size(); ++i) + out[i] = cs::ConvertToC(urls[i]); + return out; +} + +void CS_FreeHttpCameraUrls(char** urls, int count) { + if (!urls) return; + for (int i = 0; i < count; ++i) std::free(urls[i]); + std::free(urls); +} + +} // extern "C" diff --git a/src/HttpCameraImpl.h b/src/HttpCameraImpl.h new file mode 100644 index 0000000000..463a97db1f --- /dev/null +++ b/src/HttpCameraImpl.h @@ -0,0 +1,141 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) FIRST 2016. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#ifndef CS_HTTPCAMERAIMPL_H_ +#define CS_HTTPCAMERAIMPL_H_ + +#include +#include +#include +#include +#include +#include + +#include "llvm/SmallString.h" +#include "llvm/StringMap.h" +#include "support/raw_istream.h" + +#include "cscore_cpp.h" +#include "HttpUtil.h" +#include "SourceImpl.h" + +namespace cs { + +class HttpCameraImpl : public SourceImpl { + public: + HttpCameraImpl(llvm::StringRef name, CS_HttpCameraKind kind); + ~HttpCameraImpl() override; + + void Start(); + + // Property functions + void SetProperty(int property, int value, CS_Status* status) override; + void SetStringProperty(int property, llvm::StringRef value, + CS_Status* status) override; + + bool SetVideoMode(const VideoMode& mode, CS_Status* status) override; + + void NumSinksChanged() override; + void NumSinksEnabledChanged() override; + + CS_HttpCameraKind GetKind() const; + bool SetUrls(llvm::ArrayRef urls, CS_Status* status); + std::vector GetUrls() const; + + // Property data + class PropertyData : public PropertyImpl { + public: + PropertyData() = default; + PropertyData(llvm::StringRef name_) : PropertyImpl{name_} {} + PropertyData(llvm::StringRef name_, llvm::StringRef httpParam_, + bool viaSettings_, CS_PropertyKind kind_, int minimum_, + int maximum_, int step_, int defaultValue_, int value_) + : PropertyImpl(name_, kind_, step_, defaultValue_, value_), + viaSettings(viaSettings_), + httpParam(httpParam_) { + hasMinimum = true; + minimum = minimum_; + hasMaximum = true; + maximum = maximum_; + } + ~PropertyData() override = default; + + bool viaSettings{false}; + std::string httpParam; + }; + + protected: + std::unique_ptr CreateEmptyProperty( + llvm::StringRef name) const override; + + bool CacheProperties(CS_Status* status) const override; + + void CreateProperty(llvm::StringRef name, llvm::StringRef httpParam, + bool viaSettings, CS_PropertyKind kind, int minimum, + int maximum, int step, int defaultValue, int value) const; + + template + void CreateEnumProperty(llvm::StringRef name, llvm::StringRef httpParam, + bool viaSettings, int defaultValue, int value, + std::initializer_list choices) const; + + private: + // The camera streaming thread + void StreamThreadMain(); + + // Functions used by StreamThreadMain() + HttpConnection* DeviceStreamConnect(llvm::SmallVectorImpl& boundary); + void DeviceStream(wpi::raw_istream& is, llvm::StringRef boundary); + bool DeviceStreamFrame(wpi::raw_istream& is, std::string& imageBuf); + + // The camera settings thread + void SettingsThreadMain(); + void DeviceSendSettings(HttpRequest& req); + + std::atomic_bool m_connected{false}; + std::atomic_bool m_active{true}; // set to false to terminate thread + std::thread m_streamThread; + std::thread m_settingsThread; + + // + // Variables protected by m_mutex + // + + // The camera connections + std::unique_ptr m_streamConn; + std::unique_ptr m_settingsConn; + + CS_HttpCameraKind m_kind; + + std::vector m_locations; + std::size_t m_nextLocation{0}; + int m_prefLocation{-1}; // preferred location + + std::condition_variable m_sinkEnabledCond; + + llvm::StringMap> m_settings; + std::condition_variable m_settingsCond; + + llvm::StringMap> m_streamSettings; + std::atomic_bool m_streamSettingsUpdated{false}; +}; + +class AxisCameraImpl : public HttpCameraImpl { + public: + AxisCameraImpl(llvm::StringRef name) : HttpCameraImpl{name, CS_HTTP_AXIS} {} +#if 0 + void SetProperty(int property, int value, CS_Status* status) override; + void SetStringProperty(int property, llvm::StringRef value, + CS_Status* status) override; +#endif + protected: + bool CacheProperties(CS_Status* status) const override; +}; + +} // namespace cs + +#endif // CS_HTTPCAMERAIMPL_H_ diff --git a/src/HttpUtil.cpp b/src/HttpUtil.cpp index 435dba01bc..557898903a 100644 --- a/src/HttpUtil.cpp +++ b/src/HttpUtil.cpp @@ -9,8 +9,12 @@ #include -#include "support/raw_istream.h" +#include "llvm/STLExtras.h" +#include "support/Base64.h" #include "llvm/StringExtras.h" +#include "tcpsockets/TCPConnector.h" + +#include "Log.h" namespace cs { @@ -97,4 +101,254 @@ llvm::StringRef EscapeURI(llvm::StringRef str, llvm::SmallVectorImpl& buf, return llvm::StringRef{buf.data(), buf.size()}; } +bool ParseHttpHeaders(wpi::raw_istream& is, + llvm::SmallVectorImpl* contentType, + llvm::SmallVectorImpl* contentLength) { + + if (contentType) contentType->clear(); + if (contentLength) contentLength->clear(); + + bool inContentType = false; + bool inContentLength = false; + llvm::SmallString<64> lineBuf; + for (;;) { + bool error; + llvm::StringRef line = ReadLine(is, lineBuf, 1024, &error).rtrim(); + if (error) return false; + if (line.empty()) return true; // empty line signals end of headers + + // header fields start at the beginning of the line + if (!std::isspace(line[0])) { + inContentType = false; + inContentLength = false; + llvm::StringRef field; + std::tie(field, line) = line.split(':'); + field = field.rtrim(); + if (field == "Content-Type") + inContentType = true; + else if (field == "Content-Length") + inContentLength = true; + else + continue; // ignore other fields + } + + // collapse whitespace + line = line.ltrim(); + + // save field data + if (inContentType && contentType) + contentType->append(line.begin(), line.end()); + else if (inContentLength && contentLength) + contentLength->append(line.begin(), line.end()); + } +} + +bool FindMultipartBoundary(wpi::raw_istream& is, llvm::StringRef boundary, + std::string* saveBuf) { + llvm::SmallString<64> searchBuf; + searchBuf.resize(boundary.size() + 2); + size_t searchPos = 0; + + // Per the spec, the --boundary should be preceded by \r\n, so do a first + // pass of 1-byte reads to throw those away (common case) and keep the + // last non-\r\n character in searchBuf. + if (!saveBuf) { + do { + is.read(searchBuf.data(), 1); + if (is.has_error()) return false; + } while (searchBuf[0] == '\r' || searchBuf[0] == '\n'); + searchPos = 1; + } + + // Look for --boundary. Read boundarysize+2 bytes at a time + // during the search to speed up the reads, then fast-scan for -, + // and only then match the entire boundary. This will be slow if + // there's a bunch of continuous -'s in the output, but that's unlikely. + for (;;) { + is.read(searchBuf.data() + searchPos, searchBuf.size() - searchPos); + if (is.has_error()) return false; + + // Did we find the boundary? + if (searchBuf[0] == '-' && searchBuf[1] == '-' && + searchBuf.substr(2) == boundary) + return true; + + // Fast-scan for '-' + size_t pos = searchBuf.find('-', searchBuf[0] == '-' ? 1 : 0); + if (pos == llvm::StringRef::npos) { + if (saveBuf) + saveBuf->append(searchBuf.data(), searchBuf.size()); + } else { + if (saveBuf) + saveBuf->append(searchBuf.data(), pos); + + // move '-' and following to start of buffer (next read will fill) + std::memmove(searchBuf.data(), searchBuf.data() + pos, + searchBuf.size() - pos); + searchPos = searchBuf.size() - pos; + } + } +} + +HttpLocation::HttpLocation(llvm::StringRef url_, bool* error, + llvm::StringRef cameraName) + : url{url_} { + // Split apart into components + llvm::StringRef query{url_}; + + // scheme: + llvm::StringRef scheme; + std::tie(scheme, query) = query.split(':'); + if (scheme != "http") { + ERROR(cameraName << ": only supports http URLs, got \"" << url << "\""); + *error = true; + return; + } + + // "//" + if (!query.startswith("//")) { + ERROR(cameraName << ": expected http://..., got \"" << url << "\""); + *error = true; + return; + } + query = query.drop_front(2); + + // user:password@host:port/ + llvm::StringRef authority; + std::tie(authority, query) = query.split('/'); + + llvm::StringRef userpass, hostport; + std::tie(userpass, hostport) = authority.split('@'); + // split leaves the RHS empty if the split char isn't present... + if (hostport.empty()) { + hostport = userpass; + userpass = llvm::StringRef{}; + } + + if (!userpass.empty()) { + llvm::StringRef rawUser, rawPassword; + std::tie(rawUser, rawPassword) = userpass.split(':'); + llvm::SmallString<64> userBuf, passBuf; + user = UnescapeURI(rawUser, userBuf, error); + if (*error) { + ERROR(cameraName << ": could not unescape user \"" << rawUser + << "\" in \"" << url << "\""); + return; + } + password = UnescapeURI(rawPassword, passBuf, error); + if (*error) { + ERROR(cameraName << ": could not unescape password \"" << rawPassword + << "\" in \"" << url << "\""); + return; + } + } + + llvm::StringRef portStr; + std::tie(host, portStr) = hostport.rsplit(':'); + if (host.empty()) { + ERROR(cameraName << ": host is empty in \"" << url << "\""); + *error = true; + return; + } + if (portStr.empty()) { + port = 80; + } else if (portStr.getAsInteger(10, port)) { + ERROR(cameraName << ": port \"" << portStr << "\" is not an integer in \"" + << url << "\""); + *error = true; + return; + } + + // path?query#fragment + std::tie(query, fragment) = query.split('#'); + std::tie(path, query) = query.split('?'); + + // Split query string into parameters + while (!query.empty()) { + // split out next param and value + llvm::StringRef rawParam, rawValue; + std::tie(rawParam, query) = query.split('&'); + if (rawParam.empty()) continue; // ignore "&&" + std::tie(rawParam, rawValue) = rawParam.split('='); + + // unescape param + *error = false; + llvm::SmallString<64> paramBuf; + llvm::StringRef param = UnescapeURI(rawParam, paramBuf, error); + if (*error) { + ERROR(cameraName << ": could not unescape parameter \"" << rawParam + << "\" in \"" << url << "\""); + return; + } + + // unescape value + llvm::SmallString<64> valueBuf; + llvm::StringRef value = UnescapeURI(rawValue, valueBuf, error); + if (*error) { + ERROR(cameraName << ": could not unescape value \"" << rawValue + << "\" in \"" << url << "\""); + return; + } + + params.emplace_back(std::make_pair(param, value)); + } + + *error = false; +} + +void HttpRequest::SetAuth(const HttpLocation& loc) { + if (!loc.user.empty()) { + llvm::SmallString<64> userpass; + userpass += loc.user; + userpass += ':'; + userpass += loc.password; + wpi::Base64Encode(userpass, &auth); + } +} + +bool HttpConnection::Handshake(const HttpRequest& request, + llvm::StringRef cameraName) { + // send GET request + os << "GET /" << request.path << " HTTP/1.1\r\n"; + os << "Host: " << request.host << "\r\n"; + if (!request.auth.empty()) + os << "Authorization: Basic " << request.auth << "\r\n"; + os << "\r\n"; + os.flush(); + + // read first line of response + bool error = false; + llvm::SmallString<64> lineBuf; + llvm::StringRef line = ReadLine(is, lineBuf, 1024, &error).rtrim(); + if (error) { + WARNING(cameraName << ": \"" << request.host + << "\": disconnected before response"); + return false; + } + + // see if we got a HTTP 200 response + llvm::StringRef httpver, code, codeText; + std::tie(httpver, line) = line.split(' '); + std::tie(code, codeText) = line.split(' '); + if (!httpver.startswith("HTTP")) { + WARNING(cameraName << ": \"" << request.host + << "\": did not receive HTTP response"); + return false; + } + if (code != "200") { + WARNING(cameraName << ": \"" << request.host << "\": received " << code + << " " << codeText << " response"); + return false; + } + + // Parse headers + if (!ParseHttpHeaders(is, &contentType, &contentLength)) { + WARNING(cameraName << ": \"" << request.host + << "\": disconnected during headers"); + return false; + } + + return true; +} + } // namespace cs diff --git a/src/HttpUtil.h b/src/HttpUtil.h index cd3faf0c19..95873e52c3 100644 --- a/src/HttpUtil.h +++ b/src/HttpUtil.h @@ -8,12 +8,18 @@ #ifndef CS_HTTPUTIL_H_ #define CS_HTTPUTIL_H_ -#include "llvm/SmallVector.h" -#include "llvm/StringRef.h" +#include +#include -namespace wpi { -class raw_istream; -} +#include "llvm/ArrayRef.h" +#include "llvm/SmallString.h" +#include "llvm/SmallVector.h" +#include "llvm/StringMap.h" +#include "llvm/StringRef.h" +#include "support/raw_istream.h" +#include "support/raw_socket_istream.h" +#include "support/raw_socket_ostream.h" +#include "tcpsockets/NetworkStream.h" namespace cs { @@ -40,6 +46,105 @@ llvm::StringRef UnescapeURI(llvm::StringRef str, llvm::StringRef EscapeURI(llvm::StringRef str, llvm::SmallVectorImpl& buf, bool spacePlus = true); +// Parse a set of HTTP headers. Saves just the Content-Type and Content-Length +// fields. +// @param is Input stream +// @param contentType If not null, Content-Type contents are saved here. +// @param contentLength If not null, Content-Length contents are saved here. +// @return False if error occurred in input stream +bool ParseHttpHeaders(wpi::raw_istream& is, + llvm::SmallVectorImpl* contentType, + llvm::SmallVectorImpl* contentLength); + +// Look for a MIME multi-part boundary. On return, the input stream will +// be located at the character following the boundary (usually "\r\n"). +// @param is Input stream +// @param boundary Boundary string to scan for (not including "--" prefix) +// @param saveBuf If not null, all scanned characters up to but not including +// the boundary are saved to this string +// @return False if error occurred on input stream, true if boundary found. +bool FindMultipartBoundary(wpi::raw_istream& is, llvm::StringRef boundary, + std::string* saveBuf); + +class HttpLocation { + public: + HttpLocation() = default; + HttpLocation(llvm::StringRef url_, bool* error, llvm::StringRef cameraName); + + std::string url; // retain copy + std::string user; // unescaped + std::string password; // unescaped + std::string host; + int port; + std::string path; // escaped, not including leading '/' + std::vector> params; // unescaped + std::string fragment; +}; + +class HttpRequest { + public: + HttpRequest() = default; + + HttpRequest(const HttpLocation& loc) : host{loc.host}, port{loc.port} { + SetPath(loc.path, loc.params); + SetAuth(loc); + } + + template + HttpRequest(const HttpLocation& loc, const T& extraParams); + + HttpRequest(const HttpLocation& loc, llvm::StringRef path_) + : host{loc.host}, port{loc.port}, path{path_} { + SetAuth(loc); + } + + template + HttpRequest(const HttpLocation& loc, llvm::StringRef path_, const T& params) + : host{loc.host}, port{loc.port} { + SetPath(path_, params); + SetAuth(loc); + } + + llvm::SmallString<128> host; + int port; + std::string auth; + llvm::SmallString<128> path; + + private: + void SetAuth(const HttpLocation& loc); + template + void SetPath(llvm::StringRef path_, const T& params); + + template + static llvm::StringRef GetFirst(const T& elem) { return elem.first; } + template + static llvm::StringRef GetFirst(const llvm::StringMapEntry& elem) { + return elem.getKey(); + } + template + static llvm::StringRef GetSecond(const T& elem) { return elem.second; } +}; + +class HttpConnection { + public: + HttpConnection(std::unique_ptr stream_, int timeout) + : stream{std::move(stream_)}, is{*stream, timeout}, os{*stream, true} {} + + bool Handshake(const HttpRequest& request, llvm::StringRef cameraName); + + std::unique_ptr stream; + wpi::raw_socket_istream is; + wpi::raw_socket_ostream os; + + // Valid after Handshake() is successful + llvm::SmallString<64> contentType; + llvm::SmallString<64> contentLength; + + explicit operator bool() const { return stream && !is.has_error(); } +}; + } // namespace cs +#include "HttpUtil.inl" + #endif // CS_HTTPUTIL_H_ diff --git a/src/HttpUtil.inl b/src/HttpUtil.inl new file mode 100644 index 0000000000..1ea992f277 --- /dev/null +++ b/src/HttpUtil.inl @@ -0,0 +1,48 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) FIRST 2015. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#ifndef HTTPUTIL_INL_ +#define HTTPUTIL_INL_ + +namespace cs { + +template +HttpRequest::HttpRequest(const HttpLocation& loc, const T& extraParams) + : host{loc.host}, port{loc.port} { + llvm::StringMap params; + for (const auto& p : loc.params) + params.insert(std::make_pair(GetFirst(p), GetSecond(p))); + for (const auto& p : extraParams) + params.insert(std::make_pair(GetFirst(p), GetSecond(p))); + SetPath(loc.path, params); + SetAuth(loc); +} + +template +void HttpRequest::SetPath(llvm::StringRef path_, const T& params) { + // Build location including query string + llvm::raw_svector_ostream pathOs{path}; + pathOs << path_; + bool first = true; + for (const auto& param : params) { + if (first) { + pathOs << '?'; + first = false; + } else { + pathOs << '&'; + } + llvm::SmallString<64> escapeBuf; + pathOs << EscapeURI(GetFirst(param), escapeBuf); + if (!GetSecond(param).empty()) { + pathOs << '=' << EscapeURI(GetSecond(param), escapeBuf); + } + } +} + +} // namespace cs + +#endif // HTTPUTIL_INL_ diff --git a/src/cscore_c.cpp b/src/cscore_c.cpp index 7b763daab6..9172f1f1e1 100644 --- a/src/cscore_c.cpp +++ b/src/cscore_c.cpp @@ -75,11 +75,6 @@ char** CS_GetEnumPropertyChoices(CS_Property property, int* count, return out; } -CS_Source CS_CreateHttpCamera(const char* name, const char* url, - CS_Status* status) { - return cs::CreateHttpCamera(name, url, status); -} - CS_SourceKind CS_GetSourceKind(CS_Source source, CS_Status* status) { return cs::GetSourceKind(source, status); } diff --git a/src/cscore_cpp.cpp b/src/cscore_cpp.cpp index 5ac494869d..2c9274f5b7 100644 --- a/src/cscore_cpp.cpp +++ b/src/cscore_cpp.cpp @@ -148,15 +148,6 @@ std::vector GetEnumPropertyChoices(CS_Property property, return source->GetEnumPropertyChoices(propertyIndex, status); } -// -// Source Creation Functions -// - -CS_Source CreateHttpCamera(llvm::StringRef name, llvm::StringRef url, - CS_Status* status) { - return 0; // TODO -} - // // Source Functions //