Add minimal player HTTP server (#2830).
authorCarl Hetherington <cth@carlh.net>
Thu, 13 Jun 2024 23:45:18 +0000 (01:45 +0200)
committerCarl Hetherington <cth@carlh.net>
Sun, 23 Jun 2024 17:51:28 +0000 (19:51 +0200)
13 files changed:
cscript
platform/osx/make_dmg.sh
platform/windows/wscript
src/lib/config.cc
src/lib/config.h
src/lib/http_server.cc [new file with mode: 0644]
src/lib/http_server.h [new file with mode: 0644]
src/lib/wscript
src/tools/dcpomatic_player.cc
src/wx/player_config_dialog.cc
web/index.html [new file with mode: 0644]
web/wscript [new file with mode: 0644]
wscript

diff --git a/cscript b/cscript
index 96ed51e5329aa11a1f911c3b1fce0c36e498e3de..baece29e1b2f458bf12ef1ed61c82052a8403352 100644 (file)
--- a/cscript
+++ b/cscript
@@ -721,6 +721,7 @@ def package_windows(target):
     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))
index dd42ebe3274a59a5f2becd33322aad6f71d9f362..da7c3881241c91a0c07be4bcbf3394e94382e6bc 100644 (file)
@@ -272,6 +272,7 @@ function copy_resources {
     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"
index 2c53a1422b96a747310c31072f7cd6ef784c5987..f64923b2c65090835026506fd9283f37b81a7c61 100644 (file)
@@ -368,6 +368,8 @@ File "%graphics%/add_black.png"
 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"
index 1ca73c400402350f207ead0a3b296d2902d101de..180ce5c551376eaffaccecf58f6b3ed4d1bed7e0 100644 (file)
@@ -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) {
index 397af22226aa0842c37d4faff38468e90ef8afe2..1f13381b703080f59ccda45f7ab3b2eaa09ea5e2 100644 (file)
@@ -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 (file)
index 0000000..0ee6275
--- /dev/null
@@ -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 (file)
index 0000000..f0ee50e
--- /dev/null
@@ -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;
+};
+
index 1c53538a12f9dca37bb129afab13c3721f4c63e2..dfe3ce4872cd48f8dab08a05cce5b3befa0675d4 100644 (file)
@@ -127,6 +127,7 @@ sources = """
           frame_rate_change.cc
           guess_crop.cc
           hints.cc
+          http_server.cc
           id.cc
           internet.cc
           image.cc
index f6e052b41994b3e2dd2244781d5108ef143f8183..55cd000276cf666c52838623184cfffbff8c703a 100644 (file)
@@ -51,6 +51,7 @@
 #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"
@@ -312,12 +313,14 @@ public:
                _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.
                 */
@@ -539,6 +542,25 @@ public:
                _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 ()
@@ -1040,6 +1062,34 @@ private:
                }
 
                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()
@@ -1172,6 +1222,9 @@ private:
        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[] = {
@@ -1355,6 +1408,9 @@ private:
        void idle ()
        {
                signal_manager->ui_idle ();
+               if (_frame) {
+                       _frame->idle();
+               }
        }
 
        void config_failed_to_load ()
index d7bba4dcc032b763e07d2a6dc511d6631059510a..b2a02be24d641a9965a766a0b3ba55b87f66edc4 100644 (file)
@@ -86,13 +86,19 @@ public:
 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"));
@@ -115,7 +121,7 @@ private:
                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);
@@ -136,6 +142,11 @@ private:
                _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
@@ -170,6 +181,11 @@ private:
                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:
@@ -214,11 +230,28 @@ 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;
 };
 
 
diff --git a/web/index.html b/web/index.html
new file mode 100644 (file)
index 0000000..0eea15a
--- /dev/null
@@ -0,0 +1,90 @@
+<!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>
diff --git a/web/wscript b/web/wscript
new file mode 100644 (file)
index 0000000..d9ebc76
--- /dev/null
@@ -0,0 +1,5 @@
+
+def build(bld):
+    for file in ('index.html',):
+        bld.install_files('${PREFIX}/share/dcpomatic2/web', file)
+
diff --git a/wscript b/wscript
index 97fbc3fa212f5327c3513dfdfcdb6573d380516d..a92668ee0bdbce4a6cd8205b30cb1ce51202919f 100644 (file)
--- a/wscript
+++ b/wscript
@@ -706,6 +706,7 @@ def build(bld):
 
     bld.recurse('src')
     bld.recurse('graphics')
+    bld.recurse('web')
 
     if not bld.env.DISABLE_TESTS:
         bld.recurse('test')