2020-12-26 14:12:05 -08:00
|
|
|
// Copyright (c) FIRST and other WPILib contributors.
|
|
|
|
|
// Open Source Software; you can modify and/or share it under the terms of
|
|
|
|
|
// the WPILib BSD license file in the root directory of this project.
|
2016-09-08 00:05:10 -07:00
|
|
|
|
2016-12-04 00:08:47 -08:00
|
|
|
#include "MjpegServerImpl.h"
|
2016-09-08 00:05:10 -07:00
|
|
|
|
|
|
|
|
#include <chrono>
|
|
|
|
|
|
2021-06-06 16:13:58 -07:00
|
|
|
#include <fmt/format.h>
|
2018-04-29 23:33:19 -07:00
|
|
|
#include <wpi/SmallString.h>
|
2021-06-06 16:13:58 -07:00
|
|
|
#include <wpi/StringExtras.h>
|
|
|
|
|
#include <wpi/fmt/raw_ostream.h>
|
2022-05-07 10:54:14 -07:00
|
|
|
#include <wpinet/HttpUtil.h>
|
|
|
|
|
#include <wpinet/TCPAcceptor.h>
|
|
|
|
|
#include <wpinet/raw_socket_istream.h>
|
|
|
|
|
#include <wpinet/raw_socket_ostream.h>
|
2016-09-08 00:05:10 -07:00
|
|
|
|
|
|
|
|
#include "Handle.h"
|
2018-10-31 20:22:58 -07:00
|
|
|
#include "Instance.h"
|
2016-12-02 23:33:29 -08:00
|
|
|
#include "JpegUtil.h"
|
2016-09-08 00:05:10 -07:00
|
|
|
#include "Log.h"
|
2016-11-18 08:49:20 -08:00
|
|
|
#include "Notifier.h"
|
2016-09-08 00:05:10 -07:00
|
|
|
#include "SourceImpl.h"
|
2017-08-25 17:48:06 -07:00
|
|
|
#include "c_util.h"
|
|
|
|
|
#include "cscore_cpp.h"
|
2016-09-08 00:05:10 -07:00
|
|
|
|
|
|
|
|
using namespace cs;
|
|
|
|
|
|
|
|
|
|
// The boundary used for the M-JPEG stream.
|
|
|
|
|
// It separates the multipart stream of pictures
|
|
|
|
|
#define BOUNDARY "boundarydonotcross"
|
|
|
|
|
|
2016-11-18 18:52:18 -08:00
|
|
|
// A bare-bones HTML webpage for user friendliness.
|
2017-01-19 00:02:37 -08:00
|
|
|
static const char* emptyRootPage =
|
2017-11-17 09:34:30 -08:00
|
|
|
"</head><body>"
|
2016-11-18 18:52:18 -08:00
|
|
|
"<img src=\"/stream.mjpg\" /><p />"
|
|
|
|
|
"<a href=\"/settings.json\">Settings JSON</a>"
|
|
|
|
|
"</body></html>";
|
|
|
|
|
|
2017-01-19 00:02:37 -08:00
|
|
|
// An HTML page to be sent when a source exists
|
2017-08-25 17:48:06 -07:00
|
|
|
static const char* startRootPage =
|
2017-01-19 00:02:37 -08:00
|
|
|
"<script>\n"
|
|
|
|
|
"function httpGetAsync(name, val)\n"
|
|
|
|
|
"{\n"
|
2017-08-25 17:48:06 -07:00
|
|
|
" var host = location.protocol + '//' + location.host + "
|
|
|
|
|
"'/?action=command&' + name + '=' + val;\n"
|
2017-01-19 00:02:37 -08:00
|
|
|
" var xmlHttp = new XMLHttpRequest();\n"
|
|
|
|
|
" xmlHttp.open(\"GET\", host, true);\n"
|
|
|
|
|
" xmlHttp.send(null);\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
"function updateInt(prop, name, val) {\n"
|
|
|
|
|
" document.querySelector(prop).value = val;\n"
|
|
|
|
|
" httpGetAsync(name, val);\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
"function update(name, val) {\n"
|
|
|
|
|
" httpGetAsync(name, val);\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
"</script>\n"
|
2017-02-16 01:06:38 -08:00
|
|
|
"<style>\n"
|
|
|
|
|
"table, th, td {\n"
|
|
|
|
|
" border: 1px solid black;\n"
|
|
|
|
|
" border-collapse: collapse;\n"
|
|
|
|
|
"}\n"
|
2017-04-30 11:49:21 -04:00
|
|
|
".settings { float: left; }\n"
|
|
|
|
|
".stream { display: inline-block; margin-left: 10px; }\n"
|
2017-02-16 01:06:38 -08:00
|
|
|
"</style>\n"
|
2017-11-17 09:34:30 -08:00
|
|
|
"</head><body>\n"
|
2017-04-30 11:49:21 -04:00
|
|
|
"<div class=\"stream\">\n"
|
2017-01-19 00:02:37 -08:00
|
|
|
"<img src=\"/stream.mjpg\" /><p />\n"
|
2018-11-10 20:30:02 -08:00
|
|
|
"<a href=\"/settings.json\">Settings JSON</a> |\n"
|
|
|
|
|
"<a href=\"/config.json\">Source Config JSON</a>\n"
|
2017-04-30 11:49:21 -04:00
|
|
|
"</div>\n"
|
|
|
|
|
"<div class=\"settings\">\n";
|
2017-08-25 17:48:06 -07:00
|
|
|
static const char* endRootPage = "</div></body></html>";
|
2017-01-19 00:02:37 -08:00
|
|
|
|
2016-12-04 00:08:47 -08:00
|
|
|
class MjpegServerImpl::ConnThread : public wpi::SafeThread {
|
2016-11-11 00:01:58 -08:00
|
|
|
public:
|
2021-06-06 16:13:58 -07:00
|
|
|
explicit ConnThread(std::string_view name, wpi::Logger& logger)
|
|
|
|
|
: m_name(name), m_logger(logger) {}
|
2016-12-10 23:36:35 -08:00
|
|
|
|
2020-12-28 00:10:13 -08:00
|
|
|
void Main() override;
|
2016-11-11 00:01:58 -08:00
|
|
|
|
2018-04-29 23:33:19 -07:00
|
|
|
bool ProcessCommand(wpi::raw_ostream& os, SourceImpl& source,
|
2021-06-06 16:13:58 -07:00
|
|
|
std::string_view parameters, bool respond);
|
2018-04-29 23:33:19 -07:00
|
|
|
void SendJSON(wpi::raw_ostream& os, SourceImpl& source, bool header);
|
|
|
|
|
void SendHTMLHeadTitle(wpi::raw_ostream& os) const;
|
|
|
|
|
void SendHTML(wpi::raw_ostream& os, SourceImpl& source, bool header);
|
2016-11-11 00:01:58 -08:00
|
|
|
void SendStream(wpi::raw_socket_ostream& os);
|
|
|
|
|
void ProcessRequest();
|
|
|
|
|
|
|
|
|
|
std::unique_ptr<wpi::NetworkStream> m_stream;
|
|
|
|
|
std::shared_ptr<SourceImpl> m_source;
|
|
|
|
|
bool m_streaming = false;
|
2016-12-23 22:01:35 -08:00
|
|
|
bool m_noStreaming = false;
|
2018-07-27 23:00:15 -07:00
|
|
|
int m_width = 0;
|
|
|
|
|
int m_height = 0;
|
|
|
|
|
int m_compression = -1;
|
|
|
|
|
int m_defaultCompression = 80;
|
|
|
|
|
int m_fps = 0;
|
2016-11-11 00:01:58 -08:00
|
|
|
|
|
|
|
|
private:
|
2016-12-10 23:36:35 -08:00
|
|
|
std::string m_name;
|
2018-10-31 20:22:58 -07:00
|
|
|
wpi::Logger& m_logger;
|
2016-12-23 22:01:35 -08:00
|
|
|
|
2021-06-06 16:13:58 -07:00
|
|
|
std::string_view GetName() { return m_name; }
|
2016-12-10 23:36:35 -08:00
|
|
|
|
2016-11-11 00:01:58 -08:00
|
|
|
std::shared_ptr<SourceImpl> GetSource() {
|
2019-07-08 22:58:39 -07:00
|
|
|
std::scoped_lock lock(m_mutex);
|
2016-11-11 00:01:58 -08:00
|
|
|
return m_source;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void StartStream() {
|
2019-07-08 22:58:39 -07:00
|
|
|
std::scoped_lock lock(m_mutex);
|
2020-12-28 12:58:06 -08:00
|
|
|
if (m_source) {
|
|
|
|
|
m_source->EnableSink();
|
|
|
|
|
}
|
2016-11-11 00:01:58 -08:00
|
|
|
m_streaming = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void StopStream() {
|
2019-07-08 22:58:39 -07:00
|
|
|
std::scoped_lock lock(m_mutex);
|
2020-12-28 12:58:06 -08:00
|
|
|
if (m_source) {
|
|
|
|
|
m_source->DisableSink();
|
|
|
|
|
}
|
2016-11-11 00:01:58 -08:00
|
|
|
m_streaming = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2016-09-08 00:05:10 -07:00
|
|
|
// Standard header to send along with other header information like mimetype.
|
|
|
|
|
//
|
|
|
|
|
// The parameters should ensure the browser does not cache our answer.
|
|
|
|
|
// A browser should connect for each file and not serve files from its cache.
|
|
|
|
|
// Using cached pictures would lead to showing old/outdated pictures.
|
|
|
|
|
// Many browsers seem to ignore, or at least not always obey, those headers.
|
2018-07-29 12:53:41 -07:00
|
|
|
static void SendHeader(wpi::raw_ostream& os, int code,
|
2021-06-06 16:13:58 -07:00
|
|
|
std::string_view codeText, std::string_view contentType,
|
|
|
|
|
std::string_view extra = {}) {
|
|
|
|
|
fmt::print(os, "HTTP/1.0 {} {}\r\n", code, codeText);
|
2016-09-08 00:05:10 -07:00
|
|
|
os << "Connection: close\r\n"
|
|
|
|
|
"Server: CameraServer/1.0\r\n"
|
|
|
|
|
"Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, "
|
|
|
|
|
"post-check=0, max-age=0\r\n"
|
|
|
|
|
"Pragma: no-cache\r\n"
|
|
|
|
|
"Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n";
|
|
|
|
|
os << "Content-Type: " << contentType << "\r\n";
|
2017-08-07 17:46:33 -07:00
|
|
|
os << "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: *\r\n";
|
2021-06-06 16:13:58 -07:00
|
|
|
if (!extra.empty()) {
|
|
|
|
|
os << extra << "\r\n";
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
os << "\r\n"; // header ends with a blank line
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send error header and message
|
|
|
|
|
// @param code HTTP error code (e.g. 404)
|
|
|
|
|
// @param message Additional message text
|
2018-07-29 12:53:41 -07:00
|
|
|
static void SendError(wpi::raw_ostream& os, int code,
|
2021-06-06 16:13:58 -07:00
|
|
|
std::string_view message) {
|
|
|
|
|
std::string_view codeText, extra, baseMessage;
|
2016-09-08 00:05:10 -07:00
|
|
|
switch (code) {
|
|
|
|
|
case 401:
|
|
|
|
|
codeText = "Unauthorized";
|
|
|
|
|
extra = "WWW-Authenticate: Basic realm=\"CameraServer\"";
|
|
|
|
|
baseMessage = "401: Not Authenticated!";
|
|
|
|
|
break;
|
|
|
|
|
case 404:
|
|
|
|
|
codeText = "Not Found";
|
|
|
|
|
baseMessage = "404: Not Found!";
|
|
|
|
|
break;
|
|
|
|
|
case 500:
|
|
|
|
|
codeText = "Internal Server Error";
|
|
|
|
|
baseMessage = "500: Internal Server Error!";
|
|
|
|
|
break;
|
|
|
|
|
case 400:
|
|
|
|
|
codeText = "Bad Request";
|
|
|
|
|
baseMessage = "400: Not Found!";
|
|
|
|
|
break;
|
|
|
|
|
case 403:
|
|
|
|
|
codeText = "Forbidden";
|
|
|
|
|
baseMessage = "403: Forbidden!";
|
|
|
|
|
break;
|
2016-12-23 22:01:35 -08:00
|
|
|
case 503:
|
|
|
|
|
codeText = "Service Unavailable";
|
|
|
|
|
baseMessage = "503: Service Unavailable";
|
|
|
|
|
break;
|
2016-09-08 00:05:10 -07:00
|
|
|
default:
|
|
|
|
|
code = 501;
|
|
|
|
|
codeText = "Not Implemented";
|
|
|
|
|
baseMessage = "501: Not Implemented!";
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
SendHeader(os, code, codeText, "text/plain", extra);
|
|
|
|
|
os << baseMessage << "\r\n" << message;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Perform a command specified by HTTP GET parameters.
|
2018-04-29 23:33:19 -07:00
|
|
|
bool MjpegServerImpl::ConnThread::ProcessCommand(wpi::raw_ostream& os,
|
2016-11-11 00:01:58 -08:00
|
|
|
SourceImpl& source,
|
2021-06-06 16:13:58 -07:00
|
|
|
std::string_view parameters,
|
2016-11-11 00:01:58 -08:00
|
|
|
bool respond) {
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::SmallString<256> responseBuf;
|
|
|
|
|
wpi::raw_svector_ostream response{responseBuf};
|
2016-09-08 00:05:10 -07:00
|
|
|
// command format: param1=value1¶m2=value2...
|
|
|
|
|
while (!parameters.empty()) {
|
|
|
|
|
// split out next param and value
|
2021-06-06 16:13:58 -07:00
|
|
|
std::string_view rawParam, rawValue;
|
|
|
|
|
std::tie(rawParam, parameters) = wpi::split(parameters, '&');
|
2020-12-28 12:58:06 -08:00
|
|
|
if (rawParam.empty()) {
|
|
|
|
|
continue; // ignore "&&"
|
|
|
|
|
}
|
2021-06-06 16:13:58 -07:00
|
|
|
std::tie(rawParam, rawValue) = wpi::split(rawParam, '=');
|
2020-12-28 12:58:06 -08:00
|
|
|
if (rawParam.empty() || rawValue.empty()) {
|
|
|
|
|
continue; // ignore "param="
|
|
|
|
|
}
|
2021-06-06 16:13:58 -07:00
|
|
|
SDEBUG4("HTTP parameter \"{}\" value \"{}\"", rawParam, rawValue);
|
2016-09-08 00:05:10 -07:00
|
|
|
|
|
|
|
|
// unescape param
|
2016-12-02 19:15:40 -08:00
|
|
|
bool error = false;
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::SmallString<64> paramBuf;
|
2021-06-06 16:13:58 -07:00
|
|
|
std::string_view param = wpi::UnescapeURI(rawParam, paramBuf, &error);
|
2016-12-02 19:15:40 -08:00
|
|
|
if (error) {
|
2021-06-06 16:13:58 -07:00
|
|
|
auto estr = fmt::format("could not unescape parameter \"{}\"", rawParam);
|
|
|
|
|
SendError(os, 500, estr);
|
|
|
|
|
SDEBUG("{}", estr);
|
2016-09-08 00:05:10 -07:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// unescape value
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::SmallString<64> valueBuf;
|
2021-06-06 16:13:58 -07:00
|
|
|
std::string_view value = wpi::UnescapeURI(rawValue, valueBuf, &error);
|
2016-12-02 19:15:40 -08:00
|
|
|
if (error) {
|
2021-06-06 16:13:58 -07:00
|
|
|
auto estr = fmt::format("could not unescape value \"{}\"", rawValue);
|
|
|
|
|
SendError(os, 500, estr);
|
|
|
|
|
SDEBUG("{}", estr);
|
2016-09-08 00:05:10 -07:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-20 20:48:31 -08:00
|
|
|
// Handle resolution, compression, and FPS. These are handled locally
|
|
|
|
|
// rather than passed to the source.
|
2016-10-17 20:16:49 -07:00
|
|
|
if (param == "resolution") {
|
2021-06-06 16:13:58 -07:00
|
|
|
auto [widthStr, heightStr] = wpi::split(value, 'x');
|
|
|
|
|
int width = wpi::parse_integer<int>(widthStr, 10).value_or(-1);
|
|
|
|
|
int height = wpi::parse_integer<int>(heightStr, 10).value_or(-1);
|
|
|
|
|
if (width < 0) {
|
2016-12-20 20:48:31 -08:00
|
|
|
response << param << ": \"width is not an integer\"\r\n";
|
2021-06-06 16:13:58 -07:00
|
|
|
SWARNING("HTTP parameter \"{}\" width \"{}\" is not an integer", param,
|
|
|
|
|
widthStr);
|
2016-10-17 20:16:49 -07:00
|
|
|
continue;
|
|
|
|
|
}
|
2021-06-06 16:13:58 -07:00
|
|
|
if (height < 0) {
|
2016-12-20 20:48:31 -08:00
|
|
|
response << param << ": \"height is not an integer\"\r\n";
|
2021-06-06 16:13:58 -07:00
|
|
|
SWARNING("HTTP parameter \"{}\" height \"{}\" is not an integer", param,
|
|
|
|
|
heightStr);
|
2016-10-17 20:16:49 -07:00
|
|
|
continue;
|
|
|
|
|
}
|
2016-12-20 20:48:31 -08:00
|
|
|
m_width = width;
|
|
|
|
|
m_height = height;
|
|
|
|
|
response << param << ": \"ok\"\r\n";
|
2016-10-17 20:16:49 -07:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (param == "fps") {
|
2021-06-06 16:13:58 -07:00
|
|
|
if (auto v = wpi::parse_integer<int>(value, 10)) {
|
|
|
|
|
m_fps = v.value();
|
2016-12-20 20:48:31 -08:00
|
|
|
response << param << ": \"ok\"\r\n";
|
2021-06-06 16:13:58 -07:00
|
|
|
} else {
|
|
|
|
|
response << param << ": \"invalid integer\"\r\n";
|
|
|
|
|
SWARNING("HTTP parameter \"{}\" value \"{}\" is not an integer", param,
|
|
|
|
|
value);
|
2016-10-17 20:16:49 -07:00
|
|
|
}
|
2016-12-20 20:48:31 -08:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (param == "compression") {
|
2021-06-06 16:13:58 -07:00
|
|
|
if (auto v = wpi::parse_integer<int>(value, 10)) {
|
|
|
|
|
m_compression = v.value();
|
2016-10-17 20:16:49 -07:00
|
|
|
response << param << ": \"ok\"\r\n";
|
2021-06-06 16:13:58 -07:00
|
|
|
} else {
|
|
|
|
|
response << param << ": \"invalid integer\"\r\n";
|
|
|
|
|
SWARNING("HTTP parameter \"{}\" value \"{}\" is not an integer", param,
|
|
|
|
|
value);
|
2016-10-17 20:16:49 -07:00
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-18 18:33:49 -08:00
|
|
|
// ignore name parameter
|
2020-12-28 12:58:06 -08:00
|
|
|
if (param == "name") {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2016-11-18 18:33:49 -08:00
|
|
|
|
2016-09-08 00:05:10 -07:00
|
|
|
// try to assign parameter
|
2016-09-20 23:27:21 -07:00
|
|
|
auto prop = source.GetPropertyIndex(param);
|
|
|
|
|
if (!prop) {
|
2016-10-13 00:16:24 -07:00
|
|
|
response << param << ": \"ignored\"\r\n";
|
2021-06-06 16:13:58 -07:00
|
|
|
SWARNING("ignoring HTTP parameter \"{}\"", param);
|
2016-09-20 23:27:21 -07:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CS_Status status = 0;
|
2016-11-15 23:54:50 -08:00
|
|
|
auto kind = source.GetPropertyKind(prop);
|
|
|
|
|
switch (kind) {
|
2016-09-20 23:27:21 -07:00
|
|
|
case CS_PROP_BOOLEAN:
|
|
|
|
|
case CS_PROP_INTEGER:
|
|
|
|
|
case CS_PROP_ENUM: {
|
2021-06-06 16:13:58 -07:00
|
|
|
if (auto v = wpi::parse_integer<int>(value, 10)) {
|
|
|
|
|
fmt::print(response, "{}: {}\r\n", param, v.value());
|
|
|
|
|
SDEBUG4("HTTP parameter \"{}\" value {}", param, value);
|
|
|
|
|
source.SetProperty(prop, v.value(), &status);
|
2016-10-13 00:16:24 -07:00
|
|
|
} else {
|
2021-06-06 16:13:58 -07:00
|
|
|
response << param << ": \"invalid integer\"\r\n";
|
|
|
|
|
SWARNING("HTTP parameter \"{}\" value \"{}\" is not an integer",
|
|
|
|
|
param, value);
|
2016-10-13 00:16:24 -07:00
|
|
|
}
|
2016-09-20 23:27:21 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case CS_PROP_STRING: {
|
2016-10-13 00:16:24 -07:00
|
|
|
response << param << ": \"ok\"\r\n";
|
2021-06-06 16:13:58 -07:00
|
|
|
SDEBUG4("HTTP parameter \"{}\" value \"{}\"", param, value);
|
2016-09-20 23:27:21 -07:00
|
|
|
source.SetStringProperty(prop, value, &status);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Send HTTP response
|
2016-09-08 00:05:10 -07:00
|
|
|
if (respond) {
|
|
|
|
|
SendHeader(os, 200, "OK", "text/plain");
|
2016-10-13 00:16:24 -07:00
|
|
|
os << response.str() << "\r\n";
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-17 09:34:30 -08:00
|
|
|
void MjpegServerImpl::ConnThread::SendHTMLHeadTitle(
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::raw_ostream& os) const {
|
2018-12-31 02:49:24 -05:00
|
|
|
os << "<html><head><title>" << m_name << " CameraServer</title>"
|
|
|
|
|
<< "<meta charset=\"UTF-8\">";
|
2017-11-17 09:34:30 -08:00
|
|
|
}
|
|
|
|
|
|
2017-01-19 00:02:37 -08:00
|
|
|
// Send the root html file with controls for all the settable properties.
|
2018-04-29 23:33:19 -07:00
|
|
|
void MjpegServerImpl::ConnThread::SendHTML(wpi::raw_ostream& os,
|
2017-01-19 00:02:37 -08:00
|
|
|
SourceImpl& source, bool header) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (header) {
|
|
|
|
|
SendHeader(os, 200, "OK", "text/html");
|
|
|
|
|
}
|
2017-01-19 00:02:37 -08:00
|
|
|
|
2017-11-17 09:34:30 -08:00
|
|
|
SendHTMLHeadTitle(os);
|
2017-01-19 00:02:37 -08:00
|
|
|
os << startRootPage;
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::SmallVector<int, 32> properties_vec;
|
2017-01-19 00:02:37 -08:00
|
|
|
CS_Status status = 0;
|
|
|
|
|
for (auto prop : source.EnumerateProperties(properties_vec, &status)) {
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::SmallString<128> name_buf;
|
2017-01-19 00:02:37 -08:00
|
|
|
auto name = source.GetPropertyName(prop, name_buf, &status);
|
2021-06-06 16:13:58 -07:00
|
|
|
if (wpi::starts_with(name, "raw_")) {
|
2020-12-28 12:58:06 -08:00
|
|
|
continue;
|
|
|
|
|
}
|
2017-01-19 00:02:37 -08:00
|
|
|
auto kind = source.GetPropertyKind(prop);
|
2021-06-06 16:13:58 -07:00
|
|
|
fmt::print(os, "<p /><label for=\"{0}\">{0}</label>\n", name);
|
2017-01-19 00:02:37 -08:00
|
|
|
switch (kind) {
|
|
|
|
|
case CS_PROP_BOOLEAN:
|
2021-06-06 16:13:58 -07:00
|
|
|
fmt::print(os,
|
|
|
|
|
"<input id=\"{0}\" type=\"checkbox\" "
|
|
|
|
|
"onclick=\"update('{0}', this.checked ? 1 : 0)\" ",
|
|
|
|
|
name);
|
2020-12-28 12:58:06 -08:00
|
|
|
if (source.GetProperty(prop, &status) != 0) {
|
2017-01-19 00:02:37 -08:00
|
|
|
os << "checked />\n";
|
2020-12-28 12:58:06 -08:00
|
|
|
} else {
|
2017-01-19 00:02:37 -08:00
|
|
|
os << " />\n";
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2017-01-19 00:02:37 -08:00
|
|
|
break;
|
|
|
|
|
case CS_PROP_INTEGER: {
|
|
|
|
|
auto valI = source.GetProperty(prop, &status);
|
|
|
|
|
auto min = source.GetPropertyMin(prop, &status);
|
|
|
|
|
auto max = source.GetPropertyMax(prop, &status);
|
|
|
|
|
auto step = source.GetPropertyStep(prop, &status);
|
2021-06-06 16:13:58 -07:00
|
|
|
fmt::print(os,
|
|
|
|
|
"<input type=\"range\" min=\"{1}\" max=\"{2}\" "
|
|
|
|
|
"value=\"{3}\" id=\"{0}\" step=\"{4}\" "
|
|
|
|
|
"oninput=\"updateInt('#{0}op', '{0}', value)\" />\n",
|
|
|
|
|
name, min, max, valI, step);
|
|
|
|
|
fmt::print(os, "<output for=\"{0}\" id=\"{0}op\">{1}</output>\n", name,
|
|
|
|
|
valI);
|
2017-01-19 00:02:37 -08:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case CS_PROP_ENUM: {
|
|
|
|
|
auto valE = source.GetProperty(prop, &status);
|
|
|
|
|
auto choices = source.GetEnumPropertyChoices(prop, &status);
|
|
|
|
|
int j = 0;
|
|
|
|
|
for (auto choice = choices.begin(), end = choices.end(); choice != end;
|
|
|
|
|
++j, ++choice) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (choice->empty()) {
|
|
|
|
|
continue; // skip empty choices
|
|
|
|
|
}
|
2017-01-19 00:02:37 -08:00
|
|
|
// replace any non-printable characters in name with spaces
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::SmallString<128> ch_name;
|
2020-12-28 12:58:06 -08:00
|
|
|
for (char ch : *choice) {
|
2021-06-06 16:13:58 -07:00
|
|
|
ch_name.push_back(wpi::isPrint(ch) ? ch : ' ');
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2021-06-06 16:13:58 -07:00
|
|
|
fmt::print(os,
|
|
|
|
|
"<input id=\"{0}{1}\" type=\"radio\" name=\"{0}\" "
|
|
|
|
|
"value=\"{2}\" onclick=\"update('{0}', {1})\"",
|
|
|
|
|
name, j, ch_name);
|
2017-01-19 00:02:37 -08:00
|
|
|
if (j == valE) {
|
|
|
|
|
os << " checked";
|
|
|
|
|
}
|
2021-06-06 16:13:58 -07:00
|
|
|
fmt::print(os, " /><label for=\"{}{}\">{}</label>\n", name, j,
|
|
|
|
|
ch_name);
|
2017-01-19 00:02:37 -08:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case CS_PROP_STRING: {
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::SmallString<128> strval_buf;
|
2021-06-06 16:13:58 -07:00
|
|
|
fmt::print(os,
|
|
|
|
|
"<input type=\"text\" id=\"{0}box\" name=\"{0}\" "
|
|
|
|
|
"value=\"{1}\" />\n",
|
|
|
|
|
name, source.GetStringProperty(prop, strval_buf, &status));
|
|
|
|
|
fmt::print(os,
|
|
|
|
|
"<input type=\"button\" value =\"Submit\" "
|
|
|
|
|
"onclick=\"update('{0}', {0}box.value)\" />\n",
|
|
|
|
|
name);
|
2017-01-19 00:02:37 -08:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-30 11:48:18 -08:00
|
|
|
status = 0;
|
|
|
|
|
auto info = GetUsbCameraInfo(Instance::GetInstance().FindSource(source).first,
|
|
|
|
|
&status);
|
|
|
|
|
if (status == CS_OK) {
|
|
|
|
|
os << "<p>USB device path: " << info.path << '\n';
|
2020-12-28 12:58:06 -08:00
|
|
|
for (auto&& path : info.otherPaths) {
|
2018-12-30 11:48:18 -08:00
|
|
|
os << "<p>Alternate device path: " << path << '\n';
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2018-12-30 11:48:18 -08:00
|
|
|
}
|
|
|
|
|
|
2017-02-16 01:06:38 -08:00
|
|
|
os << "<p>Supported Video Modes:</p>\n";
|
|
|
|
|
os << "<table cols=\"4\" style=\"border: 1px solid black\">\n";
|
|
|
|
|
os << "<tr><th>Pixel Format</th>"
|
|
|
|
|
<< "<th>Width</th>"
|
|
|
|
|
<< "<th>Height</th>"
|
|
|
|
|
<< "<th>FPS</th></tr>";
|
|
|
|
|
for (auto mode : source.EnumerateVideoModes(&status)) {
|
|
|
|
|
os << "<tr><td>";
|
|
|
|
|
switch (mode.pixelFormat) {
|
2017-08-25 17:48:06 -07:00
|
|
|
case VideoMode::kMJPEG:
|
|
|
|
|
os << "MJPEG";
|
|
|
|
|
break;
|
|
|
|
|
case VideoMode::kYUYV:
|
|
|
|
|
os << "YUYV";
|
|
|
|
|
break;
|
|
|
|
|
case VideoMode::kRGB565:
|
|
|
|
|
os << "RGB565";
|
|
|
|
|
break;
|
|
|
|
|
case VideoMode::kBGR:
|
|
|
|
|
os << "BGR";
|
|
|
|
|
break;
|
|
|
|
|
case VideoMode::kGray:
|
|
|
|
|
os << "gray";
|
|
|
|
|
break;
|
2022-11-23 22:00:31 -08:00
|
|
|
case VideoMode::kUYVY:
|
|
|
|
|
os << "UYVY";
|
|
|
|
|
break;
|
2017-08-25 17:48:06 -07:00
|
|
|
default:
|
|
|
|
|
os << "unknown";
|
|
|
|
|
break;
|
2017-02-16 01:06:38 -08:00
|
|
|
}
|
2021-06-06 16:13:58 -07:00
|
|
|
fmt::print(os, "</td><td>{}</td><td>{}</td><td>{}</td></tr>", mode.width,
|
|
|
|
|
mode.height, mode.fps);
|
2017-02-16 01:06:38 -08:00
|
|
|
}
|
|
|
|
|
os << "</table>\n";
|
2017-01-19 00:02:37 -08:00
|
|
|
os << endRootPage << "\r\n";
|
|
|
|
|
os.flush();
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-08 00:05:10 -07:00
|
|
|
// Send a JSON file which is contains information about the source parameters.
|
2018-04-29 23:33:19 -07:00
|
|
|
void MjpegServerImpl::ConnThread::SendJSON(wpi::raw_ostream& os,
|
2016-11-11 00:01:58 -08:00
|
|
|
SourceImpl& source, bool header) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (header) {
|
|
|
|
|
SendHeader(os, 200, "OK", "application/json");
|
|
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
|
|
|
|
|
os << "{\n\"controls\": [\n";
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::SmallVector<int, 32> properties_vec;
|
2016-09-08 00:05:10 -07:00
|
|
|
bool first = true;
|
2016-10-13 00:16:24 -07:00
|
|
|
CS_Status status = 0;
|
|
|
|
|
for (auto prop : source.EnumerateProperties(properties_vec, &status)) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (first) {
|
2016-09-08 00:05:10 -07:00
|
|
|
first = false;
|
2020-12-28 12:58:06 -08:00
|
|
|
} else {
|
2016-09-08 00:05:10 -07:00
|
|
|
os << ",\n";
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2017-02-16 01:06:38 -08:00
|
|
|
os << '{';
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::SmallString<128> name_buf;
|
2016-09-20 20:40:58 -07:00
|
|
|
auto name = source.GetPropertyName(prop, name_buf, &status);
|
2016-11-15 23:54:50 -08:00
|
|
|
auto kind = source.GetPropertyKind(prop);
|
2021-06-06 16:13:58 -07:00
|
|
|
fmt::print(os, "\n\"name\": \"{}\"", name);
|
|
|
|
|
fmt::print(os, ",\n\"id\": \"{}\"", prop);
|
2022-08-16 15:35:26 -07:00
|
|
|
fmt::print(os, ",\n\"type\": \"{}\"", static_cast<int>(kind));
|
2021-06-06 16:13:58 -07:00
|
|
|
fmt::print(os, ",\n\"min\": \"{}\"", source.GetPropertyMin(prop, &status));
|
|
|
|
|
fmt::print(os, ",\n\"max\": \"{}\"", source.GetPropertyMax(prop, &status));
|
|
|
|
|
fmt::print(os, ",\n\"step\": \"{}\"",
|
|
|
|
|
source.GetPropertyStep(prop, &status));
|
|
|
|
|
fmt::print(os, ",\n\"default\": \"{}\"",
|
|
|
|
|
source.GetPropertyDefault(prop, &status));
|
2016-09-08 00:05:10 -07:00
|
|
|
os << ",\n\"value\": \"";
|
2016-11-15 23:54:50 -08:00
|
|
|
switch (kind) {
|
2016-09-08 00:05:10 -07:00
|
|
|
case CS_PROP_BOOLEAN:
|
2016-09-20 22:17:12 -07:00
|
|
|
case CS_PROP_INTEGER:
|
|
|
|
|
case CS_PROP_ENUM:
|
2021-06-06 16:13:58 -07:00
|
|
|
fmt::print(os, "{}", source.GetProperty(prop, &status));
|
2016-09-08 00:05:10 -07:00
|
|
|
break;
|
|
|
|
|
case CS_PROP_STRING: {
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::SmallString<128> strval_buf;
|
2016-09-20 20:40:58 -07:00
|
|
|
os << source.GetStringProperty(prop, strval_buf, &status);
|
2016-09-08 00:05:10 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
os << '"';
|
2016-09-20 20:40:58 -07:00
|
|
|
// os << ",\n\"dest\": \"0\"";
|
2016-09-08 00:05:10 -07:00
|
|
|
// os << ",\n\"flags\": \"" << param->flags << '"';
|
|
|
|
|
// os << ",\n\"group\": \"" << param->group << '"';
|
|
|
|
|
|
|
|
|
|
// append the menu object to the menu typecontrols
|
2016-11-15 23:54:50 -08:00
|
|
|
if (source.GetPropertyKind(prop) == CS_PROP_ENUM) {
|
2016-09-08 00:05:10 -07:00
|
|
|
os << ",\n\"menu\": {";
|
2016-09-19 23:50:47 -07:00
|
|
|
auto choices = source.GetEnumPropertyChoices(prop, &status);
|
2016-09-08 00:05:10 -07:00
|
|
|
int j = 0;
|
|
|
|
|
for (auto choice = choices.begin(), end = choices.end(); choice != end;
|
|
|
|
|
++j, ++choice) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (j != 0) {
|
|
|
|
|
os << ", ";
|
|
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
// replace any non-printable characters in name with spaces
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::SmallString<128> ch_name;
|
2020-12-28 12:58:06 -08:00
|
|
|
for (char ch : *choice) {
|
|
|
|
|
ch_name.push_back(std::isprint(ch) ? ch : ' ');
|
|
|
|
|
}
|
2021-06-06 16:13:58 -07:00
|
|
|
fmt::print(os, "\"{}\": \"{}\"", j, ch_name);
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
|
|
|
|
os << "}\n";
|
|
|
|
|
}
|
|
|
|
|
os << '}';
|
|
|
|
|
}
|
2017-02-16 01:06:38 -08:00
|
|
|
os << "\n],\n\"modes\": [\n";
|
|
|
|
|
first = true;
|
|
|
|
|
for (auto mode : source.EnumerateVideoModes(&status)) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (first) {
|
2017-02-16 01:06:38 -08:00
|
|
|
first = false;
|
2020-12-28 12:58:06 -08:00
|
|
|
} else {
|
2017-02-16 01:06:38 -08:00
|
|
|
os << ",\n";
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2017-02-16 01:06:38 -08:00
|
|
|
os << '{';
|
|
|
|
|
os << "\n\"pixelFormat\": \"";
|
|
|
|
|
switch (mode.pixelFormat) {
|
2017-08-25 17:48:06 -07:00
|
|
|
case VideoMode::kMJPEG:
|
|
|
|
|
os << "MJPEG";
|
|
|
|
|
break;
|
|
|
|
|
case VideoMode::kYUYV:
|
|
|
|
|
os << "YUYV";
|
|
|
|
|
break;
|
|
|
|
|
case VideoMode::kRGB565:
|
|
|
|
|
os << "RGB565";
|
|
|
|
|
break;
|
|
|
|
|
case VideoMode::kBGR:
|
|
|
|
|
os << "BGR";
|
|
|
|
|
break;
|
|
|
|
|
case VideoMode::kGray:
|
|
|
|
|
os << "gray";
|
|
|
|
|
break;
|
2022-11-23 22:00:31 -08:00
|
|
|
case VideoMode::kUYVY:
|
|
|
|
|
os << "UYVY";
|
|
|
|
|
break;
|
2017-08-25 17:48:06 -07:00
|
|
|
default:
|
|
|
|
|
os << "unknown";
|
|
|
|
|
break;
|
2017-02-16 01:06:38 -08:00
|
|
|
}
|
2021-06-06 16:13:58 -07:00
|
|
|
fmt::print(os, "\",\n\"width\": \"{}\"", mode.width);
|
|
|
|
|
fmt::print(os, ",\n\"height\": \"{}\"", mode.height);
|
|
|
|
|
fmt::print(os, ",\n\"fps\": \"{}\"", mode.fps);
|
2017-02-16 01:06:38 -08:00
|
|
|
os << '}';
|
|
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
os << "\n]\n}\n";
|
|
|
|
|
os.flush();
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-06 16:13:58 -07:00
|
|
|
MjpegServerImpl::MjpegServerImpl(std::string_view name, wpi::Logger& logger,
|
2018-10-31 20:22:58 -07:00
|
|
|
Notifier& notifier, Telemetry& telemetry,
|
2021-06-06 16:13:58 -07:00
|
|
|
std::string_view listenAddress, int port,
|
2016-10-15 22:44:26 -07:00
|
|
|
std::unique_ptr<wpi::NetworkAcceptor> acceptor)
|
2018-10-31 20:22:58 -07:00
|
|
|
: SinkImpl{name, logger, notifier, telemetry},
|
2021-06-06 16:13:58 -07:00
|
|
|
m_listenAddress(listenAddress),
|
2016-11-18 17:21:03 -08:00
|
|
|
m_port(port),
|
|
|
|
|
m_acceptor{std::move(acceptor)} {
|
2016-09-20 20:40:58 -07:00
|
|
|
m_active = true;
|
2016-11-18 17:21:03 -08:00
|
|
|
|
2021-06-06 16:13:58 -07:00
|
|
|
SetDescription(fmt::format("HTTP Server on port {}", port));
|
2016-11-18 17:21:03 -08:00
|
|
|
|
2018-07-27 23:00:15 -07:00
|
|
|
// Create properties
|
|
|
|
|
m_widthProp = CreateProperty("width", [] {
|
|
|
|
|
return std::make_unique<PropertyImpl>("width", CS_PROP_INTEGER, 1, 0, 0);
|
|
|
|
|
});
|
|
|
|
|
m_heightProp = CreateProperty("height", [] {
|
|
|
|
|
return std::make_unique<PropertyImpl>("height", CS_PROP_INTEGER, 1, 0, 0);
|
|
|
|
|
});
|
|
|
|
|
m_compressionProp = CreateProperty("compression", [] {
|
|
|
|
|
return std::make_unique<PropertyImpl>("compression", CS_PROP_INTEGER, -1,
|
|
|
|
|
100, 1, -1, -1);
|
|
|
|
|
});
|
|
|
|
|
m_defaultCompressionProp = CreateProperty("default_compression", [] {
|
|
|
|
|
return std::make_unique<PropertyImpl>("default_compression",
|
|
|
|
|
CS_PROP_INTEGER, 0, 100, 1, 80, 80);
|
|
|
|
|
});
|
|
|
|
|
m_fpsProp = CreateProperty("fps", [] {
|
|
|
|
|
return std::make_unique<PropertyImpl>("fps", CS_PROP_INTEGER, 1, 0, 0);
|
|
|
|
|
});
|
|
|
|
|
|
2016-12-04 00:08:47 -08:00
|
|
|
m_serverThread = std::thread(&MjpegServerImpl::ServerThreadMain, this);
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
|
|
|
|
|
2020-12-28 12:58:06 -08:00
|
|
|
MjpegServerImpl::~MjpegServerImpl() {
|
|
|
|
|
Stop();
|
|
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
|
2016-12-04 00:08:47 -08:00
|
|
|
void MjpegServerImpl::Stop() {
|
2016-09-08 00:05:10 -07:00
|
|
|
m_active = false;
|
|
|
|
|
|
|
|
|
|
// wake up server thread by shutting down the socket
|
|
|
|
|
m_acceptor->shutdown();
|
|
|
|
|
|
|
|
|
|
// join server thread
|
2020-12-28 12:58:06 -08:00
|
|
|
if (m_serverThread.joinable()) {
|
|
|
|
|
m_serverThread.join();
|
|
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
|
|
|
|
|
// close streams
|
2016-11-11 00:01:58 -08:00
|
|
|
for (auto& connThread : m_connThreads) {
|
|
|
|
|
if (auto thr = connThread.GetThread()) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (thr->m_stream) {
|
|
|
|
|
thr->m_stream->close();
|
|
|
|
|
}
|
2016-11-11 00:01:58 -08:00
|
|
|
}
|
|
|
|
|
connThread.Stop();
|
|
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
|
|
|
|
|
// wake up connection threads by forcing an empty frame to be sent
|
2020-12-28 12:58:06 -08:00
|
|
|
if (auto source = GetSource()) {
|
|
|
|
|
source->Wakeup();
|
|
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send HTTP response and a stream of JPG-frames
|
2016-12-04 00:08:47 -08:00
|
|
|
void MjpegServerImpl::ConnThread::SendStream(wpi::raw_socket_ostream& os) {
|
2016-12-23 22:01:35 -08:00
|
|
|
if (m_noStreaming) {
|
2022-10-19 10:49:27 -07:00
|
|
|
SERROR("Too many simultaneous client streams");
|
2016-12-23 22:01:35 -08:00
|
|
|
SendError(os, 503, "Too many simultaneous streams");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-08 00:05:10 -07:00
|
|
|
os.SetUnbuffered();
|
|
|
|
|
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::SmallString<256> header;
|
|
|
|
|
wpi::raw_svector_ostream oss{header};
|
2016-09-08 00:05:10 -07:00
|
|
|
|
2017-08-07 17:46:33 -07:00
|
|
|
SendHeader(oss, 200, "OK", "multipart/x-mixed-replace;boundary=" BOUNDARY);
|
2016-09-08 00:05:10 -07:00
|
|
|
os << oss.str();
|
|
|
|
|
|
2022-10-19 10:49:27 -07:00
|
|
|
SDEBUG("Headers send, sending stream now");
|
2016-09-08 00:05:10 -07:00
|
|
|
|
2018-03-01 20:00:25 -08:00
|
|
|
Frame::Time lastFrameTime = 0;
|
|
|
|
|
Frame::Time timePerFrame = 0;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (m_fps != 0) {
|
|
|
|
|
timePerFrame = 1000000.0 / m_fps;
|
|
|
|
|
}
|
2019-01-09 22:50:34 -08:00
|
|
|
Frame::Time averageFrameTime = 0;
|
|
|
|
|
Frame::Time averagePeriod = 1000000; // 1 second window
|
2020-12-28 12:58:06 -08:00
|
|
|
if (averagePeriod < timePerFrame) {
|
|
|
|
|
averagePeriod = timePerFrame * 10;
|
|
|
|
|
}
|
2018-03-01 20:00:25 -08:00
|
|
|
|
2016-11-11 00:01:58 -08:00
|
|
|
StartStream();
|
2016-09-08 00:05:10 -07:00
|
|
|
while (m_active && !os.has_error()) {
|
|
|
|
|
auto source = GetSource();
|
|
|
|
|
if (!source) {
|
2017-02-17 01:17:12 -08:00
|
|
|
// Source disconnected; sleep so we don't consume all processor time.
|
|
|
|
|
os << "\r\n"; // Keep connection alive
|
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
2016-09-08 21:00:23 -07:00
|
|
|
continue;
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
2022-10-19 10:49:27 -07:00
|
|
|
SDEBUG4("waiting for frame");
|
2017-02-17 01:17:12 -08:00
|
|
|
Frame frame = source->GetNextFrame(0.225); // blocks
|
2020-12-28 12:58:06 -08:00
|
|
|
if (!m_active) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
if (!frame) {
|
2016-12-02 23:41:35 -08:00
|
|
|
// Bad frame; sleep for 20 ms so we don't consume all processor time.
|
2017-02-17 01:17:12 -08:00
|
|
|
os << "\r\n"; // Keep connection alive
|
2016-12-02 23:41:35 -08:00
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
2016-09-08 21:00:23 -07:00
|
|
|
continue;
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
2016-11-10 00:00:20 -08:00
|
|
|
|
2019-01-09 22:50:34 -08:00
|
|
|
auto thisFrameTime = frame.GetTime();
|
|
|
|
|
if (thisFrameTime != 0 && timePerFrame != 0 && lastFrameTime != 0) {
|
|
|
|
|
Frame::Time deltaTime = thisFrameTime - lastFrameTime;
|
|
|
|
|
|
|
|
|
|
// drop frame if it is early compared to the desired frame rate AND
|
|
|
|
|
// the current average is higher than the desired average
|
|
|
|
|
if (deltaTime < timePerFrame && averageFrameTime < timePerFrame) {
|
|
|
|
|
// sleep for 1 ms so we don't consume all processor time
|
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// update average
|
|
|
|
|
if (averageFrameTime != 0) {
|
|
|
|
|
averageFrameTime =
|
|
|
|
|
averageFrameTime * (averagePeriod - timePerFrame) / averagePeriod +
|
|
|
|
|
deltaTime * timePerFrame / averagePeriod;
|
|
|
|
|
} else {
|
|
|
|
|
averageFrameTime = deltaTime;
|
|
|
|
|
}
|
2018-03-01 20:00:25 -08:00
|
|
|
}
|
|
|
|
|
|
2016-12-20 20:48:31 -08:00
|
|
|
int width = m_width != 0 ? m_width : frame.GetOriginalWidth();
|
|
|
|
|
int height = m_height != 0 ? m_height : frame.GetOriginalHeight();
|
2018-07-27 21:51:40 -07:00
|
|
|
Image* image = frame.GetImageMJPEG(
|
2018-07-27 23:00:15 -07:00
|
|
|
width, height, m_compression,
|
|
|
|
|
m_compression == -1 ? m_defaultCompression : m_compression);
|
2016-12-20 20:48:31 -08:00
|
|
|
if (!image) {
|
|
|
|
|
// Shouldn't happen, but just in case...
|
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char* data = image->data();
|
2017-08-25 17:48:06 -07:00
|
|
|
size_t size = image->size();
|
2016-11-11 23:17:39 -08:00
|
|
|
bool addDHT = false;
|
2017-08-25 17:48:06 -07:00
|
|
|
size_t locSOF = size;
|
2016-12-20 20:48:31 -08:00
|
|
|
switch (image->pixelFormat) {
|
2016-11-10 00:00:20 -08:00
|
|
|
case VideoMode::kMJPEG:
|
2016-11-11 23:17:39 -08:00
|
|
|
// Determine if we need to add DHT to it, and allocate enough space
|
|
|
|
|
// for adding it if required.
|
2016-12-02 23:33:29 -08:00
|
|
|
addDHT = JpegNeedsDHT(data, &size, &locSOF);
|
2016-11-10 00:00:20 -08:00
|
|
|
break;
|
2022-11-23 22:00:31 -08:00
|
|
|
case VideoMode::kUYVY:
|
2016-11-10 00:00:20 -08:00
|
|
|
case VideoMode::kRGB565:
|
2022-11-23 22:00:31 -08:00
|
|
|
case VideoMode::kYUYV:
|
2016-11-10 00:00:20 -08:00
|
|
|
default:
|
|
|
|
|
// Bad frame; sleep for 10 ms so we don't consume all processor time.
|
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-06 16:13:58 -07:00
|
|
|
SDEBUG4("sending frame size={} addDHT={}", size, addDHT);
|
2016-09-08 00:05:10 -07:00
|
|
|
|
|
|
|
|
// print the individual mimetype and the length
|
|
|
|
|
// sending the content-length fixes random stream disruption observed
|
|
|
|
|
// with firefox
|
2019-01-09 22:50:34 -08:00
|
|
|
lastFrameTime = thisFrameTime;
|
2018-03-01 20:00:25 -08:00
|
|
|
double timestamp = lastFrameTime / 1000000.0;
|
2016-09-08 00:05:10 -07:00
|
|
|
header.clear();
|
2016-11-30 21:50:15 -08:00
|
|
|
oss << "\r\n--" BOUNDARY "\r\n"
|
2021-06-06 16:13:58 -07:00
|
|
|
<< "Content-Type: image/jpeg\r\n";
|
|
|
|
|
fmt::print(oss, "Content-Length: {}\r\n", size);
|
|
|
|
|
fmt::print(oss, "X-Timestamp: {}\r\n", timestamp);
|
|
|
|
|
oss << "\r\n";
|
2016-09-08 00:05:10 -07:00
|
|
|
os << oss.str();
|
2016-11-11 23:17:39 -08:00
|
|
|
if (addDHT) {
|
|
|
|
|
// Insert DHT data immediately before SOF
|
2021-06-06 16:13:58 -07:00
|
|
|
os << std::string_view(data, locSOF);
|
2016-12-02 23:33:29 -08:00
|
|
|
os << JpegGetDHT();
|
2021-06-06 16:13:58 -07:00
|
|
|
os << std::string_view(data + locSOF, image->size() - locSOF);
|
2016-11-11 23:17:39 -08:00
|
|
|
} else {
|
2021-06-06 16:13:58 -07:00
|
|
|
os << std::string_view(data, size);
|
2016-11-11 23:17:39 -08:00
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
// os.flush();
|
|
|
|
|
}
|
2016-11-11 00:01:58 -08:00
|
|
|
StopStream();
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
|
|
|
|
|
2016-12-04 00:08:47 -08:00
|
|
|
void MjpegServerImpl::ConnThread::ProcessRequest() {
|
2016-11-11 00:01:58 -08:00
|
|
|
wpi::raw_socket_istream is{*m_stream};
|
|
|
|
|
wpi::raw_socket_ostream os{*m_stream, true};
|
2016-09-08 00:05:10 -07:00
|
|
|
|
|
|
|
|
// Read the request string from the stream
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::SmallString<128> reqBuf;
|
2021-06-06 16:13:58 -07:00
|
|
|
std::string_view req = is.getline(reqBuf, 4096);
|
2017-08-25 18:10:47 -07:00
|
|
|
if (is.has_error()) {
|
2022-10-19 10:49:27 -07:00
|
|
|
SDEBUG("error getting request string");
|
2016-10-13 00:16:24 -07:00
|
|
|
return;
|
|
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
|
2018-11-10 20:30:02 -08:00
|
|
|
enum { kCommand, kStream, kGetSettings, kGetSourceConfig, kRootPage } kind;
|
2021-06-06 16:13:58 -07:00
|
|
|
std::string_view parameters;
|
2016-09-08 00:05:10 -07:00
|
|
|
size_t pos;
|
|
|
|
|
|
2021-06-06 16:13:58 -07:00
|
|
|
SDEBUG("HTTP request: '{}'\n", req);
|
2016-11-18 18:52:18 -08:00
|
|
|
|
2016-11-15 23:54:50 -08:00
|
|
|
// Determine request kind. Most of these are for mjpgstreamer
|
2016-11-18 18:33:49 -08:00
|
|
|
// compatibility, others are for Axis camera compatibility.
|
2021-06-06 16:13:58 -07:00
|
|
|
if ((pos = req.find("POST /stream")) != std::string_view::npos) {
|
2016-11-15 23:54:50 -08:00
|
|
|
kind = kStream;
|
2021-11-27 21:31:40 -08:00
|
|
|
parameters = wpi::substr(wpi::substr(req, req.find('?', pos + 12)), 1);
|
2021-06-06 16:13:58 -07:00
|
|
|
} else if ((pos = req.find("GET /?action=stream")) !=
|
|
|
|
|
std::string_view::npos) {
|
2016-11-15 23:54:50 -08:00
|
|
|
kind = kStream;
|
2021-11-27 21:31:40 -08:00
|
|
|
parameters = wpi::substr(wpi::substr(req, req.find('&', pos + 19)), 1);
|
2021-06-06 16:13:58 -07:00
|
|
|
} else if ((pos = req.find("GET /stream.mjpg")) != std::string_view::npos) {
|
2016-11-18 18:33:49 -08:00
|
|
|
kind = kStream;
|
2021-11-27 21:31:40 -08:00
|
|
|
parameters = wpi::substr(wpi::substr(req, req.find('?', pos + 16)), 1);
|
2021-06-06 16:13:58 -07:00
|
|
|
} else if (req.find("GET /settings") != std::string_view::npos &&
|
|
|
|
|
req.find(".json") != std::string_view::npos) {
|
2016-11-18 18:52:18 -08:00
|
|
|
kind = kGetSettings;
|
2021-06-06 16:13:58 -07:00
|
|
|
} else if (req.find("GET /config") != std::string_view::npos &&
|
|
|
|
|
req.find(".json") != std::string_view::npos) {
|
2018-11-10 20:30:02 -08:00
|
|
|
kind = kGetSourceConfig;
|
2021-06-06 16:13:58 -07:00
|
|
|
} else if (req.find("GET /input") != std::string_view::npos &&
|
|
|
|
|
req.find(".json") != std::string_view::npos) {
|
2016-11-15 23:54:50 -08:00
|
|
|
kind = kGetSettings;
|
2021-06-06 16:13:58 -07:00
|
|
|
} else if (req.find("GET /output") != std::string_view::npos &&
|
|
|
|
|
req.find(".json") != std::string_view::npos) {
|
2016-11-15 23:54:50 -08:00
|
|
|
kind = kGetSettings;
|
2021-06-06 16:13:58 -07:00
|
|
|
} else if ((pos = req.find("GET /?action=command")) !=
|
|
|
|
|
std::string_view::npos) {
|
2016-11-15 23:54:50 -08:00
|
|
|
kind = kCommand;
|
2021-11-27 21:31:40 -08:00
|
|
|
parameters = wpi::substr(wpi::substr(req, req.find('&', pos + 20)), 1);
|
2021-06-06 16:13:58 -07:00
|
|
|
} else if (req.find("GET / ") != std::string_view::npos || req == "GET /\n") {
|
2016-11-18 18:52:18 -08:00
|
|
|
kind = kRootPage;
|
2016-09-08 00:05:10 -07:00
|
|
|
} else {
|
2022-10-19 10:49:27 -07:00
|
|
|
SDEBUG("HTTP request resource not found");
|
2016-09-08 00:05:10 -07:00
|
|
|
SendError(os, 404, "Resource not found");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parameter can only be certain characters. This also strips the EOL.
|
|
|
|
|
pos = parameters.find_first_not_of(
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"
|
|
|
|
|
"-=&1234567890%./");
|
2021-11-27 21:31:40 -08:00
|
|
|
parameters = wpi::substr(parameters, 0, pos);
|
2021-06-06 16:13:58 -07:00
|
|
|
SDEBUG("command parameters: \"{}\"", parameters);
|
2016-09-08 00:05:10 -07:00
|
|
|
|
|
|
|
|
// Read the rest of the HTTP request.
|
2016-11-18 18:52:18 -08:00
|
|
|
// The end of the request is marked by a single, empty line
|
2018-04-29 23:33:19 -07:00
|
|
|
wpi::SmallString<128> lineBuf;
|
2016-12-02 19:15:40 -08:00
|
|
|
for (;;) {
|
2021-06-06 16:13:58 -07:00
|
|
|
if (wpi::starts_with(is.getline(lineBuf, 4096), "\n")) {
|
2020-12-28 12:58:06 -08:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (is.has_error()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2016-12-02 19:15:40 -08:00
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
|
|
|
|
|
// Send response
|
2016-11-15 23:54:50 -08:00
|
|
|
switch (kind) {
|
2016-09-08 00:05:10 -07:00
|
|
|
case kStream:
|
|
|
|
|
if (auto source = GetSource()) {
|
2021-06-06 16:13:58 -07:00
|
|
|
SDEBUG("request for stream {}", source->GetName());
|
2020-12-28 12:58:06 -08:00
|
|
|
if (!ProcessCommand(os, *source, parameters, false)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
|
|
|
|
SendStream(os);
|
|
|
|
|
break;
|
|
|
|
|
case kCommand:
|
|
|
|
|
if (auto source = GetSource()) {
|
2016-10-13 00:16:24 -07:00
|
|
|
ProcessCommand(os, *source, parameters, true);
|
2016-09-08 00:05:10 -07:00
|
|
|
} else {
|
|
|
|
|
SendHeader(os, 200, "OK", "text/plain");
|
2017-08-25 17:48:06 -07:00
|
|
|
os << "Ignored due to no connected source."
|
|
|
|
|
<< "\r\n";
|
2022-10-19 10:49:27 -07:00
|
|
|
SDEBUG("Ignored due to no connected source.");
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case kGetSettings:
|
2022-10-19 10:49:27 -07:00
|
|
|
SDEBUG("request for JSON file");
|
2020-12-28 12:58:06 -08:00
|
|
|
if (auto source = GetSource()) {
|
2016-09-08 00:05:10 -07:00
|
|
|
SendJSON(os, *source, true);
|
2020-12-28 12:58:06 -08:00
|
|
|
} else {
|
2016-09-08 00:05:10 -07:00
|
|
|
SendError(os, 404, "Resource not found");
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
break;
|
2018-11-10 20:30:02 -08:00
|
|
|
case kGetSourceConfig:
|
2022-10-19 10:49:27 -07:00
|
|
|
SDEBUG("request for JSON file");
|
2018-11-10 20:30:02 -08:00
|
|
|
if (auto source = GetSource()) {
|
|
|
|
|
SendHeader(os, 200, "OK", "application/json");
|
|
|
|
|
CS_Status status = CS_OK;
|
|
|
|
|
os << source->GetConfigJson(&status);
|
|
|
|
|
os.flush();
|
|
|
|
|
} else {
|
|
|
|
|
SendError(os, 404, "Resource not found");
|
|
|
|
|
}
|
|
|
|
|
break;
|
2016-11-18 18:52:18 -08:00
|
|
|
case kRootPage:
|
2022-10-19 10:49:27 -07:00
|
|
|
SDEBUG("request for root page");
|
2016-11-18 18:52:18 -08:00
|
|
|
SendHeader(os, 200, "OK", "text/html");
|
2017-01-19 00:02:37 -08:00
|
|
|
if (auto source = GetSource()) {
|
|
|
|
|
SendHTML(os, *source, false);
|
|
|
|
|
} else {
|
2017-11-17 09:34:30 -08:00
|
|
|
SendHTMLHeadTitle(os);
|
2017-01-19 00:02:37 -08:00
|
|
|
os << emptyRootPage << "\r\n";
|
|
|
|
|
}
|
2016-11-18 18:52:18 -08:00
|
|
|
break;
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
|
|
|
|
|
2022-10-19 10:49:27 -07:00
|
|
|
SDEBUG("leaving HTTP client thread");
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
|
|
|
|
|
2016-11-11 00:01:58 -08:00
|
|
|
// worker thread for clients that connected to this server
|
2016-12-04 00:08:47 -08:00
|
|
|
void MjpegServerImpl::ConnThread::Main() {
|
2019-07-07 19:17:14 -07:00
|
|
|
std::unique_lock lock(m_mutex);
|
2016-11-11 00:01:58 -08:00
|
|
|
while (m_active) {
|
|
|
|
|
while (!m_stream) {
|
|
|
|
|
m_cond.wait(lock);
|
2020-12-28 12:58:06 -08:00
|
|
|
if (!m_active) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2016-11-11 00:01:58 -08:00
|
|
|
}
|
|
|
|
|
lock.unlock();
|
|
|
|
|
ProcessRequest();
|
|
|
|
|
lock.lock();
|
|
|
|
|
m_stream = nullptr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-08 00:05:10 -07:00
|
|
|
// Main server thread
|
2016-12-04 00:08:47 -08:00
|
|
|
void MjpegServerImpl::ServerThreadMain() {
|
2016-09-08 00:05:10 -07:00
|
|
|
if (m_acceptor->start() != 0) {
|
|
|
|
|
m_active = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-19 10:49:27 -07:00
|
|
|
SDEBUG("waiting for clients to connect");
|
2016-09-08 00:05:10 -07:00
|
|
|
while (m_active) {
|
|
|
|
|
auto stream = m_acceptor->accept();
|
|
|
|
|
if (!stream) {
|
|
|
|
|
m_active = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-12-28 12:58:06 -08:00
|
|
|
if (!m_active) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2016-09-08 00:05:10 -07:00
|
|
|
|
2021-06-06 16:13:58 -07:00
|
|
|
SDEBUG("client connection from {}", stream->getPeerIP());
|
2016-09-08 00:05:10 -07:00
|
|
|
|
2016-11-11 00:01:58 -08:00
|
|
|
auto source = GetSource();
|
|
|
|
|
|
2019-07-08 22:58:39 -07:00
|
|
|
std::scoped_lock lock(m_mutex);
|
2016-11-11 00:01:58 -08:00
|
|
|
// Find unoccupied worker thread, or create one if necessary
|
|
|
|
|
auto it = std::find_if(m_connThreads.begin(), m_connThreads.end(),
|
|
|
|
|
[](const wpi::SafeThreadOwner<ConnThread>& owner) {
|
|
|
|
|
auto thr = owner.GetThread();
|
|
|
|
|
return !thr || !thr->m_stream;
|
|
|
|
|
});
|
|
|
|
|
if (it == m_connThreads.end()) {
|
|
|
|
|
m_connThreads.emplace_back();
|
|
|
|
|
it = std::prev(m_connThreads.end());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start it if not already started
|
2018-10-31 20:22:58 -07:00
|
|
|
it->Start(GetName(), m_logger);
|
2016-11-11 00:01:58 -08:00
|
|
|
|
2016-12-23 22:01:35 -08:00
|
|
|
auto nstreams =
|
|
|
|
|
std::count_if(m_connThreads.begin(), m_connThreads.end(),
|
|
|
|
|
[](const wpi::SafeThreadOwner<ConnThread>& owner) {
|
|
|
|
|
auto thr = owner.GetThread();
|
|
|
|
|
return thr && thr->m_streaming;
|
|
|
|
|
});
|
|
|
|
|
|
2016-11-11 00:01:58 -08:00
|
|
|
// Hand off connection to it
|
|
|
|
|
auto thr = it->GetThread();
|
|
|
|
|
thr->m_stream = std::move(stream);
|
|
|
|
|
thr->m_source = source;
|
2016-12-23 22:01:35 -08:00
|
|
|
thr->m_noStreaming = nstreams >= 10;
|
2018-07-27 23:00:15 -07:00
|
|
|
thr->m_width = GetProperty(m_widthProp)->value;
|
|
|
|
|
thr->m_height = GetProperty(m_heightProp)->value;
|
|
|
|
|
thr->m_compression = GetProperty(m_compressionProp)->value;
|
|
|
|
|
thr->m_defaultCompression = GetProperty(m_defaultCompressionProp)->value;
|
|
|
|
|
thr->m_fps = GetProperty(m_fpsProp)->value;
|
2016-11-11 00:01:58 -08:00
|
|
|
thr->m_cond.notify_one();
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
|
|
|
|
|
2022-10-19 10:49:27 -07:00
|
|
|
SDEBUG("leaving server thread");
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
|
|
|
|
|
2016-12-04 00:08:47 -08:00
|
|
|
void MjpegServerImpl::SetSourceImpl(std::shared_ptr<SourceImpl> source) {
|
2019-07-08 22:58:39 -07:00
|
|
|
std::scoped_lock lock(m_mutex);
|
2016-11-11 00:01:58 -08:00
|
|
|
for (auto& connThread : m_connThreads) {
|
|
|
|
|
if (auto thr = connThread.GetThread()) {
|
|
|
|
|
if (thr->m_source != source) {
|
|
|
|
|
bool streaming = thr->m_streaming;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (thr->m_source && streaming) {
|
|
|
|
|
thr->m_source->DisableSink();
|
|
|
|
|
}
|
2016-11-11 00:01:58 -08:00
|
|
|
thr->m_source = source;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (source && streaming) {
|
|
|
|
|
thr->m_source->EnableSink();
|
|
|
|
|
}
|
2016-11-11 00:01:58 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-08 00:05:10 -07:00
|
|
|
namespace cs {
|
|
|
|
|
|
2021-06-06 16:13:58 -07:00
|
|
|
CS_Sink CreateMjpegServer(std::string_view name, std::string_view listenAddress,
|
|
|
|
|
int port, CS_Status* status) {
|
2018-10-31 20:22:58 -07:00
|
|
|
auto& inst = Instance::GetInstance();
|
2018-11-06 19:42:39 -08:00
|
|
|
return inst.CreateSink(
|
|
|
|
|
CS_SINK_MJPEG,
|
|
|
|
|
std::make_shared<MjpegServerImpl>(
|
|
|
|
|
name, inst.logger, inst.notifier, inst.telemetry, listenAddress, port,
|
2021-06-06 16:13:58 -07:00
|
|
|
std::unique_ptr<wpi::NetworkAcceptor>(
|
|
|
|
|
new wpi::TCPAcceptor(port, listenAddress, inst.logger))));
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
|
|
|
|
|
2016-12-04 00:08:47 -08:00
|
|
|
std::string GetMjpegServerListenAddress(CS_Sink sink, CS_Status* status) {
|
2018-11-06 19:42:39 -08:00
|
|
|
auto data = Instance::GetInstance().GetSink(sink);
|
2016-11-18 17:21:03 -08:00
|
|
|
if (!data || data->kind != CS_SINK_MJPEG) {
|
|
|
|
|
*status = CS_INVALID_HANDLE;
|
|
|
|
|
return std::string{};
|
|
|
|
|
}
|
2016-12-04 00:08:47 -08:00
|
|
|
return static_cast<MjpegServerImpl&>(*data->sink).GetListenAddress();
|
2016-11-18 17:21:03 -08:00
|
|
|
}
|
|
|
|
|
|
2016-12-04 00:08:47 -08:00
|
|
|
int GetMjpegServerPort(CS_Sink sink, CS_Status* status) {
|
2018-11-06 19:42:39 -08:00
|
|
|
auto data = Instance::GetInstance().GetSink(sink);
|
2016-11-18 17:21:03 -08:00
|
|
|
if (!data || data->kind != CS_SINK_MJPEG) {
|
|
|
|
|
*status = CS_INVALID_HANDLE;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2016-12-04 00:08:47 -08:00
|
|
|
return static_cast<MjpegServerImpl&>(*data->sink).GetPort();
|
2016-11-18 17:21:03 -08:00
|
|
|
}
|
|
|
|
|
|
2016-09-08 00:05:10 -07:00
|
|
|
} // namespace cs
|
|
|
|
|
|
|
|
|
|
extern "C" {
|
|
|
|
|
|
2016-12-04 00:08:47 -08:00
|
|
|
CS_Sink CS_CreateMjpegServer(const char* name, const char* listenAddress,
|
2016-10-15 22:44:26 -07:00
|
|
|
int port, CS_Status* status) {
|
2016-12-04 00:08:47 -08:00
|
|
|
return cs::CreateMjpegServer(name, listenAddress, port, status);
|
2016-09-08 00:05:10 -07:00
|
|
|
}
|
|
|
|
|
|
2016-12-04 00:08:47 -08:00
|
|
|
char* CS_GetMjpegServerListenAddress(CS_Sink sink, CS_Status* status) {
|
|
|
|
|
return ConvertToC(cs::GetMjpegServerListenAddress(sink, status));
|
2016-11-18 17:21:03 -08:00
|
|
|
}
|
|
|
|
|
|
2016-12-04 00:08:47 -08:00
|
|
|
int CS_GetMjpegServerPort(CS_Sink sink, CS_Status* status) {
|
|
|
|
|
return cs::GetMjpegServerPort(sink, status);
|
2016-11-18 17:21:03 -08:00
|
|
|
}
|
|
|
|
|
|
2016-09-08 00:05:10 -07:00
|
|
|
} // extern "C"
|