diff options
| author | Carl Hetherington <cth@carlh.net> | 2024-06-14 01:45:18 +0200 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2024-06-23 19:51:28 +0200 |
| commit | 117f6bd199479fdaeff665acbea109e967500308 (patch) | |
| tree | b3da204320e58c55cdde2538921e1cc51eb4ba38 /src/lib | |
| parent | d04355507baefd5fa42629341ed422f7402772f4 (diff) | |
Add minimal player HTTP server (#2830).
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/config.cc | 8 | ||||
| -rw-r--r-- | src/lib/config.h | 18 | ||||
| -rw-r--r-- | src/lib/http_server.cc | 255 | ||||
| -rw-r--r-- | src/lib/http_server.h | 91 | ||||
| -rw-r--r-- | src/lib/wscript | 1 |
5 files changed, 373 insertions, 0 deletions
diff --git a/src/lib/config.cc b/src/lib/config.cc index 1ca73c400..180ce5c55 100644 --- a/src/lib/config.cc +++ b/src/lib/config.cc @@ -214,6 +214,8 @@ Config::set_defaults () _last_release_notes_version = boost::none; _allow_smpte_bv20 = false; _isdcf_name_part_length = 14; + _enable_player_http_server = false; + _player_http_server_port = 8080; _allowed_dcp_frame_rates.clear (); _allowed_dcp_frame_rates.push_back (24); @@ -651,6 +653,8 @@ try _allow_smpte_bv20 = f.optional_bool_child("AllowSMPTEBv20").get_value_or(false); _isdcf_name_part_length = f.optional_number_child<int>("ISDCFNamePartLength").get_value_or(14); + _enable_player_http_server = f.optional_bool_child("EnablePlayerHTTPServer").get_value_or(false); + _player_http_server_port = f.optional_number_child<int>("PlayerHTTPServerPort").get_value_or(8080); #ifdef DCPOMATIC_GROK if (auto grok = f.optional_node_child("Grok")) { @@ -1122,6 +1126,10 @@ Config::write_config () const cxml::add_text_child(root, "AllowSMPTEBv20", _allow_smpte_bv20 ? "1" : "0"); /* [XML] ISDCFNamePartLength Maximum length of the "name" part of an ISDCF name, which should be 14 according to the standard */ cxml::add_text_child(root, "ISDCFNamePartLength", raw_convert<string>(_isdcf_name_part_length)); + /* [XML] EnablePlayerHTTPServer 1 to enable a HTTP server to control the player, otherwise 0 */ + cxml::add_text_child(root, "EnablePlayerHTTPServer", _enable_player_http_server ? "1" : "0"); + /* [XML] PlayerHTTPServerPort Port to use for player HTTP server (if enabled) */ + cxml::add_text_child(root, "PlayerHTTPServerPort", raw_convert<string>(_player_http_server_port)); #ifdef DCPOMATIC_GROK if (_grok) { diff --git a/src/lib/config.h b/src/lib/config.h index 397af2222..1f13381b7 100644 --- a/src/lib/config.h +++ b/src/lib/config.h @@ -670,6 +670,14 @@ public: return _isdcf_name_part_length; } + bool enable_player_http_server() const { + return _enable_player_http_server; + } + + int player_http_server_port() const { + return _player_http_server_port; + } + /* SET (mostly) */ void set_master_encoding_threads (int n) { @@ -1209,6 +1217,14 @@ public: maybe_set(_isdcf_name_part_length, length, ISDCF_NAME_PART_LENGTH); } + void set_enable_player_http_server(bool enable) { + maybe_set(_enable_player_http_server, enable); + } + + void set_player_http_server_port(int port) { + maybe_set(_player_http_server_port, port); + } + void changed (Property p = OTHER); boost::signals2::signal<void (Property)> Changed; @@ -1447,6 +1463,8 @@ private: DefaultAddFileLocation _default_add_file_location; bool _allow_smpte_bv20; int _isdcf_name_part_length; + bool _enable_player_http_server; + int _player_http_server_port; #ifdef DCPOMATIC_GROK boost::optional<Grok> _grok; diff --git a/src/lib/http_server.cc b/src/lib/http_server.cc new file mode 100644 index 000000000..0ee62756a --- /dev/null +++ b/src/lib/http_server.cc @@ -0,0 +1,255 @@ +/* + Copyright (C) 2024 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "cross.h" +#include "dcpomatic_log.h" +#include "dcpomatic_socket.h" +#include "http_server.h" +#include "util.h" +#include "variant.h" +#include <boost/algorithm/string.hpp> +#include <stdexcept> + + +using std::make_pair; +using std::runtime_error; +using std::shared_ptr; +using std::string; +using std::vector; + + +Response Response::ERROR_404 = { 404, "<html><head><title>Error 404</title></head><body><h1>Error 404</h1></body></html>"}; + + +HTTPServer::HTTPServer(int port, int timeout) + : Server(port, timeout) +{ + +} + + + +Response::Response(int code) + : _code(code) +{ + +} + + +Response::Response(int code, string payload) + : _code(code) + , _payload(payload) +{ + +} + + +void +Response::add_header(string key, string value) +{ + _headers.push_back(make_pair(key, value)); +} + + +void +Response::send(shared_ptr<Socket> socket) +{ + socket->write(String::compose("HTTP/1.1 %1 OK\r\n", _code)); + switch (_type) { + case Type::HTML: + socket->write("Content-Type: text/html; charset=utf-8\r\n"); + break; + case Type::JSON: + socket->write("Content-Type: text/json; charset=utf-8\r\n"); + break; + } + socket->write(String::compose("Content-Length: %1\r\n", _payload.length())); + for (auto const& header: _headers) { + socket->write(String::compose("%1: %2\r\n", header.first, header.second)); + } + socket->write("\r\n"); + socket->write(_payload); +} + + +Response +HTTPServer::get(string const& url) +{ + if (url == "/") { + return Response(200, String::compose(dcp::file_to_string(resources_path() / "web" / "index.html"), variant::dcpomatic_player())); + } else if (url == "/api/v1/status") { + auto json = string{"{ "}; + { + boost::mutex::scoped_lock lm(_mutex); + json += String::compose("\"playing\": %1, ", _playing ? "true" : "false"); + json += String::compose("\"position\": \"%1\", ", seconds_to_hms(_position.seconds())); + json += String::compose("\"dcp_name\": \"%1\"", _dcp_name); + } + json += " }"; + auto response = Response(200, json); + response.set_type(Response::Type::JSON); + return response; + } else { + LOG_HTTP("404 %1", url); + return Response::ERROR_404; + } +} + + +Response +HTTPServer::post(string const& url) +{ + if (url == "/api/v1/play") { + emit(boost::bind(boost::ref(Play))); + auto response = Response(303); + response.add_header("Location", "/"); + return response; + } else if (url == "/api/v1/stop") { + emit(boost::bind(boost::ref(Stop))); + auto response = Response(303); + response.add_header("Location", "/"); + return response; + } else { + return Response::ERROR_404; + } +} + + +Response +HTTPServer::request(vector<string> const& request) +{ + vector<string> parts; + boost::split(parts, request[0], boost::is_any_of(" ")); + if (parts.size() != 3) { + return Response::ERROR_404; + } + + try { + if (parts[0] == "GET") { + LOG_HTTP("GET %1", parts[1]); + return get(parts[1]); + } else if (parts[0] == "POST") { + LOG_HTTP("POST %1", parts[1]); + return post(parts[1]); + } + } catch (std::exception& e) { + LOG_ERROR("Error while handling HTTP request: %1", e.what()); + } catch (...) { + LOG_ERROR_NC("Unknown exception while handling HTTP request"); + } + + LOG_HTTP("404 %1", parts[0]); + return Response::ERROR_404; +} + + +void +HTTPServer::handle(shared_ptr<Socket> socket) +{ + class Reader + { + public: + void read_block(boost::system::error_code const& ec, uint8_t* data, std::size_t size) + { + if (ec.value() != boost::system::errc::success) { + _close = true; + _error_code = ec; + return; + } + + for (std::size_t i = 0; i < size; ++i) { + if (_line.length() >= 1024) { + _close = true; + return; + } + _line += data[i]; + if (_line.length() >= 2 && _line.substr(_line.length() - 2) == "\r\n") { + if (_line.length() == 2) { + _got_request = true; + return; + } else if (_request.size() > 64) { + _close = true; + return; + } else if (_line.size() >= 2) { + _line = _line.substr(0, _line.length() - 2); + } + LOG_HTTP("Receive: %1", _line); + _request.push_back(_line); + _line = ""; + } + } + } + + + bool got_request() const { + return _got_request; + } + + bool close() const { + return _close; + } + + boost::system::error_code error_code() const { + return _error_code; + } + + vector<std::string> const& get() const { + return _request; + } + + private: + std::string _line; + vector<std::string> _request; + bool _got_request = false; + bool _close = false; + boost::system::error_code _error_code; + }; + + while (true) { + + Reader reader; + + vector<uint8_t> buffer(2048); + socket->socket().async_read_some( + boost::asio::buffer(buffer.data(), buffer.size()), + [&reader, &buffer, socket](boost::system::error_code const& ec, std::size_t bytes_transferred) { + socket->set_deadline_from_now(1); + reader.read_block(ec, buffer.data(), bytes_transferred); + }); + + while (!reader.got_request() && !reader.close() && socket->is_open()) { + socket->run(); + } + + if (reader.got_request() && !reader.close()) { + try { + auto response = request(reader.get()); + response.send(socket); + } catch (runtime_error& e) { + LOG_ERROR_NC(e.what()); + } + } + + if (reader.close()) { + break; + } + } +} diff --git a/src/lib/http_server.h b/src/lib/http_server.h new file mode 100644 index 000000000..f0ee50ef5 --- /dev/null +++ b/src/lib/http_server.h @@ -0,0 +1,91 @@ +/* + Copyright (C) 2024 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "dcpomatic_time.h" +#include "server.h" +#include "signaller.h" +#include <boost/signals2.hpp> + + +class Response +{ +public: + Response(int code); + Response(int code, std::string payload); + + enum class Type { + HTML, + JSON + }; + + void add_header(std::string key, std::string value); + void set_type(Type type) { + _type = type; + } + + void send(std::shared_ptr<Socket> socket); + + static Response ERROR_404; + +private: + int _code; + + Type _type = Type::HTML; + std::string _payload; + std::vector<std::pair<std::string, std::string>> _headers; +}; + + +class HTTPServer : public Server, public Signaller +{ +public: + explicit HTTPServer(int port, int timeout = 30); + + boost::signals2::signal<void ()> Play; + boost::signals2::signal<void ()> Stop; + + void set_playing(bool playing) { + boost::mutex::scoped_lock lm(_mutex); + _playing = playing; + } + + void set_position(dcpomatic::DCPTime position) { + boost::mutex::scoped_lock lm(_mutex); + _position = position; + } + + void set_dcp_name(std::string name) { + boost::mutex::scoped_lock lm(_mutex); + _dcp_name = name; + } + +private: + void handle(std::shared_ptr<Socket> socket) override; + Response request(std::vector<std::string> const& request); + Response get(std::string const& url); + Response post(std::string const& url); + + boost::mutex _mutex; + bool _playing = false; + dcpomatic::DCPTime _position; + std::string _dcp_name; +}; + diff --git a/src/lib/wscript b/src/lib/wscript index 1c53538a1..dfe3ce487 100644 --- a/src/lib/wscript +++ b/src/lib/wscript @@ -127,6 +127,7 @@ sources = """ frame_rate_change.cc guess_crop.cc hints.cc + http_server.cc id.cc internet.cc image.cc |
