shutil.copyfile('build/platform/windows/installer.%s.nsi' % identifier, 'build/platform/windows/installer2.%s.nsi' % identifier)
target.command('sed -i "s~%%resources%%~%s/platform/windows~g" build/platform/windows/installer2.%s.nsi' % (os.getcwd(), identifier))
target.command('sed -i "s~%%graphics%%~%s/graphics~g" build/platform/windows/installer2.%s.nsi' % (os.getcwd(), identifier))
+ target.command('sed -i "s~%%web%%~%s/web~g" build/platform/windows/installer2.%s.nsi' % (os.getcwd(), identifier))
target.command('sed -i "s~%%static_deps%%~%s~g" build/platform/windows/installer2.%s.nsi' % (target.windows_prefix, identifier))
target.command('sed -i "s~%%cdist_deps%%~%s~g" build/platform/windows/installer2.%s.nsi' % (target.directory, identifier))
target.command('sed -i "s~%%mingw%%~%s~g" build/platform/windows/installer2.%s.nsi' % (target.environment_prefix, identifier))
cp $source/graphics/link*.png "$dest"
cp $source/graphics/add*.png "$dest"
cp $source/graphics/pause*.png "$dest"
+ cp -r $source/web "$dest"
cp -r $prefix/share/libdcp/xsd "$dest"
cp -r $prefix/share/libdcp/tags "$dest"
cp -r $prefix/share/libdcp/ratings "$dest"
File "%graphics%/add_white.png"
File "%graphics%/pause_black.png"
File "%graphics%/pause_white.png"
+SetOutPath "$INSTDIR\\web"
+File "%web%/index.html"
SetOutPath "$INSTDIR\\xsd"
File "%cdist_deps%/share/libdcp/xsd/DCDMSubtitle-2010.xsd"
File "%cdist_deps%/share/libdcp/xsd/DCDMSubtitle-2014.xsd"
_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);
_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")) {
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) {
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) {
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;
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;
--- /dev/null
+/*
+ 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;
+ }
+ }
+}
--- /dev/null
+/*
+ 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;
+};
+
frame_rate_change.cc
guess_crop.cc
hints.cc
+ http_server.cc
id.cc
internet.cc
image.cc
#include "lib/file_log.h"
#include "lib/film.h"
#include "lib/font_config.h"
+#include "lib/http_server.h"
#include "lib/image.h"
#include "lib/image_jpeg.h"
#include "lib/image_png.h"
_stress.LoadDCP.connect (boost::bind(&DOMFrame::load_dcp, this, _1));
setup_internal_player_server();
+ setup_http_server();
SetDropTarget(new DCPDropTarget(this));
}
~DOMFrame ()
{
+ stop_http_server();
/* It's important that this is stopped before our frame starts destroying its children,
* otherwise UI elements that it depends on will disappear from under it.
*/
_stress.load_script (path);
}
+ void idle()
+ {
+ if (_http_server) {
+ struct timeval now;
+ gettimeofday(&now, 0);
+ auto time_since_last_update = (now.tv_sec + now.tv_usec / 1e6) - (_last_http_server_update.tv_sec + _last_http_server_update.tv_usec / 1e6);
+ if (time_since_last_update > 0.25) {
+ _http_server->set_playing(_viewer.playing());
+ if (auto dcp = _viewer.dcp()) {
+ _http_server->set_dcp_name(dcp->name());
+ } else {
+ _http_server->set_dcp_name("");
+ }
+ _http_server->set_position(_viewer.position());
+ _last_http_server_update = now;
+ }
+ }
+ }
+
private:
void examine_content ()
}
update_from_config (prop);
+
+ setup_http_server();
+ }
+
+ void stop_http_server()
+ {
+ if (_http_server) {
+ _http_server->stop();
+ _http_server_thread.join();
+ _http_server.reset();
+ }
+ }
+
+ void setup_http_server()
+ {
+ stop_http_server();
+
+ auto config = Config::instance();
+ try {
+ if (config->enable_player_http_server()) {
+ _http_server.reset(new HTTPServer(config->player_http_server_port()));
+ _http_server->Play.connect(boost::bind(&FilmViewer::start, &_viewer));
+ _http_server->Stop.connect(boost::bind(&FilmViewer::stop, &_viewer));
+ _http_server_thread = boost::thread(boost::bind(&HTTPServer::run, _http_server.get()));
+ }
+ } catch (std::exception& e) {
+ LOG_DEBUG_PLAYER("Failed to start player HTTP server (%1)", e.what());
+ }
}
void setup_internal_player_server()
PlayerStressTester _stress;
/** KDMs that have been loaded, so that we can pass them to the verifier */
std::vector<boost::filesystem::path> _kdms;
+ boost::thread _http_server_thread;
+ std::unique_ptr<HTTPServer> _http_server;
+ struct timeval _last_http_server_update = { 0, 0 };
};
static const wxCmdLineEntryDesc command_line_description[] = {
void idle ()
{
signal_manager->ui_idle ();
+ if (_frame) {
+ _frame->idle();
+ }
}
void config_failed_to_load ()
private:
void setup () override
{
- wxGridBagSizer* table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+ auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
_panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
int r = 0;
add_language_controls (table, r);
add_update_controls (table, r);
+ _enable_http_server = new CheckBox(_panel, _("Enable HTTP control interface on port"));
+ table->Add(_enable_http_server, wxGBPosition(r, 0));
+ _http_server_port = new wxSpinCtrl(_panel);
+ table->Add(_http_server_port, wxGBPosition(r, 1));
+ ++r;
+
add_label_to_sizer (table, _panel, _("Start player as"), true, wxGBPosition(r, 0));
_player_mode = new wxChoice (_panel, wxID_ANY);
_player_mode->Append (_("window"));
table->Add (_video_display_mode, wxGBPosition(r, 1));
++r;
- wxStaticText* restart = add_label_to_sizer(table, _panel, variant::wx::insert_dcpomatic_player(_("(restart %s to change display mode)")), false, wxGBPosition(r, 0));
+ auto restart = add_label_to_sizer(table, _panel, variant::wx::insert_dcpomatic_player(_("(restart %s to change display mode)")), false, wxGBPosition(r, 0));
wxFont font = restart->GetFont();
font.SetStyle (wxFONTSTYLE_ITALIC);
font.SetPointSize (font.GetPointSize() - 1);
_video_display_mode->Bind (wxEVT_CHOICE, bind(&PlayerGeneralPage::video_display_mode_changed, this));
_respect_kdm->bind(&PlayerGeneralPage::respect_kdm_changed, this);
_debug_log_file->Bind (wxEVT_FILEPICKER_CHANGED, bind(&PlayerGeneralPage::debug_log_file_changed, this));
+ _enable_http_server->bind(&PlayerGeneralPage::enable_http_server_changed, this);
+ _http_server_port->SetRange(1, 32767);
+ _http_server_port->Bind(wxEVT_SPINCTRL, boost::bind(&PlayerGeneralPage::http_server_port_changed, this));
+
+ setup_sensitivity();
}
void config_changed () override
if (config->player_debug_log_file()) {
checked_set (_debug_log_file, *config->player_debug_log_file());
}
+
+ checked_set(_enable_http_server, config->enable_player_http_server());
+ checked_set(_http_server_port, config->player_http_server_port());
+
+ setup_sensitivity();
}
private:
}
}
+ void enable_http_server_changed()
+ {
+ Config::instance()->set_enable_player_http_server(_enable_http_server->get());
+ }
+
+ void http_server_port_changed()
+ {
+ Config::instance()->set_player_http_server_port(_http_server_port->GetValue());
+ }
+
+ void setup_sensitivity()
+ {
+ _http_server_port->Enable(_enable_http_server->get());
+ }
+
wxChoice* _player_mode;
wxChoice* _image_display;
wxChoice* _video_display_mode;
CheckBox* _respect_kdm;
FilePickerCtrl* _debug_log_file;
+ CheckBox* _enable_http_server;
+ wxSpinCtrl* _http_server_port;
};
--- /dev/null
+<!DOCTYPE html>
+<html>
+<script>
+ setInterval(function() {
+ status = fetch("/api/v1/status").then(response => {
+ response.json().then(data => {
+ if (data.playing) {
+ document.getElementById('playing').innerHTML = "Playing";
+ } else {
+ document.getElementById('playing').innerHTML = "Stopped";
+ }
+ document.getElementById('dcp_name').innerHTML = data.dcp_name;
+ document.getElementById('position').innerHTML = data.position;
+ });
+ });
+ }, 250);
+ function play() {
+ fetch("/api/v1/play", { method: "POST" });
+ }
+ function stop() {
+ fetch("/api/v1/stop", { method: "POST" });
+ }
+</script>
+<style>
+button {
+ border: 1px solid rgba(27, 31, 35, 0.15);
+ border-radius: 6px;
+ color: #24292E;
+ display: inline-block;
+ line-height: 20px;
+ padding: 6px 16px;
+ vertical-align: middle;
+ white-space: nowrap;
+ word-wrap: break-word;
+}
+
+button:hover {
+ background-color: #F3F4F6;
+ text-decoration: none;
+ transition-duration: 0.1s;
+}
+
+button:active {
+ background-color: #EDEFF2;
+ box-shadow: rgba(225, 228, 232, 0.2) 0 1px 0 inset;
+ transition: none 0s;
+}
+
+button:focus {
+ outline: 1px transparent;
+}
+
+button:before {
+ display: none;
+}
+
+table {
+ border-collapse: collapse;
+ margin: 25px 0;
+ font-size: 0.9em;
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
+ border: 2px solid black;
+}
+
+tr {
+ text-align: left;
+ border: 1px solid black;
+}
+
+td {
+ padding: 4px 16px;
+ text-align: left;
+ border: 1px solid black;
+}
+
+
+</style>
+ <head>
+ <title>%1</title>
+ </head>
+ <body>
+ <button name="play" value="play" onclick="play()">Play</button>
+ <button name="Stop" value="Stop" onclick="stop()">Stop</button>
+ <table>
+ <tr><td>DCP</td><td><p id="dcp_name"></td></tr>
+ <tr><td>State</td><td><p id="playing"></td></tr>
+ <tr><td>Position</td><td><p id="position"></td></tr>
+ </table>
+ </body>
+</html>
--- /dev/null
+
+def build(bld):
+ for file in ('index.html',):
+ bld.install_files('${PREFIX}/share/dcpomatic2/web', file)
+
bld.recurse('src')
bld.recurse('graphics')
+ bld.recurse('web')
if not bld.env.DISABLE_TESTS:
bld.recurse('test')